當前位置:
首頁 > 知識 > 如何利用 Android 自定義控制項實現炫酷的動畫?|CSDN 博文精選

如何利用 Android 自定義控制項實現炫酷的動畫?|CSDN 博文精選

如何利用 Android 自定義控制項實現炫酷的動畫?|CSDN 博文精選

如何利用 Android 自定義控制項實現炫酷的動畫?|CSDN 博文精選

作者 | u012551350

本文精選自 CSDN 博客,已獲作者授權

「知足常樂」,很多人不滿足現狀,各種折騰,往往捨本逐末,常樂才能少一分浮躁,多一分寧靜。近期在筆者身上發生了許多事情,心態也發生了很大的改變,有感於現實的無奈,在離家鄉遙遠城市裡的落寂,追逐名利的浮躁;可能生活就是這樣的,每個年齡段都有自己的煩惱。

說到折騰,很久以前就看到了各種自定義LayoutManager做出各種炫酷的動畫,就想自己也要實現。但每次都因為系統自帶的LinearLayoutManager源碼搞得一臉懵逼。正好這段時間不忙,折騰了一天,寫了個簡單的Demo,效果如下:

如何利用 Android 自定義控制項實現炫酷的動畫?|CSDN 博文精選

RecyclerView的重要性不必多說,據過往開發經驗而談,超過一屏可滑動的界面,基本都可以採用 「RecyclerView的多類型」 來做,不管維護還是擴展都是非常有效率的。RecyclerView相關的面試題也是各大廠常問的問題之一(權重非常高)。

如何利用 Android 自定義控制項實現炫酷的動畫?|CSDN 博文精選

使用

mRecyclerView.setLayoutManager(stackLayoutManager = new StackLayoutManager(this));

跟系統的LinearLayoutManager使用方式一致,文本只是簡單的Demo,功能單一,主要講解流程與步驟,請根據特定的需求修改。

各屬性意義見圖:

如何利用 Android 自定義控制項實現炫酷的動畫?|CSDN 博文精選

注意:因為item隨著滑動會有不同的縮放,所以實際normalViewGap會被縮放計算。

如何利用 Android 自定義控制項實現炫酷的動畫?|CSDN 博文精選

自定義LayoutManager基礎知識

有關自定義LayoutManager基礎知識,請查閱以下文章,寫的非常棒:

1、陳小緣的自定義LayoutManager第十一式之飛龍在天(小緣大佬自定義文章邏輯清晰明了,堪稱教科書,非常經典)

https://blog.csdn.net/u011387817/article/details/81875021

2、 張旭童的掌握自定義LayoutManager(一) 系列開篇 常見誤區、問題、注意事項,常用API

https://blog.csdn.net/zxt0601/article/details/52948009

3、張旭童的掌握自定義LayoutManager(二) 實現流式布局

https://blog.csdn.net/zxt0601/article/details/52956504

4、勇朝陳的Android仿豆瓣書影音頻道推薦表單堆疊列表RecyclerView-LayoutManager

https://blog.csdn.net/ccy0122/article/details/90515386

這幾篇文章針對自定義LayoutManager的誤區、注意事項,分析的非常到位,來來回回我看了好幾篇,希望對你有所幫助。

如何利用 Android 自定義控制項實現炫酷的動畫?|CSDN 博文精選

自定義LayoutManager基本流程

讓Items顯示出來

我們在自定義ViewGroup中,想要顯示子View,無非就三件事:

  • 添加 通過addView方法把子View添加進ViewGroup或直接在xml中直接添加;

  • 測量 重寫onMeasure方法並在這裡決定自身尺寸以及每一個子View大小;

  • 布局 重寫onLayout方法,在裡面調用子View的layout方法來確定它的位置和尺寸。

其實在自定義LayoutManager中,在流程上也是差不多的,我們需要重寫onLayoutChildren方法,這個方法會在初始化或者Adapter數據集更新時回調,在這方法裡面,需要做以下事情:

  • 進行布局之前,我們需要調用detachAndScrapAttachedViews方法把屏幕中的Items都分離出來,內部調整好位置和數據後,再把它添加回去(如果需要的話);

  • 分離了之後,我們就要想辦法把它們再添加回去了,所以需要通過addView方法來添加,那這些View在哪裡得到呢? 我們需要調用 Recycler的getViewForPosition(int position) 方法來獲取;

  • 獲取到Item並重新添加了之後,我們還需要對它進行測量,這時候可以調用measureChild或measureChildWithMargins方法,兩者的區別我們已經了解過了,相信同學們都能根據需求選擇更合適的方法;

  • 在測量完還需要做什麼呢? 沒錯,就是布局了,我們也是根據需求來決定使用layoutDecorated還是layoutDecoratedWithMargins方法;

  • 在自定義ViewGroup中,layout完就可以運行看效果了,但在LayoutManager還有一件非常重要的事情,就是回收了,我們在layout之後,還要把一些不再需要的Items回收,以保證滑動的流暢度。

以上內容出自陳小緣的自定義LayoutManager第十一式之飛龍在天(https://blog.csdn.net/u011387817/article/details/81875021)。

布局實現

再看下相關參數:

如果去掉itemView的縮放,透明度動畫,那麼效果是這樣的:

如何利用 Android 自定義控制項實現炫酷的動畫?|CSDN 博文精選

看到的效果與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) {
//----------------1、邊界檢測-----------------
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 = ;
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;

//----------------3、開始布局-----------------
for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
View item;
if (i == tempPosition && tempView != ) {
// 如果初始化數據時已經取了一個臨時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中(https://blog.csdn.net/ccy0122/article/details/90515386)使用的回收技巧:

/**
* @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);
}
}

回收復用這裡就不驗證了,感興趣的小夥伴可自行驗證。

如何利用 Android 自定義控制項實現炫酷的動畫?|CSDN 博文精選

動畫效果

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、滾動停止後自動選中

監聽 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;
}

2、點擊非焦點view自動將其選中為焦點view

我們可以直接拿到 view 的 position,直接調用 smoothScrollToPosition 方法,就可以實現自動選中為焦點。

中間view覆蓋在兩邊view之上,效果是這樣的:

如何利用 Android 自定義控制項實現炫酷的動畫?|CSDN 博文精選

從效果中可以看出,索引為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) {
//省略 ......
//----------------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;
}

文章到這裡就差不多要結束了。

源碼地址:https://github.com/HpWens/MeiWidgetView。

CSDN博客原文:https://blog.csdn.net/u012551350/article/details/93971801,歡迎大家入駐 CSDN 博客。

【END】

如何利用 Android 自定義控制項實現炫酷的動畫?|CSDN 博文精選

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 CSDN 的精彩文章:

大賽響鑼、Call 你來戰……對面的開發者看過來
智能家居不「智能」

TAG:CSDN |