全解析布局神器vlayout

为什么要使用vlayout

[vlayout]是对RecyclerView的LayoutManage的扩展,所以它接管了RecyclerView的整个布局方式,可以通过它实现各种各样的布局方式,非常的灵活。它提供的默认布局方式解耦所有的View和布局之间的关系: Linear, Grid, 吸顶, 浮动, 固定位置等,大致分为两类:一是非fix类型布局,像线性、Grid、栏格等,它们的特点是布局在整个页面流里,随页面滚动而滚动;另一类就是fix类型的布局,它们的子节点往往不随页面滚动而滚动。除了这个,它用来管理大的模块布局组合,使得同一RecyclerView内的组件可以复用,减少View的创建和销毁过程

通过demo学习

Paste_Image.png

上面这张图是通过运行vlayout提供的demo得到的效果图,看起来挺多view,也挺复杂的,很难相信这只是一个RecyclerView,是不是也间接证明了vLayout的强大。

下面来拆解下这么复杂的布局是如何在一个RecyclerView里实现的。

开始使用

基本用法

final VirtualLayoutManager layoutManager = new VirtualLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
final DelegateAdapter delegateAdapter = new DelegateAdapter(layoutManager, true);
recyclerView.setAdapter(delegateAdapter);
final List<DelegateAdapter.Adapter> adapters = new LinkedList<>();
//需要添加新的布局,就定义一个新的adapter
delegateAdapter.setAdapters(adapters);

需要定义DelegateAdapter.Adapter的子类,如下

static class SubAdapter extends DelegateAdapter.Adapter<MainViewHolder> {

        private Context mContext;

        private LayoutHelper mLayoutHelper;


        private LayoutParams mLayoutParams;
        private int mCount = 0;


        public SubAdapter(Context context, LayoutHelper layoutHelper, int count) {
            this(context, layoutHelper, count, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 300));
        }

        public SubAdapter(Context context, LayoutHelper layoutHelper, int count, @NonNull LayoutParams layoutParams) {
            this.mContext = context;
            this.mLayoutHelper = layoutHelper;
            this.mCount = count;
            this.mLayoutParams = layoutParams;
        }

        @Override
        public LayoutHelper onCreateLayoutHelper() {
            return mLayoutHelper;
        }

        @Override
        public MainViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return new MainViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item, parent, false));
        }

        @Override
        public void onBindViewHolder(MainViewHolder holder, int position) {
            // only vertical
            holder.itemView.setLayoutParams(
                    new LayoutParams(mLayoutParams));
        }


        @Override
        protected void onBindViewHolderWithOffset(MainViewHolder holder, int position, int offsetTotal) {
            ((TextView) holder.itemView.findViewById(R.id.title)).setText(Integer.toString(offsetTotal));
        }

        @Override
        public int getItemCount() {
            return mCount;
        }
    }

添加一个普通的布局:adapter

adapters.add(new SubAdapter(this, new LinearLayoutHelper(), 1) {

                @Override
                public void onViewRecycled(MainViewHolder holder) {
                    if (holder.itemView instanceof ViewPager) {
                        ((ViewPager) holder.itemView).setAdapter(null);
                    }
                }

                @Override
                public MainViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
                    if (viewType == 1)
                        return new MainViewHolder(
                                LayoutInflater.from(VLayoutActivity.this).inflate(R.layout.view_pager, parent, false));

                    return super.onCreateViewHolder(parent, viewType);
                }

                @Override
                public int getItemViewType(int position) {
                    return 1;
                }

                @Override
                protected void onBindViewHolderWithOffset(MainViewHolder holder, int position, int offsetTotal) {

                }

                @Override
                public void onBindViewHolder(MainViewHolder holder, int position) {
                    if (holder.itemView instanceof ViewPager) {
                        ViewPager viewPager = (ViewPager) holder.itemView;

                        viewPager.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 200));

                        // from position to get adapter
                        viewPager.setAdapter(new PagerAdapter(this, viewPool));
                    }
                }
            });

看下效果图

Paste_Image.png

添加一个悬浮的布局:adapter

FloatLayoutHelper layoutHelper = new FloatLayoutHelper();
            layoutHelper.setAlignType(FixLayoutHelper.BOTTOM_LEFT);
            layoutHelper.setDefaultLocation(100, 400);
            LayoutParams layoutParams = new LayoutParams(150, 150);
            adapters.add(new SubAdapter(this, layoutHelper, 1, layoutParams));

效果图:

Paste_Image.png

分析:

  • FloatLayoutHelper是悬浮布局的实现类
  • setAlignType 有四种: TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT
  • setDefaultLocation: x, y 代表与边界的距离,和alignType有关系,比如:TOP_LEFT, x 是与左边的距离,y是与上边的距离
  • 可以随手势拖动

添加 宽比高为2的布局

