The use of recyclerview

Time:2021-5-12

target

It was built some time agoA simple and easy to use picture upload component with comprehensive functionsNow let’s show the uploaded images to the app in the form of image set. For the sake of user experience, the [infinite] scrolling mode is adopted for loading new pictures. On Android platform, we prefer recyclerview component.

Show the picture, using the natural isImageViewHowever, it does not support direct loading of network images, which requires other network components (such asHttpURLConnectionokhttp3And so on) get the image to the local, get theBitMapData, and then through thesetImageBitmap()load.
ImageView also hassetImageURI(Uri uri)Method, the naming of URI is easy to give people the illusion that it can only be the local file path.

Fortunately, some open source components encapsulate tedious network operations and caching strategies, and provide easy-to-use APIs. Here I chooseGlide

realization

Load more

Item layout

There are two, one for each picture in the list, and one for displayMore / all loadedPut it at the end of the list to prompt the user.

RecyclerView.Adapter

There are many online materials about the design mode of recyclerview, which will not be repeated here. Implementation firstRecyclerView.Adapter

class ThumbnailListAdapter(
    private val thumbnails: List,
    private val totalCount: Long,
    private val context: Context
) :
    RecyclerView.Adapter() {

    //Call several times
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ThumbnailViewHolder {
        //Viewtype is obtained by getitemviewtype
        val itemView = LayoutInflater.from(context).inflate(viewType, parent, false)
        return ThumbnailViewHolder(itemView)
    }

    //Students engaged in paging / waterfall loading should not confuse this with the total number of databases. The itemcount here represents the amount of data in memory
    //We can get new data [from the back end] and add it to the dataset to implement the loadmore function
    override fun getItemCount(): Int {
        return if (thumbnails.isNotEmpty())
            Thumbtails. Size + 1 / + 1 is because there is a dead loadmore entry in addition to the thumbtails dataset
        else
            0
    }

    //R.layout.xxx is an int type and can be returned directly
    override fun getItemViewType(position: Int): Int {
        return if (position < thumbnails.size)
            R.layout.list_ thumbnail_ Image // normal image display
        else
            R.layout.list_ loadmore_ Footer // loadmore at the end
    }

    //It will be called when an off screen item enters the screen
    override fun onBindViewHolder(holder: ThumbnailViewHolder, position: Int) {
        if (position < thumbnails.size) {
            Glide.with(context)
                .load(thumbnails[position].uri)
                .into(holder.itemView.thumbnail_view)
        } else {
            if (thumbnails.size >= totalCount)
                holder.itemView.tv_ load_ More.text = "all loaded"
        }
    }
    
    //It must be inherited in this way
    class ThumbnailViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
}

Rolling monitoring

Add rolling monitoring for recyclerview, and load new data into the dataset when appropriate.

recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        super.onScrollStateChanged(recyclerView, newState)
        //If it is already loading, skip
        if (!_thumbnailsLoading) {
            //Find the index of the last visible item
            val lastPos = layoutManager.findLastVisibleItemPosition()
            val sum = adapter.itemCount
            //When approaching the end item (the difference here is 10, which means that there will be no data if another 10 items are displayed), new data will be obtained
            if (newState == RecyclerView.SCROLL_STATE_IDLE && sum - lastPos <= 10) {
                VM. Thumbnails. Addall (VM. Getmorealbumcovers()) // load new data into dataset
                _thumbnailsLoading = true
            }
        }
    }
})

Don’t confuse the preloaded data above with glide’s preloaded image. There are two steps: getting the data and downloading the image through the URI in the data. Glide provides a preloading scheme specifically for recyclerview, which is to reduce the waiting for loading caused by the picture not yet requested from the network when sliding. At present, glide only supportsLinearLayoutManagerOr its subclass layout

layout

StaggeredGridLayoutManager

Display images by column waterfall flow. Simply set the layout manager of recyclerview as an instance of staged GridLayout manager. Note that staged GridLayout manager is still in beta.

val sgLayoutManager =
    StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)
recyclerview.layoutManager = sgLayoutManager

