想經濟上支持我 or 想經過視頻看我是怎麼實現的:git
edu.csdn.net/course/deta…github
前幾天看有人實現了仿人人美劇的訂閱界面,不過在細節之處以及實現方式我我的認爲都不是最佳的姿式。
因而我也動手擼了一個,還順帶擼了個探探的界面,先看GIF:
canvas
這裏吐個槽,探探這種設計真的像皇帝翻牌子的感受,不喜歡左滑,喜歡右滑。markdown
人人影視版特色(需求):app
除上述動畫特色,探探版特色(需求):ide
咱們的效果,基本上和原版一致了,寫起來怎麼樣呢?
我不是標題黨,如標題所說:函數
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層,你只須要修改一個參數便可,效果如圖:
oop
正確的姿式就是:佈局
LayoutManager
實現卡片層疊佈局,值得注意的是,只layout出界面上可能會看見的那些View。ItemTouchHelper
,它自己實現了拖拽&滑動刪除邏輯,咱們只須要在onChildDraw()
中繪製動畫和onSwiped()
中處理數據集(循環or刪除)。因此本文也算是填了LayoutManger系列的坑,實現了一個酷炫效果的佈局。
Let's Go!
轉載請標明出處: juejin.cn/post/684490…
juejin.cn/post/684490…
本文出自:【張旭童的稀土掘金】(juejin.cn/user/905653…)
代碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/Z…
其實本例中的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之間的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
的基礎知識,建議你們自行學習,網上文章不少,我簡單介紹一下,
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........
哎喲呵,十分鐘過去了,我還在滑動看美女 忘記了要幹什麼,被女票看到胖揍了我一頓。
好的,我捂着臉繼續分析。
探探和人人影視有兩點不一樣:
感受也是炒雞簡單,來吧。五分鐘擼完吃外賣。修改點:
onChildDraw()
裏,按比例修改TopView的Rotate & Alpha還有一點小不一樣,上滑下滑再也不能刪除,因此咱們構造時只傳入左右便可:
ItemTouchHelper.Callback callback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT)複製代碼
略
在上文人人影視的基礎上擴展,上文的效果,對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;複製代碼
轉載請標明出處:
juejin.cn/post/684490…
本文出自:【張旭童的稀土掘金】(juejin.cn/user/905653…)
代碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/Z…
剛建了個QQ搞基交流羣:557266366裏面如今沒有人。嗯,就這樣吧。