Android仿豆瓣书影音频道推荐表单堆叠列表RecyclerView-LayoutManager
Android仿豆瓣書影音頻道推薦表單堆疊列表RecyclerView-LayoutManager
項目地址:https://github.com/CCY0122/FocusLayoutManager
效果預覽
截圖:
GIF:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-dq1PKEIw-1589702037735)(https://github.com/CCY0122/FocusLayoutManager/blob/master/pic/gif_hor_2.gif?raw=true)]
可自己監聽滾動編寫效果,如修改成仿MacOS文件瀏覽:
使用
focusLayoutManager =new FocusLayoutManager.Builder().layerPadding(dp2px(this, 14)).normalViewGap(dp2px(this, 14)).focusOrientation(FocusLayoutManager.FOCUS_LEFT).isAutoSelect(true).maxLayerCount(3).setOnFocusChangeListener(new FocusLayoutManager.OnFocusChangeListener() {@Overridepublic void onFocusChanged(int focusdPosition, int lastFocusdPosition) {}}).build(); recyclerView.setLayoutManager(focusLayoutManager);各屬性意義見圖:
注意:因為item在不同區域隨著滑動會有不同的縮放,所以實際layerPadding、normalViewGap會被縮放計算。
調整動畫效果:
new FocusLayoutManager.Builder().......setSimpleTrasitionListener(new FocusLayoutManager.SimpleTrasitionListener() {@Overridepublic float getLayerViewMaxAlpha(int maxLayerCount) {return super.getLayerViewMaxAlpha(maxLayerCount);}@Overridepublic float getLayerViewMinAlpha(int maxLayerCount) {return super.getLayerViewMinAlpha(maxLayerCount);}@Overridepublic float getLayerChangeRangePercent() {return super.getLayerChangeRangePercent();}//and more//更多可重寫方法和釋義見接口聲明}).build();自定義動畫/滾動監聽:
如果你想在滑動時不僅僅改變item的大小、透明度,你有更多的想法,可以監聽TrasitionListener,該監聽暴露了很多關鍵布局數據,
.......setSimpleTrasitionListener(null) //如果默認動畫不想要,移除之。or use removeTrasitionlistener(XXX) .addTrasitionListener(new FocusLayoutManager.TrasitionListener() {@Overridepublic void handleLayerView(FocusLayoutManager focusLayoutManager,View view, int viewLayer,int maxLayerCount, int position,float fraction, float offset) {}@Overridepublic void handleFocusingView(FocusLayoutManager focusLayoutManager,View view, int position,float fraction, float offset) {}@Overridepublic void handleNormalView(FocusLayoutManager focusLayoutManager, View view, int position, float fraction, float offset) {}})各參數意義見接口注釋。
實際上SimpleTrasitionListener內部就會被轉為TrasitionListener
解析
自定義LayoutManager基礎知識
自備。
這個項目就我學習LayoutManager的實戰項目。(斷斷續續學習過很多次,還是得實際編碼才能掌握)
推薦幾篇我覺得好的自定義LayoutManager文章:
1、 張旭童的掌握自定義LayoutManager(一) 系列開篇 常見誤區、問題、注意事項,常用API
2、張旭童的掌握自定義LayoutManager(二) 實現流式布局
3、陳小緣的自定義LayoutManager第十一式之飛龍在天
自定義LayoutManager的注意事項
上面張旭童的文章里有指出很多自定義LayoutManager的誤區、注意事項,我補充幾點:
1、不要遍歷ItemCount
這個真的,是我認為最關鍵的一個注意事項。getItemCount獲取到的是什么?是列表的總item數量,它可能有幾千條幾萬條,甚至某些情況使用者會特意重寫getItemCount將其返回為Integer.MAX_VALUE(比如為了實現無限循環輪播)。你之所以自定義LayoutManager而不自定義ViewGroup,就是為了不管itemCount多少你都能hold住。所以你不應該在布局相關代碼中遍歷ItemCount!!誠然,遍歷它往往可以獲取很多有用的數據,對后續的布局的計算、子View是否在屏幕內等判斷非常有用,但請盡量不要遍歷它(除非你的LM夠特殊)。
張旭童說的沒錯,很多文章都存在誤導,我還看到過有篇”喜歡“數很多的文章里有類似這么一段代碼:
???
對于初次布局,這不就是有多少item就onCreateViewHolder多少次了么。緩存池總數 = item總數?之后的回收復用操作也沒意義了。
2、注意調用getChildCount時機
在列表滾動時,一般都要判斷子View是否還在屏幕內,若不在了則回收。那么獲取子View的邏輯應該在detachAndScrapAttachedViews(or detachAndScrapView等)之前。見下面代碼的打印:
//分離全部的view,放入臨時緩存log("before。child count = " + getChildCount() + ";scrap count = " + recycler.getScrapList().size());detachAndScrapAttachedViews(recycler);log("after。child count = " + getChildCount() + ";scrap count = " + recycler.getScrapList().size()); //打印結果://before。child count = 5;scrap count = 0//after。child count = 0;scrap count = 5另外,不用多說,recycler.getViewForPosition應在detachAndScrapAttachedViews之后
3、回收子View小技巧
這是在陳小緣那篇文章里學到的:
可以直接把Recycler里面的mAttachedScrap全部放進mRecyclerPool中,因為我們在一開始就已經調用了detachAndScrapAttachedViews方法將當前屏幕中有效的ViewHolder全部放進了mAttachedScrap,而在重新布局的時候,有用的Holder已經被重用了,也就是拿出去了,這個mAttachedScrap中剩下的Holder,都是不需要layout的,所以可以把它們都回收進mRecyclerPool中
實用哦。
(不知道對預布局是否有影響,但我代碼中并沒有判斷過isPreLayout,也測試過notifyItemRemoved,動畫正常)
布局實現
先把上面的細節圖重新貼一下
首先無視掉view的縮放、透明度變化。那么布局其實就這樣:
我們稱一個view從”普通view“滾動到”焦點view“為一次完整的聚焦滑動所需要移動的距離,定義其為onceCompleteScrollLength。
在普通view移動了一個onceCompleteScrollLength,堆疊View只移動了一個layerPadding。核心邏輯就這一句。
我們在scrollHorizontallyBy中記錄偏移量dx,保存一個累計偏移量mHorizontalOffset,然后用該偏移量除以onceCompleteScrollLength,就知道當前已經滾動了多少個item了,換句話說就是屏幕內第一個可見view的position知道了。同時能計算出一個onceCompleteScrollLength已經滾動了的百分比fraction,再用這個百分比換算出堆疊區域和普通區域布局起始位置的偏移量,然后可以開始布局了,對于堆疊區域的view,彼此之間距離一個layerPadding,對于普通區域view,彼此之間距離一個onceCompleteScrollLength。
見代碼:
因為measure、layout調用的都是考慮了margin的api,所以布局時也要考慮到margin:
/*** 獲取某個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;}回收復用
用上面講的回收技巧:
/*** @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);}}接下來驗證下。
- 驗證1
張旭童:通過getChildCount()和recycler.getScrapList().size() 查看當前屏幕上的Item數量 和 scrapCache緩存區域的Item數量,合格的LayoutManager,childCount數量不應大于屏幕上顯示的Item數量,而scrapCache緩存區域的Item數量應該是0.
編寫log并打印:
合格。
- 驗證2
用最直接的方法,打印onCreateViewHolder、onBindViewHolder看看到底復用了沒:
在onCreateViewHolder創建view時,給他一個tag,然后onBindViewHolder中打印這個tag,以此查看是不用復用了view。打印如下
onCreateViewHolder = 1 onBindViewHolder,index = 1 onCreateViewHolder = 2 onBindViewHolder,index = 2 onCreateViewHolder = 3 onBindViewHolder,index = 3 onCreateViewHolder = 4 onBindViewHolder,index = 4 onCreateViewHolder = 5 onBindViewHolder,index = 5 onCreateViewHolder = 6 onBindViewHolder,index = 6 onCreateViewHolder = 7 onBindViewHolder,index = 7 onCreateViewHolder = 8 onBindViewHolder,index = 8 onBindViewHolder,index = 1 onBindViewHolder,index = 2 onBindViewHolder,index = 3 onBindViewHolder,index = 4 onBindViewHolder,index = 5 onBindViewHolder,index = 6 onBindViewHolder,index = 7 onBindViewHolder,index = 8 onCreateViewHolder = 9 onBindViewHolder,index = 9 onBindViewHolder,index = 2 onBindViewHolder,index = 3 onBindViewHolder,index = 1 onBindViewHolder,index = 4 onBindViewHolder,index = 5 onBindViewHolder,index = 6我測試時手機一屏內最多可見約6個,從打印中可見它最多調用了9次onCreateViewHolder,這個次數完全可以接受。并且onBindViewHolder也在復用view。完全ojbk沒得問題
動畫效果
我做的動畫,就是在滑動期間漸變view的縮放比例、透明度,使得view看上去像一層一層堆疊上去的樣子。其實就是各種y = kx + b之類的計算,因為fill系列方法中已經計算出很多有用的數據了。
我的做法是,暴露出這么個接口:
然后在fill系列方法的對應位置回調該接口即可:
/*** 變換監聽接口。*/private List<TrasitionListener> trasitionListeners; /*** 水平滾動、向左堆疊布局** @param recycler* @param state* @param dx 偏移量。手指從右向左滑動,dx > 0; 手指從左向右滑動,dx < 0;*/private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state,int dx) {//省略。。。。。 //----------------3、開始布局-----------------for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {//屬于堆疊區域if (i - mFirstVisiPos < maxLayerCount) {//省略。。。。。 if (trasitionListeners != null && !trasitionListeners.isEmpty()) {for (TrasitionListener trasitionListener : trasitionListeners) {trasitionListener.handleLayerView(this, item, i - mFirstVisiPos,maxLayerCount, i, fraction, dx);}}} else {//屬于普通區域//省略。。。。。 if (trasitionListeners != null && !trasitionListeners.isEmpty()) {for (TrasitionListener trasitionListener : trasitionListeners) {if (i - mFirstVisiPos == maxLayerCount) {trasitionListener.handleFocusingView(this, item, i, fraction, dx);} else {trasitionListener.handleNormalView(this, item, i, fraction, dx);}}}}}return dx;}然后使用者可以自己注冊該接口,天馬行空。
那么我這個項目默認的動畫具體實現是怎么樣的呢?
先這樣,再那樣,效果就出來啦:
哈哈哈。代碼中stl 存儲著堆疊區域view、焦點view、普通view的最大和最小縮放比、透明度,然后利用fraction計算出當前位置真實的縮放比、透明度設置之。
上面只貼了堆疊區域view的實現,完整實現見源碼中的TrasitionListenerConvert
自動選中
1、滾動停止后自動選中
我的實現方式是這樣的:監聽onScrollStateChanged,在滾動停止時計算出應當停留的position,再計算出停留時的mHorizontalOffset值,播放屬性動畫將當前mHorizontalOffset不斷更新至最終值即可。具體代碼參考源碼中的onScrollStateChanged和smoothScrollToPosition。
(思考:能通過自定義SnapHelper實現么?)
2、點擊非焦點view自動將其選中為焦點view
已經實現了setFocusdPosition方法。內部邏輯就是計算出實際position并調用smoothScrollToPosition或scrollToPosition 。示例代碼:
public ViewHolder(@NonNull final View itemView) {super(itemView);itemView.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {int pos = getAdapterPosition();if (pos == focusLayoutManager.getFocusdPosition()) {//是焦點view} else {focusLayoutManager.setFocusdPosition(pos, true);}}});}無限循環滾動
因為FocusLayoutManager內部沒有遍歷itemCount這種bad操作,你可以自己通過重寫getItemCount返回Integer.MAX_VALUE實現偽無限循環。示例代碼:
public void initView(){recyclerView.post(new Runnable() {@Overridepublic void run() {focusLayoutManager.scrollToPosition(1000); //差不多大行了,畢竟mHorizontalOffset是會一直累加的}});}public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {@Overridepublic void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {int realPosition = position % datas.size();Bean bean = datas.get(realPosition);//...}@Overridepublic int getItemCount() {return Integer.MAX_VALUE;}}讓開頭(堆疊數-1)個View可見
按目前布局邏輯,開頭的position = 0 到position = maxLayerCount - 1個view永遠只能在堆疊區域,沒法拉出來到焦點view。解決方式也簡單,給你的源數據開頭插入maxLayerCount - 1個假數據,然后當adapter中識別到假數據時讓其布局不可見即可
結束
剩下的三個堆疊方向的實現就是加加減減的變化,不用貼出來了。
思考:按目前代碼邏輯,onceCompleteScrollLength賦值后是固定的,即“普通區域”的view之間的距離是一樣的,這在所有item寬度(若是垂直滾動則指的是item高度)一樣的情況下沒什么問題。但如果item的寬度是不固定的,那么實際效果就不盡人意了。
那 onceCompleteScrollLength如果動態計算呢?我思考過。有很多難點。比如屏幕第一個可見view的position計算難度大大增加。。以后再說吧(逃)
給個贊唄~
給個star唄~
項目地址:https://github.com/CCY0122/FocusLayoutManager
阿里內推(長期)
可幫阿里內推。將想投的阿里招聘官網職位鏈接+簡歷簡介+備注“csdn#1”(用于讓我知道你是在哪里看到我的)發我郵箱671518768@qq.com。
tips:
1.簡歷絕對真實,背調階段查出誠信問題會被阿里拉黑。
總結
以上是生活随笔為你收集整理的Android仿豆瓣书影音频道推荐表单堆叠列表RecyclerView-LayoutManager的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: STM32F429串口设置调试笔记
- 下一篇: 初学ZYNQ(理论准备)