效果:
自定义View
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147public 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布局
1
2
3
4
5
6
7
8
9<?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背景
1
2
3
4
5
6
7
8<?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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82public 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
最后
以上就是勤劳樱桃最近收集整理的关于红橙Darren视频笔记 流式布局tagLayout measure layout方法学习 adapter使用 学习感悟的全部内容,更多相关红橙Darren视频笔记内容请搜索靠谱客的其他文章。
发表评论 取消回复