156

I'm really tired looking for a solution for vertical and horizontal Scrollview.

I read that there are not any views/layouts in the framework which implement this feature, but I need something like this:

I need to define a layout within other, the child layout must implement scrolling vertical/horizontal for moving.

Initially implemented a code that moved the layout pixel by pixel, but I think that is not the right way. I tried it with ScrollView and Horizontal ScrollView but nothing works like I want it to, because it only implements vertical or horizontal scrolling.

Canvas is not my solution because I need to attach listeners in someones child elements.

What can I do?

Damien Sawyer
  • 5,323
  • 3
  • 44
  • 56
Kronos
  • 7,541
  • 5
  • 22
  • 11
  • http://stackoverflow.com/questions/11775677/android-horizontal-list-view-in-vertical-scroll-view – Vins Aug 02 '12 at 11:55
  • 17
    It is absolutely ridiculous that this is not available out of the box with Android. We have worked with half a dozen other UI technologies and it is unheard of to not have such a basic thing as a horizontal/vertical scrolling container. – flexicious.com Jun 15 '14 at 01:43
  • Ok, so finally we wrote our own for our datagrid : http://www.androidjetpack.com/Home/AndroidDataGrid. Does a lot more than just horizontal/vertical scroll. But the idea is basically wrapping a HScroller inside a Vertical Scroller. You also have to override add/remove child methods to target the inner scroller and a few other shenanigans, but it works. – flexicious.com Sep 11 '14 at 03:47
  • You might find this answer helpful https://stackoverflow.com/a/62733625/311445 – deej Jul 29 '20 at 14:48

12 Answers12

93

Mixing some of the suggestions above, and was able to get a good solution:

Custom ScrollView:

package com.scrollable.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ScrollView;

public class VScroll extends ScrollView {

