UI lags and is choppy when using Glide to load images in a RecyclerView
Asked Answered
L

6

7

I have a RecyclerView that loads images from URLs using Glide. Now the URLs are retrieved from Firebase using pagination as you can see below. The issue is that when the MainActivity (which contains the below code and the recyclerview) is first initialized there is a substantial lag in the UI (very laggy and choppy scrolling, options menu takes 3 seconds to open etc.) and the images take a while to load. After i scroll down though and reach the end of the RecyclerView for the first page of data, the OnScrollListener is triggered and i start loading new data from a new query. I've tried my best to optimize what Glide does based on suggestions from a user on another post i made and i also set the adapter.setHasFixedSize to true without luck. Any idea what's happening here? Am i hanging the UI thread somehow despite the queries being async?

EDIT : Could Glide be causing the lag on the main Thread due to it having to load multiple images into the recycler view's imageViews? And if so, what can i do to counter that?

Here's how i handle the pagination of the data i get from Firebase and notify the adapter:

class MainActivity : AppCompatActivity() {

private val TAG: String = MainActivity::class.java.simpleName // Tag used for debugging
private var queryLimit : Long = 50 // how many documents should the query request from firebase
private lateinit var iconsRCV : RecyclerView // card icons recycler view
private lateinit var lastVisible:DocumentSnapshot

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val rootRef: FirebaseFirestore = FirebaseFirestore.getInstance()
    val urlsRef : CollectionReference = rootRef.collection("CardIconUrls")
    val query : Query = urlsRef.orderBy("resID",Query.Direction.ASCENDING).limit(queryLimit) // create a query for the first queryLimit documents in the urlsRef collection

    // Setting Toolbar default settings
    val toolbar : Toolbar = findViewById(R.id.mainToolbar)
    setSupportActionBar(toolbar) // set the custom toolbar as the support action bar
    supportActionBar?.setDisplayShowTitleEnabled(false) // remove the default action bar title

    // RecyclerView initializations
    iconsRCV = findViewById(R.id.cardIconsRCV)
    iconsRCV.layoutManager = GridLayoutManager(this,5) // set the layout manager for the rcv
    val iconUrls : ArrayList<String> = ArrayList() // initialize the data with an empty array list
    val adapter = CardIconAdapter(this,iconUrls) // initialize the adapter for the recyclerview
    iconsRCV.adapter = adapter // set the adapter
    iconsRCV.setHasFixedSize(true)

    iconsRCV.addOnScrollListener(object:OnScrollListener(){
        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            super.onScrollStateChanged(recyclerView, newState)

            if(!iconsRCV.canScrollVertically(1) && (newState == RecyclerView.SCROLL_STATE_IDLE) && ((iconsRCV.layoutManager as GridLayoutManager).findLastVisibleItemPosition() == (iconsRCV.layoutManager as GridLayoutManager).itemCount-1)) {
                Log.d(TAG,"End of rcv-Starting query")
                val nextQuery = urlsRef.orderBy("resID",Query.Direction.ASCENDING).startAfter(lastVisible).limit(queryLimit).get().addOnCompleteListener { task ->
                    if(task.isSuccessful) {
                        Log.d(TAG,"Next query called")
                        for(document:DocumentSnapshot in task.result!!) {
                            iconUrls.add(document.get("url").toString())
                        }
                        lastVisible = task.result!!.documents[task.result!!.size()-1]
                        adapter.notifyDataSetChanged()
                    }
                }
            }
        }
    })

    query.get().addOnCompleteListener {task: Task<QuerySnapshot> ->
        if(task.isSuccessful) {
            Log.d(TAG,"Success")
            for(document:DocumentSnapshot in task.result!!) {
                Log.d(TAG,"Task size = " + task.result!!.size())
                iconUrls.add(document.get("url").toString()) // add the url to the list
            }
            lastVisible = task.result!!.documents[task.result!!.size()-1]
            adapter.notifyDataSetChanged() // notify the adapter about the new data
        }
    }
}

Here's the recyclerview adapter:

