分析CSDN搜索文字布局控件
首先,我们需要来分析一下,CSDN搜素框文字提示控件的布局,下图为最新CSDN的搜索框布局提示图:
经过前面的自定义控件,布局等学习后,我们来简单的来分析上面这个控件,可以看到文字的提示是由很多的文字(TextView)组成,而每排的文字(TextView)数量不固定,根据长度自适应每行多少个文字,同时他们有上间距,也有下间距,也就是布局中常用的Margin。
ok,我们看到的也就这些,主要实现的就是这个容器规则,但是我们前面只介绍了如果布局容器,并没有介绍如何使用Margin,所以我们先来了解一下ViewGroup中是如何获取,以及设置Margin的。
ViewGroup获取子控件的Margin
我们先来看看,ViewGroup需要重写获取设置Margin值的三个方法:
@Override protected LayoutParams generateDefaultLayoutParams() { return new MarginLayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(),attrs); } @Override protected LayoutParams generateLayoutParams(LayoutParams p) { return new MarginLayoutParams(p); }
每个自定义控件都需要这样写,至于原因,我们还需要来看一段源代码,比如new MarginLayoutParams(getContext(),attrs),你可以通过Android Studio开发工具点进去看下,就会发现其实跟前面获取自定义属性的代码基本一样,代码如下:
public MarginLayoutParams(Context c, AttributeSet attrs) { super(); TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout); setBaseAttributes(a, R.styleable.ViewGroup_MarginLayout_layout_width, R.styleable.ViewGroup_MarginLayout_layout_height); int margin = a.getDimensionPixelSize( com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1); if (margin >= 0) { leftMargin = margin; topMargin = margin; rightMargin= margin; bottomMargin = margin; } else { int horizontalMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginHorizontal, -1); int verticalMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginVertical, -1); if (horizontalMargin >= 0) { leftMargin = horizontalMargin; rightMargin = horizontalMargin; } else { leftMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginLeft, UNDEFINED_MARGIN); if (leftMargin == UNDEFINED_MARGIN) { mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK; leftMargin = DEFAULT_MARGIN_RESOLVED; } rightMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginRight, UNDEFINED_MARGIN); if (rightMargin == UNDEFINED_MARGIN) { mMarginFlags |= RIGHT_MARGIN_UNDEFINED_MASK; rightMargin = DEFAULT_MARGIN_RESOLVED; } } startMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginStart, DEFAULT_MARGIN_RELATIVE); endMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginEnd, DEFAULT_MARGIN_RELATIVE); if (verticalMargin >= 0) { topMargin = verticalMargin; bottomMargin = verticalMargin; } else { topMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginTop, DEFAULT_MARGIN_RESOLVED); bottomMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginBottom, DEFAULT_MARGIN_RESOLVED); } if (isMarginRelative()) { mMarginFlags |= NEED_RESOLUTION_MASK; } } final boolean hasRtlSupport = c.getApplicationInfo().hasRtlSupport(); final int targetSdkVersion = c.getApplicationInfo().targetSdkVersion; if (targetSdkVersion < JELLY_BEAN_MR1 || !hasRtlSupport) { mMarginFlags |= RTL_COMPATIBILITY_MODE_MASK; } // Layout direction is LTR by default mMarginFlags |= LAYOUT_DIRECTION_LTR; a.recycle(); }
简单的来理解,这段代码先是提取了layout_margin的值并进行设置,然后,如果用户没有设置layout_margin,而是单独设置的,就一个一个提取。
需要注意的是,之所以需要重写这些函数,是因为默认的ViewGroup方法只会获取layout_width和layout_height,只有MarginLayoutParams()方法才具有提取margin值的功能。
实现CSDN搜索文字提示布局
今天讲解的Margin加上前面讲解的自定义布局的知识,就可以实现CSDN搜索文字提示布局,首先,我们还是来分析一下博文开始的图片,是不是发现所有的TextView的style除了文字不一样,其他的都一样呢?所以我为了减少代码额冗余,我们先自定义一个style,代码如下:
<style name="textview"> <item name="android:layout_width">wrap_content</item> <item name="android:layout_height">wrap_content</item> <item name="android:layout_margin">4dp</item> <item name="android:background">@drawable/textviewshap</item> <item name="android:textColor">#FF1E90FF</item> <item name="android:textSize">16sp</item> </style>
接着,我们设置我们的布局文件,主Activity的XML布局文件如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.liyuanjinglyj.csdnserachapplication.CSDNSerachView android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView style="@style/textview" android:text="Python"/> <TextView style="@style/textview" android:text="Java"/> <TextView style="@style/textview" android:text="Spring Boot"/> <TextView style="@style/textview" android:text="PHP"/> <TextView style="@style/textview" android:text="Vue"/> <TextView style="@style/textview" android:text="Flutter"/> <TextView style="@style/textview" android:text="Python基础教程"/> <TextView style="@style/textview" android:text="Java学习路线"/> <TextView style="@style/textview" android:text="C语言"/> <!--省略其他TextView--> </com.liyuanjinglyj.csdnserachapplication.CSDNSerachView>
然后就是常规的自定义ViewGroup步骤,测量onMeasure(),布局onLayout(),首先肯定是测量所有子控件的大小,计算整个自定义ViewGroup的宽高,代码如下(还是常规套路):
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measureWidth=MeasureSpec.getSize(widthMeasureSpec); int measureHeight=MeasureSpec.getSize(heightMeasureSpec); int measureWidthMode=MeasureSpec.getMode(widthMeasureSpec); int measureHeightMode=MeasureSpec.getMode(heightMeasureSpec); int width=0; int height=0; //中间代码 setMeasuredDimension((measureWidthMode==MeasureSpec.EXACTLY)?measureWidth:width, (measureHeightMode==MeasureSpec.EXACTLY)?measureHeight:height); }
最重要的还是中间计算的代码,但是我们现在增加了Margin值,所以我们需要多申请2个变量代码如下:
int lineWidth=0;//记录每一行的宽度 int lineHeight=0;//记录每一行的高度
定义变量后,我们需要遍历每个子控件,然后根据子空间的大小是否还能放在当前行,不行的话跳转到下一行,并记录当前行使用的宽度,以便后续判断其他子控件是否装的下,完整代码如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int measureHeight = MeasureSpec.getSize(heightMeasureSpec); int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec); int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec); int width = 0;//整体宽度 int height = 0;//整体高度 int lineWidth = 0;//记录每一行的宽度 int lineHeight = 0;//记录每一行的高度 int count = getChildCount();//获取子控件数量 for (int i = 0; i < count; i++) { View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; if (lineWidth + childWidth > measureWidth) { //如果当前行占位Textview+新占位如果当前行占位Textview大于整体宽度,就需要换行设置控件 width = Math.max(lineWidth, width);//获取最大值的宽度值 height += lineHeight;//换行后直接加上宽度就是整体宽度 //因为当前行放不下控件,所以宽高直接等于新换行的控件宽高 lineWidth = childWidth; lineHeight = childHeight; } else { //否则 lineWidth += childWidth;//如果控件没有换行,就是之前的宽度加上新TextView宽度 lineHeight = Math.max(lineHeight, childHeight);//而如果之前高度高于当前子控件高度,那么该行还是之前的宽度,否则为新控件高度 } //因为最后一行不管填不填满,高度都需要加上最后一行所以,没经过最后换行的操作,上面是不会计算加最后一行的高度的,所以必须单独写出来 if (i == count - 1) { height += lineHeight; width = Math.max(width, lineWidth); } } setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height); }
上面的注释应该够详细了,这里就不再赘述了,不过需要说明的是子控件的宽度是自身宽度加上margin_right和margin_left,子空间高度是自身高度加上margin_top和margin_bottom,上面自定义ViewGroup也就这个值的获取与之前博文不同,其他的都是一些常规操作与判断。
测量onMeasure()写完了,我们还需要进行布局代码的编写,因为我们在控件中加入了margin属性,所以我们需要标记当前子控件的top坐标和left坐标,为什么不需要bottom,以及right呢?因为定位一个控件是其左上角坐标(如上图所示,看完就会明白),具体的代码如下:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int count = getChildCount(); int lineWidth = 0; int lineHeight = 0; int top = 0, left = 0; for (int i = 0; i < count; i++) { View child = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; if (childWidth + lineWidth > getMeasuredWidth()) { //如果换行,与onMeasure基本类同 top += lineHeight; left = 0; lineHeight = childHeight; lineWidth = childWidth; } else { lineHeight = Math.max(lineHeight, childHeight); lineWidth += childWidth; } //计算子空间的left,top,right,bottom int lc = left + lp.leftMargin; int tc = top + lp.topMargin; int rc = lc + child.getMeasuredWidth(); int bc = tc + child.getMeasuredHeight(); child.layout(lc, tc, rc, bc); left += childWidth;//将left更改为下个子空间的起始点 } }
这样我们就是实现了CSDN搜索框下文字布局的容器控件效果,当然,我这里还设置了边框等样式,本文主要介绍自定义ViewGroup,边框样式属于基础,这里就不再介绍了。(边框效果最后源代码里面有),实现效果如下:
本文GitHub下载地址:点击下载