LayoutManager自定义

From here https://blog.csdn.net/zxt0601/article/details/52956504

照着写一遍,熟悉代码逻辑,修正那块看着脑袋晕,暂不考虑,复制作者代码就行。
下边是效果图,增加了一个boolean,默认为false,如果为true的话就是下图效果,每行控件都居中显示。


image.png

代码如下:

package com.charliesong.demo0327;

import android.graphics.Rect;
import android.support.v7.widget.OrientationHelper;
import android.support.v7.widget.RecyclerView;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by charlie.song on 2018/5/8.
 */

public class FlowGravityLayoutManager extends RecyclerView.LayoutManager {

    private int mVerticalOffset;//竖直偏移量 每次换行时,要根据这个offset判断
    private int mFirstVisiPos;//屏幕可见的第一个View的Position
    private int mLastVisiPos;//屏幕可见的最后一个View的Position
    private OrientationHelper orientationHelper;//系统自带的类,这里选择的是垂直方向的,用来获取top,bottom,以及child的宽高等
    private SparseArray<Rect> mItemRects;//key 是position,保存的是view的bound属性
    private boolean horizontalCenter=false;//同一行的控件是否居中显示
    public FlowGravityLayoutManager() {
        orientationHelper = OrientationHelper.createOrientationHelper(this, OrientationHelper.VERTICAL);
        mItemRects = new SparseArray<>();
      setAutoMeasureEnabled(true);
    }

