HorizontalScrollView: auto-scroll to end when new Views are added?

AndroidUser InterfaceHorizontal Scrolling

Android Problem Overview


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>

Android Solutions


Solution 1 - Android

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).

Solution 2 - Android

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);
    }
});

Solution 3 - Android

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();
    }
}

Solution 4 - Android

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

Solution 5 - Android

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);
    			}
    		});

Solution 6 - Android

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);
            }
     });
   }

Solution 7 - Android

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);
}

Solution 8 - Android

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);
    }
});

Solution 9 - Android

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)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRick BarkhouseView Question on Stackoverflow
Solution 1 - AndroidTed HoppView Answer on Stackoverflow
Solution 2 - AndroidNewtzView Answer on Stackoverflow
Solution 3 - AndroidmonxaloView Answer on Stackoverflow
Solution 4 - AndroidGabriel GuerreroView Answer on Stackoverflow
Solution 5 - Androidsharma_kunalView Answer on Stackoverflow
Solution 6 - AndroidArstView Answer on Stackoverflow
Solution 7 - Androidgranko87View Answer on Stackoverflow
Solution 8 - AndroidNabin BhandariView Answer on Stackoverflow
Solution 9 - AndroidHossein YousefpourView Answer on Stackoverflow