想經濟上支援我 or 想通過視訊看我是怎麼實現的:
概述
前幾天看有人實現了仿人人美劇的訂閱介面,不過在細節之處以及實現方式我個人認為都不是最佳的姿勢。
於是我也動手擼了一個,還順帶擼了個探探的介面,先看GIF:
這裡吐個槽,探探這種設計真的像皇帝翻牌子的感覺,不喜歡左滑,喜歡右滑。
人人影視版特點(需求):
- 動畫:最多可見的這四層,在頂層卡片滑動時,每一層都會位移&放大動畫,有種補充到頂層的感覺。
- 動畫:鬆手時,如果未被判定為刪除,則會有頂層以下每一層卡片收縮回原位的動畫。
- 無限迴圈:模仿人人影視,頂層卡片被刪除後,補充到最底層。
除上述動畫特點,探探版特點(需求):
- Roate的變化:左右滑動時,頂層卡片會慢慢旋轉,到閾值max大概十五度。
- Alpha的變化:左滑時頂層卡片的刪除按鈕會慢慢顯現,右滑時愛心按鈕會慢慢顯現。
- 顯然,鬆手時,以上動畫也需要復位。
我們的效果,基本上和原版一致了,寫起來怎麼樣呢?
我不是標題黨,如標題所說:
- 簡單:思路簡單清晰易理解
- 優雅:效能沒有任何隱患,
LayoutManager
只會載入顯示螢幕上可見的數量的View。 - 快速:利用
ItemTouchHelper
處理拖拽&滑動刪除邏輯,核心程式碼不超過50行。且經過封裝,四行程式碼就可以用。
伸手黨福利:
如果懶得看這麼多文字只想用,直接移步gayhub,gradle匯入相關檔案or複製。然後如下,搞定。
mRv.setLayoutManager(new OverLayCardLayoutManager());
CardConfig.initConfig(this);
ItemTouchHelper.Callback callback = new RenRenCallback(mRv, mAdapter, mDatas);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(mRv);複製程式碼
而且我將一些參
數都以變數形式計算,這樣就做到了可配置,假如老闆讓你一開始多顯示幾層卡片,例如6層,你只需要修改一個引數即可,效果如圖:
正確的姿勢
正確的姿勢就是:
- 利用
LayoutManager
實現卡片層疊佈局,值得注意的是,只layout出介面上可能會看見的那些View。 - 搭配
ItemTouchHelper
,它本身實現了拖拽&滑動刪除邏輯,我們只需要在onChildDraw()
中繪製動畫和onSwiped()
中處理資料集(迴圈or刪除)。
所以本文也算是填了LayoutManger系列的坑,實現了一個酷炫效果的佈局。
Let's Go!
轉載請標明出處: gold.xitu.io/post/585682…
gold.xitu.io/post/585682…
本文出自:【張旭童的稀土掘金】(gold.xitu.io/user/56de21…)
程式碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/Z…
LayoutManager的實現卡片層疊
其實本例中的LayoutManager
十分簡單,因為ItemTouchHelper
的存在,LayoutManager
根本不需要處理它的滑動事件,而LayoutManager
中最難寫的就是在滑動時的View
回收和複用,以及layout
新View
的處理。
關於LayoutManager的基礎知識和鋪墊,我就不再贅述,可參考我以前的文章:LayoutManager實現流式佈局
唯一注意事項
但是即便如此,還是有一個唯一的注意事項。我們只layout
出介面上可能會看見的那些View
即可。
因為考慮到動畫,所以是可能會看見。
我們看人人美劇的介面:
初始化時,介面上可見三個View
,我們分別起名:TopView,Top-1View,Top-2View
。其中TopView
完全可見,Top-1View,Top-2View
只有下邊緣可見。
如文首GIF,滑動TopView
時,Top-1View,Top-2View
開始慢慢放大,並且向上位移,直至填充至它們各自上層的View。這時候露出了Top-3View
。
所以我們在書寫LayoutManager
的onLayoutChildren()
方法時,只要layout
出當前資料集最後四個View即可。
前文提到的引數配置如下:
包括一些配置
- 介面最多顯示幾個View
每一級View之間的Scale差異、translationY等等
public class CardConfig { //螢幕上最多同時顯示幾個Item public static int MAX_SHOW_COUNT; //每一級Scale相差0.05f,translationY相差7dp左右 public static float SCALE_GAP; public static int TRANS_Y_GAP; public static void initConfig(Context context) { MAX_SHOW_COUNT = 6; SCALE_GAP = 0.05f; TRANS_Y_GAP = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, context.getResources().getDisplayMetrics()); } }複製程式碼
LayoutManager全部程式碼如下,佈滿註釋,如果看不懂,建議閱讀前置文章LayoutManger系列:
public class OverLayCardLayoutManager extends RecyclerView.LayoutManager {
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
int itemCount = getItemCount();
if (itemCount >= MAX_SHOW_COUNT) {
//從可見的最底層View開始layout,依次層疊上去
for (int position = itemCount - MAX_SHOW_COUNT; position < itemCount; position++) {
View view = recycler.getViewForPosition(position);
addView(view);
measureChildWithMargins(view, 0, 0);
int widthSpace = getWidth() - getDecoratedMeasuredWidth(view);
int heightSpace = getHeight() - getDecoratedMeasuredHeight(view);
//我們在佈局時,將childView居中處理,這裡也可以改為只水平居中
layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2,
widthSpace / 2 + getDecoratedMeasuredWidth(view),
heightSpace / 2 + getDecoratedMeasuredHeight(view));
/**
* TopView的Scale 為1,translationY 0
* 每一級Scale相差0.05f,translationY相差7dp左右
*
* 觀察人人影視的UI,拖動時,topView被拖動,Scale不變,一直為1.
* top-1View 的Scale慢慢變化至1,translation也慢慢恢復0
* top-2View的Scale慢慢變化至 top-1View的Scale,translation 也慢慢變化只top-1View的translation
* top-3View的Scale要變化,translation巋然不動
*/
//第幾層,舉例子,count =7, 最後一個TopView(6)是第0層,
int level = itemCount - position - 1;
//除了頂層不需要縮小和位移
if (level > 0 /*&& level < mShowCount - 1*/) {
//每一層都需要X方向的縮小
view.setScaleX(1 - SCALE_GAP * level);
//前N層,依次向下位移和Y方向的縮小
if (level < MAX_SHOW_COUNT - 1) {
view.setTranslationY(TRANS_Y_GAP * level);
view.setScaleY(1 - SCALE_GAP * level);
} else {//第N層在 向下位移和Y方向的縮小的成都與 N-1層保持一致
view.setTranslationY(TRANS_Y_GAP * (level - 1));
view.setScaleY(1 - SCALE_GAP * (level - 1));
}
}
}
}
}
}複製程式碼
擼到這裡,我們的靜態介面已經成型,下面讓我們動起來:
ItemTouchHelper實現炫動滑動:
ItemTouchHelper
的基礎知識,建議大家自行學習,網上文章很多,我簡單介紹一下,
This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView.
It works with a RecyclerView and a Callback class, which configures what type of interactions
are enabled and also receives events when user performs these actions.
Depending on which functionality you support, you should override
{@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder)} and / or
{@link Callback#onSwiped(ViewHolder, int)}.
翻譯 + 總結:
這貨是一個工具類,為RecyclerView擴充套件滑動消失(刪除)和drag & drop效果的。
它需要和RecyclerView、Callback 一起工作。Callback 類裡定義了 允許哪些互動,並且會接收到對應的互動事件
根據你需要哪種功能(滑動消失(刪除)和drag & drop),你需要重寫
Callback#onMove(RecyclerView, ViewHolder, ViewHolder)-----drag & drop
Callback#onSwiped(ViewHolder, int) 方法。 -----滑動消失(刪除)
總結一下入門級用法如下,三個步驟:
定義一個Callback:
ItemTouchHelper.Callback callback = new ItemTouchHelper.SimpleCallback(int,int)
,這兩個int分別代表要 監聽哪幾個方向上的拖拽、滑動事件。 常用:ItemTouchHelper.DOWN | ItemTouchHelper.UP | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT
將Callback傳給ItemTouchHelper:
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
關聯ItemTouchHelper和RecyclerView:
itemTouchHelper.attachToRecyclerView(mRv)
這三個步驟做完後,ItemTouchHelper就會自動幫我們完成 滑動消失(刪除)和drag & drop 的功能。
滑動刪除
我們本例中,需要的是滑動消失(刪除) ,所以我們的Callback
不需要關注onMove()
方法。
且我們需要上下左右滑動都可以刪除的效果。
則如下構造Callback,傳入上下左右:
ItemTouchHelper.Callback callback = new ItemTouchHelper.SimpleCallback(0,
ItemTouchHelper.DOWN | ItemTouchHelper.UP | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT)複製程式碼
onSwiped()
方法,是滑動刪除動作已經發生後回撥的,即,我們先滑動卡片,然後鬆手,此時ItemTouchHelper
判斷我們的手勢是刪除手勢,會自動對這個卡片執行丟出螢幕外的動畫,同時回撥onSwiped()
方法。
所以我們需要在其中如下寫:
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
//★實現迴圈的要點
SwipeCardBean remove = mDatas.remove(viewHolder.getLayoutPosition());
mDatas.add(0, remove);
mAdapter.notifyDataSetChanged();
}複製程式碼
在這裡我們完成了迴圈的操作:
- 利用當前被刪除的
View
的ViewHolder
拿到Position
- 刪除資料集中對應
Position
的資料來源 - 同時將該資料來源插入資料集中的首位。
- 呼叫
notifyDataSetChanged()
,通知列表重新整理
如此我們便完成了,迴圈列表的需求。
這裡提一下為什麼我們要呼叫notifyDataSetChanged()
。
看官方文件:
ItemTouchHelper moves the items' translateX/Y properties to reposition them
即ItemTouchHelper實現的滑動刪除,其實只是隱藏了這個滑動的View。並不是真的刪除了。
在LayoutManager實現流式佈局一文第五節中,我們已經提到,notifyDataSetChanged()
會回撥onLayoutChildren()
這個函式,而在這個函式中,我們會重新佈局,即真正的移除(不再layout)滑動掉的View,同時會補充進新的最底層的View。
嗯,JavaBean也看一眼吧,沒亮點:
public class SwipeCardBean {
private int postition;//位置
private String url;
private String name;
}複製程式碼
我們寫到這裡已經完成了滑動刪除的功能,其實我們什麼都沒有寫是吧,複雜的判斷都由ItemTouchHelper幫我們處理掉了,例如速度、滑動距離是否到達刪除閾值,刪除成功移除的動畫、取消刪除復位的動畫等等。
所以我說利用ItemTouchHelper才是正確的姿勢,因為很簡單&快速。
下面我們來實現滑動時的動畫。
滑動時動畫
我們需要重寫Callback
的onChildDraw()
方法,這個方法引數較多:
* @param c The canvas which RecyclerView is drawing its children
* @param recyclerView The RecyclerView to which ItemTouchHelper is attached to
* @param viewHolder The ViewHolder which is being interacted by the User or it was
interacted and simply animating to its original position
* @param dX The amount of horizontal displacement caused by user's action
* @param dY The amount of vertical displacement caused by user's action
* @param actionState 是拖拽還是滑動事件 The type of interaction on the View. Is either {@link #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}.
* @param isCurrentlyActive 事件是使用者產生還是動畫產生的 True if this view is currently being controlled by the user or false it is simply animating back to its original state. 複製程式碼
對我們比較有用的有dX dX
,可以判斷滑動方向,以及計算滑動的比例,從而控制縮放、位移動畫的程度。
本文如下編寫,對View的縮放、位移,其實是對LayoutManager裡的操作的逆操作,值得注意的是最後一層,即top-3View
在Y軸上是保持不變的:
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
//先根據滑動的dxdy 算出現在動畫的比例係數fraction
double swipValue = Math.sqrt(dX * dX + dY * dY);
double fraction = swipValue / getThreshold(viewHolder);
//邊界修正 最大為1
if (fraction > 1) {
fraction = 1;
}
//對每個ChildView進行縮放 位移
int childCount = recyclerView.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = recyclerView.getChildAt(i);
//第幾層,舉例子,count =7, 最後一個TopView(6)是第0層,
int level = childCount - i - 1;
if (level > 0) {
child.setScaleX((float) (1 - SCALE_GAP * level + fraction * SCALE_GAP));
if (level < MAX_SHOW_COUNT - 1) {
child.setScaleY((float) (1 - SCALE_GAP * level + fraction * SCALE_GAP));
child.setTranslationY((float) (TRANS_Y_GAP * level - fraction * TRANS_Y_GAP));
}
}
}
}複製程式碼
getThreshold(viewHolder)
函式,返回是否可以被回收掉的閾值,關於它為什麼這麼寫,我是從原始碼裡找到的,本末會講解:
//水平方向是否可以被回收掉的閾值
public float getThreshold(RecyclerView.ViewHolder viewHolder) {
return mRv.getWidth() * getSwipeThreshold(viewHolder);
}複製程式碼
探探效果的實現
一開始文章擼到這裡應該結束了,群裡出來一個馬小跳,告訴我探探和這略有不同,希望我一併實現。
嗯,好吧。表示沒聽說過探探,那我先去下載一個看看吧。
loading-install-open........
哎喲呵,十分鐘過去了,我還在滑動看美女 忘記了要幹什麼,被女票看到胖揍了我一頓。
好的,我捂著臉繼續分析。
探探和人人影視有兩點不同:
- Roate的變化:左右滑動時,頂層卡片會慢慢旋轉,到閾值max大概十五度。
- Alpha的變化:左滑時頂層卡片的刪除按鈕會慢慢顯現,右滑時愛心按鈕會慢慢顯現。
感覺也是炒雞簡單,來吧。五分鐘擼完吃外賣。修改點:
- 在layout佈局新增『 X 』&『 愛心 』。
- 在
onChildDraw()
裡,按比例修改TopView的Rotate & Alpha
監聽方向
還有一點小不同,上滑下滑不再能刪除,所以我們構造時只傳入左右即可:
ItemTouchHelper.Callback callback = new ItemTouchHelper.SimpleCallback(0,
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT)複製程式碼
佈局新增兩個按鈕
略
onChildDraw()
在上文人人影視的基礎上擴充套件,上文的效果,對TopView
是不做任何操作的。這裡只需要再對TopView
做額外操作即可:
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
...
for (int i = 0; i < childCount; i++) {
View child = recyclerView.getChildAt(i);
//第幾層,舉例子,count =7, 最後一個TopView(6)是第0層,
int level = childCount - i - 1;
if (level > 0) {
...
} else {
//探探只是第一層加了rotate & alpha的操作
//不過他區分左右
float xFraction = dX / getThreshold(viewHolder);
//邊界修正 最大為1
if (xFraction > 1) {
xFraction = 1;
} else if (xFraction < -1) {
xFraction = -1;
}
//rotate
child.setRotation(xFraction * MAX_ROTATION);
//自己感受一下吧 Alpha
if (viewHolder instanceof ViewHolder) {
ViewHolder holder = (ViewHolder) viewHolder;
if (dX > 0) {
//露出左邊,比心
holder.setAlpha(R.id.iv_love, xFraction);
} else {
//露出右邊,滾犢子
holder.setAlpha(R.id.iv_del, -xFraction);
}
}
}
}
}複製程式碼
實現完後,我以為結束了,結果比我們想象的還要複雜一丟丟。因為此時刪除後,notifyDataSetChanged()
重新整理介面,而TopView
還是傾斜的,愛心、刪除圖示也是出現的。這顯然與預期不符。所以我們需要在onSwiped()
裡將其復位:
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
...
//探探只是第一層加了rotate & alpha的操作
//對rotate進行復位
viewHolder.itemView.setRotation(0);
//自己感受一下吧 Alpha
if (viewHolder instanceof ViewHolder) {
ViewHolder holder = (ViewHolder) viewHolder;
holder.setAlpha(R.id.iv_love, 0);
holder.setAlpha(R.id.iv_del, 0);
}
}複製程式碼
Ok,大功告成。效果和文首一樣,盡情去跟產品UI嘚瑟吧。
閾值的尋找之路
閾值的尋找,花費了我一些時間,因為我想做到和系統的行為保持一致。
即,當刪除、喜歡圖示全顯,當Top-1View
顯示完畢時,鬆手 TopView
會回收。
這就決定了我們的縮放、位移的閾值不能隨便定,所以我們必須去原始碼裡找答案。
//水平方向是否可以被回收掉的閾值
public float getThreshold(RecyclerView.ViewHolder viewHolder) {
return mRv.getWidth() * getSwipeThreshold(viewHolder);
}複製程式碼
因為滑動刪除操作是touch事件導致的,且應該是ACTION_UP時,觸發的,
所以在ItemTouchHelper
原始碼裡,搜尋onTouch字樣:
定位到:mOnItemTouchListener
,->
繼續定位其中的onTouchEvent()
,->case MotionEvent.ACTION_UP:
,->void select(ViewHolder selected, int actionState)
->
在這裡我注意到有一句程式碼:animationType = ANIMATION_TYPE_SWIPE_SUCCESS;
這說明刪除成功,它的觸發條件是:if (swipeDir > 0)
->swipeDir
的值: final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0
: swipeIfNecessary(prevSelected);
->int swipeIfNecessary(ViewHolder viewHolder)
->
if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) {
return swipeDir;
}複製程式碼
如此返回1的話,則->checkHorizontalSwipe(viewHolder, flags)
->
在其中終於找到原始碼裡閾值的獲取之處:
final float threshold = mRecyclerView.getWidth() * mCallback
.getSwipeThreshold(viewHolder);複製程式碼
於是我就直接複製出來。
總結
程式碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/Z…
想經濟上支援我 or 想通過視訊看我是怎麼實現的:
edu.csdn.net/course/deta…
本文利用LayoutManager
載入顯示螢幕上可見的數量的View,搭配ItemTouchHelper
處理拖拽&滑動刪除邏輯,核心程式碼不超過50行。且經過封裝,四行程式碼就可以用。
記住LayoutManager
,我們寫,只layout出介面上可能會看見的那些View即可。
關於ItemTouchHelper
,它本身實現了拖拽&滑動刪除邏輯,我們只需要在onChildDraw()
中繪製動畫和onSwiped()
中處理資料集(迴圈or刪除)即可。
以後老闆讓你做這種效果,你只需要:
CardConfig.initConfig(this);
ItemTouchHelper.Callback callback = new RenRenCallback(mRv, mAdapter, mDatas);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(mRv);複製程式碼
如果需要定製特殊的引數,例如顯示6層:
CardConfig.MAX_SHOW_COUNT = 6;複製程式碼
轉載請標明出處:
gold.xitu.io/post/585682…
本文出自:【張旭童的稀土掘金】(gold.xitu.io/user/56de21…)
程式碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/Z…
剛建了個QQ搞基交流群:
557266366
裡面現在沒有人。
嗯,就這樣吧。