60

I have a HorizontalScrollView containing a LinearLayout. On screen I have a Button that will add new Views to the LinearLayout at runtime, and I'd like the scroll view to scroll to the end of the list when a new View is added.

I almost have it working - except that it always scrolls one view short of the last view. It seems like it's scrolling without first calculating the inclusion of the new view.

In my app I am using a custom View object, but I made a small test application that uses ImageView and has the same symptom. I tried various things like requestLayout() on both the Layout and ScrollView, I tried scrollTo(Integer.MAX_VALUE) and it scrolled into the netherverse :) Am I violating a UI thread issue or something?

  • Rick

======

    public class Main extends Activity {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);

             Button b = (Button) findViewById(R.id.addButton);
             b.setOnClickListener(new AddListener());

             add();
        }

        private void add() {
             LinearLayout l = (LinearLayout) findViewById(R.id.list);
             HorizontalScrollView s = 
                 (HorizontalScrollView) findViewById(R.id.scroller);

             ImageView i = new ImageView(getApplicationContext());
             i.setImageResource(android.R.drawable.btn_star_big_on);
             l.addView(i);

             s.fullScroll(HorizontalScrollView.FOCUS_RIGHT);
        }

        private class AddListener implements View.OnClickListener {
             @Override
             public void onClick(View v) {
                 add();
             }
        }
    }

Layout XML:

    <?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">

        <HorizontalScrollView
            android:id="@+id/scroller"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scrollbarSize="50px">
            <LinearLayout
                android:id="@+id/list"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:padding="4px"/>
        </HorizontalScrollView>

        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"            
            android:layout_gravity="center">
            <Button
                android:id="@+id/addButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:paddingLeft="80px"
                android:paddingRight="80px"
                android:paddingTop="40px"
                android:paddingBottom="40px"
                android:text="Add"/>
        </LinearLayout>

    </LinearLayout>
Rick Barkhouse
  • 1,186
  • 2
  • 10
  • 15

10 Answers10

146

I think there's a timing issue. Layout isn't done when a view is added. It is requested and done a short time later. When you call fullScroll immediately after adding the view, the width of the linearlayout hasn't had a chance to expand.

Try replacing:

s.fullScroll(HorizontalScrollView.FOCUS_RIGHT);

with:

s.postDelayed(new Runnable() {
    public void run() {
        s.fullScroll(HorizontalScrollView.FOCUS_RIGHT);
    }
}, 100L);

The short delay should give the system enough time to settle.

P.S. It might be sufficient to simply delay the scrolling until after the current iteration of the UI loop. I have not tested this theory, but if it's right, it would be sufficient to do the following:

s.post(new Runnable() {
    public void run() {
        s.fullScroll(HorizontalScrollView.FOCUS_RIGHT);
    }
});

I like this better because introducing an arbitrary delay seems hacky to me (even though it was my suggestion).

Ted Hopp
  • 232,168
  • 48
  • 399
  • 521
  • 5
    That did it exactly Ted, thanks very much, that's been bugging me for days! :) – Rick Barkhouse Jan 18 '11 at 04:54
  • Hi Ted....when I am trying to use above code then it showing error can not resolve method 'postDelayed(java.lang.runnable, long)'... please help me.... – Vinit ... May 18 '15 at 07:10
  • @VinitVikash - That's a method in the `View` class. You may need to do `s.postDelayed(...)`. Alternatively, create a `Handler` and use it's `postDelayed()` method. – Ted Hopp May 18 '15 at 07:12
  • `new Handler().postDelayed()` if you don't know how to use `postDelayed()` – Pratik Butani Jun 11 '15 at 10:38
  • 2
    @PratikButani - That works, but it would be more efficient to allocate a `Handler` once as a field and use that, rather than creating one on the fly each time. However, since we have the view reference `s` already available, it's best to just use `s.postDelayed(...)`, which nicely sidesteps the need for a `Handler` of our own. – Ted Hopp Jun 11 '15 at 13:37
20

I agree with @monxalo and @granko87 that the better approach is with a listener instead of making an assumption that the layout will be complete if we let some arbitrary amount of time pass.

In case, like me, you don't need to use a custom HorizontalScrollView subclass you can just add an OnLayoutChangeListener to keep things simple:

