12

I would like to have a linearlayout with a header section on top and a webview below. The header will be short and the webview may be longer and wider than the screen.

What is the best way to get horizontal and vertical scrolling? Is a ScrollView nested inside a HorizontalScrollView a good idea?

700 Software
  • 85,281
  • 83
  • 234
  • 341

9 Answers9

9

Is a ScrollView nested inside a HorizontalScrollView a good idea?

Yes, and no.

Yes, my understanding is that ScrollView and HorizontalScrollView can be nested.

No, AFAIK, neither ScrollView nor HorizontalScrollView work with WebView.

I suggest that you have your WebView fit on the screen.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • 1
    I can say for certain that a ScrollView and HorizontalScrollView can be nested as I've done it, but the user experience is poor as it can be difficult to make it scroll in the direction you want it to, and there is no diagonal interpretation--you either get all vertical or all horizontal movement. – Blumer Oct 05 '10 at 19:21
  • "have your WebView fit on the screen." Unfortunately I do not have any way to do that :( – 700 Software Oct 05 '10 at 19:26
  • @Blumer I noticed. That is acceptable for me but not optimal. – 700 Software Oct 05 '10 at 19:27
  • @GeorgeBailey: "Unfortunately I do not have any way to do that" -- what is stopping you? – CommonsWare Oct 05 '10 at 20:08
  • The pages are user generated and if it contains a long line of dashes or an image it would be too wide and if it had too much content it would be too long. – 700 Software Oct 05 '10 at 21:32
  • @cjavapro: So? `WebView` scrolls internally, like a regular Web browser. – CommonsWare Oct 05 '10 at 21:38
  • Oh - well that is a good thing.. I will try it today, thanks. – 700 Software Oct 05 '10 at 21:44
  • I was able to use width fill_parent and height wrap_content on the web view. When the web view is a child of a vertical linear layout which is a child of a scroll view, the vertical scrolling applies to the whole linear layout and the horizontal is only on the web view, the diagonal does not work. I may put everything in the WebView to get diagonal scroll but that is not likely. – 700 Software Oct 06 '10 at 13:05
  • @CommonsWare please help me https://stackoverflow.com/questions/47952573/scrollview-not-working-when-rotate-the-layout-dynamically?noredirect=1#comment82874486_47952573 – kartheeki j Dec 26 '17 at 08:25
8

Two years further down the line I think the open source community might have to your rescue: 2D Scroll View.

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
MrCeeJ
  • 714
  • 8
  • 13
  • 14
    3 years further down the line and the link has gone – jiduvah Jan 21 '14 at 15:54
  • 1
    Thanks! Nice to see people care :) – MrCeeJ May 12 '14 at 09:50
  • 2
    links are dead again. I created a library project with this class and pushed it to maven for others: https://github.com/jaredrummler/TwoDScrollView – Jared Rummler Apr 05 '16 at 08:12
  • Problems may occur with TwoDScrollView class linked above. For example when the scrollview's parent is FrameLayout which has some dimension set to a constant value [not wrap/match], ScrollView's size in this dimension will be equal to the full size of its [scrollview's] child which in turn will make `availableToScroll` variable equal to 0. – Android developer Jan 03 '18 at 09:12
8

there is another way. moddified HorizontalScrollView as a wrapper for ScrollView. normal HorizontalScrollView when catch touch events don't forward them to ScrollView and you only can scroll one way at time. here is solution:

package your.package;

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

public class WScrollView extends HorizontalScrollView
{
    public ScrollView sv;
    public WScrollView(Context context)
    {
        super(context);
    }

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

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

    @Override public boolean onTouchEvent(MotionEvent event)
    {
        boolean ret = super.onTouchEvent(event);
        ret = ret | sv.onTouchEvent(event);
        return ret;
  }

    @Override public boolean onInterceptTouchEvent(MotionEvent event)
    {
        boolean ret = super.onInterceptTouchEvent(event);
        ret = ret | sv.onInterceptTouchEvent(event);
        return ret;
    }
}

using:

    @Override public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

/*BIDIRECTIONAL SCROLLVIEW*/
        ScrollView sv = new ScrollView(this);
        WScrollView hsv = new WScrollView(this);
        hsv.sv = sv;
/*END OF BIDIRECTIONAL SCROLLVIEW*/

        RelativeLayout rl = new RelativeLayout(this);
        rl.setBackgroundColor(0xFF0000FF);
        sv.addView(rl, new LayoutParams(500, 500));
        hsv.addView(sv, new LayoutParams(WRAP_CONTENT, MATCH_PARENT /*or FILL_PARENT if API < 8*/));
        setContentView(hsv);
    }