public class CardIconAdapter extends RecyclerView.Adapter<CardIconAdapter.ViewHolder> {

    private List<String> urlsList;
    private Context context;

    class ViewHolder extends RecyclerView.ViewHolder {
        ImageView iconImg;
        ViewHolder(@NonNull View view) {
            super(view);
            iconImg = view.findViewById(R.id.cardIcon);
        }
    }

    public CardIconAdapter(Context cntxt, List<String> data) {
        context = cntxt;
        urlsList = data;
    }

    @NonNull
    @Override
    public CardIconAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view =  LayoutInflater.from(parent.getContext()).inflate(R.layout.card_icons_rcv_item,parent,false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull CardIconAdapter.ViewHolder holder, int position) {
        RequestOptions requestOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL);
        GlideApp.with(context).load(urlsList.get(position)).thumbnail(0.25f).centerCrop().dontTransform().apply(requestOptions).into(holder.iconImg);
    }

    @Override
    public int getItemCount() {
        return urlsList.size();
    }
}

scrollingup

Luetic answered 22/11, 2019 at 21:36 Comment(11)
Self dupe of Glide loads images from firebase painfully slow using URLs.Imparipinnate
@MartinZeitler why is it a duplicate? In the post you linked, i was having trouble loading images faster through Glide in a RecyclerView and that was solved (user still hasn't added the answer though) and in this one i'm having performance issues with my Recycler view (lag, UI lag, removal of previous data etc.).Luetic
To me this seems quite alike the same problem; try caching these thumbnails instead of down-scaling them on the fly (which might be a waste of battery, because it needs CPU). Already serving them at the intended size would eliminate the need to manipulate them, altogether (and Glide is not required to do so). I mean, down-scaling one image is no problem - but down-scaling a whole bunch of images is.Imparipinnate
@MartinZeitler doesn't Glide automatically cache them? Even when i wasn't down-sampling them on the fly i was still getting long loading issues but now the recycler view is also lagging and the previous data are loading weirdly (if they load at all)Luetic
your recycler view implimentation is different little bit. kindly post your node structure of firebase db how data is going to store.Cummings
@Cummings my node structure is pretty simple : CardIconUrls -> (document) -> field : url . The collection is the CardIconUrls and it contains around 1.5k documents all of which have 2 fields : 1) id and 2) the url stringLuetic
@SteliosPapamichail are images gif?Ate
@Mr.AF although their formats are presented as .png or .jpg files in firebase, when i open the first 46 of them, they seem to be gifs (they are animated). But just the first 46Luetic
Could you add your XML file? I want to check an ImageView I guess it has a "wrap_content" size. try to fix a sizeDandruff
@Dandruff the imageView has a fixed size of 70 x 70Luetic
Ok. I need a link to the project. If you create a sample with the problem. I will check it. I think it is a simple mistakeDandruff
L
8

I finally figured out the issue after a lot of trial and error. My first mistake was not posting my xml layout file for the recyclerview item because that was the source of the performance issues. The second mistake was that I was using a LinearLayout and had set its own layout_width and layout_height attributes to 75dp instead of the ImageView's which is nested inside of the LinearLayout and was using wrap_content for the ImageView's respective attributes. So to fix the performance issues i did the following :

  1. I changed the LinearLayout to a ConstraintLayout (i read that it is much more optimized in general)
  2. I set the ConstraintLayout's layout_width & layout_height attributes to wrap_content and finally
  3. I set the actual ImageView's layout_width & layout_height attributes to 75dp which is the actual size that i want the image to be

Here's the final layout file for each item of the recyclerview after the changes :

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="horizontal"
    android:padding="5dp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/cardIcon"
        android:layout_width="75dp"
        android:layout_height="75dp"
        android:contentDescription="cardIcon"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:srcCompat="@tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>
Luetic answered 5/12, 2019 at 14:10 Comment(1)
Why do you even wrap a ConstraintLayout around your ImageView? You are not constraining anything, its just one child view.Hexahedron
F
2

