I open source a convenient RecyclerView top top Android library, welcome you to visit github.com/lizijin/Sti… , if you use this library, please give your valuable comments.
It currently supports the following features:
- Support single type top suction function
- Support multiple types of top suction function
- Enable or disable the top suction function
- Support the function of suction top in specified position
- Supports setting top offset
- Support custom RecyclerView Item top boundary custom
- It works seamlessly with AppBarLayout
When it comes to RecyclerView, reuse is one of the things we can talk about. The built-in ViewHolder avoids the hassle of manually creating a ViewHolder when using a ListView. Can we be sure when to recycle and reuse views? When we slide a RecyclerView, do we recycle the View and reuse the View? Or do you reuse the View first and then recycle the View? The answer is both. For details, see the following analysis:
Noun explanation
1. Recycling: The View does not need to be displayed on the screen any more and is recycled to the recycling pool
2. Reuse: Reuse in this article refers to the call to the onCreateViewHolder or onBindViewHolder methods
This article outline
1. Two scenarios of sliding RV
2. Verify the answer by DEMO
3. Explain the sliding principle
4. Source code analysis
5. Ask questions and interact
1. Two scenarios of sliding RV
1.1 a scene
Each Item in the RV is 100px high, and the last Item is 50px off the screen. The initial state of RV is shown below
Q1 assumes an upward slide of 40px
Have views been recycled or reused? If so, reuse or recycle first?
Q2 is assumed to slide up 60px
Have views been recycled or reused? If so, reuse or recycle first?
Q3 assumes an upward swipe of 120px
Have views been recycled or reused? If so, reuse or recycle first?
1.2 scenario 2
The first Item in the RV is 50px tall, the others are 100px, and the last Item is 95px off the screen. The initial state of the RV is as follows
Q1 assumes an upward slide of 40px
Have views been recycled or reused? If so, reuse or recycle first?
Q2 is assumed to slide up 60px
Have views been recycled or reused? If so, reuse or recycle first?
Q3 assumes an upward swipe of 120px
Have views been recycled or reused? If so, reuse or recycle first?
You can see from the answer. There is no fixed answer to recycling and reuse. It varies from scene to scene. Here’s an example to verify the answer.
2. Verify the answer by DEMO
2.1 Let’s verify scenario 1
Program running diagram
The program code
class RecyclerViewActivity1 : AppCompatActivity() {
private lateinit var mRecyclerView: RecyclerView
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recycler_view1)
mRecyclerView = findViewById(R.id.recyclerview)
mRecyclerView.setHasFixedSize(true)
mRecyclerView.setItemViewCacheSize(0)
mRecyclerView.layoutManager =
LinearLayoutManager(this).apply {
orientation = LinearLayoutManager.VERTICAL
isItemPrefetchEnabled = false
}
val list: MutableList<String> =
ArrayList()
repeat(100) {
list.add("item $it")
}
mRecyclerView.adapter = MyAdapter(list)
}
inner class MyAdapter(val mStrings: MutableList<String>) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
println(RecyclerView scene onCreateViewHolder)
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.view_item, parent, false)
return object : RecyclerView.ViewHolder(view) {}
}
override fun getItemCount(a): Int {
return mStrings.size
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
println(RecyclerView scene one onBindViewHolder$position ")
val textView = holder.itemView as TextView
textView.layoutParams.height = (resources.displayMetrics.density * 100).toInt()
textView.text = mStrings[position]
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
println("Recycle occurs in RecyclerView scene one" + (holder.itemView as TextView).text)
super.onViewRecycled(holder)
}
}
fun scroll120(view: View) {
mRecyclerView.scrollBy(0, (resources.displayMetrics.density * 120).toInt())
}
fun scroll60(view: View) {
mRecyclerView.scrollBy(0, (resources.displayMetrics.density * 60).toInt())
}
fun scroll40(view: View) {
mRecyclerView.scrollBy(0, (resources.displayMetrics.density * 40).toInt())
}
}
Copy the code
The log output is as follows
Let’s go to the initial state
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 0
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 1
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 2
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 3
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 4
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 5
Click and slide up 40px. The printed logs remain unchanged. Prove that neither recycling nor reuse has occurred
Click and slide up 60px. The following logs are displayed: Prove that no recycling has occurred and that reuse has occurred
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 0
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 1
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 2
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 3
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 4
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 5
RecyclerView onCreateViewHolder // Only RecyclerView onBindViewHolder 6 occurred
Click and swipe up 120px. The following logs are displayed: Demonstrate that recycling and reuse have occurred. Reuse before recycling
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 0
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 1
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 2
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 3
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 4
RecyclerView Scenario 1 onCreateViewHolder
RecyclerView Scenario 1 onBindViewHolder 5
RecyclerView Scenario 1 onCreateViewHolder // Reuse
RecyclerView Scenario 1 onBindViewHolder 6
RecyclerView Scenario 1 Recycle item 0 // Recycle item
2.2 Let’s verify scenario 2
Program running diagram
The program code
class RecyclerViewActivity2 : AppCompatActivity() {
private lateinit var mRecyclerView: RecyclerView
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recycler_view2)
mRecyclerView = findViewById(R.id.recyclerview)
mRecyclerView.setHasFixedSize(true)
mRecyclerView.setItemViewCacheSize(0)
mRecyclerView.layoutManager =
LinearLayoutManager(this).apply {
orientation = LinearLayoutManager.VERTICAL
isItemPrefetchEnabled = false
}
val list: MutableList<String> =
ArrayList()
repeat(100) {
list.add("item $it")
}
mRecyclerView.adapter = MyAdapter(list)
}
inner class MyAdapter(val mStrings: MutableList<String>) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
println(RecyclerView scenario 2 onCreateViewHolder)
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.view_item, parent, false)
return object : RecyclerView.ViewHolder(view) {}
}
override fun getItemCount(a): Int {
return mStrings.size
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
println(RecyclerView scene two onBindViewHolder$position ")
val textView = holder.itemView as TextView
textView.layoutParams.height =
if (position == 0) (resources.displayMetrics.density * 50).toInt() else (resources.displayMetrics.density * 100).toInt()
textView.text = mStrings[position]
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
println("Recycle occurs in RecyclerView scenario 2" + (holder.itemView as TextView).text)
super.onViewRecycled(holder)
}
}
fun scroll120(view: View) {
mRecyclerView.scrollBy(0, (resources.displayMetrics.density * 120).toInt())
}
fun scroll60(view: View) {
mRecyclerView.scrollBy(0, (resources.displayMetrics.density * 60).toInt())
}
fun scroll40(view: View) {
mRecyclerView.scrollBy(0, (resources.displayMetrics.density * 40).toInt())
}
}
Copy the code
Log output enters the initial state first
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 0
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 1
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 2
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 3
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 4
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 5
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 6
Click and slide up 40px. The printed logs remain unchanged. Prove that neither recycling nor reuse has occurred
Click and slide up 60px. The following logs are displayed: Prove that recycling has occurred and no reuse has occurred
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 0
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 1
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 2
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 3
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 4
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 5
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 6
RecyclerView Scenario 2 Recycle Item 0 // Recycle only
Click and swipe up 120px. The following logs are displayed: Demonstrate that recycling and reuse have occurred. Recycle before reuse
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 0
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 1
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 2
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 3
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 4
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 5
RecyclerView Scenario 2 onCreateViewHolder
RecyclerView Scenario 2 onBindViewHolder 6
RecyclerView Scenario 2 Recycle item 0 // Recycle item first
RecyclerView Scenario 2 onBindViewHolder 7 // Reuse
3. Sliding principle analysis
As shown, introduce some parameters about coordinates
-
Delta: finger slide distance 120px.
-
MOffset: RV The Bottom of the last subview is 600px Y in the screen coordinate system. The next View(Item7) of the RV is laid out from the mOffset.
-
MScrollingOffset: RV The distance between the Bottom of the last subview and the RV Bottom is 50px. Slide up not to exceed this distance. If it does, create a new View fill.
-
MVailable: delta – mScrollingOffset. You can fill the View space. If it is greater than 0, there is room to fill the new View
-
If delta<mScrollingOffset, mScrollingOffset=delta, mVailable<0
The sliding logic is as follows
-
RecyclerView from the 0 View start traversal, until the View Bottom>mScrollingOffset, and record the View index, recycle [0,index) interval View,index is open interval, If index>=1, the View in the range [0,index) will be removed from the screen and put into the recycling pool according to the recycling algorithm. For the specific recycling algorithm, press no table first.
-
If mVailable>0, fill it with a new View from mOffset. MOffset += New View height, mVailable-= New View height, mScrollingOffset+= New View height, if mVailable<0, mScrollingOffset+=mVailable. After the layout is complete, recycle the View as required using the algorithm of Step 1.
-
Repeat Step 2
-
Move the RV as a whole up the delta or consumed distance (generally delta distance, but the specific consumed distance when there is no Item under RecyclerView)
Based on this slide logic, we analyzed the slide up 120px in scene 1
mOffset = 600px
mScrollingOffset = 50px
mAvailable = 70px
Item1 height 100 px
-
Start by traversing Bottom>50px from the 0th View. Find Item1. bottom=100px and record index=0. Because the index < 1. So no collection takes place
-
MAvailable >0, add View Item7 height 100px, mOffset=700px, mAvailable=-30 from the bottom of Item6 MScrollingOffset = mScrollingOffset + 100-30 = 120 px. Then check for recycling. Start by traversing the Bottom>120px from the 0th View. Find Item2. bottom=200px and record index=1. Recycle View in range [0,1), i.e. recycle Item1
-
MAvailable =-30<0, exit the filling logic
-
Overall move up 120px
We saw that we created Item7 and then reclaimed Item1. It matches the log
RecyclerView onCreateViewHolder // Reuse RecyclerView onBindViewHolder 6
RecyclerView Scenario 1 Recycle item 0 // Recycle item
The same logic can be used to analyze the slide up 120px in scenario 2. In scenario 2, recycling occurs before reuse. Readers can find out for themselves.
4. Source code analysis
The RV slide ends up calling the scrollBy method of LayoutManager. We use a linear Layer outManager.
//LinearLayoutManager.java
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || delta == 0) {
return 0;
}
ensureLayoutState();
mLayoutState.mRecycle = true;
final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final intabsDelta = Math.abs(delta); # # code1The updateLayoutState method is used to calculate parameters such as mOffset. updateLayoutState(layoutDirection, absDelta,true, state); # # code2The fill method fills the View according to the remaining spacefinal int consumed = mLayoutState.mScrollingOffset
+
fill(recycler, mLayoutState, state, false);
if (consumed < 0) {
if (DEBUG) {
Log.d(TAG, "Don't have any more elements to scroll");
}
return 0;
}
final intscrolled = absDelta > consumed ? layoutDirection * consumed : delta; # # code3OffsetChildren, whole mobile RV child View mOrientationHelper. OffsetChildren (- scrolled);if (DEBUG) {
Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);
}
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
Copy the code
## code 1 updateLayoutState (mOffset)
Fill method, fill the View according to the remaining space
## Code 3 offsetChildren, moving the RV subview as a whole
// Mostly calculations
private void updateLayoutState(int layoutDirection, int requiredSpace,
boolean canUseExistingSpace, RecyclerView.State state) {
// If parent provides a hint, don't measure unlimited.
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mLayoutDirection = layoutDirection;
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
calculateExtraLayoutSpace(state, mReusableIntPair);
int extraForStart = Math.max(0, mReusableIntPair[0]);
int extraForEnd = Math.max(0, mReusableIntPair[1]);
boolean layoutToEnd = layoutDirection == LayoutState.LAYOUT_END;
mLayoutState.mExtraFillSpace = layoutToEnd ? extraForEnd : extraForStart;
mLayoutState.mNoRecycleSpace = layoutToEnd ? extraForStart : extraForEnd;
int scrollingOffset;
if (layoutToEnd) {
mLayoutState.mExtraFillSpace += mOrientationHelper.getEndPadding();
// get the first child in the direction we are going
final View child = getChildClosestToEnd();
// the direction in which we are traversing children
mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
: LayoutState.ITEM_DIRECTION_TAIL;
mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
// calculate how much we can scroll without adding new children (independent of layout)
scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
- mOrientationHelper.getEndAfterPadding();
} else {
final View child = getChildClosestToStart();
mLayoutState.mExtraFillSpace += mOrientationHelper.getStartAfterPadding();
mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
: LayoutState.ITEM_DIRECTION_HEAD;
mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child);
scrollingOffset = -mOrientationHelper.getDecoratedStart(child)
+ mOrientationHelper.getStartAfterPadding();
}
mLayoutState.mAvailable = requiredSpace;
if (canUseExistingSpace) {
mLayoutState.mAvailable -= scrollingOffset;
}
mLayoutState.mScrollingOffset = scrollingOffset;
}
Copy the code
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
if(layoutState.mScrollingOffset ! = LayoutState.SCROLLING_OFFSET_NaN) {// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } # # code1First determine whether you need to recycleView
recycleByLayoutState(recycler, layoutState);
}
intremainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace; LayoutChunkResult layoutChunkResult = mLayoutChunkResult; # # code2Determine whether to fill the remaining spaceView
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk"); } # # code3Is the specific layout method layoutChunk(Recycler, State, layoutState, layoutChunkResult);if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.endSection();
}
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
/** * Consume the available space if: * * layoutChunk did not request to be ignored * * OR we are laying out scrap children * * OR we are not doing pre-layout * /
if(! layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList ! =null| |! state.isPreLayout()) { layoutState.mAvailable -= layoutChunkResult.mConsumed;// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
}
if(layoutState.mScrollingOffset ! = LayoutState.SCROLLING_OFFSET_NaN) { layoutState.mScrollingOffset += layoutChunkResult.mConsumed;if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } # # code4Check whether recycling is required after the layout is completedView
recycleByLayoutState(recycler, layoutState);
}
if (stopOnFocusable && layoutChunkResult.mFocusable) {
break; }}if (DEBUG) {
validateChildOrder();
}
return start - layoutState.mAvailable;
}
Copy the code
## Code 1, first determine whether the View needs to be recycled
## Code 2, based on the remaining space, determine whether the View needs to be filled
## Code 3 is the specific layout method
## code 4 is to determine whether the View needs to be recycled after the layout is completed
This article focuses on the logic of recycling and reuse when sliding. How to recycle and reuse. RecyclerView three level cache is how to achieve. Listen to the breakdown next time.
5. Ask questions and interact
Finally, to reinforce your understanding, ask a question, please write your answer in the comments section.
In case3 of scenario 1, swipe up 120px and 120px is 100px higher than the height of the first Item. Why not recycle Item1 first?
Question 2: What might be the reason for Google’s design?
If you’re afraid you won’t find this post for a long time, please like it and it will appear in your likes list.