Search code examples
javaandroidscrollview

Scrollview flashing when inflating view and calling scrollBy


I am developing an Android application where an activity displays content in a scrollview. At the top of the content there is a placeholder for an image to be displayed. The image is downloaded from the Internet and may take a few seconds until it is ready to be displayed. The image placeholder is initially empty. When the image is downloaded, it is dynamically added to the placeholder.

Initially I had the following problem.

  • The user starts the activity and scrolls down
  • The image starts to download in the background. When available, it is added to the placeholder
  • When the image is added to the placeholder, the contents of the scrollview change and the user experience is disrupted by the unwanted scrolling that occurs

To fix this, I added code to adjust the scroll position once the image view is added to the placeholder. The problem with this is that a flickering is caused on the scrollview during the display-image and adjust-scrollview process. The reason is that the scrollBy function is called from a runnable. Calling scrollBy outside the runnable does not cause flickering but the scroll position is incorrect - the reason for this is that there is not enough time for the items on the scroll view to recalculate/measure their dimensions/heights.

Here is a sample application the illustrates this problem:

public class MainActivity extends AppCompatActivity {

    ScrollView scrollView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        scrollView = findViewById(R.id.scrollview);

        startImageDownload();
        simulateImageScroll();
    }

    private void simulateImageScroll() {
        // scroll to the bottom of the scroll view
        scrollView.post(new Runnable() {
            @Override
            public void run() {
                scrollView.scrollTo(0, scrollView.getMaxScrollAmount());
            }
        });
    }

    private void startImageDownload() {
        Handler handler = new Handler(getMainLooper());
        // simulate a delay for the image download to illustrate the flashing problem in the scrollview
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                displayImage("");
            }
        }, 2000);

    }

    // when the image is downloaded we add it to the image container
    private void displayImage(String imageFilename) {
        // dynamically create an image and add it to the image container layout
        RelativeLayout container = findViewById(R.id.imageContainer);
        ImageView img = new ImageView(this);

        // image should be loaded from the given filename - for now use a solid background and fixed height
        img.setBackgroundColor(Color.BLUE);
        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
                RelativeLayout.LayoutParams.MATCH_PARENT, 500);
        container.addView(img, params);

        adjustScrolling(container);
    }

    private void adjustScrolling(RelativeLayout container) {
        // adjust scroll if the image is loaded before the current content
        if (scrollView.getScrollY() > container.getTop()) {
            container.measure(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
            final int amountToScroll = container.getMeasuredHeight();

            // the following does not cause flickering but scrolls to the wrong position
            //scrollView.scrollBy(0, amountToScroll);

            // adjust the scrollview so that it keeps the current view unchanged
            scrollView.post(new Runnable() {
                @Override
                public void run() {
                    // this causes flickering but scrolls to the correct position
                    scrollView.scrollBy(0, amountToScroll);
                }
            });
        }
    }

}

And here is the layout file:

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

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <RelativeLayout
            android:id="@+id/imageContainer"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:gravity="center"
            android:background="#aa0000" >

        </RelativeLayout>

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:gravity="center"
            android:background="#aa0000" >
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="1"
                android:textColor="#ffffff"
                android:textSize="128dp"/>
        </RelativeLayout>

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:gravity="center"
            android:background="#aa0000" >
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="2"
                android:textColor="#ffffff"
                android:textSize="128dp"/>
        </RelativeLayout>

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:gravity="center"
            android:background="#aa0000" >
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="3"
                android:textColor="#ffffff"
                android:textSize="128dp"/>
        </RelativeLayout>

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:gravity="center"
            android:background="#aa0000" >
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="4"
                android:textColor="#ffffff"
                android:textSize="128dp"/>
        </RelativeLayout>

    </LinearLayout>
</ScrollView>

Any ideas on how to fix this problem?


Solution

  • Edited: Currently, your layout is flickering, because adding blue view cause redraw layout (and scroll). So scroll occurred once, and next you scrolled to the position you want. That's the second moving.

    To solve this problem, you need to know how android draws view. https://developer.android.com/guide/topics/ui/how-android-draws.html

    Simply, onMeasure() - onLayout() - onDraw(). And you can add your layout code between onLayout() and onDraw(), by ViewTreeObserver().addOnGlobalLayoutListener().

    https://developer.android.com/reference/android/view/ViewTreeObserver.OnGlobalLayoutListener.html

    ps: I still recommend using nice and lovely image library, Picasso.

    Fixed code is: Set scroll before draw() called. By this, you can draw only once.

    public class MainActivity extends AppCompatActivity {
    
        ScrollView scrollView;
        int amountToScroll = 0;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            scrollView = findViewById(R.id.scrollview);
            scrollView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    scrollView.scrollBy(0, amountToScroll);
                    amountToScroll = 0;
                }
            });
            startImageDownload();
            simulateImageScroll();
        }
    
        private void simulateImageScroll() {
            // scroll to the bottom of the scroll view
            scrollView.post(new Runnable() {
                @Override
                public void run() {
                    scrollView.scrollTo(0, scrollView.getMaxScrollAmount());
                }
            });
        }
    
        private void startImageDownload() {
            Handler handler = new Handler(getMainLooper());
            // simulate a delay for the image download to illustrate the flashing problem in the scrollview
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    displayImage("");
                }
            }, 2000);
    
        }
    
        // when the image is downloaded we add it to the image container
        private void displayImage(String imageFilename) {
            // dynamically create an image and add it to the image container layout
            RelativeLayout container = findViewById(R.id.imageContainer);
            ImageView img = new ImageView(this);
    
            // image should be loaded from the given filename - for now use a solid background and fixed height
            img.setBackgroundColor(Color.BLUE);
            RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
                    RelativeLayout.LayoutParams.MATCH_PARENT, 500);
            container.addView(img, params);
    
            adjustScrolling(container);
        }
    
        private void adjustScrolling(RelativeLayout container) {
            // adjust scroll if the image is loaded before the current content
            if (scrollView.getScrollY() > container.getTop()) {
                container.measure(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
                amountToScroll = container.getMeasuredHeight();
            }
        }
    }
    

    I strongly recommend using Picasso. http://square.github.io/picasso/

    This one line will fix all of your problem.

    Picasso.with(context).load("http://i.imgur.com/DvpvklR.png").into(imageView);
    

    You can load your local image file or network image (url) into your imageView.


    In your case, remove both startImageDownload() and simulateImageScroll(), and on onResume(), call displayImage().

    Fixed displayImage():

    private void displayImage(String imageFilename) {
        // dynamically create an image and add it to the image container layout
        RelativeLayout container = findViewById(R.id.imageContainer);
        ImageView img = new ImageView(this);
    
        // image should be loaded from the given filename - for now use a solid background and fixed height
        img.setBackgroundColor(Color.BLUE);
        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
                RelativeLayout.LayoutParams.MATCH_PARENT, 500);
        container.addView(img, params);
        Picasso.with(this).load(imageFilename).into(img);
    
        adjustScrolling(container);
    }
    


    Or, if you want to solve this problem directly for academic reasons,

    1. Do not adjust your scroll. It seems that it is not a real solution to use scrollBy to fix your problem. The real cause is the code that cause the UI to redraw. May be calling invalidate() or something like that.

    2. Adding ImageView programmatically is not a good idea. Because your RecyclerView or ViewHolder of ListView cannot reuse the view, so it cause degrade performance. If you can avoid it, do that. (eg. use xml)

    3. It seems that adding your ImageView to imageContainer is real problem. imageContainer has android:layout_height="wrap_content" property, and this means it has no fixed height, it depends on it's own child. Try to change to fixed value, for example: android:layout_height="500dp"