前言
「知足常樂」锦茁,很多人不滿足現(xiàn)狀,各種折騰叉存,往往舍本逐末码俩,常樂才能少一分浮躁,多一分寧靜歼捏。近期在小編身上發(fā)生了許多事情稿存,心態(tài)也發(fā)生了很大的改變,有感于現(xiàn)實(shí)的無奈瞳秽,在離家鄉(xiāng)遙遠(yuǎn)城市里的落寂挠铲,追逐名利的浮躁;可能生活就是這樣的寂诱,每個年齡段都有自己的煩惱拂苹。
說道折騰,很久以前就看到了各種自定義LayoutManager做出各種炫酷的動畫痰洒,就想自己也要實(shí)現(xiàn)瓢棒。但每次都因?yàn)橄到y(tǒng)自帶的LinearLayoutManager源碼搞得一臉懵逼。正好這段時間不忙丘喻,折騰了一天脯宿,寫了個簡單的Demo,效果如下:
效果預(yù)覽
RecyclerView的重要性不必多說泉粉,據(jù)過往開發(fā)經(jīng)驗(yàn)而談连霉,超過一屏可滑動的界面,基本都可以采用 「RecyclerView的多類型」 來做嗡靡,不僅維護(hù)還是擴(kuò)展都是非常有效率的跺撼。RecyclerView相關(guān)的面試題也是各大廠常問的問題之一(權(quán)重非常高)。
使用
mRecyclerView.setLayoutManager(stackLayoutManager = new StackLayoutManager(this));
跟系統(tǒng)的LinearLayoutManager使用方式一致讨彼,文本只是簡單的Demo歉井,功能單一,主要講解流程與步驟哈误,請根據(jù)特定的需求修改哩至。
各屬性意義見圖:
湊合看躏嚎,由于ps太爛。注意:因?yàn)閕tem隨著滑動會有不同的縮放菩貌,所以實(shí)際normalViewGap會被縮放計(jì)算卢佣。
自定義LayoutManager基礎(chǔ)知識
有關(guān)自定義LayoutManager基礎(chǔ)知識,請查閱以下文章箭阶,寫的非常棒:
1珠漂、陳小緣的自定義LayoutManager第十一式之飛龍?jiān)谔欤ㄐ【壌罄凶远x文章邏輯清晰明了,堪稱教科書尾膊,非常經(jīng)典)
https://blog.csdn.net/u011387817/article/details/81875021
2媳危、 張旭童的掌握自定義LayoutManager(一) 系列開篇 常見誤區(qū)、問題冈敛、注意事項(xiàng)待笑,常用API
https://blog.csdn.net/zxt0601/article/details/52948009
3、張旭童的掌握自定義LayoutManager(二) 實(shí)現(xiàn)流式布局
https://blog.csdn.net/zxt0601/article/details/52956504
4抓谴、勇朝陳的Android仿豆瓣書影音頻道推薦表單堆疊列表RecyclerView-LayoutManager
https://blog.csdn.net/ccy0122/article/details/90515386
這幾篇文章針對自定義LayoutManager的誤區(qū)暮蹂、注意事項(xiàng),分析的非常到位癌压,來來回回我看了好幾篇仰泻,希望對你有所幫助。
自定義LayoutManager基本流程
讓Items顯示出來
我們在自定義ViewGroup中滩届,想要顯示子View集侯,無非就三件事:
- 添加 通過addView方法把子View添加進(jìn)ViewGroup或直接在xml中直接添加;
- 測量 重寫onMeasure方法并在這里決定自身尺寸以及每一個子View大兄南棠枉;
- 布局 重寫onLayout方法,在里面調(diào)用子View的layout方法來確定它的位置和尺寸泡挺;
其實(shí)在自定義LayoutManager中辈讶,在流程上也是差不多的,我們需要重寫onLayoutChildren方法娄猫,這個方法會在初始化或者Adapter數(shù)據(jù)集更新時回調(diào)贱除,在這方法里面,需要做以下事情:
- 進(jìn)行布局之前媳溺,我們需要調(diào)用detachAndScrapAttachedViews方法把屏幕中的Items都分離出來月幌,內(nèi)部調(diào)整好位置和數(shù)據(jù)后,再把它添加回去(如果需要的話)褂删;
- 分離了之后飞醉,我們就要想辦法把它們再添加回去了冲茸,所以需要通過addView方法來添加屯阀,那這些View在哪里得到呢缅帘? 我們需要調(diào)用 Recycler的getViewForPosition(int position) 方法來獲取难衰;
- 獲取到Item并重新添加了之后钦无,我們還需要對它進(jìn)行測量,這時候可以調(diào)用measureChild或measureChildWithMargins方法盖袭,兩者的區(qū)別我們已經(jīng)了解過了失暂,相信同學(xué)們都能根據(jù)需求選擇更合適的方法;
- 在測量完還需要做什么呢鳄虱? 沒錯弟塞,就是布局了,我們也是根據(jù)需求來決定使用layoutDecorated還是layoutDecoratedWithMargins方法拙已;
- 在自定義ViewGroup中决记,layout完就可以運(yùn)行看效果了,但在LayoutManager還有一件非常重要的事情倍踪,就是回收了系宫,我們在layout之后,還要把一些不再需要的Items回收建车,以保證滑動的流暢度扩借;
以上內(nèi)容出自陳小緣的自定義LayoutManager第十一式之飛龍?jiān)谔?/a>。
布局實(shí)現(xiàn)
再看下相關(guān)參數(shù):
如果去掉itemView的縮放缤至,透明度動畫潮罪,那么效果是這樣的:
看到的效果與LinearLayoutManager一樣,但本篇并不使用LinearLayoutManager领斥,而是通過自定義LayoutManager來實(shí)現(xiàn)错洁。
索引值為0的view 一次完全滑出屏幕所需要的移動距離,定位為 firstChildCompleteScrollLength
戒突;非索引值為0的view滑出屏幕所需要移動的距離為:
firstChildCompleteScrollLength
+ onceCompleteScrollLength
; item 之間的間距為 normalViewGap
我們在 scrollHorizontallyBy
方法中記錄偏移量 dx
屯碴,保存一個累計(jì)偏移量 mHorizontalOffset
,然后針對索引值為0與非0兩種情況膊存,在 mHorizontalOffset
小于 firstChildCompleteScrollLength
情況下导而,用該偏移量除以 firstChildCompleteScrollLength
獲取到已經(jīng)滾動了的百分比 fraction
;同理索引值非0的情況下隔崎,偏移量需要減去 firstChildCompleteScrollLength
來獲取到滾動的百分比今艺。根據(jù)百分比,怎么布局childview就很容易了爵卒。
接下來開始寫代碼虚缎,先取個比較接地氣的名字,就叫 StackLayoutManager
钓株,好普通的名字实牡,哈哈陌僵。
StackLayoutManager
繼承 RecyclerView.LayoutManager
,需要重寫 generateDefaultLayoutParams
方法:
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
}
先看看成員變量:
/**
* 一次完整的聚焦滑動所需要的移動距離
*/
private float onceCompleteScrollLength = -1;
/**
* 第一個子view的偏移量
*/
private float firstChildCompleteScrollLength = -1;
/**
* 屏幕可見第一個view的position
*/
private int mFirstVisiPos;
/**
* 屏幕可見的最后一個view的position
*/
private int mLastVisiPos;
/**
* 水平方向累計(jì)偏移量
*/
private long mHorizontalOffset;
/**
* view之間的margin
*/
private float normalViewGap = 30;
private int childWidth = 0;
/**
* 是否自動選中
*/
private boolean isAutoSelect = true;
// 選中動畫
private ValueAnimator selectAnimator;
接著看看 scrollHorizontallyBy
方法:
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
// 手指從右向左滑動创坞,dx > 0; 手指從左向右滑動碗短,dx < 0;
// 位移0、沒有子View 當(dāng)然不移動
if (dx == 0 || getChildCount() == 0) {
return 0;
}
// 誤差處理
float realDx = dx / 1.0f;
if (Math.abs(realDx) < 0.00000001f) {
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题涨、邊界檢測-----------------
if (dx < 0) {
// 已到達(dá)左邊界
if (mHorizontalOffset < 0) {
mHorizontalOffset = dx = 0;
}
}
if (dx > 0) {
if (mHorizontalOffset >= getMaxOffset()) {
// 根據(jù)最大偏移量來計(jì)算滑動到最右側(cè)邊緣
mHorizontalOffset = (long) getMaxOffset();
dx = 0;
}
}
// 分離全部的view偎谁,加入到臨時緩存
detachAndScrapAttachedViews(recycler);
float startX = 0;
float fraction = 0f;
boolean isChildLayoutLeft = true;
View tempView = null;
int tempPosition = -1;
if (onceCompleteScrollLength == -1) {
// 因?yàn)閙FirstVisiPos在下面可能被改變,所以用tempPosition暫存一下
tempPosition = mFirstVisiPos;
tempView = recycler.getViewForPosition(tempPosition);
measureChildWithMargins(tempView, 0, 0);
childWidth = getDecoratedMeasurementHorizontal(tempView);
}
// 修正第一個可見view mFirstVisiPos 已經(jīng)滑動了多少個完整的onceCompleteScrollLength就代表滑動了多少個item
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.0f);
} else {
mFirstVisiPos = 0;
startX = getMinOffset();
onceCompleteScrollLength = firstChildCompleteScrollLength;
fraction = (Math.abs(mHorizontalOffset) % onceCompleteScrollLength) / (onceCompleteScrollLength * 1.0f);
}
// 臨時將mLastVisiPos賦值為getItemCount() - 1纲堵,放心巡雨,下面遍歷時會判斷view是否已溢出屏幕,并及時修正該值并結(jié)束布局
mLastVisiPos = getItemCount() - 1;
float normalViewOffset = onceCompleteScrollLength * fraction;
boolean isNormalViewOffsetSetted = false;
//----------------3席函、開始布局-----------------
for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
View item;
if (i == tempPosition && tempView != null) {
// 如果初始化數(shù)據(jù)時已經(jīng)取了一個臨時view
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;
}
涉及的方法:
/**
* 最大偏移量
*
* @return
*/
private float getMaxOffset() {
if (childWidth == 0 || getItemCount() == 0) return 0;
return (childWidth + normalViewGap) * (getItemCount() - 1);
}
/**
* 獲取某個childView在水平方向所占的空間鸯隅,將margin考慮進(jìn)去
*
* @param view
* @return
*/
public int getDecoratedMeasurementHorizontal(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredWidth(view) + params.leftMargin
+ params.rightMargin;
}
/**
* 獲取某個childView在豎直方向所占的空間,將margin考慮進(jìn)去
*
* @param view
* @return
*/
public int getDecoratedMeasurementVertical(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredHeight(view) + params.topMargin
+ params.bottomMargin;
}
回收復(fù)用
這里使用Android仿豆瓣書影音頻道推薦表單堆疊列表RecyclerView-LayoutManager中使用的回收技巧:
/**
* @param recycler
* @param state
* @param delta
*/
private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int delta) {
int resultDelta = delta;
//。向挖。蝌以。省略
recycleChildren(recycler);
log("childCount= [" + getChildCount() + "]" + ",[recycler.getScrapList().size():" + recycler.getScrapList().size());
return resultDelta;
}
/**
* 回收需回收的Item。
*/
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);
}
}
回收復(fù)用這里就不驗(yàn)證了何之,感興趣的小伙伴可自行驗(yàn)證跟畅。
動畫效果
private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
// 省略 ......
//----------------3、開始布局-----------------
for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
// 省略 ......
// 縮放子view
final float minScale = 0.6f;
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.0f);
currentScale = 1.0f - (1.0f - minScale) * fractionScale;
} else {
final float fractionScale = (childCenterX - parentCenterX) / (parentCenterX * 1.0f);
currentScale = 1.0f - (1.0f - minScale) * fractionScale;
}
item.setScaleX(currentScale);
item.setScaleY(currentScale);
item.setAlpha(currentScale);
layoutDecoratedWithMargins(item, l, t, r, b);
// 省略 ......
}
return dx;
}
childView
越向屏幕中間移動縮放比越大溶推,越向兩邊移動縮放比越小徊件。
自動選中
1、滾動停止后自動選中
監(jiān)聽 onScrollStateChanged
蒜危,在滾動停止時計(jì)算出應(yīng)當(dāng)停留的 position
虱痕,再計(jì)算出停留時的 mHorizontalOffset
值,播放屬性動畫將當(dāng)前 mHorizontalOffset
不斷更新至最終值即可辐赞。相關(guān)代碼如下:
@Override
public void onScrollStateChanged(int state) {
super.onScrollStateChanged(state);
switch (state) {
case RecyclerView.SCROLL_STATE_DRAGGING:
//當(dāng)手指按下時响委,停止當(dāng)前正在播放的動畫
cancelAnimator();
break;
case RecyclerView.SCROLL_STATE_IDLE:
//當(dāng)列表滾動停止后夹囚,判斷一下自動選中是否打開
if (isAutoSelect) {
//找到離目標(biāo)落點(diǎn)最近的item索引
smoothScrollToPosition(findShouldSelectPosition());
}
break;
default:
break;
}
}
/**
* 平滑滾動到某個位置
*
* @param position 目標(biāo)Item索引
*/
public void smoothScrollToPosition(int position) {
if (position > -1 && position < getItemCount()) {
startValueAnimator(position);
}
}
private int findShouldSelectPosition() {
if (onceCompleteScrollLength == -1 || mFirstVisiPos == -1) {
return -1;
}
int position = (int) (Math.abs(mHorizontalOffset) / (childWidth + normalViewGap));
int remainder = (int) (Math.abs(mHorizontalOffset) % (childWidth + normalViewGap));
// 超過一半,應(yīng)當(dāng)選中下一項(xiàng)
if (remainder >= (childWidth + normalViewGap) / 2.0f) {
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.0f, 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();
}
2舵抹、點(diǎn)擊非焦點(diǎn)view自動將其選中為焦點(diǎn)view
我們可以直接拿到 view
的 position
堰燎,直接調(diào)用 smoothScrollToPosition
方法秆剪,就可以實(shí)現(xiàn)自動選中為焦點(diǎn)。
中間view覆蓋在兩邊view之上
效果是這樣的:
從效果中可以看出,索引為2的view覆蓋在1,3的上面汤锨,同時1又覆蓋在0的上面双抽,以此內(nèi)推柬泽。
RecyclerView
繼承于 ViewGroup
,那么在添加子view addView(View child, int index)
中 index
的索引值越大有决,越顯示在上層揽趾。那么可以得出痒芝,為2的綠色卡片被添加是 index
最大牵素,分析可以得出以下結(jié)論:
index
的大小:
0 < 1 < 2 > 3 > 4
中間最大榕堰,兩邊逐漸減小的原則砍的。
獲取到中間 view
的索引值,如果小于等于該索引值則調(diào)用 addView(item)
沫勿,反之調(diào)用 addView(item, 0)
挨约;相關(guān)代碼如下:
private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
//省略 ......
//----------------3、開始布局-----------------
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;
}
文章到這里就差不多要結(jié)束了产雹。
源碼地址:
https://github.com/HpWens/MeiWidgetView
給個star唄~
結(jié)語
愛笑的人诫惭,運(yùn)氣一般都不會太差。同時也給自己一個鼓勵蔓挖,我們下期見夕土。