改造LayoutManager實現(xiàn)不一樣的列表效果

1.前言

????我們都知道,對于RecyclerView而言,android自帶的有三種類型的LayoutManager,分別是LinearLayoutManagr(線性布局器),GridLayoutManager(網(wǎng)格布局器)和StaggeredGridLayoutManager(瀑布流布局器)。然而在實際的開發(fā)過程中,這三種往往不能夠滿足實際效果的需要,那么就需要開發(fā)者自己去打造自己的LayoutManager。網(wǎng)上找了很多篇的博客和文章,自己研究并實現(xiàn)了下如下的效果,在這里分享給大家。


demo.gif

2.RecyclerView機制

????要想打造屬于自己的LayoutManager,首先得了解下RecyclerView的機制是什么樣的。關(guān)于RecyclerView的機制是什么樣的以及最基礎(chǔ)的LayoutManager的改造方法,大家可以看csdn地址http://blog.csdn.net/huachao1001/article/details/51594004 這篇文章。這篇是我看的文章里面描述最詳細的。我在這邊主要講述如何實現(xiàn)上述的效果

3.效果的分析

????從上面的列表效果可以看出,RecyclerView支持的是左右進行滑動,滑動方式是沿著一條曲線進行,并且在經(jīng)過中心位置的時候item有個放大縮小的效果。那么從列表的布局來看,我們可以將其排布在一個圓形弧線上,中心位置的角度為0度,左側(cè)遞減,右側(cè)遞增,那么每個item的位置就在對應(yīng)的角度上。然后在經(jīng)過0度區(qū)域時(可以是-30--30或者其他),對item進行放大縮小操作,可以使用 view.setScaleX()和 view.setScaleY(scale)方法。

4.效果的實現(xiàn)

??4.1 開始自定義LayoutManager

????首先將我們自己的CustomLayoutManager繼承RecyclerView.LayoutManager,實現(xiàn)抽象類RecyclerView.LayoutManager里面的抽象方法generateDefaultLayoutParams(),實現(xiàn)如下

@Overridepublic RecyclerView.LayoutParams generateDefaultLayoutParams() {  
  return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}

????當然光重寫上述的方法并沒有什么效果,我們還要重寫LayoutManager
的onLayoutChildren()方法,這個方法從名稱就可以看出來是對子view的一個布局,要實現(xiàn)上面的圖片效果,自然需要對應(yīng)的代碼支持

 @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        //如果沒有item,直接返回
        //跳過preLayout,preLayout主要用于支持動畫
        if (getItemCount() <= 0 || state.isPreLayout()) {
            offsetRotate = 0;
            return;
        }
       //得到子view的寬和高,這邊的item的寬高都是一樣的,所以只需要進行一次測量
       View scrap = recycler.getViewForPosition(0);
       addView(scrap);
       measureChildWithMargins(scrap, 0, 0);
       //計算測量布局的寬高
       mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
       mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);
       //確定起始位置,在最上方的中心處
       startLeft = (getHorizontalSpace() - mDecoratedChildWidth) / 2;
       startTop = 0;
        
        //記錄每個item旋轉(zhuǎn)的角度
        float rotate = firstChildRotate;
        for (int i = 0; i < getItemCount(); i++) {
            itemsRotate.put(i, rotate);
            itemAttached.put(i, false);
            rotate += intervalAngle;
        }
        //在布局之前,將所有的子View先Detach掉,放入到Scrap緩存中
        detachAndScrapAttachedViews(recycler);
        fixRotateOffset();
        layoutItems(recycler, state);
    }