wasikuss
  • 969
  • 8
  • 13
  • I can confirm this is working! In my version I have a View containing nested X and Y scroll. I override the View's `onTouchEvent` and `onInterceptTouchEvent` and forward the MotionEvent object to both scroll views, just as described in this answer. Smooth experience, both directions. – sulai Nov 21 '12 at 12:11
  • 1
    In order to make it work I had to adapt onTouchEvent to ALWAYS call sv.onTouchEvent, and I modified the return value to true. Note also that you must extend the outer view. In my case I had a horizontalScrollView nested in a (vertical) ScrollView so I had to extend the vertical one. – jeroent Apr 23 '13 at 14:59
  • Can you provide xml? How to use this? – Selman Tosun Sep 19 '19 at 13:29
3

I searched really long to make this work and finally found this thread here. wasikuss' answer came quite close to the solution, but still it did not work properly. Here is how it works very well (at least for me (Android 2.3.7)). I hope, it works on any other Android version as well.

Create a class called VScrollView:

package your.package.name;

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


public class VScrollView extends ScrollView {
    public HorizontalScrollView sv;

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

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

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        sv.dispatchTouchEvent(event);
        return true;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        super.onInterceptTouchEvent(event);
        sv.onInterceptTouchEvent(event);
        return true;
    }
}

Your layout should look like:

<your.package.name.VScrollView
    android:id="@+id/scrollVertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

    <HorizontalScrollView
        android:id="@+id/scrollHorizontal"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" >

        <TableLayout
            android:id="@+id/table"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:clickable="false"
            android:stretchColumns="*" >
        </TableLayout>
    </HorizontalScrollView>
</your.package.name.VScrollView>

In your activity, you should do something like:

hScroll = (HorizontalScrollView) findViewById(R.id.scrollHorizontal);
vScroll = (VScrollView) findViewById(R.id.scrollVertical);
vScroll.sv = hScroll;

... and that's how it works. At least for me.

flxapps
  • 1,066
  • 1
  • 11
  • 24
3

Late to answer, but hopefully might be helpful to someone. You can check out droid-uiscrollview. This is heavily based on @MrCeeJ's answer, but I seemed to have a lot of trouble getting the actual content to be rendered. Hence I pulled in the latest source from HorizontalScrollView & ScrollView to create droid-uiscrollview. There are a few todo's left which I haven't gotten around to finish, but it does suffice to get content to scroll both horizontally & vertically at the same time enter image description here

Community
  • 1
  • 1
Pawan Kumar
  • 273
  • 1
  • 10
2

There is an easy workaround: In you activity get a reference to the outer scrollView (I'm going to assume a vertical scrollview) and a reference to the first child of that scroll view.

Scrollview scrollY = (ScrollView)findViewById(R.id.scrollY);
LinearLayout scrollYChild = (LinearLayout)findViewById(R.id.scrollYChild);

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    scrollYChild.dispatchTouchEvent(event);
    scrollY.onTouchEvent(event);
    return true;
}

One could argue that this solution is a bit hacky. But it has worked great for me in several applications!

Risch
  • 564
  • 7
  • 20
1

I've try both wasikuss and user1684030 solutions and I had to adapt them because of one warning log: HorizontalScrollView: Invalid pointerId=-1 in onTouchEvent, and because I wasn't fan of this need of creating 2 scroll views.

So here is my class:

public class ScrollView2D extends ScrollView {

    private HorizontalScrollView innerScrollView;

    public ScrollView2D(Context context) {
        super(context);

        addInnerScrollView(context);
    }

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


    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        if (getChildCount() == 1) {
            View subView = getChildAt(0);
            removeViewAt(0);
            addInnerScrollView(getContext());
            this.innerScrollView.addView(subView);
        } else {
            addInnerScrollView(getContext());
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean handled = super.onTouchEvent(event);
        handled |= this.innerScrollView.dispatchTouchEvent(event);
        return handled;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        super.onInterceptTouchEvent(event);
        return true;
    }