LinearLayoutHelper layoutHelper1 = new LinearLayoutHelper();
layoutHelper1.setAspectRatio(2.0f);
adapters.add(new SubAdapter(this, layoutHelper1, 1));

效果图:

Paste_Image.png

分析:
setAspectRatio(2.0f)设置宽比高比例为2的布局

添加子列表

LinearLayoutHelper layoutHelper2 = new LinearLayoutHelper();
            layoutHelper2.setAspectRatio(4.0f);
            layoutHelper2.setDividerHeight(10);
            layoutHelper2.setMargin(10, 30, 10, 10);
            layoutHelper2.setPadding(10, 30, 10, 10);
            layoutHelper2.setBgColor(0xFFF5A623);
            adapters.add(new SubAdapter(this, layoutHelper2, 6) {

                @Override
                public void onBindViewHolder(MainViewHolder holder, int position) {
                    if (position % 2 == 0) {
                        LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 300);
                        layoutParams.mAspectRatio = 5;
                        holder.itemView.setLayoutParams(layoutParams);
                    }
                }
            });
Paste_Image.png

分析:
我们看到的是一个垂直方向的列表,如果想要变成一个水平方向上的列表,需要修改VirtualLayoutManagerorientation

layoutManager.setOrientation(VirtualLayoutManager.HORIZONTAL);

效果图:

Paste_Image.png

添加一个sticky布局,黏住但不固定,手势之后会变化

StickyLayoutHelper layoutHelper = new StickyLayoutHelper();
            layoutHelper.setOffset(100);
            layoutHelper.setAspectRatio(4);
            adapters.add(new SubAdapter(this, layoutHelper, 1, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 100)));
Paste_Image.png

添加只有一个view的布局

            SingleLayoutHelper singleLayoutHelper = new SingleLayoutHelper();
            singleLayoutHelper.setBgColor(Color.rgb(135, 225, 90));
            singleLayoutHelper.setAspectRatio(4);
            singleLayoutHelper.setMargin(10, 20, 10, 20);
            singleLayoutHelper.setPadding(10, 10, 10, 10);
            adapters.add(new SubAdapter(this, singleLayoutHelper, 2, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 100)));
Paste_Image.png

分析:
当你使用SingleLayoutHelper时,如果SubAdapter的数量是多少,界面上只有显示一个view.

添加一个表格布局

ColumnLayoutHelper layoutHelper = new ColumnLayoutHelper();
            layoutHelper.setBgColor(0xff00f0f0);
            layoutHelper.setWeights(new float[]{40.0f, Float.NaN, 40});
            adapters.add(new SubAdapter(this, layoutHelper, 5) {

                @Override
                public void onBindViewHolder(MainViewHolder holder, int position) {
                    if (position == 0) {
                        LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 300);
                        layoutParams.mAspectRatio = 4;
                        holder.itemView.setLayoutParams(layoutParams);
                    } else {
                        LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 300);
                        layoutParams.mAspectRatio = Float.NaN;
                        holder.itemView.setLayoutParams(layoutParams);
                    }
                }

            });
Paste_Image.png

分析:
setWeights(new float[]{40.0f, Float.NaN, 40})确定了每列的比重权值,默认总权值是100,第一列和第三列是40,其他都是任意。我们看第一行为什么是这样的一个高度,因为下面的代码layoutParams.mAspectRatio = 4限制了宽高比是4。

添加一个OnePlusNLayoutHelper(数量为2)

需要先了解下它支持的布局方式有下面几种

 * 1 + 0
 * -------------------------
 * |           1           |
 * -------------------------
 *
 * 1 + 1
 * -------------------------
 * |           |           |
 * |     1     |     2     |
 * |           |           |
 * -------------------------
 *
 * 1 + 2
 * -------------------------
 * |           |     2     |
 * |     1     |-----------|
 * |           |     3     |
 * -------------------------
 *
 * 1 + 3
 * -------------------------
 * |           |     2     |
 * |     1     |-----------|
 * |           |  3  |  4  |
 * -------------------------
 *  1 + 4
 * -------------------------
 * |           |     2     |
 * |     1     |-----------|
 * |           | 3 | 4 | 5 |
 * -------------------------

比如下面

OnePlusNLayoutHelper helper = new OnePlusNLayoutHelper();
            helper.setBgColor(0xff876384);
            helper.setAspectRatio(4.0f);
            helper.setColWeights(new float[]{40f, 45f});
            helper.setMargin(10, 20, 10, 20);
            helper.setPadding(10, 10, 10, 10);
            adapters.add(new SubAdapter(this, helper, 2));
Paste_Image.png

分析:
setAspectRatio(4.0f)设置整体布局的宽高比,setColWeights(new float[]{40f, 45f})设置列的权重,总权重是100,这里设置了第一列权重是40,第二列权重是45

添加一个OnePlusNLayoutHelper(数量为4)