????從上面的代碼可以看出,因為在這里每個item的大小都是一致的,所以我們只需要測量一次得到item的寬高之后的item就可以直接使用。然后還需要確定最中心的item的位置,這里是startLeft和startTop。然后對每個item進行角度的保存和處理,如下

   /**
     * 默認每個item之間的角度
     **/
    private static float INTERVAL_ANGLE = 30f;
   /**
     * 第一個的角度是為0
     **/
    private int firstChildRotate = 0;
    /**
     * 第一個的角度是為0
     **/
    private int firstChildRotate = 0;
   //最大和最小的移除角度
    private int minRemoveDegree;
    private int maxRemoveDegree;

    //記錄Item是否出現(xiàn)過屏幕且還沒有回收。true表示出現(xiàn)過屏幕上,并且還沒被回收
    private SparseBooleanArray itemAttached = new SparseBooleanArray();
    //保存所有的Item的上下左右的偏移量信息
    private SparseArray<Float> itemsRotate = new SparseArray<>();

    /**
     * 設(shè)置滾動時候的角度
     **/
    private void fixRotateOffset() {
        if (offsetRotate < 0) {
            offsetRotate = 0;
        }
        if (offsetRotate > getMaxOffsetDegree()) {
            offsetRotate = getMaxOffsetDegree();
        }
    }

????上述代碼的作用是對每個item位置的一個記錄和item是否顯示的一個記錄,offsetRotate 保存當前item布局的一個角度總量,將其限制在一個范圍之內(nèi)。在范圍內(nèi)的item用于顯示,之外的進行回收。下面來看layoutItems()方法,該方法是對item的一個回收和顯示。

/**
     * 進行view的回收和顯示
     **/
    private void layoutItems(RecyclerView.Recycler recycler, RecyclerView.State state) {
        layoutItems(recycler, state, SCROLL_RIGHT);
    }

    /**
     * 進行view的回收和顯示的具體實現(xiàn)
     **/
    private void layoutItems(RecyclerView.Recycler recycler,
                             RecyclerView.State state, int oritention) {
        if (state.isPreLayout()) return;

        //移除界面之外的view
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            int position = getPosition(view);
            if (itemsRotate.get(position) - offsetRotate > maxRemoveDegree
                    || itemsRotate.get(position) - offsetRotate < minRemoveDegree) {
                itemAttached.put(position, false);
                removeAndRecycleView(view, recycler);
            }
        }

        //將要顯示的view進行顯示出來
        for (int i = 0; i < getItemCount(); i++) {
            if (itemsRotate.get(i) - offsetRotate <= maxRemoveDegree
                    && itemsRotate.get(i) - offsetRotate >= minRemoveDegree) {
                if (!itemAttached.get(i)) {
                    View scrap = recycler.getViewForPosition(i);
                    measureChildWithMargins(scrap, 0, 0);
                    if (oritention == SCROLL_LEFT)
                        addView(scrap, 0);
                    else
                        addView(scrap);
                    float rotate = itemsRotate.get(i) - offsetRotate;

                    int left = calLeftPosition(rotate);
                    int top =  calTopPosition(rotate);

                    scrap.setRotation(rotate);

                    layoutDecorated(scrap, startLeft + left, startTop + top,
                            startLeft + left + mDecoratedChildWidth, startTop + top + mDecoratedChildHeight);

                    itemAttached.put(i, true);

                    //計算角度然后進行放大
                    float scale = calculateScale(rotate);
                    scrap.setScaleX(scale);
                    scrap.setScaleY(scale);
                }
            }
        }
    }

????首先對于角度在范圍之外的item進行remove掉,對于范圍之內(nèi)的item,通過layoutDecorated()方法進行顯示,根據(jù)左滑還是右滑,判斷item顯示的位置。同時根據(jù)當前item的角度位置,判斷當前item是否需要進行放大或者縮小,計算方法如下。

    /**
     * 最大放大倍數(shù)
     **/
    private static final float SCALE_RATE = 1.4f;
    //放大的倍數(shù)
    private float maxScale;
    //在什么角度變化之內(nèi)
    private float minScaleRotate = 40;
    /**
     * 默認每個item之間的角度
     **/
    private static float INTERVAL_ANGLE = 30f;
    /**
     * 默認的半徑長度
     **/
    private static final int DEFAULT_RADIO = 100;
    /**
     * 半徑默認為100
     **/
    private int mRadius;
    /**
     * 根據(jù)角度計算大小,0度的時候最大,minScaleRotate度的時候最小,然后其他時候變小
     **/
    private float calculateScale(float rotate) {
        rotate = Math.abs(rotate) > minScaleRotate ? minScaleRotate : Math.abs(rotate);
        return (1 - rotate / minScaleRotate) * (maxScale - 1) + 1;
    }

    /**
     * 當前item的x的坐標
     **/
    private int calLeftPosition(float rotate) {
        return (int) (mRadius * Math.cos(Math.toRadians(90 - rotate)));
    }

    /**
     * 當前item的y的坐標
     **/
    private int calTopPosition(float rotate) {
        return (int) (mRadius * Math.sin(Math.toRadians(90 - rotate)));
    }

