preface
“Contentment”, a lot of people are not satisfied with the status quo, all kinds of toss and toss, often abandon their roots to the last, chang Le can be less impetuous, more quiet. Recently, a lot of things have happened in xiaobian, and the mentality has changed a lot. I feel helpless in reality, lonely in the distant city away from my hometown, and impetuous in pursuit of fame and wealth. Maybe life is like this, each age has its own troubles.
Speaking of thrashing, I’ve seen all sorts of custom LayoutManagers making all sorts of cool animations a long time ago and thought I’d do it myself. But every time because the system comes with the LinearLayoutManager source code to make a face confused. Just this time not busy, toss about a day, wrote a simple Demo, the effect is as follows:
Results the preview
“Multiple Types of RecyclerView”
use
mRecyclerView.setLayoutManager(stackLayoutManager = new StackLayoutManager(this));
Copy the code
The text is just a simple Demo with a single function. It mainly explains the process and steps. Please modify it according to specific requirements.
The significance of each attribute is shown in the figure:
Customize LayoutManager basics
For the basics of custom LayoutManager, check out the following excellent article:
1, Chen Xiaoyuan’s custom LayoutManager 11 type flying Dragon in the sky (xiaoyuan boss custom article logic is clear, can be called a textbook, very classic)
Blog.csdn.net/u011387817/…
2, Zhang Xutong master custom LayoutManager(a) series of common mistakes, problems, notes, common API
Blog.csdn.net/zxt0601/art…
3. Zhang Xutong’s mastery of custom LayoutManager(2) to achieve flow layout
Blog.csdn.net/zxt0601/art…
4, Yong Chao Chen Android imitation douban book video channel recommendation form stack list recyclerView-LayoutManager
Blog.csdn.net/ccy0122/art…
These articles are very accurate in their analysis of the misunderstandings and precautions of custom LayoutManager. I have read several articles back and forth, hoping to help you.
Customize the basic LayoutManager flow
Make Items appear
In a custom ViewGroup, we want to display child views in three ways:
- Add Add a child View to a ViewGroup by using the addView method or directly in XML;
- Measure overrides the onMeasure method and determines its own size and the size of each subview;
- Layout overrides the onLayout method, which calls the layout method of the child View to determine its position and size.
In the custom LayoutManager, the process is similar. We need to override the onLayoutChildren method, which calls back during initialization or Adapter data set update. In this method, we need to do the following:
- Layout before, we need to call detachAndScrapAttachedViews method to separate the Items of screen, adjust position and internal data, and then add it back (if required);
- So once we’ve separated them, we have to figure out how to add them back, so we need to add them with the addView method, so where do we get those views? We need to call the Recycler’s getViewForPosition method to get it.
- Once the Item is fetched and repopulated, measure it by calling measureChild or measureChildWithMargins.
- What else do I need to do after I measure it? Yes, the layout, we also decided to use layoutDecorated or layoutDecoratedWithMargins method according to the requirements;
- In the custom ViewGroup, the layout can be run to see the effect, but in LayoutManager there is a very important thing, is the recycling, we in the layout, but also some Items are no longer needed to recycle, to ensure the smoothness of sliding;
The above content comes from Chen Xiaoyuan’s custom LayoutManager type 11 Flying Dragon in the sky.
Layout of the implementation
Take a look at the relevant parameters:
The index value of 0 view a completely mobile distance, slip out of the screen need to firstChildCompleteScrollLength; The index value of 0 slip out of screen needed to move the view distance is: firstChildCompleteScrollLength + onceCompleteScrollLength; The spacing between items is normalViewGap
We record the offset dx in the scrollHorizontallyBy method, save a cumulative offset mHorizontalOffset, and then for both cases where the index is 0 and non-0, In less than firstChildCompleteScrollLength mHorizontalOffset case, use the offset divided by the percentage firstChildCompleteScrollLength access to already rolling fraction; Under the condition of the same index value of 0, minus the firstChildCompleteScrollLength offset needed to get the percentage of the scroll. Depending on the percentage, it’s easy to lay out childView.
I’m going to start writing code. I’m going to call it StackLayoutManager.
StackLayoutManager inheritance RecyclerView LayoutManager, needs to be rewritten generateDefaultLayoutParams method:
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams(a) {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
}
Copy the code
Let’s start with member variables:
/** * The distance required for a complete focus slide */
private float onceCompleteScrollLength = -1;
/** * The offset of the first child view */
private float firstChildCompleteScrollLength = -1;
/** * The first view's position */ is visible on the screen
private int mFirstVisiPos;
/** * Position of the last view visible on the screen */
private int mLastVisiPos;
/** * accumulated offset in horizontal direction */
private long mHorizontalOffset;
/** * margin between views */
private float normalViewGap = 30;
private int childWidth = 0;
/** * Indicates whether */ is automatically selected
private boolean isAutoSelect = true;
// Select the animation
private ValueAnimator selectAnimator;
Copy the code
Then look at the scrollHorizontallyBy method:
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
// Swipe from right to left, dx > 0; Slide your finger from left to right, dx < 0;
// The View will not move without its child
if (dx == 0 || getChildCount() == 0) {
return 0;
}
// Error handling
float realDx = dx / 1.0 f;
if (Math.abs(realDx) < 0.00000001 f) {
return 0;
}
mHorizontalOffset += dx;
dx = fill(recycler, state, dx);
return dx;
}
private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
int resultDelta = dx;
resultDelta = fillHorizontalLeft(recycler, state, dx);
recycleChildren(recycler);
return resultDelta;
}
private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
//----------------1. Boundary detection -----------------
if (dx < 0) {
// Left boundary has been reached
if (mHorizontalOffset < 0) {
mHorizontalOffset = dx = 0; }}if (dx > 0) {
if (mHorizontalOffset >= getMaxOffset()) {
// Slide to the rightmost edge according to the maximum offset
mHorizontalOffset = (long) getMaxOffset();
dx = 0; }}// Separate all views and add them to the temporary cache
detachAndScrapAttachedViews(recycler);
float startX = 0;
float fraction = 0f;
boolean isChildLayoutLeft = true;
View tempView = null;
int tempPosition = -1;
if (onceCompleteScrollLength == -1) {
// Since mFirstVisiPos may change below, use tempPosition to store it temporarily
tempPosition = mFirstVisiPos;
tempView = recycler.getViewForPosition(tempPosition);
measureChildWithMargins(tempView, 0.0);
childWidth = getDecoratedMeasurementHorizontal(tempView);
}
// Fix the first visible view mFirstVisiPos sliding how many full onceCompleteScrollLength represents sliding how many items
firstChildCompleteScrollLength = getWidth() / 2 + childWidth / 2;
if (mHorizontalOffset >= firstChildCompleteScrollLength) {
startX = normalViewGap;
onceCompleteScrollLength = childWidth + normalViewGap;
mFirstVisiPos = (int) Math.floor(Math.abs(mHorizontalOffset - firstChildCompleteScrollLength) / onceCompleteScrollLength) + 1;
fraction = (Math.abs(mHorizontalOffset - firstChildCompleteScrollLength) % onceCompleteScrollLength) / (onceCompleteScrollLength * 1.0 f);
} else {
mFirstVisiPos = 0;
startX = getMinOffset();
onceCompleteScrollLength = firstChildCompleteScrollLength;
fraction = (Math.abs(mHorizontalOffset) % onceCompleteScrollLength) / (onceCompleteScrollLength * 1.0 f);
}
// Temporarily assign mLastVisiPos to getItemCount() -1, rest easy, the next run will determine whether the view has overflowed the screen, and correct the value in time to finish the layout
mLastVisiPos = getItemCount() - 1;
float normalViewOffset = onceCompleteScrollLength * fraction;
boolean isNormalViewOffsetSetted = false;
//----------------3. Start layout -----------------
for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
View item;
if(i == tempPosition && tempView ! =null) {
// If a temporary view is already taken when initializing data
item = tempView;
} else {
item = recycler.getViewForPosition(i);
}
addView(item);
measureChildWithMargins(item, 0.0);
if(! isNormalViewOffsetSetted) { startX -= normalViewOffset; isNormalViewOffsetSetted =true;
}
int l, t, r, b;
l = (int) startX;
t = getPaddingTop();
r = l + getDecoratedMeasurementHorizontal(item);
b = t + getDecoratedMeasurementVertical(item);
layoutDecoratedWithMargins(item, l, t, r, b);
startX += (childWidth + normalViewGap);
if (startX > getWidth() - getPaddingRight()) {
mLastVisiPos = i;
break; }}return dx;
}
Copy the code
Methods involved:
/** * Maximum offset **@return* /
private float getMaxOffset(a) {
if (childWidth == 0 || getItemCount() == 0) return 0;
return (childWidth + normalViewGap) * (getItemCount() - 1);
}
/** * Get the horizontal space of a childView, taking margin into account **@param view
* @return* /
public int getDecoratedMeasurementHorizontal(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredWidth(view) + params.leftMargin
+ params.rightMargin;
}
/** * Get the vertical space of a childView, taking margin into account **@param view
* @return* /
public int getDecoratedMeasurementVertical(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredHeight(view) + params.topMargin
+ params.bottomMargin;
}
Copy the code
Recycling reuse
Recyclerview-layoutmanager RecyclerView-LayoutManager RecyclerView-LayoutManager
/ * * *@param recycler
* @param state
* @param delta
*/
private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int delta) {
int resultDelta = delta;
/ /... omit
recycleChildren(recycler);
log("childCount= [" + getChildCount() + "]" + ",[recycler.getScrapList().size():" + recycler.getScrapList().size());
return resultDelta;
}
/** * Reclaim the Item to be reclaimed. * /
private void recycleChildren(RecyclerView.Recycler recycler) {
List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
for (int i = 0; i < scrapList.size(); i++) { RecyclerView.ViewHolder holder = scrapList.get(i); removeAndRecycleView(holder.itemView, recycler); }}Copy the code
Recycling here will not verify, interested partners can verify themselves.
Animation effects
private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
/ / omit...
//----------------3. Start layout -----------------
for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
/ / omit...
// Zoom the subview
final float minScale = 0.6 f;
float currentScale = 0f;
final int childCenterX = (r + l) / 2;
final int parentCenterX = getWidth() / 2;
isChildLayoutLeft = childCenterX <= parentCenterX;
if (isChildLayoutLeft) {
final float fractionScale = (parentCenterX - childCenterX) / (parentCenterX * 1.0 f);
currentScale = 1.0 f - (1.0 f - minScale) * fractionScale;
} else {
final float fractionScale = (childCenterX - parentCenterX) / (parentCenterX * 1.0 f);
currentScale = 1.0 f - (1.0 f - minScale) * fractionScale;
}
item.setScaleX(currentScale);
item.setScaleY(currentScale);
item.setAlpha(currentScale);
layoutDecoratedWithMargins(item, l, t, r, b);
/ / omit...
}
return dx;
}
Copy the code
The more you move childView toward the middle of the screen, the larger the zoom ratio, and the more you move toward the sides, the smaller the zoom ratio.
Automatically selected
1. Automatically selected after scrolling stops
Listen onScrollStateChanged, calculate the position that should stay while scrolling stops, and then calculate the mHorizontalOffset value when stopping. Play the property animation to update the current mHorizontalOffset to the final value. The relevant codes are as follows:
@Override
public void onScrollStateChanged(int state) {
super.onScrollStateChanged(state);
switch (state) {
case RecyclerView.SCROLL_STATE_DRAGGING:
// Stops the currently playing animation when the finger is pressed
cancelAnimator();
break;
case RecyclerView.SCROLL_STATE_IDLE:
// When the list scrolling stops, check to see if automatic selection is turned on
if (isAutoSelect) {
// Find the closest item index to the target drop point
smoothScrollToPosition(findShouldSelectPosition());
}
break;
default:
break; }}/** * smooth scroll to a position **@paramPosition Index of the target Item */
public void smoothScrollToPosition(int position) {
if (position > -1&& position < getItemCount()) { startValueAnimator(position); }}private int findShouldSelectPosition(a) {
if (onceCompleteScrollLength == -1 || mFirstVisiPos == -1) {
return -1;
}
int position = (int) (Math.abs(mHorizontalOffset) / (childWidth + normalViewGap));
int remainder = (int) (Math.abs(mHorizontalOffset) % (childWidth + normalViewGap));
// More than half, the next item should be selected
if (remainder >= (childWidth + normalViewGap) / 2.0 f) {
if (position + 1 <= getItemCount() - 1) {
return position + 1; }}return position;
}
private void startValueAnimator(int position) {
cancelAnimator();
final float distance = getScrollToPositionOffset(position);
long minDuration = 100;
long maxDuration = 300;
long duration;
float distanceFraction = (Math.abs(distance) / (childWidth + normalViewGap));
if (distance <= (childWidth + normalViewGap)) {
duration = (long) (minDuration + (maxDuration - minDuration) * distanceFraction);
} else {
duration = (long) (maxDuration * distanceFraction);
}
selectAnimator = ValueAnimator.ofFloat(0.0 f, distance);
selectAnimator.setDuration(duration);
selectAnimator.setInterpolator(new LinearInterpolator());
final float startedOffset = mHorizontalOffset;
selectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
mHorizontalOffset = (long) (startedOffset + value); requestLayout(); }}); selectAnimator.start(); }Copy the code
2. Click the non-focus view to automatically select it as the focus view
SmoothScrollToPosition method to automatically select focus.
The middle view overlays the views on both sides
It works like this:
RecyclerView inherits from ViewGroup, so the larger the index value of index in addView(View Child, int index), the more it will be displayed in the upper layer. Therefore, it can be concluded that the added green card of 2 is the largest index, and the analysis can draw the following conclusions:
Index size:
0 < 1 < 2 > 3 > 4
The principle of maximum in the middle and gradual diminution on both sides.
If the index value is less than or equal to the index value, call addView(item), otherwise call addView(item, 0); The relevant codes are as follows:
private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
//省略 ......
//----------------3. Start layout -----------------
for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
//省略 ......
int focusPosition = (int) (Math.abs(mHorizontalOffset) / (childWidth + normalViewGap));
if (i <= focusPosition) {
addView(item);
} else {
addView(item, 0);
}
//省略 ......
}
return dx;
}
Copy the code
That’s about the end of the article.
Source code address:
Github.com/HpWens/MeiW…
Give me a star
conclusion
People who love to laugh, luck is generally not too bad. And give yourself a pat on the back, and we’ll see you next time.