「滿足常樂」,不少人不知足現狀,各類折騰,每每捨本逐末,常樂才能少一分浮躁,多一分寧靜。近期在小編身上發生了許多事情,心態也發生了很大的改變,有感於現實的無奈,在離家鄉遙遠城市裏的落寂,追逐名利的浮躁;可能生活就是這樣的,每一個年齡段都有本身的煩惱。java
說道折騰,好久之前就看到了各類自定義LayoutManager作出各類炫酷的動畫,就想本身也要實現。但每次都由於系統自帶的LinearLayoutManager源碼搞得一臉懵逼。正好這段時間不忙,折騰了一天,寫了個簡單的Demo,效果以下:git
mRecyclerView.setLayoutManager(stackLayoutManager = new StackLayoutManager(this));
複製代碼
跟系統的LinearLayoutManager使用方式一致,文本只是簡單的Demo,功能單一,主要講解流程與步驟,請根據特定的需求修改。github
各屬性意義見圖: 面試
湊合看,因爲ps太爛。注意:由於item隨着滑動會有不一樣的縮放,因此實際normalViewGap會被縮放計算。有關自定義LayoutManager基礎知識,請查閱如下文章,寫的很是棒:緩存
一、陳小緣的自定義LayoutManager第十一式之飛龍在天(小緣大佬自定義文章邏輯清晰明瞭,堪稱教科書,很是經典)ide
二、 張旭童的掌握自定義LayoutManager(一) 系列開篇 常見誤區、問題、注意事項,經常使用API動畫
blog.csdn.net/zxt0601/art…this
三、張旭童的掌握自定義LayoutManager(二) 實現流式佈局spa
四、勇朝陳的Android仿豆瓣書影音頻道推薦表單堆疊列表RecyclerView-LayoutManager
這幾篇文章針對自定義LayoutManager的誤區、注意事項,分析的很是到位,來來回回我看了好幾篇,但願對你有所幫助。
咱們在自定義ViewGroup中,想要顯示子View,無非就三件事:
其實在自定義LayoutManager中,在流程上也是差很少的,咱們須要重寫onLayoutChildren方法,這個方法會在初始化或者Adapter數據集更新時回調,在這方法裏面,須要作如下事情:
以上內容出自陳小緣的自定義LayoutManager第十一式之飛龍在天。
再看下相關參數:
若是去掉itemView的縮放,透明度動畫,那麼效果是這樣的: 看到的效果與LinearLayoutManager同樣,但本篇並不使用LinearLayoutManager,而是經過自定義LayoutManager來實現。索引值爲0的view 一次徹底滑出屏幕所須要的移動距離,定位爲 firstChildCompleteScrollLength
;非索引值爲0的view滑出屏幕所須要移動的距離爲: firstChildCompleteScrollLength
+ onceCompleteScrollLength
; item 之間的間距爲 normalViewGap
咱們在 scrollHorizontallyBy
方法中記錄偏移量 dx
,保存一個累計偏移量 mHorizontalOffset
,而後針對索引值爲0與非0兩種狀況,在 mHorizontalOffset
小於 firstChildCompleteScrollLength
狀況下,用該偏移量除以 firstChildCompleteScrollLength
獲取到已經滾動了的百分比 fraction
;同理索引值非0的狀況下,偏移量須要減去 firstChildCompleteScrollLength
來獲取到滾動的百分比。根據百分比,怎麼佈局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;
/** * 水平方向累計偏移量 */
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 固然不移動
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) {
//----------------一、邊界檢測-----------------
if (dx < 0) {
// 已到達左邊界
if (mHorizontalOffset < 0) {
mHorizontalOffset = dx = 0;
}
}
if (dx > 0) {
if (mHorizontalOffset >= getMaxOffset()) {
// 根據最大偏移量來計算滑動到最右側邊緣
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) {
// 由於mFirstVisiPos在下面可能被改變,因此用tempPosition暫存一下
tempPosition = mFirstVisiPos;
tempView = recycler.getViewForPosition(tempPosition);
measureChildWithMargins(tempView, 0, 0);
childWidth = getDecoratedMeasurementHorizontal(tempView);
}
// 修正第一個可見view mFirstVisiPos 已經滑動了多少個完整的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是否已溢出屏幕,並及時修正該值並結束佈局
mLastVisiPos = getItemCount() - 1;
float normalViewOffset = onceCompleteScrollLength * fraction;
boolean isNormalViewOffsetSetted = false;
//----------------三、開始佈局-----------------
for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
View item;
if (i == tempPosition && tempView != null) {
// 若是初始化數據時已經取了一個臨時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考慮進去 * * @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考慮進去 * * @param view * @return */
public int getDecoratedMeasurementVertical(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredHeight(view) + params.topMargin
+ params.bottomMargin;
}
複製代碼
這裏使用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);
}
}
複製代碼
回收複用這裏就不驗證了,感興趣的小夥伴可自行驗證。
private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
// 省略 ......
//----------------三、開始佈局-----------------
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
越向屏幕中間移動縮放比越大,越向兩邊移動縮放比越小。
監聽 onScrollStateChanged
,在滾動中止時計算出應當停留的 position
,再計算出停留時的 mHorizontalOffset
值,播放屬性動畫將當前 mHorizontalOffset
不斷更新至最終值便可。相關代碼以下:
@Override
public void onScrollStateChanged(int state) {
super.onScrollStateChanged(state);
switch (state) {
case RecyclerView.SCROLL_STATE_DRAGGING:
//當手指按下時,中止當前正在播放的動畫
cancelAnimator();
break;
case RecyclerView.SCROLL_STATE_IDLE:
//當列表滾動中止後,判斷一下自動選中是否打開
if (isAutoSelect) {
//找到離目標落點最近的item索引
smoothScrollToPosition(findShouldSelectPosition());
}
break;
default:
break;
}
}
/** * 平滑滾動到某個位置 * * @param position 目標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));
// 超過一半,應當選中下一項
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();
}
複製代碼
咱們能夠直接拿到 view
的 position
,直接調用 smoothScrollToPosition
方法,就能夠實現自動選中爲焦點。
效果是這樣的:
從效果中能夠看出,索引爲2的view覆蓋在1,3的上面,同時1又覆蓋在0的上面,以此內推。RecyclerView
繼承於 ViewGroup
,那麼在添加子view addView(View child, int index)
中 index
的索引值越大,越顯示在上層。那麼能夠得出,爲2的綠色卡片被添加是 index
最大,分析能夠得出如下結論:
index
的大小:
0 < 1 < 2 > 3 > 4
中間最大,兩邊逐漸減少的原則。
獲取到中間 view
的索引值,若是小於等於該索引值則調用 addView(item)
,反之調用 addView(item, 0)
;相關代碼以下:
private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
//省略 ......
//----------------三、開始佈局-----------------
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;
}
複製代碼
文章到這裏就差很少要結束了。
源碼地址:
給 個 star 唄 ~
愛笑的人,運氣通常都不會太差。同時也給本身一個鼓勵,咱們下期見。