红橙Darren视频笔记 流式布局tagLayout measure layout方法学习 adapter使用 学习感悟

效果:
在这里插入图片描述

自定义View

public class TagLayout extends ViewGroup {
    private static final String TAG = "TagLayout";
    List<List> childViewsInLines = new ArrayList<>();
    List<View> oneLineViews = new ArrayList<>();

    public TagLayout(Context context) {
        this(context, null);
    }

    public TagLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TagLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        childViewsInLines.clear();
        oneLineViews.clear();
        //根据源码 先测量一遍所有子view 以获取子view的宽高
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        getLayoutParams();
        int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
        int parentHeight = 0;

        int totalChildNum = getChildCount();
        int currentLineWidth = 0;
        int currentChildHeight;//包括child margin
        int currentChildWidth;//包括child margin
        for (int i = 0; i < totalChildNum; i++) {
            View currentChild = getChildAt(i);
            if (currentChild.getVisibility() == GONE) {
                continue;
            }
            currentChildWidth = getChildWidthIncludeMargin(currentChild);
            currentChildHeight = getChildHeightIncludeMargin(currentChild);
            //计算是否需要换行
            if (currentLineWidth + currentChildWidth > parentWidth) {
                //需要换行 //TODO 考虑高度不一样
                currentLineWidth = 0;//重置当前行宽度的累计值
                parentHeight += currentChildHeight;//高度累加 //TODO 暂且使用最后一个view的高度作为此行高度
                childViewsInLines.add(oneLineViews);//记录一行的view
                oneLineViews = new ArrayList<>();//为下一行view记录做准备
            }
            //此行记录长度增加
            currentLineWidth += currentChildWidth;//当前行宽度的累计值增加
            oneLineViews.add(currentChild);//当前行view增加

            //最后一个view即使宽度没有达到换行 仍然需要累计高度 作为新的一行
            if (i == getChildCount() - 1) {
                parentHeight += currentChildHeight;//高度累加
                childViewsInLines.add(oneLineViews);//记录一行的view
            }
        }
        //Log.d(TAG, "onMeasure: parentWidth" + parentWidth + " parentHeight " + parentHeight);
        setMeasuredDimension(parentWidth, parentHeight);
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int lineStartX = 0;
        int lineStartY = 0;
        int currentChildWidth;
        int currentChildHeight = 0;
        for (List lineViews : childViewsInLines) {

            for (Object view : lineViews) {
                View currentChild = (View) view;
                if (currentChild.getVisibility() == GONE) {
                    continue;
                }
                currentChildWidth = getChildWidthIncludeMargin(currentChild);
                currentChildHeight = getChildHeightIncludeMargin(currentChild);
                int[] childMargins = getChildMargins(currentChild);
                currentChild.layout(lineStartX + childMargins[0], lineStartY + childMargins[1], lineStartX + currentChildWidth - childMargins[2], lineStartY + currentChildHeight - childMargins[3]);
                Log.d(TAG, "onLayout: " + "lineStartX->" + lineStartX + "lineStartY->" + lineStartY + "currentChildWidth->" + currentChildWidth + "+currentChildHeight->" + currentChildHeight);
                lineStartX += currentChildWidth;
            }
            lineStartX = 0;//换行 起始绘制点x重置
            lineStartY += currentChildHeight;换行 高度累加
        }
    }

    private int getChildHeightIncludeMargin(View currentChild) {
        MarginLayoutParams currentChildLayout = null;
        if (currentChild.getLayoutParams() instanceof MarginLayoutParams) {
            currentChildLayout = (MarginLayoutParams) currentChild.getLayoutParams();
        }
        return currentChild.getMeasuredHeight() + (currentChildLayout == null ? 0 : (currentChildLayout.topMargin + currentChildLayout.bottomMargin));
    }

    private int getChildWidthIncludeMargin(View currentChild) {
        MarginLayoutParams currentChildLayout = null;
        if (currentChild.getLayoutParams() instanceof MarginLayoutParams) {
            currentChildLayout = (MarginLayoutParams) currentChild.getLayoutParams();
        }
        return currentChild.getMeasuredWidth() + (currentChildLayout == null ? 0 : (currentChildLayout.leftMargin + currentChildLayout.rightMargin));
    }

    private int[] getChildMargins(View currentChild) {
        MarginLayoutParams currentChildLayout = null;
        if (currentChild.getLayoutParams() instanceof MarginLayoutParams) {
            currentChildLayout = (MarginLayoutParams) currentChild.getLayoutParams();
        }
        return currentChildLayout == null ? new int[]{0, 0, 0, 0} : new int[]{currentChildLayout.leftMargin, currentChildLayout.topMargin, currentChildLayout.rightMargin, currentChildLayout.bottomMargin};
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        //会影响子view getLayoutParams是否可以转型为MarginLayoutParams
        //更直接的说 影响能不能获取子view的margin
        //具体原因可以参考 https://www.jianshu.com/p/99c27e2db843
        return new MarginLayoutParams(getContext(), attrs);
    }


    public void setAdapter(final TagLayoutAdapter adapter) {
        if (adapter == null) {
            throw new NullPointerException("TagLayoutAdapter must not null!!");
        }
        removeAllViews();

        for (int i = 0; i < adapter.getCount(); i++) {
            final TextView textView = (TextView) adapter.getViewAtPosition(i, this);
            addView(textView);
            textView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    adapter.itemClick(textView.getText().toString());
                }
            });
        }
    }

    abstract static class TagLayoutAdapter {
        abstract int getCount();

        abstract View getViewAtPosition(int index, ViewGroup parent);

        abstract void itemClick(String s);
    }
}