    public void setHorizontalCenter(boolean horizontalCenter) {
        this.horizontalCenter = horizontalCenter;
    }
    @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) {
        if (getItemCount() == 0) {
            detachAndScrapAttachedViews(recycler);
            oneRowViews.clear();
            return;
        }
        if (getChildCount() == 0 && state.isPreLayout()) {
            return;
        }
        detachAndScrapAttachedViews(recycler);
        mVerticalOffset = 0;
        mFirstVisiPos = 0;
        mLastVisiPos = getItemCount() ;
        fill(recycler, state);
    }

    private void fill(RecyclerView.Recycler recycler, RecyclerView.State state) {
        fill(recycler, state, 0);
    }
    int leftOffset = getPaddingLeft();
    int lineMaxHeight = 0;//每行的最大高度,因为同一行的控件可能高度不一样,取最大值,好决定下一行的位置
    int topOffset;
    SparseArray<View> oneRowViews=new SparseArray<>();//保存在同一行的view,方便在这一行控件加载完以后平移,只对居中对齐的情况下处理
    /**
     * 填充childView的核心方法,应该先填充,再移动。
     * 在填充时,预先计算dy的在内,如果View越界,回收掉。
     * 一般情况是返回dy,如果出现View数量不足,则返回修正后的dy.
     *
     * @param dy RecyclerView给我们的位移量,+,显示底端, -,显示头部,手指往上滑是正的,往下滑是负的
     * @return 修正以后真正的dy(可能剩余空间不够移动那么多了 所以return <|dy|)
     */
    private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int dy) {
         topOffset = orientationHelper.getStartAfterPadding();
        //回收越界子View
        if (getChildCount() > 0) {//滑动时进来的
            for (int i = getChildCount() - 1; i >= 0; i--) {
                View child = getChildAt(i);
                if (dy > 0) {//上滑,顶部的view可能消失
                    if (getDecoratedBottom(child) - dy < topOffset) {
                        removeAndRecycleView(child, recycler);
                        mFirstVisiPos++;
                    }
                } else if (dy < 0) {//手指往下滑,底部的view 可能消失
                    if (getDecoratedTop(child) - dy > orientationHelper.getEndAfterPadding()) {
                        removeAndRecycleView(child, recycler);
                        mLastVisiPos--;
                    }
                }
            }
        }

        //
        leftOffset = getPaddingLeft();
         lineMaxHeight = 0;
        //布局子View阶段
        if (dy >= 0) {
            int minPos = mFirstVisiPos;
            mLastVisiPos = getItemCount() - 1;
            if (getChildCount() > 0) {
                View lastVisibleView = getChildAt(getChildCount() - 1);
                minPos = getPosition(lastVisibleView) + 1;
                topOffset = getDecoratedTop(lastVisibleView);
                leftOffset = getDecoratedRight(lastVisibleView);
                lineMaxHeight = Math.max(lineMaxHeight, orientationHelper.getDecoratedMeasurement(lastVisibleView));
            }
            for (int i = minPos; i <=mLastVisiPos; i++) {
            //找recycler要一个childItemView,我们不管它是从scrap里取,还是从RecyclerViewPool里取,亦或是onCreateViewHolder里拿。
                View child = recycler.getViewForPosition(i);
                addView(child);
                measureChildWithMargins(child, 0, 0);
                if (leftOffset + orientationHelper.getDecoratedMeasurementInOther(child) <= getWidth() - getPaddingRight()) {//这一行可以装的下,不用换行
                    layoutChild(child,i);
                } else {
                    transViewX();
                    leftOffset = getPaddingLeft();
                    topOffset += lineMaxHeight;
                    lineMaxHeight = 0;
                    //新起一行的时候要判断一下边界
                    if (topOffset - dy > orientationHelper.getEndAfterPadding()) {
                        removeAndRecycleView(child, recycler);
                        mLastVisiPos=i-1;//已经跑到recyclerview最底部,不可见了,修改mLastVisPos,循环结束
                    } else {
                        layoutChild(child,i);
                    }
                }
            }
            //添加完后,判断是否已经没有更多的ItemView,并且此时屏幕仍有空白,则需要修正dy
            View lastVisibleView = getChildAt(getChildCount() - 1);
            int lastPosition = getPosition(lastVisibleView);
            if (lastPosition == getItemCount() - 1) {
                int gap = orientationHelper.getEndAfterPadding() - getDecoratedBottom(lastVisibleView);
                if (gap > 0) {
                    dy -= gap;
                }
            }
        } else {

            int maxPosition = getItemCount() - 1;
            mFirstVisiPos = 0;
            if (getChildCount() > 0) {
                View firstView = getChildAt(0);
                maxPosition = getPosition(firstView) - 1;
            }
            for (int i = maxPosition; i >= mFirstVisiPos; i--) {
                Rect rect = mItemRects.get(i);
                if (rect.bottom - mVerticalOffset - dy < getPaddingTop()) {
                    mFirstVisiPos = i + 1;
                    break;
                } else {
                    View child = recycler.getViewForPosition(i);
                    addView(child, 0);//将View添加至RecyclerView中,childIndex为1,但是View的位置还是由layout的位置决定
                    measureChildWithMargins(child, 0, 0);
                    layoutDecoratedWithMargins(child, rect.left, rect.top - mVerticalOffset, rect.right, rect.bottom - mVerticalOffset);
                }
            }

        }
        return  dy;
    }
    //换行或者add最后一个view的时候,平移下最终的位置,
    private void transViewX(){
        if(!horizontalCenter){
            return;
        }
        int transX=(getWidth()-getPaddingRight()-leftOffset)/2;
        for(int i=0;i<oneRowViews.size();i++){
            int key=oneRowViews.keyAt(i);
            View transChild= oneRowViews.get(key);
            transChild.offsetLeftAndRight(transX);
            Rect rect = mItemRects.get(key);
            rect.offset(transX,0);
        }
        oneRowViews.clear();
    }

    private void layoutChild(View child,int i){
        oneRowViews.put(i,child);
        layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + orientationHelper.getDecoratedMeasurementInOther(child),
                topOffset + orientationHelper.getDecoratedMeasurement(child));
        //保存Rect供逆序layout用
        Rect rect = new Rect(leftOffset, topOffset + mVerticalOffset, leftOffset + orientationHelper.getDecoratedMeasurementInOther(child),
                topOffset + orientationHelper.getDecoratedMeasurement(child) + mVerticalOffset);
        mItemRects.put(i, rect);

        leftOffset += orientationHelper.getDecoratedMeasurementInOther(child);
        lineMaxHeight = Math.max(lineMaxHeight, orientationHelper.getDecoratedMeasurement(child));
        if(i==mLastVisiPos){
            transViewX();
        }
    }
    @Override
    public boolean canScrollVertically() {
        return true;
    }

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (dy == 0 || getChildCount() == 0) {
            return 0;
        }
        int realOffset = dy;
        //边界修复代码
        if (mVerticalOffset + realOffset < 0) {//上边界
            realOffset = -mVerticalOffset;
        }else if (realOffset > 0){
            //利用最后一个子View比较修正
            View lastView=getChildAt(getChildCount()-1);
            if(getPosition(lastView)==getItemCount()-1){
                int gap = getHeight() - getPaddingBottom() - getDecoratedBottom(lastView);
                if (gap > 0) {
                    realOffset = -gap;
                } else if (gap == 0) {
                    realOffset = 0;
                } else {
                    realOffset = Math.min(realOffset, -gap);
                }
            }
        }

        realOffset= fill(recycler, state, realOffset);//先填充,再位移。

        mVerticalOffset += realOffset;//累加实际滑动距离

        offsetChildrenVertical(-realOffset);//滑动
        return realOffset;
    }

}

简单记录下修改代码碰到的问题:

居中显示

最开始我的思路是用个tag【默认是0】,然后每次换行,平移完控件以后,tag就修改为换行后的position
结果我发现没有效果,offsetLeftAndRight不行我换成layoutDecoratedWithMargins也不行,这就奇怪了。
后来想着可能这个child不是我们要的child,也就是说通过recycler.getViewForPosition(start),虽然里边的pisition是一样的,可获取到的child不是一个东西,所以操作没反应。
最后就是上边的代码了,将每行的child,都放到oneRowViews里,换行的时候取出来进行平移,果然没问题。

//这里的代码最早就是写在换行的逻辑里的,结果无效
                    int transX=(getWidth()-getPaddingRight()-leftOffset)/2;
                    for(int start=tag;start<i;start++){
                        Rect rect = mItemRects.get(start);
                        rect.offset(transX,0);
                        View transChild = recycler.getViewForPosition(start);

                        transChild.offsetLeftAndRight(transX);
//                        layoutDecoratedWithMargins(transChild, rect.left, rect.top , rect.right, rect.bottom );
                        System.out.println("138=============="+start+"=="+rect.toShortString()+"==="+transChild.getRight());
                    }
        tag=i;
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容