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.
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?
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,
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.
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)
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"