    public VScroll(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public VScroll(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public VScroll(Context context) {
        super(context);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return false;
    }
}

Custom HorizontalScrollView:

package com.scrollable.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.HorizontalScrollView;

public class HScroll extends HorizontalScrollView {

    public HScroll(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public HScroll(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public HScroll(Context context) {
        super(context);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return false;
    }
}

the ScrollableImageActivity:

package com.scrollable.view;

import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;

public class ScrollableImageActivity extends Activity {

    private float mx, my;
    private float curX, curY;

    private ScrollView vScroll;
    private HorizontalScrollView hScroll;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        vScroll = (ScrollView) findViewById(R.id.vScroll);
        hScroll = (HorizontalScrollView) findViewById(R.id.hScroll);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float curX, curY;

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                mx = event.getX();
                my = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                curX = event.getX();
                curY = event.getY();
                vScroll.scrollBy((int) (mx - curX), (int) (my - curY));
                hScroll.scrollBy((int) (mx - curX), (int) (my - curY));
                mx = curX;
                my = curY;
                break;
            case MotionEvent.ACTION_UP:
                curX = event.getX();
                curY = event.getY();
                vScroll.scrollBy((int) (mx - curX), (int) (my - curY));
                hScroll.scrollBy((int) (mx - curX), (int) (my - curY));
                break;
        }

        return true;
    }

}

the layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <com.scrollable.view.VScroll android:layout_height="fill_parent"
        android:layout_width="fill_parent" android:id="@+id/vScroll">
        <com.scrollable.view.HScroll android:id="@+id/hScroll"
            android:layout_width="fill_parent" android:layout_height="fill_parent">
            <ImageView android:layout_width="fill_parent" android:layout_height="fill_parent" android:src="@drawable/bg"></ImageView>
        </com.scrollable.view.HScroll>
    </com.scrollable.view.VScroll>

</LinearLayout>
Mahdi Hijazi
  • 4,424
  • 3
  • 24
  • 29
  • This is a great answer, I had other scrolling elements in the view that also needed to change. All I had to do was trigger their scrollby along with the others. – T. Markle Apr 10 '12 at 15:31
  • 1
    Great! A few adjustments to take velocity into account, and this would be perfect (also, you don't need the initial `LinearLayout` I guess) – Romuald Brunet Oct 05 '12 at 06:50
  • Hey Mahdi can you let me know , on which control/widget you have reigstered the touch listener in activity class . – Shachillies Jan 24 '13 at 09:50
  • Sorry for the delay @DeepakSharma. I didn't register any touch listener in the activity, I've just override the onTouchEvent of the activity. – Mahdi Hijazi Feb 18 '13 at 07:32
  • @MahdiHijazi can u please add your code of Horizontal Scrolling and Vertical Scrolling in https://github.com/MikeOrtiz/TouchImageView this is successfully zooming image on button click but failed to scroll image – Erum Jan 31 '14 at 12:24
  • Can you add a little more feature like smooth scrolling? And the default behavior when scrollbars bump the screen edges are gone :( – mr5 May 27 '14 at 06:25
  • @MahdiHijazi Did you test this code? I tried to execute in both phone and toughpad but the application crash. Unfortunately I did not get any Exception in the logcat. – Zusee Weekin May 30 '14 at 06:44
  • On the first time I used this it was great but as I go deeper in my dev't I see the holes, those were: Scroll effects are removed, events from scrollview's (both horizontal and vertical) children are not handled properly i.e there's a race condition in w/c event should be trigger. – mr5 Jun 11 '14 at 01:55
  • android.view.InflateException: Binary XML file line #5: Binary XML file line #5: Error inflating class – salih kallai Mar 08 '17 at 19:22
  • For those of you trying to do the same for a Fragment (instead of an Activity): In your Fragment class, in onCreateView: use the inflated view and view.call setOnTouchListener https://stackoverflow.com/questions/21882251/how-to-handle-touch-events-on-a-fragment – PhoenixB Jun 25 '18 at 07:40
43

Since this seems to be the first search result in Google for "Android vertical+horizontal ScrollView", I thought I should add this here. Matt Clark has built a custom view based on the Android source, and it seems to work perfectly: Two Dimensional ScrollView

Beware that the class in that page has a bug calculating the view's horizonal width. A fix by Manuel Hilty is in the comments:

Solution: Replace the statement on line 808 by the following:

final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);


Edit: The Link doesn't work anymore but here is a link to an old version of the blogpost.

Morten Holmgaard
  • 7,484
  • 8
  • 63
  • 85
Cachapa
  • 1,761
  • 1
  • 15
  • 16
  • 1
    This works like a charm after some minor modification for me. I reimplemented `fillViewport` and `onMeasure` exactly like ScrollView source (v2.1). I also changed the two first constructor with : `this( context, null );` and `this(context, attrs, R.attr.scrollViewStyle);`. With that, I can use scrollbar using `setVerticalScrollBarEnabled` and `setHorizontalScrollBarEnabled` in code. – olivier_sdg May 11 '12 at 07:42
  • but how to get the scroll thumb size and scroll position from bottom vertical and from right in horizontal mode – Ravi May 06 '13 at 04:42
  • 11
    Looks like the linked post has disappeared. – loeschg Mar 24 '14 at 22:10
  • Thanks! This solves my problem: http://stackoverflow.com/questions/24152846/prevent-imageview-from-receiving-touch-events-when-scrolling – mr5 Jun 11 '14 at 03:32
  • FYI, the line you have to replace w/ that fix is: //final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, // getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); – kenyee Mar 09 '17 at 17:13
28

I found a better solution.

XML: (design.xml)

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent">
  <FrameLayout android:layout_width="90px" android:layout_height="90px">
    <RelativeLayout android:id="@+id/container" android:layout_width="fill_parent" android:layout_height="fill_parent">        
    </RelativeLayout>
</FrameLayout>
</FrameLayout>

Java Code:

public class Example extends Activity {
  private RelativeLayout container;
  private int currentX;
  private int currentY;

  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.design);

    container = (RelativeLayout)findViewById(R.id.container);

    int top = 0;
    int left = 0;

    ImageView image1 = ...
    RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
    layoutParams.setMargins(left, top, 0, 0);               
    container.addView(image1, layoutParams);

    ImageView image2 = ...
    left+= 100;
    RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
    layoutParams.setMargins(left, top, 0, 0);               
    container.addView(image2, layoutParams);

    ImageView image3 = ...
    left= 0;
    top+= 100;
    RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
    layoutParams.setMargins(left, top, 0, 0);               
    container.addView(image3, layoutParams);

    ImageView image4 = ...
    left+= 100;     
    RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
    layoutParams.setMargins(left, top, 0, 0);               
    container.addView(image4, layoutParams);
  }     

  @Override 
  public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            currentX = (int) event.getRawX();
            currentY = (int) event.getRawY();
            break;
        }

