日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

由旋转画廊,看自定义RecyclerView.LayoutManager

發布時間:2025/3/20 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 由旋转画廊,看自定义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
  • 設置Adapter(繼承RecyclerView.Adapter)
  • 其中,LayoutManager用于指定布局管理器,官方已經提供了幾個布局管理器,可以滿足大部分需求:

    • LinearLayoutManger:提供了豎向和橫向線性布局(可實現ListView功能)
    • GridLayoutManager:表格布局(可實現GridView功能)
    • StaggeredGridLayoutManager:瀑布流布局

    Adapter的定義與ListView的Adapter用法類似。

    重點來看LayoutManage

    LinearLayoutManager與其他幾個布局管理器都是繼承了該類,從而實現了對每個Item的布局。那么我們也可以通過自定義LayoutManager來實現旋轉畫廊的效果。

    看下要實現的效果:

    旋轉畫廊.gif

    二、自定義LayoutManager

    首先,我們來看看,自定義LayoutManager是什么樣的流程:

  • 計算每個Item的位置,并對Item布局。重寫onLayoutChildren()方法
  • 處理滑動事件(包括橫向和豎向滾動、滑動結束、滑動到指定位置等)

    i.橫向滾動:重寫scrollHorizontallyBy()方法

    ii.豎向滾動:重寫scrollVerticallyBy()方法

    iii.滑動結束:重寫onScrollStateChanged()方法

    iiii.指定滾動位置:重寫scrollToPosition()和smoothScrollToPosition()方法

  • 重用和回收Item
  • 重設Adapter 重寫onAdapterChanged()方法
  • 接下來,就來實現這個流程

    第一步,定義CoverFlowLayoutManager繼承RecyclerView.LayoutManager

    public class CoverFlowLayoutManger extends RecyclerView.LayoutManager {@Overridepublic 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位置
    @Override 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. 處理橫向滾動事件
    由于旋轉畫廊只需橫向滾動,所以這里只處理橫向滾動事件復制代碼@Override public boolean canScrollHorizontally() {return true; }@Override 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居中顯示
    @Override 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,然后啟動居中顯示動畫。

    private void startScroll(int from, int to) {if (mAnimation != null && mAnimation.isRunning()) {mAnimation.cancel();}final int direction = from < to ? SCROLL_RIGHT : SCROLL_LEFT;mAnimation = ValueAnimator.ofFloat(from, to);mAnimation.setDuration(500);mAnimation.setInterpolator(new DecelerateInterpolator());mAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mOffsetAll = Math.round((float) animation.getAnimatedValue());layoutItems(mRecycle, mState, direction);}}); }復制代碼

    動畫很簡單,從滑動停止的位置,不斷刷新Item布局,直到滾動到最終位置。

    iii.處理指定位置滾動事件
    @Override 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);} }@Override 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的所有狀態進行重置

    @Override 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); //開啟重新排序......}@Overrideprotected 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的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。