概述
效果:
自定义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
最后
以上就是勤劳樱桃为你收集整理的红橙Darren视频笔记 流式布局tagLayout measure layout方法学习 adapter使用 学习感悟的全部内容,希望文章能够帮你解决红橙Darren视频笔记 流式布局tagLayout measure layout方法学习 adapter使用 学习感悟所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复