OnePlusNLayoutHelper helper = new OnePlusNLayoutHelper();
            helper.setBgColor(0xffef8ba3);
            helper.setAspectRatio(2.0f);
            helper.setColWeights(new float[]{40f});
            helper.setRowWeight(30f);
            helper.setMargin(10, 20, 10, 20);
            helper.setPadding(10, 10, 10, 10);
            adapters.add(new SubAdapter(this, helper, 4) {
                @Override
                public void onBindViewHolder(MainViewHolder holder, int position) {
                    super.onBindViewHolder(holder, position);
                    LayoutParams lp = (LayoutParams) holder.itemView.getLayoutParams();
                    if (position == 0) {
                        lp.rightMargin = 1;
                    } else if (position == 1) {

                    } else if (position == 2) {
                        lp.topMargin = 1;
                        lp.rightMargin = 1;
                    }
                }
            });
Paste_Image.png

分析:
当数量为4,为什么是这样一个展示页面,参考上面的(1+3)
setAspectRatio(2.0f)设置整个布局宽高比是2,setColWeights(new float[]{40f})设置第一列权重是40,
setRowWeight(30f)设置第一行的权重是30,行总权重是100

添加一个固定布局

FixLayoutHelper layoutHelper = new FixLayoutHelper(FixLayoutHelper.TOP_RIGHT, 20, 20);

            adapters.add(new SubAdapter(this, layoutHelper, 1) {//29
                @Override
                public void onBindViewHolder(MainViewHolder holder, int position) {
                    super.onBindViewHolder(holder, position);
                    LayoutParams layoutParams = new LayoutParams(200, 200);
                    holder.itemView.setLayoutParams(layoutParams);
                }
            });
Paste_Image.png

分析:

  • 固定类型(alignType)有四种:TOP_LEFT(左上角),TOP_RIGHT(右上角),BOTTOM_LEFT(左下角),BOTTOM_RIGHT(右下角)
  • 固定的边距,如上x=20, y=20, 所以就得到上面的固定样式
  • LayoutParams layoutParams = new LayoutParams(200, 200); 设置固定布局的子view的宽高尺寸

添加一个方格

GridLayoutHelper layoutHelper = new GridLayoutHelper(4);
            layoutHelper.setWeights(new float[]{20f, 26.665f});
            layoutHelper.setMargin(7, 0, 7, 0);
            layoutHelper.setHGap(3);
            adapters.add(new SubAdapter(this, layoutHelper, 8));
Paste_Image.png

分析:

  • GridLayoutHelper(4)类似于GridView,设置列数量位4
  • setWeights(new float[]{20f, 26.665f})设置第一列和第二列的权重分别是20,26.665,其他列均分,列总权重是100,计算一下,第三列和第四列的权重是26.6675
  • setHGap(3)类似于GridView,设置水平间距是3像素
  • new SubAdapter(this, layoutHelper, 8) 设置方格总数量是8

添加一个Staggered布局

final StaggeredGridLayoutHelper helper = new StaggeredGridLayoutHelper(2, 10);
            helper.setMargin(20, 10, 10, 10);
            helper.setPadding(10, 10, 20, 10);
            helper.setBgColor(0xFF86345A);
            adapters.add(new SubAdapter(this, helper, 27) {
                @Override
                public void onBindViewHolder(MainViewHolder holder, int position) {
                    super.onBindViewHolder(holder, position);
                    LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 200);
                    if (position % 2 == 0) {
                        layoutParams.mAspectRatio = 1.0f;
                    } else {
                        layoutParams.height = 340 + position % 7 * 20;
                    }
                    holder.itemView.setLayoutParams(layoutParams);
                }
            });
Paste_Image.png

分析:

  • StaggeredGridLayoutHelper(2, 10)设置列数量为2,间距为10像素
  • new SubAdapter(this, helper, 27) 设置子view总数量为27
  • 下面这段代码是设置每个子view的宽高尺寸,所以才形成了Staggered
LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 200);
                    if (position % 2 == 0) {
                        layoutParams.mAspectRatio = 1.0f;
                    } else {
                        layoutParams.height = 340 + position % 7 * 20;
                    }

vLayout提供的默认布局

Paste_Image.png

一般情况下,这些都是够用的,如果出现不够的情况,可以参考上面的布局来实现自己的。

源码分析其实现的原理

因为篇幅有限,这里就直接定位到其实现的最底层代码,定位到VirtualLayoutManagerpublic void setLayoutHelpers(@Nullable List<LayoutHelper> helpers)方法,通过这个方法我们注意到上面我们所调用的helper,都已经转换成配置信息了,最后一段代码requestLayout();重新刷新布局,接下来会调用public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state),然后就会调用public void layoutChild(View child, int left, int top, int right, int bottom)对每个子view进行布局

下面是我画的简单的调用示意图

vlayout.jpg

有兴趣交流和讨论的,请在下面发评论。

有兴趣一起学习和讨论kotlin,请加Q群:248445350
[vlayout]:https://github.com/alibaba/vlayout

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

推荐阅读更多精彩内容