item布局

<?xml version="1.0" encoding="utf-8"?>
<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_margin="10dp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/tag_view_item_bg">
</TextView>

item背景

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="3dp"/>
    <padding android:top="10dp" android:right="10dp" android:left="10dp" android:bottom="10dp"/>
    <solid android:color="#ccc"/>
    <stroke android:width="2dp" android:color="@color/colorAccent"/>
</shape>

mainActivity

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TagLayout layout = new TagLayout(this);
        final List<String> stringOfViews = new ArrayList<>();
        stringOfViews.add("Java Book1");
        stringOfViews.add("Java Book2");
        stringOfViews.add("Java Book3");
        stringOfViews.add("Java Book1");
        stringOfViews.add("Java Book2");
        stringOfViews.add("Java Book3");
        stringOfViews.add("Java Book1");
        stringOfViews.add("Java Book2");
        stringOfViews.add("Java Book3");
        stringOfViews.add("Java Book1");
        stringOfViews.add("Java Book2");
        stringOfViews.add("Java Book3");
        stringOfViews.add("Java Book1");
        stringOfViews.add("Java Book2");
        stringOfViews.add("Java Book3");
        stringOfViews.add("Java Book1");
        stringOfViews.add("Java Book2");
        stringOfViews.add("Java Book3");
        stringOfViews.add("Java Book1");
        stringOfViews.add("Java Book2");
        stringOfViews.add("Java Book3");
        stringOfViews.add("Java Book1");
        stringOfViews.add("Java Book2");
        stringOfViews.add("Java Book3");
        stringOfViews.add("Java Book1");
        stringOfViews.add("Java Book2");
        stringOfViews.add("Java Book3");
        stringOfViews.add("C++ Book1");
        stringOfViews.add("C# Book2");
        stringOfViews.add("ACSS Book3");
        stringOfViews.add("哈十三点v是 1");
        stringOfViews.add("111111111111111111");
        stringOfViews.add("22");
        stringOfViews.add("33333333333");
        stringOfViews.add("44");
        stringOfViews.add("555553");
        stringOfViews.add("6666666661");
        stringOfViews.add("77777");
        stringOfViews.add("88888");
        stringOfViews.add("999");
        stringOfViews.add("111111111111111111");
        stringOfViews.add("22");
        stringOfViews.add("333333333");
        stringOfViews.add("44");
        stringOfViews.add("555553");
        stringOfViews.add("6666666661");
        stringOfViews.add("77777");
        stringOfViews.add("88888");
        stringOfViews.add("999");
        stringOfViews.add("流浪地球");
        stringOfViews.add("OverLord不死者之王");
        layout.setAdapter(new TagLayout.TagLayoutAdapter() {
            @Override
            public View getViewAtPosition(int index, ViewGroup parent) {
                LayoutInflater inflater = LayoutInflater.from(MainActivity.this);
                TextView textView = (TextView) inflater.inflate(R.layout.tag_view_items, parent, false);
                textView.setText(stringOfViews.get(index));
                return textView;
            }

            @Override
            void itemClick(String textString) {
                Toast.makeText(MainActivity.this, textString, Toast.LENGTH_SHORT).show();
            }

            @Override
            public int getCount() {
                return stringOfViews.size();
            }
        });
        addContentView(layout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
    }
}