經(jīng)過以上的步驟,該RecyclerView已經(jīng)能夠進行列表顯示了,但是還不能夠進行滑動。

??4.2 LayoutManager實現(xiàn)滑動

????LayoutManager中還有canScrollHorizontally()和canScrollVertically()分別代表是否可以進行橫向和縱向的滑動。
同時還有scrollHorizontallyBy和scrollVerticallyBy方法來實現(xiàn)對應(yīng)的邏輯。這邊我們需要的是進行橫向的滑動,那么實現(xiàn)方法如下

  @Override
    public boolean canScrollHorizontally() {
        return true;
    }
@Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        int willScroll = dx;

        //每個item x方向上的移動距離
        float theta = dx / DISTANCE_RATIO;
        float targetRotate = offsetRotate + theta;

        //目標角度
        if (targetRotate < 0) {
            willScroll = (int) (-offsetRotate * DISTANCE_RATIO);
        } else if (targetRotate > getMaxOffsetDegree()) {
            willScroll = (int) ((getMaxOffsetDegree() - offsetRotate) * DISTANCE_RATIO);
        }
        theta = willScroll / DISTANCE_RATIO;

        //當前移動的總角度
        offsetRotate += theta;

        //重新設(shè)置每個item的x和y的坐標
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            float newRotate = view.getRotation() - theta;
            int offsetX = calLeftPosition(newRotate);
            int offsetY =  calTopPosition(newRotate);

            layoutDecorated(view, startLeft + offsetX, startTop + offsetY,
                    startLeft + offsetX + mDecoratedChildWidth, startTop + offsetY + mDecoratedChildHeight);
            view.setRotation(newRotate);

            //計算角度然后進行放大
            float scale = calculateScale(newRotate);
            view.setScaleX(scale);
            view.setScaleY(scale);
        }

        //根據(jù)dx的大小判斷是左滑還是右滑
        if (dx < 0)
            layoutItems(recycler, state, SCROLL_LEFT);
        else
            layoutItems(recycler, state, SCROLL_RIGHT);
        return willScroll;
    }

????可以看出來scrollHorizontallyBy()里面的操作類似與layoutItem()方法中的操作,只不過在這里需要對橫向滾動量dx進行角度的裝換,這邊給的比例為DISTANCE_RATIO,然后需要對offsetRotate進行累加操作,最后調(diào)用layoutItems進行item的view的回收和顯示。經(jīng)過以上的操作,RecyclerView就可以進行列表的展示和滑動了。

?? 4.3 RecyclerView的自行滾動到某一項

????需要注意的是,由于我們已經(jīng)修改了LayoutManager的滾動規(guī)則。

     /**
     * Convenience method to scroll to a certain position.
     *
     * RecyclerView does not implement scrolling logic, rather forwards the call to
     * {@link android.support.v7.widget.RecyclerView.LayoutManager#scrollToPosition(int)}
     * @param position Scroll to this adapter position
     * @see android.support.v7.widget.RecyclerView.LayoutManager#scrollToPosition(int)
     */
    public void scrollToPosition(int position) {
        if (mLayoutFrozen) {
            return;
        }
        stopScroll();
        if (mLayout == null) {
            Log.e(TAG, "Cannot scroll to position a LayoutManager set. " +
                    "Call setLayoutManager with a non-null argument.");
            return;
        }
        mLayout.scrollToPosition(position);
        awakenScrollBars();
    }