Some of the ways to fix lagging when loading large number of images

  • Setting fixed size true to your RecyclerView but it wont help you if pagination involves

    mRecyclerView.setHasFixedSize(true);
    
  • Overriding pixels of the image using glide like this

    Glide.with(this)
         .load(YOUR_URL)
         .apply(new RequestOptions().override(dimenInPx, dimenInPx)
         .placeholder(R.drawable.placeHolder).error(R.drawable.error_image))
         .into(imageview);
    
  • Add hardwareAccelerated="true" property to your activity tag inside manifest like

    <activity
         ...
         android:hardwareAccelerated="true"
         ...
    />
    

I think this may help.

Florencio answered 27/11, 2019 at 10:33 Comment(2)
Sadly i don't see any difference, i mean they are loading non-stop without progress after like 2 minutes and still going. This is what the ui looks like : imgur.com/a/Ie45UCi . I also noticed that in the logcat i get no actual errors so i find it really hard to get to the root of this issue.Luetic
overriding height and width of image can improve recyclerview performance if u need to show progress when loading refer this https://mcmap.net/q/172472/-progress-bar-while-loading-image-using-glideFlorencio
B
0

This answer from Github was very helpful

Basically, if you set the exact dimensions of your image, it stops Glide from constantly having to figure out how big the image needs to be, and significantly improves performance. This helped take my RecyclerView from unusable to perfectly fine, just by setting a defined height and width on the ImageViews I was loading.

Barred answered 22/8, 2020 at 13:53 Comment(0)
F
0

Trust me. Don't use ConstraintLayout as in the accepted answer. It's performance is low when compared to the LinearLayout. According to my measurements, it took average of 16 ms to inflate a layout file with ConstraintLayout and 10 ms for a layout file with LinearLayout. To have smooth scroll effect, it has to inflate layout at least within 16 ms.

In a Glide sample app here, they have created a SquareImageView which returns same width and height.

class SquareImageView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) :
    AppCompatImageView(context, attrs, defStyleAttr) {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, widthMeasureSpec)
    }
}

Don't forget to declare styleable in attrs.xml

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

    <com.example.SquareImageView
        android:id="@+id/image_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="@dimen/padding_normal"
        android:scaleType="centerCrop" />

    <TextView
        android:id="@+id/text_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="@dimen/padding_small"
        android:textAlignment="center"
        android:textColor="@color/secondaryTextColor"
        android:textSize="15sp" />
</LinearLayout>

Try to avoid ConstraintLayout in the RecyclerView as it can cause some lag when inflating ViewHolders for the first time.

Another option will be to pre-inflate some views and use them in onCreateViewHolder

init {
    val layoutInflater = LayoutInflater.from(context)
    mPreInflated = List(20) {
        MyLayoutBinding.inflate(layoutInflater, null, false)
    }.iterator()
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
    val binding = if (mPreInflated.hasNext()) {
        mPreInflated.next()
    } else {
        val layoutInflater = LayoutInflater.from(context)
        MyLayoutBinding.inflate(layoutInflater, null, false)
    }
    return MyViewHolder(binding)
}
Freetown answered 19/7, 2021 at 7:5 Comment(0)
D
0

This might not be a problem with Glide at all. Make sure to test performance using isDebuggable = false! My RecyclerView was also all choppy, but with this flag set to false, it was smooth 120 fps.

Deeplaid answered 21/6, 2024 at 12:47 Comment(0)
L
-1

Try to use paging library combined with recyclerview

link reference:

Paging Library for Android With Kotlin: Creating Infinite Lists

Android Jetpack: manage infinite lists with RecyclerView and Paging (Google I/O '18)

Lenardlenci answered 28/11, 2019 at 14:39 Comment(1)
I'm actually paging the data as you can see in my code. I load pages of data where each page consists of 50 icons and new ones are only requested after scrolling to the bottom of the recycler view. I just haven't used the Paging library because i don't think it would be worth to go through all the docs etc. for such simple data (strings). Have you tried it for something similar and saw significant performance improvements?Luetic

© 2022 - 2025 — McMap. All rights reserved.