        case MotionEvent.ACTION_MOVE: {
            int x2 = (int) event.getRawX();
            int y2 = (int) event.getRawY();
            container.scrollBy(currentX - x2 , currentY - y2);
            currentX = x2;
            currentY = y2;
            break;
        }   
        case MotionEvent.ACTION_UP: {
            break;
        }
    }
      return true; 
  }
}

That's works!!!

If you want to load other layout or control, the structure is the same.

Kronos
  • 7,541
  • 5
  • 22
  • 11
  • Just tried this, and it works very well! It doesn't have scroll indicators, or fling, but because this is a lower-level approach those things could be built on top of this fairly easily. Thanks! – ubzack Dec 03 '11 at 21:35
  • Worked well for me too for writing a two dimensional scrollable view that outputted realtime output from the result of SSH remote commands over the network. – pilcrowpipe Jul 27 '12 at 22:17
  • @Kronos : What if I only want to scroll horizontally? What should my y parameter be in container.scrollBy(currentX - x2 , currentY - y2). Is there any way where we can make it scroll horizontally only? – Ashwin Sep 21 '12 at 12:19
  • @Kronos : It scrolls too far. Is there any way to stop the scroll when the end is reached? – Ashwin Sep 21 '12 at 15:56
  • Buttons are non scrollable, Why? – salih kallai Mar 08 '17 at 20:24
25

I use it and works fine:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView android:id="@+id/ScrollView02" 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            xmlns:android="http://schemas.android.com/apk/res/android">
<HorizontalScrollView android:id="@+id/HorizontalScrollView01" 
                      android:layout_width="wrap_content" 
                      android:layout_height="wrap_content">
<ImageView android:id="@+id/ImageView01"
           android:src="@drawable/pic" 
           android:isScrollContainer="true" 
           android:layout_height="fill_parent" 
           android:layout_width="fill_parent" 
           android:adjustViewBounds="true">
</ImageView>
</HorizontalScrollView>
</ScrollView>

The source link is here: Android-spa

Redax
  • 9,231
  • 6
  • 31
  • 39
  • 2
    It works "just fine" for static contents, at best. Multiple Google engineers have said that what you are trying to use will have problems (http://groups.google.com/group/android-developers/browse_frm/thread/c50acbe68ec98174/7d234358e2a57775 and http://groups.google.com/group/android-beginners/browse_thread/thread/c05925aca7479d18). – CommonsWare Jun 04 '11 at 13:03
  • 4
    @CommonsWare: whit respect for the brilliant multiple Google engineers, in those threads they told you to rewrite from scratch your own component: this is the best way. Surely is true. But they don't say that this solution is bad, and if it works... Maybe for them it takes a minute, for me is better to write some xml using the existing components. What does it mean "static content"? I use button and images. – Redax Jun 04 '11 at 16:47
  • 3
    By "static content" I mean things that do not involve touch events themselves. My point -- for others reading this answer -- is that while your solution may work for a single `ImageView` as the scrollable content, it may or may not work for arbitrary other contents, and that Google believes that there will be issues with your approach. Google's opinion also matters with respect to future development -- they may not be trying to make sure that your approach works over the long haul, if they are advising people not to use it in the first place. – CommonsWare Jun 04 '11 at 17:37
  • This is working fine for me...I am refreshing an imageview in this scrollview using a thread – sonu thomas Jan 05 '12 at 06:02
21

My solution based on Mahdi Hijazi answer, but without any custom views:

Layout:

<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android" 
    android:id="@+id/scrollHorizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ScrollView 
        android:id="@+id/scrollVertical"
        android:layout_width="wrap_content"
        android:layout_height="match_parent" >

        <WateverViewYouWant/>

    </ScrollView>
</HorizontalScrollView>

Code (onCreate/onCreateView):

    final HorizontalScrollView hScroll = (HorizontalScrollView) value.findViewById(R.id.scrollHorizontal);
    final ScrollView vScroll = (ScrollView) value.findViewById(R.id.scrollVertical);
    vScroll.setOnTouchListener(new View.OnTouchListener() { //inner scroll listener         
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            return false;
        }
    });
    hScroll.setOnTouchListener(new View.OnTouchListener() { //outer scroll listener         
        private float mx, my, curX, curY;
        private boolean started = false;

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            curX = event.getX();
            curY = event.getY();
            int dx = (int) (mx - curX);
            int dy = (int) (my - curY);
            switch (event.getAction()) {
                case MotionEvent.ACTION_MOVE:
                    if (started) {
                        vScroll.scrollBy(0, dy);
                        hScroll.scrollBy(dx, 0);
                    } else {
                        started = true;
                    }
                    mx = curX;
                    my = curY;
                    break;
                case MotionEvent.ACTION_UP: 
                    vScroll.scrollBy(0, dy);
                    hScroll.scrollBy(dx, 0);
                    started = false;
                    break;
            }
            return true;
        }
    });