/**
     * Starts a smooth scroll to an adapter position.
     * <p>
     * To support smooth scrolling, you must override
     * {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} and create a
     * {@link SmoothScroller}.
     * <p>
     * {@link LayoutManager} is responsible for creating the actual scroll action. If you want to
     * provide a custom smooth scroll logic, override
     * {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} in your
     * LayoutManager.
     *
     * @param position The adapter position to scroll to
     * @see LayoutManager#smoothScrollToPosition(RecyclerView, State, int)
     */
    public void smoothScrollToPosition(int position) {
        if (mLayoutFrozen) {
            return;
        }
        if (mLayout == null) {
            Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. " +
                    "Call setLayoutManager with a non-null argument.");
            return;
        }
        mLayout.smoothScrollToPosition(this, mState, position);
    }

????而從源碼中可以看出來RecyclerView的scrollToPosition()(滾動到某一項),
smoothScrollToPosition()(平滑滾動到某一項)其實是調(diào)用的layoutManager中對于的方法,所以我們必須重寫我們自己的layoutManager的對于的方法,實現(xiàn)自己的滾動規(guī)則。

 private PointF computeScrollVectorForPosition(int targetPosition) {
        if (getChildCount() == 0) {
            return null;
        }
        final int firstChildPos = getPosition(getChildAt(0));
        final int direction = targetPosition < firstChildPos ? -1 : 1;
        return new PointF(direction, 0);
    }


    @Override
    public void scrollToPosition(int position) {//移動到某一項
        if (position < 0 || position > getItemCount() - 1) return;
        float targetRotate = position * intervalAngle;
        if (targetRotate == offsetRotate) return;
        offsetRotate = targetRotate;
        fixRotateOffset();
        requestLayout();
    }

    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {//平滑的移動到某一項
        LinearSmoothScroller smoothScroller = new LinearSmoothScroller(recyclerView.getContext()) {
            @Override
            public PointF computeScrollVectorForPosition(int targetPosition) {
                return CustomLayoutManager.this.computeScrollVectorForPosition(targetPosition);
            }
        };
        smoothScroller.setTargetPosition(position);
        startSmoothScroll(smoothScroller);
    }

????在scrollToPosition中,我們需要根據(jù)position獲取對應(yīng)的item在最開始布局時候的角度,賦值給offsetRotate ,讓其進行重新布局即可以實現(xiàn)效果。而在smoothScrollToPosition()中,參考LinearLayoumanager中對應(yīng)的方法。

 @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
            int position) {
        LinearSmoothScroller linearSmoothScroller =
                new LinearSmoothScroller(recyclerView.getContext());
        linearSmoothScroller.setTargetPosition(position);
        startSmoothScroll(linearSmoothScroller);
    }

    @Override
    public PointF computeScrollVectorForPosition(int targetPosition) {
        if (getChildCount() == 0) {
            return null;
        }
        final int firstChildPos = getPosition(getChildAt(0));
        final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1;
        if (mOrientation == HORIZONTAL) {
            return new PointF(direction, 0);
        } else {
            return new PointF(0, direction);
        }
    }

????從computeScrollVectorForPosition可以知道,對于我們自己的LayoutManager而言
從前面的item滾動到后面的是new PointF(-1, 0);相反的就是new PointF(1, 0);最后我們重寫onAdapterChanged()方法,當adapter發(fā)生變化的時候,讓我們自己的layoutmanager回歸原始的狀態(tài)。

  @Override
    public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {//adapter進行改變的時候
        removeAllViews();
        offsetRotate = 0;
    }

5.最后

????只要了解了RecyclerView的機制和RecyclerView.LayoutManager中需要實現(xiàn)的方法和步奏,那么對于我們自己來說,打造自己的LayoutManager并不是一件很難的事情。我在實現(xiàn)的過程中,主要參考了
csdn huachao1001的文章http://blog.csdn.net/huachao1001/article/details/51594004
簡書Dajavu的文章http://www.lxweimin.com/p/7bb7556bbe10 ,最終實現(xiàn)上面的效果,
最后奉上Github上代碼下載地址https://github.com/hzl123456/CustomLayoutManager

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內(nèi)容