useStaggeredGridLayoutManagerIt will be found that in the process of sliding up and down, image block rearrangement often occurs. According to the Internet, this is because of reuseViewHolderThe size of the image to be loaded by the viewholder is inconsistent with that of the image to be loaded by the viewholder. For example, the height of an image loaded by a viewholder is 60, and then it is recycled, but the size information is still retained. Later, it is reused by an image with a height of 80. Because the staged grid layout manager sorts the layout according to the size of the viewholder, the size changes lead to multiple sorting. The solution is when viewholder binds data (inRecyclerView.Adapter.onBindViewHolder()Set the final size of this layout in advance, as follows:

override fun onBindViewHolder(holder: ThumbnailViewHolder, position: Int) {
    val layoutParams =
        holder.itemView.thumbnail_view.layoutParams as LinearLayout.LayoutParams
    //Manually set viewholder height
    layoutParams.height = thumbnails[position].height

    Glide.with(context).load(thumbnails[position].uri)
        .into(holder.itemView.thumbnail_view)
}

When the slide back to the top, the top (the first line) images often rearrange each other. If you look carefully, this is because the first row is arranged in order rather than inserted by vacancy when it is first laid out, and when you slide back, it is inserted by vacancy (where is the emptiest row first), which may cause the order to be inconsistent with the first sorting. But fortunately, the final image will still be in accordance with the size of their own home. And this will only happen the first time from the slide back to the top.

GreedoLayoutManager

Staged grid layout manager has more than 3K lines of code, and it is also a beta version. As a code cleanliness addict, I turned my eyes to the greenlayout manager, which is500pxAn open source layoutmanager, which can display multiple images in one line while maintaining the ratio of width to height. The principle is very simple. See the following Animation:
在线动图制作brush.ninja-gif裁剪

It’s quite easy to replace the layout manager. Just reset the layout manager of recyclerview.

val layoutManager =
    GreedoLayoutManager(adapter).also { it.setMaxRowHeight(resources.displayMetrics.heightPixels / 3) }
recyclerview.layoutManager = layoutManager

Before layout, the greenlayout manager needs to know the aspect ratio of the item, just let the adapter implement itSizeCalculatorDelegateInterface

override fun aspectRatioForIndex(index: Int): Double {
    val thumbnail = thumbnails[index]
    return thumbnail.width / thumbnail.height.toDouble()
}

The running interface displays:

You can see that each picture is much bigger than expected, and only a small part can be seen. After research, it is found that the layout of the image display items defined above (LinearLayout embedded ImageView), after the final rendering, the size of LinearLayout is the size of each grid, while the embedded ImageView exceeds the LinearLayout, as if the final size isMeasuredSize——We are hereonCreateViewHolderWe used theLayoutInflater.from(context).inflate(viewType, parent, false)It’s hereparentIt is recyclerview, and in the layout XML, the width and height are set tomatch_parentTherefore, the measured size of ImageView is the same as the width and height of recyclerview – however, the final size of ImageView should be the same as the mesh size.

Take width as an example

Expectation: ImageView. Width = = LinearLayout. Width = = grid. Width
Actual: ImageView. Width = = ImageView. Measuredwith = = recyclerview. Width

We see that each frame is actually the upper left corner of the ImageView.

After a search, online all kinds of rightgetWidthandgetMeasuredWidthThe explanation of the difference did not solve my confusion until this articleFrom the perspective of source code, the differences between getwidth() and getmeasuredwidth()Let me know, in fact, Android system does not define width. When you customize the layout, you can set the sub item size at will, and there is no limit on whether it exceeds the screen. In our scenario, it is estimated that greenolayout manager does not recursively process the width of the inner control after processing the width of the outermost control (here is the linear layout), which leads to this bug.

In this case, instead of the peripheral linear layout, you can use ImageView directly, which saves a little overhead.

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ThumbnailViewHolder {
    return if (viewType == 0) {
        val imageView = ImageView(parent.context).apply {
            scaleType = ImageView.ScaleType.CENTER_CROP
            layoutParams = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
        }
        ThumbnailViewHolder(imageView)
    } else {
        val itemView = LayoutInflater.from(context).inflate(viewType, parent, false)
        ThumbnailViewHolder(itemView)
    }
}

Of course, there are also display problems caused by viewholder reuse. Only a part of the picture is displayed, and it is displayed according to the width height ratio before viewholder reuse, as follows:

It’s no use using waitforlayout () suggested by glide’s official document,override(width, height)Inform the size of the picture in advance.

Glide.with(context)
    .load(thumbnails[position].uri)
    .override(thumbnails[position].width, thumbnails[position].height)
    .into(holder.itemView as ImageView)
//. waitforlayout() // doesn't work

Drop down refresh

Use swipe refresh layout, easy, and press no table. The final product is as follows

other

Commonly useddetachAndScrapView, recyclerview will automatically help us deal with the logic of reusing view [holder]. However, in some scenarios (such as just rearranging the currently displayed views instead of removing them), we can use a more lightweight onedetachView(after detach, the view will not be displayed on the interface), but remember to call it manually before the next layoutattachView(in terms of position, where is before detach, where is after attach) orremoveDetachedView/recycleView
Note that after detach, recyclerview. Getchildcount () decreases accordingly.

It’s recyclerview that really puts the view layout on the interfacelayoutDecoratedmethod.

Recommended Today

Looking for frustration 1.0

I believe you have a basic understanding of trust in yesterday’s article. Today we will give a complete introduction to trust. Why choose rust It’s a language that gives everyone the ability to build reliable and efficient software. You can’t write unsafe code here (unsafe block is not in the scope of discussion). Most of […]