由旋转画廊,看自定义RecyclerView.LayoutManager
一、簡介
前段時間需要一個旋轉木馬效果用于展示圖片,于是第一時間在github上找了一圈,找了一個還不錯的控件,但是使用起來有點麻煩,始終覺得很不爽,所以尋思著自己做一個輪子。想起旋轉畫廊的效果不是和橫向滾動列表非常相似嗎?那么是否可以利用RecycleView實現呢?
RecyclerView是google官方在support.v7中提供的一個控件,是ListView和GridView的升級版。該控件具有高度靈活、高度解耦的特性,并且還提供了添加、刪除、移動的動畫支持,分分鐘讓你作出漂亮的列表、九宮格、瀑布流。相信使用過該控件的人必定愛不釋手。
先來看下如何簡單的使用RecyclerView
RecyclerView listView = (RecyclerView)findViewById(R.id.lsit); listView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); listView.setAdapter(new Adapter());復制代碼就是這么簡單:
其中,LayoutManager用于指定布局管理器,官方已經提供了幾個布局管理器,可以滿足大部分需求:
- LinearLayoutManger:提供了豎向和橫向線性布局(可實現ListView功能)
- GridLayoutManager:表格布局(可實現GridView功能)
- StaggeredGridLayoutManager:瀑布流布局
Adapter的定義與ListView的Adapter用法類似。
重點來看LayoutManage。
LinearLayoutManager與其他幾個布局管理器都是繼承了該類,從而實現了對每個Item的布局。那么我們也可以通過自定義LayoutManager來實現旋轉畫廊的效果。
看下要實現的效果:
旋轉畫廊.gif二、自定義LayoutManager
首先,我們來看看,自定義LayoutManager是什么樣的流程:
處理滑動事件(包括橫向和豎向滾動、滑動結束、滑動到指定位置等)
i.橫向滾動:重寫scrollHorizontallyBy()方法
ii.豎向滾動:重寫scrollVerticallyBy()方法
iii.滑動結束:重寫onScrollStateChanged()方法
iiii.指定滾動位置:重寫scrollToPosition()和smoothScrollToPosition()方法
接下來,就來實現這個流程
第一步,定義CoverFlowLayoutManager繼承RecyclerView.LayoutManager
public class CoverFlowLayoutManger extends RecyclerView.LayoutManager {public RecyclerView.LayoutParams generateDefaultLayoutParams() {return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);} }復制代碼繼承LayoutManager后,會強制要求必須實現generateDefaultLayoutParams()方法,提供默認的Item布局參數,設置為Wrap_Content,由Item自己決定。
第二步,計算Item的位置和布局,并根據顯示區域回收出界的Item
i.計算Item位置
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {//如果沒有item,直接返回//跳過preLayout,preLayout主要用于支持動畫if (getItemCount() <= 0 || state.isPreLayout()) {mOffsetAll = 0;return;}mAllItemFrames.clear(); //mAllItemFrame存儲了所有Item的位置信息mHasAttachedItems.clear(); //mHasAttachedItems存儲了Item是否已經被添加到控件中//得到子view的寬和高,這里的item的寬高都是一樣的,所以只需要進行一次測量View scrap = recycler.getViewForPosition(0);addView(scrap);measureChildWithMargins(scrap, 0, 0);//計算測量布局的寬高mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);//計算第一個Item X軸的起始位置坐標,這里第一個Item居中顯示mStartX = Math.round((getHorizontalSpace() - mDecoratedChildWidth) * 1.0f / 2);//計算第一個Item Y軸的啟始位置坐標,這里為控件豎直方向居中mStartY = Math.round((getVerticalSpace() - mDecoratedChildHeight) *1.0f / 2);float offset = mStartX; //item X軸方向的位置坐標for (int i = 0; i < getItemCount(); i++) { //存儲所有item具體位置Rect frame = mAllItemFrames.get(i);if (frame == null) {frame = new Rect();}frame.set(Math.round(offset), mStartY, Math.round(offset + mDecoratedChildWidth), mStartY + mDecoratedChildHeight);mAllItemFrames.put(i, frame); //保存位置信息mHasAttachedItems.put(i, false);//計算Item X方向的位置,即上一個Item的X位置+Item的間距offset = offset + getIntervalDistance();}detachAndScrapAttachedViews(recycler);layoutItems(recycler, state, SCROLL_RIGHT); //布局ItemmRecycle = recycler; //保存回收器mState = state; //保存狀態 }復制代碼以上,我們為Item的布局做了準備,計算了Item的寬高,以及首個Item的起始位置,并根據設置的Item間,計算每個Item的位置,并保存了下來。
接下來,來看看layoutItems()方法做了什么。
ii.布局和回收Item
private void layoutItems(RecyclerView.Recycler recycler,RecyclerView.State state, int scrollDirection) {if (state.isPreLayout()) return;Rect displayFrame = new Rect(mOffsetAll, 0, mOffsetAll + getHorizontalSpace(), getVerticalSpace()); //獲取當前顯示的區域//回收或者更新已經顯示的Itemfor (int i = 0; i < getChildCount(); i++) {View child = getChildAt(i);int position = getPosition(child);if (!Rect.intersects(displayFrame, mAllItemFrames.get(position))) {//Item沒有在顯示區域,就說明需要回收removeAndRecycleView(child, recycler); //回收滑出屏幕的ViewmHasAttachedItems.put(position, false);} else { //Item還在顯示區域內,更新滑動后Item的位置layoutItem(child, mAllItemFrames.get(position)); //更新Item位置mHasAttachedItems.put(position, true);}}for (int i = 0; i < getItemCount(); i++) {if (Rect.intersects(displayFrame, mAllItemFrames.get(i)) &&!mHasAttachedItems.get(i)) { //加載可見范圍內,并且還沒有顯示的ItemView scrap = recycler.getViewForPosition(i);measureChildWithMargins(scrap, 0, 0);if (scrollDirection == SCROLL_LEFT || mIsFlatFlow) {//向左滾動,新增的Item需要添加在最前面addView(scrap, 0);} else { //向右滾動,新增的item要添加在最后面addView(scrap);}layoutItem(scrap, mAllItemFrames.get(i)); //將這個Item布局出來mHasAttachedItems.put(i, true);}} }private void layoutItem(View child, Rect frame) {layoutDecorated(child,frame.left - mOffsetAll,frame.top,frame.right - mOffsetAll,frame.bottom);child.setScaleX(computeScale(frame.left - mOffsetAll)); //縮放child.setScaleY(computeScale(frame.left - mOffsetAll)); //縮放 }復制代碼第一個方法:在layoutItems()中
mOffsetAll記錄了當前控件滑動的總偏移量,一開始mOffsetAll為0。
在第一個for循環中,先判斷已經顯示的Item是否已經超出了顯示范圍,如果是,則回收改Item,否則更新Item的位置。
在第二個for循環中,遍歷了所有的Item,然后判斷Item是否在當前顯示的范圍內,如果是,將Item添加到控件中,并根據Item的位置信息進行布局。
第二個方法:在layoutItem()中
調用了父類方法layoutDecorated對Item進行布局,其中mOffsetAll為整個旋轉控件的滑動偏移量。
布局好后,對根據Item的位置對Item進行縮放,中間最大,距離中間越遠,Item越小。
第三步,處理滑動事件
i. 處理橫向滾動事件
由于旋轉畫廊只需橫向滾動,所以這里只處理橫向滾動事件復制代碼 public boolean canScrollHorizontally() {return true; } public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,RecyclerView.State state) {if (mAnimation != null && mAnimation.isRunning()) mAnimation.cancel();int travel = dx;if (dx + mOffsetAll < 0) {travel = -mOffsetAll;} else if (dx + mOffsetAll > getMaxOffset()){travel = (int) (getMaxOffset() - mOffsetAll);}mOffsetAll += travel; //累計偏移量layoutItems(recycler, state, dx > 0 ? SCROLL_RIGHT : SCROLL_LEFT);return travel; }復制代碼首先,需要告訴RecyclerView,我們需要接收橫向滾動事件。
當用戶滑動控件時,會回調scrollHorizontallyBy()方法對Item進行重新布局。
我們先忽略第一句代碼,mAnimation用于處理滑動停止后Item的居中顯示。
然后,我們判斷了滑動距離dx,加上之前已經滾動的總偏移量mOffsetAll,是否超出所有Item可以滑動的總距離(總距離= Item個數 * Item間隔),對滑動距離進行邊界處理,并將實際滾動的距離累加到mOffsetAll中。
當dx>0時,控件向右滾動,即<--;當dx<0時,控件向左滾動,即-->復制代碼接著,調用先前已經寫好的布局方法layoutItems(),對Item進行重新布局。
最后,返回實際滑動的距離。
ii.處理滑動結束事件,將Item居中顯示
public void onScrollStateChanged(int state) {super.onScrollStateChanged(state);switch (state){case RecyclerView.SCROLL_STATE_IDLE://滾動停止時fixOffsetWhenFinishScroll();break;case RecyclerView.SCROLL_STATE_DRAGGING://拖拽滾動時break;case RecyclerView.SCROLL_STATE_SETTLING://動畫滾動時break;} }private void fixOffsetWhenFinishScroll() {//計算滾動了多少個Itemint scrollN = (int) (mOffsetAll * 1.0f / getIntervalDistance()); //計算scrollN位置的Item超出控件中間位置的距離float moreDx = (mOffsetAll % getIntervalDistance());if (moreDx > (getIntervalDistance() * 0.5)) { //如果大于半個Item間距,則下一個Item居中scrollN ++;}//計算最終的滾動距離int finalOffset = (int) (scrollN * getIntervalDistance());//啟動居中顯示動畫startScroll(mOffsetAll, finalOffset);//計算當前居中的Item的位置mSelectPosition = Math.round (finalOffset * 1.0f / getIntervalDistance()); }復制代碼通過onScrollStateChanged()方法,可以監聽到控件的滾動狀態,這里我們只需處理滑動停止事件。
在fixOffsetWhenFinishScroll()中,getIntervalDistance()方法用于獲取Item的間距。
根據滾動的總距離除以Item的間距計算出總共滾動了多少個Item,然后啟動居中顯示動畫。
動畫很簡單,從滑動停止的位置,不斷刷新Item布局,直到滾動到最終位置。
iii.處理指定位置滾動事件
public void scrollToPosition(int position) {if (position < 0 || position > getItemCount() - 1) return;mOffsetAll = calculateOffsetForPosition(position);if (mRecycle == null || mState == null) {//如果RecyclerView還沒初始化完,先記錄下要滾動的位置mSelectPosition = position;} else {layoutItems(mRecycle, mState, position > mSelectPosition ? SCROLL_RIGHT : SCROLL_LEFT);} } public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {if (position < 0 || position > getItemCount() - 1) return;int finalOffset = calculateOffsetForPosition(position);if (mRecycle == null || mState == null) {//如果RecyclerView還沒初始化完,先記錄下要滾動的位置mSelectPosition = position;} else {startScroll(mOffsetAll, finalOffset);} }復制代碼scrollToPosition()用于不帶動畫的Item直接跳轉
smoothScrollToPosition()用于帶動畫Item滑動
也很簡單,計算要跳轉Item的所在位置需要滾動的距離,如果不需要動畫,則直接對Item進行布局,否則啟動滑動動畫。
第四,處理重新設置Adapter
當重新調用RecyclerView的setAdapter時,需要對LayoutManager的所有狀態進行重置
public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {removeAllViews();mRecycle = null;mState = null;mOffsetAll = 0;mSelectPosition = 0;mLastSelectPosition = 0;mHasAttachedItems.clear();mAllItemFrames.clear(); }復制代碼清空所有的Item,已經所有存放的位置信息和狀態。
最后RecyclerView會重新調用onLayoutChildren()進行布局。
以上,就是自定義LayoutManager的流程,但是,為了實現旋轉畫廊的功能,只自定義了LayoutManager是不夠的。旋轉畫廊中,每個Item是有重疊部分的,因此會有Item繪制順序的問題,如果不對Item的繪制順序進行調整,將出現中間Item被旁邊Item遮擋的問題。
為了解決這個問題,需要重寫RecyclerView的getChildDrawingOrder()方法,對Item的繪制順序進行調整。
三、重寫RecyclerView
這里簡單看下如何如何改變Item的繪制順序,具體可以查看源碼復制代碼 public class RecyclerCoverFlow extends RecyclerView {public RecyclerCoverFlow(Context context) {super(context);init();}public RecyclerCoverFlow(Context context, @Nullable AttributeSet attrs) {super(context, attrs);init();}public RecyclerCoverFlow(Context context, @Nullable AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);init();}private void init() {......setChildrenDrawingOrderEnabled(true); //開啟重新排序......}protected int getChildDrawingOrder(int childCount, int i) {//計算正在顯示的所有Item的中間位置int center = getCoverFlowLayout().getCenterPosition()- getCoverFlowLayout().getFirstVisiblePosition();if (center < 0) center = 0;else if (center > childCount) center = childCount;int order;if (i == center) {order = childCount - 1;} else if (i > center) {order = center + childCount - 1 - i;} else {order = i;}return order;} }復制代碼首先,需要調用setChildrenDrawingOrderEnabled(true); 開啟重新排序功能。
接著,在getChildDrawingOrder()中,childCount為當前已經顯示的Item數量,i為item的位置。
旋轉畫廊中,中間位置的優先級是最高的,兩邊item隨著遞減。因此,在這里,我們通過以上定義的LayoutManager計算了當前顯示的Item的中間位置,然后對Item的繪制進行了重新排序。
最后將計算出來的順序優先級返回給RecyclerView進行繪制。
總結
以上,通過旋轉畫廊控件,我們過了一遍自定義LayoutManager的流程。當然RecyclerView的強大遠遠不至于此,結合LayoutManager的橫豎滾動事件還可以做出更多有趣的效果。
最后,奉上源碼。
轉載于:https://juejin.im/post/59c3416a6fb9a00a562e8526
總結
以上是生活随笔為你收集整理的由旋转画廊,看自定义RecyclerView.LayoutManager的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用Azure应用服务本地验证安卓脸书
- 下一篇: 《Java核心技术 卷Ⅱ 高级特性(原书