You can change the order of the scrollviews. Just change their order in layout and in the code. And obviously instead of WateverViewYouWant you put the layout/views you want to scroll both directions.

Community
  • 1
  • 1
Yan.Yurkin
  • 970
  • 8
  • 15
  • I strongly suggest returning false in hScroll.setOnTouchListener. When I changed to false, list started to scroll "smoothly auto" on finger up in both directions. – Greta Radisauskaite Oct 29 '14 at 14:24
  • 1
    Works when scrolling horizontally first. When vertical is first, goes only vertically – Irhala Jul 04 '17 at 12:31
  • Still? Really? 7 years? I didn't touch Android development for years already. Wow. Thanks for the feedback :) – Yan.Yurkin Mar 24 '21 at 04:56
6

Try this

<?xml version="1.0" encoding="utf-8"?>
<ScrollView android:id="@+id/Sview" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content"
        xmlns:android="http://schemas.android.com/apk/res/android">
<HorizontalScrollView 
   android:id="@+id/hview" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content">
<ImageView .......
 [here xml code for image]
</ImageView>
</HorizontalScrollView>
</ScrollView>
MBMJ
  • 5,323
  • 8
  • 32
  • 51
6

Option #1: You can come up with a new UI design that does not require simultaneous horizontal and vertical scrolling.

Option #2: You can obtain the source code to ScrollView and HorizontalScrollView, learn how the core Android team implemented those, and create your own BiDirectionalScrollView implementation.

Option #3: You can get rid of the dependencies that are requiring you to use the widget system and draw straight to the Canvas.

Option #4: If you stumble upon an open source application that seems to implement what you seek, look to see how they did it.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
6

since the Two Dimensional Scrollview link is dead I could not get it so I created my own component. It handles flinging and works properly for me. The only restriction is that wrap_content might not work properly for that component.

https://gist.github.com/androidseb/9902093

androidseb
  • 1,257
  • 2
  • 14
  • 17
1

Playing with the code, you can put an HorizontalScrollView into an ScrollView. Thereby, you can have the two scroll method in the same view.

Source : http://androiddevblog.blogspot.com/2009/12/creating-two-dimensions-scroll-view.html

I hope this could help you.

Eriatolc
  • 197
  • 1
  • 1
  • 10
  • 1
    This isn't a good solution. If you notice, when dragging the view, you are always dragging either vertically or horizontally. You can't drag along a diagonal using nested scroll views. – Sky Kelsey Feb 25 '12 at 00:09
  • What's worse than the lack of diagonal scrolling is the lack of both scrollbars remaining at the view edge because one is nested. A better ScrollView is the answer and Cachapa's link to Matt Clark's code is the best solution at this time due to completeness and generalization. – Huperniketes Jun 03 '12 at 15:32
  • I used a trick way to be able to show both vertical and horizontal scrollbars, you can refer this https://stackoverflow.com/a/69925768/5426065 – tuantv.dev Nov 12 '21 at 01:14
1

I have a solution for your problem. You can check the ScrollView code it handles only vertical scrolling and ignores the horizontal one and modify this. I wanted a view like a webview, so modified ScrollView and it worked well for me. But this may not suit your needs.

Let me know what kind of UI you are targeting for.

Regards,

Ravi Pandit

Kevin van Mierlo
  • 9,554
  • 5
  • 44
  • 76
0

use this way I tried this I fixed it

Put All your XML layout inside

<android.support.v4.widget.NestedScrollView 

I explained this in this link vertical recyclerView and Horizontal recyclerview scrolling together

0

If you want to scroll vertically and horizontally this is an example:

Horizontal scroll inside vertical scroll and both works:

<ScrollView
        android:id="@+id/verticalScroll"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

...
     <HorizontalScrollView
            android:id="@+id/horizontalScroll"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            *android:overScrollMode="never"*>
...
     </HorizontalScrollView>
...
</ScrollView>