本节重点

1.onMeasure方法
重点是在换行的逻辑以及最后一个item的处理。另外onMeasure方法中先测量子view后测量父容器的思想来自于源码分析View的绘制流程,具体参见:
https://blog.csdn.net/u011109881/article/details/111148885
另外还有一个需要注意的地方就是测量和摆放的时候 getMeasuredHeight以及getMeasuredWidth是包括了padding而没有包括margin的,在计算宽高和摆放的时候需要留意
2.onLayout布局的摆放
3.Adapter相关
这个和我原本理解的Adapter设计模式有点不一样,我原先的理解是“适配器的目的是将一个对象包装成像另一种对象的样子,以达到符合接口要求的目的”
参见 https://blog.csdn.net/u011109881/article/details/82288922
但是在这里似乎只起到连接的作用 让我们在界面中得以与自定义View TagLayout交互

学习感悟

1.百闻不如一见,百见不如一干
在View的绘制流程那一块,我当时看视频没有看懂,后来将视频反反复复看了两三遍,还是没有怎么看懂,只是大略有个粗略的映像。于是后来我放下视频,自己跟了下源码,思路才开始清晰起来。所以说看视频多少次都感觉是浮光掠影而已,远没有自己实践一次映像清晰,而把这一过程以博客的形式留存下来,一个是加深映像,一个是这也是一种形式的实践吧,最后一个原因也是方便以后查看。毕竟人的脑袋虽然强大,但是要记下那么多东西总还是有些吃力,至少我自己这么觉得。比如23种设计模式,我虽然以前都看过,但是映像确实不是深刻了。但是后来在视频课程中提到的Touch事件相关的责任链模式 onDraw相关的模板模式 Adapter使用的适配器模式,虽然当时第一次听到名称时只剩下模模糊糊的映像,后来翻阅笔记只花了几分钟就想起了是怎么回事了,这也是实践的力量吧。
2.注释,清晰的命名
在写本篇的代码的时候,高度和宽度的计算老是出问题,不是出在测量,就是出在摆放上,虽然想通过debug+log打印的方式找出问题,但是总感觉逻辑不顺畅,后面通过修正变量命名以及添加关键逻辑的注释,让思路清晰起来。感觉这也是越读源码的功劳吧,在View的绘制流程里越读源码的时候,感觉注释发挥了很大的作用,即使没有读懂源码,看一下注释也知道大概干了什么。这个方面不需要做什么付出,得到的收获却是挺大的。
3.如何越读源码
以前我很畏惧源码,虽然知道越读源码很重要,为此老早就买了一本Android系统源代码情景分析,结果现在还躺在箱子里吃灰。后面我想出一种适合自己的阅读源码的方式,即先看看视频或者博客,然后自己跟踪源码尝试理解,最后对照视频或者博客再理解一遍,看看有没有疏漏。现在的网络这么发达,比过去只能一个人哼哧哼哧的啃源码方便多了,还是要感谢这个时代呀。
另外我想出一个比较好玩的越读源码的类比。我是一个RPG游戏爱好者,在游戏中往往会设计各个boss,以及各种宝物。我们之所以害怕越读源码,就像我们直接用一级的人物面对强大的最终boss感到束手无策一样。那么一般情况,我们是一边收集强力宝物武装自己,一边打败各个小boss提升实力(升级),最后再面对最终boss时,也不再是那个手无寸铁的1级人物了。游戏里面往往设计了很多迷宫,迷宫中有一个强力boss,要想打败boss,很多情况不是一条直线走下去就能战胜boss的,有时我们需要走一下这个分支,收集一件宝物,走一下那个分支,收集另一间宝物,最后收集到一定数量的宝物,我们就能轻松战胜boss了。实际上我觉得我们越读源码就像是在玩一款RPG游戏,很多情况,我们无法一下子读懂源码,代码跟着跟着就迷失在代码的迷宫中。这是因为我们的目的不够清晰,我们需要带着自己的目标出发,一旦发现迷失,就退回到起点从头开始。而且我们的目标可能一开始并不是直接面对boss,也可能先走其他分支,先找到宝物,收集到战胜boss的宝物之后,boss战就轻松多了。比如View的绘制流程,需要收集的宝物就有三样
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)
performLayout(lp, mWidth, mHeight)
performDraw()
等我们理解完这三个代码流程之后,View的绘制流程基本就理解了。还是一句话,带着目标读源码。
4.由简到难
完成代码的功能的时候,不要一开始就想把所有功能都完善,可以先把要完成的功能先记在另外的地方,写代码的时候先完成最基础的功能,这是因为如果我们在测试的时候遇到问题,往往会被那些复杂的功能的逻辑混淆,找出问题原因相对困难。之所以有这个想法,是因为我在写作本篇的时候,一下子就像把所有的功能都完善,既想实现View不同高度的功能又想实现支持margin的功能,最后出问题的时候,调研原因的时候逻辑相当混乱。最终我还是去掉了这两个功能,只保留最基本的功能才定位出问题原因的。这就是所谓的贪多嚼不烂吧。
另外,这就要求我们写的代码每一个功能要相对稳健,这样确保我们出问题的时候调查问题不会受到之前写好功能的影响,可以直接调查我们新写的逻辑。我想之所以很多的祖传代码很难修改,就是因为没有让一开始的代码稳健,后面出问题就积重难返了。
5.思想很重要
回顾这几篇自定义view的博客,其实很多自定义view的逻辑都来自源码,比如本节中,计算自身高度要先计算子view 再计算父容器的思路来自onMesure的源码,计算高度累加的思路可以参考LinearLayout measureVertical方法,Adapter的设计思路可以参考ListView的ListAdapter的基类Adapter来写,所以阅读源码很重要,还有,要灵活运用这些思想。
6.学习的方向,不要钻牛角尖
我再写作本篇的时候在适配高度不同的view的那里卡了4-5个小时,晚上坐在那里,一边听歌一边断点调试,不知不觉就到1点多了,第二天起来看看,还是没什么思路,后面我就干脆放弃了。毕竟这个只是逻辑上的问题,其实大致思路是有的,就是在一行view中取最高的view作为本行高度,虽然逻辑很简单,但是问题却找不出,确实挺恼人。不过这仅仅是个小问题,对自我的提升方面影响非常小。有这么多时间,花在git的学习,事件分发,kotlin学习或是其他框架的学习收益更大,所以我最终决定放弃。毕竟这只是自娱自乐的东西。
不过如果是工作上的话,就让不开了呢,解决方案一个是问问其他人,有时候自己写的逻辑往往别人一眼就能看出问题。另外一个就是放一下,过一段时间再回头看看,说不定会有不同思路。

完整代码:
https://github.com/caihuijian/learn_darren_android.git

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值