    public void setContent(View content) {
        if (content != null) {
            this.innerScrollView.addView(content);
        }
    }


    private void addInnerScrollView(Context context) {
        this.innerScrollView = new HorizontalScrollView(context);
        this.innerScrollView.setHorizontalScrollBarEnabled(false);
        addView(this.innerScrollView);
    }

}

And when using it in XML, you have nothing to do if the content of this scroll view is set in here. Otherwise, you just need to call the method setContent(View content) in order to let this ScrollView2D knows what is its content.

For instance:

// Get or create a ScrollView2D.
ScrollView2D scrollView2D = new ScrollView2D(getContext());
scrollView2D.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
addView(scrollView2D);

// Set the content of scrollView2D.
RelativeLayout testView = new RelativeLayout(getContext());
testView.setBackgroundColor(0xff0000ff);
testView.setLayoutParams(new ViewGroup.LayoutParams(2000, 2000));
scrollView2D.setContent(testView);
Community
  • 1
  • 1
Donkey
  • 1,176
  • 11
  • 19
1

For a while I've been trying solutions from here, but the one that worked best still had one problem: It ate all events, none were making it through to elements within the scroller.

So I've got ... yet another answer, in Github and well-commented at least hopefully: https://github.com/Wilm0r/giggity/blob/master/app/src/main/java/net/gaast/giggity/NestedScroller.java

Like all solutions, it's a nested HorizontalScrollview (outer) + ScrollView (inner), with the outer receiving touch events from Android, and the inner receiving them only internally from the outer view.

Yet I'm relying on the ScrollViews to decide whether a touch event is interesting and until they accept it, do nothing so touches (i.e. taps to open links/etc) can still make it to child elements.

(Also the view supports pinch to zoom which I needed.)

In the outer scroller:

@Override
public boolean onInterceptTouchEvent(MotionEvent event)
{
    if (super.onInterceptTouchEvent(event) || vscroll.onInterceptTouchEventInt(event)) {
        onTouchEvent(event);
        return true;
    }
    return false;
}

@Override
public boolean onTouchEvent(MotionEvent event)
{
    super.onTouchEvent(event);
    /* Beware: One ugliness of passing on events like this is that normally a ScrollView will
       do transformation of the event coordinates which we're not doing here, mostly because
       things work well enough without doing that.
       For events that we pass through to the child view, transformation *will* happen (because
       we're completely ignoring those and let the (H)ScrollView do the transformation for us).
     */
    vscroll.onTouchEventInt(event);
    return true;
}

vscroll here is the "InnerScroller", subclassed from ScrollView, with a few changes to event handling: I've done some terrible things to ensure incoming touch events directly from Android are discarded, and instead it will only take them from the outer class - and only then pass those on to the superclass:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        /* All touch events should come in via the outer horizontal scroller (using the Int
           functions below). If Android tries to send them here directly, reject. */
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        /* It will still try to send them anyway if it can't find any interested child elements.
           Reject it harder (but pretend that we took it). */
        return true;
    }

    public boolean onInterceptTouchEventInt(MotionEvent event) {
        return super.onInterceptTouchEvent(event);
    }

    public boolean onTouchEventInt(MotionEvent event) {
        super.onTouchEvent(event);
    }
Wilmer
  • 113
  • 3
-3

I know you have accepted your answer but may be this could give you some idea.

<?xml version="1.0" encoding="utf-8"?>

<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>

<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<HorizontalScrollView
    android:layout_alignParentBottom="true"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
>
<ImageView

android:src="@drawable/device_wall"
android:scaleType="center"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</HorizontalScrollView>
</RelativeLayout>
</LinearLayout>
</ScrollView>
Shardul
  • 27,760
  • 6
  • 37
  • 35
  • Looks like the reverse of *"ScrollView nested inside a HorizontalScrollView"*. Is there anything I am missing? – 700 Software Dec 28 '10 at 17:11
  • Yeah, and what in the world are the LinearLayout and RelativeLayout doing in this solution? Unless they're serving some kind of unexplained purpose, they should be removed to keep the view hierarchy as flat as possible. – SilithCrowe Feb 28 '12 at 15:55