mTagsScroller.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
    @Override
    public void onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
        mTagsScroller.removeOnLayoutChangeListener(this);
        mTagsScroller.fullScroll(View.FOCUS_RIGHT);
    }
});
Newtz
  • 1,333
  • 11
  • 9
12

Just another sugestion, since this question helped me a lot :).

You can put a listener when the view has finished its layout phase, and right after do the fullScroll althought you'll need to extend the class for that.

I only did this because i wanted to scroll to a section right after onCreate() to avoid that flickering from starting point to scroll point.

Something like:

public class PagerView extends HorizontalScrollView {

    private OnLayoutListener mListener;
    ///...
    private interface OnLayoutListener {
        void onLayout();
    }

    public void fullScrollOnLayout(final int direction) {
        mListener = new OnLayoutListener() {            
            @Override
            public void onLayout() {
                 fullScroll(direction)
                 mListener = null;
            }
        };
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if(mListener != null)
             mListener.onLayout();
    }
}
monxalo
  • 581
  • 5
  • 14
4

Use the View method smoothScrollTo

postDelayed(new Runnable() {
    public void run() {
       scrollView.smoothScrollTo(newView.getLeft(), newView.getTop());
 }
 }, 100L);

You need to put in a runnable so you give time to the layout process to position the new View

  • Not working for me. View#getTop() returns position relative to parent, thus this will not work in more nested layouts – Vouskopes Aug 05 '20 at 22:56
3

Use below code for horizontal scrollview {Scroll to right}

view.post(new Runnable() {
                @Override
                public void run() {
                    ((HorizontalScrollView) Iview
                            .findViewById(R.id.hr_scroll))
                            .fullScroll(HorizontalScrollView.FOCUS_RIGHT);
                }
            });
sharma_kunal
  • 2,152
  • 1
  • 28
  • 28
3

This is what I did.

//Observe for a layout change    
ViewTreeObserver viewTreeObserver = yourLayout.getViewTreeObserver();
   if (viewTreeObserver.isAlive()) {
     viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                yourHorizontalScrollView.fullScroll(HorizontalScrollView.FOCUS_RIGHT);
            }
     });
   }
Arst
  • 3,098
  • 1
  • 35
  • 42
2

I think the best approach is the one posted by monxalo because you can not know how much time will it take until the layout is done. I tried it and it works perfectly. The only difference is, that I added only one row to my custom horizontal scroll view:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    this.fullScroll(HorizontalScrollView.FOCUS_RIGHT);
}
granko87
  • 211
  • 2
  • 5
  • I don't think that the amount of time is relevant. The issue is that the scrolling should not happen on the current iteration of the UI thread loop. Once the current iteration is finished, the view hierarchy should be up to date and laid out. – Ted Hopp Jan 06 '16 at 16:36
0

The child View may not be immediately added in a ViewGroup. So it is required to post a Runnable which will run after other pending tasks are completed.

So after adding View, use:

horizontalScrollView.post(new Runnable() {
    @Override
    public void run() {
        horizontalScrollView.fullScroll(View.FOCUS_RIGHT);
    }
});
Nabin Bhandari
  • 15,949
  • 6
  • 45
  • 59
0

This is my way for 3 ImageViews in kotlin:

var p1 = true; var p2 = false; var p3 = false
val x = -55
val handler = Handler()
handler.postDelayed(object : Runnable {
    override fun run() {

        if (p1){
            mainView.picsScrollViewID.smoothScrollTo(mainView.bigPic1ID.x.toInt() + x, 0)
            p1 = false
            p2 = true
            p3 = false
        }
        else if(p2){
            mainView.picsScrollViewID.smoothScrollTo(mainView.bigPic2ID.x.toInt() + x, 0)
            p1 = false
            p2 = false
            p3 = true
        }
        else if(p3){
            mainView.picsScrollViewID.smoothScrollTo(mainView.bigPic3ID.x.toInt() + x, 0)
            p1 = true
            p2 = false
            p3 = false
        }
        handler.postDelayed(this, 2000)
    }
}, 1400)
Hossein Yousefpour
  • 3,827
  • 3
  • 23
  • 35
0
mainView.horizontalScrollImgs.postDelayed(Runnable { kotlin.run { mainView.horizontalScrollImgs.fullScroll(HorizontalScrollView.FOCUS_RIGHT) } },500)