前言
关于上篇的[AS3.0.1]自定义选项listview(标签流式布局)自定义view,是使用了一个很简单是计算得来的,这样会导致使用了之后,创建多个LinearLayout,并且随着越来越多的参数可能会导致绘制卡顿。所以我查了叫标签流式布局之后自己写了个新的。在学习的时候也顺便学习了下,自定义viewgroup的使用。
自定义ViewGroup
简单实现
首先我这边创建一个TestLayout继承于ViewGroup,创建之后必须实现初始化和方法onLayout
,这个方法是viewgroup用来绘制布局的方法,就是绘制子孩子到底应该如何放置位置。
然后我需要的效果是随着子孩子的不断增加。当并列的子孩子宽度超过实际宽度之后就需要换行。
我便对onLayout
进行了设置
代码如下
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int x = 0, y = 0;
int maxL = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
maxL = Math.max(maxL, child.getMeasuredHeight());
if (x + child.getMeasuredWidth() > getMeasuredWidth()) {
x = 0;
y += maxL;
maxL = 0;
}
int cl = x;
int ct = y;
int cr = cl + child.getMeasuredWidth();
int cb = ct + child.getMeasuredHeight();
child.layout(cl, ct, cr, cb);
x += child.getMeasuredWidth();
}
}
思路就是先设置起点,然后循环子孩子,如果当前宽度加上下一个孩子的宽度是超过的就换行。
在设置xml布局
<com.gjn.viewdemo.TestLayout
android:id="@+id/testlayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/str_b"
android:padding="5dp"
android:text="11111" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/str_b"
android:padding="5dp"
android:text="2222" />
</com.gjn.viewdemo.TestLayout>
运行发现没有任何效果。把数据都打印出来发现获取getChildAt(i)
中的view的getMeasuredHeight
和getMeasuredWidth
都是0,这样绘制当然就是空白的。
之后我们就是要在获取子孩子的宽高之前,先让其有值。我们就去重写onMeasure
方法
代码如下
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
我加入了一个measureChildren
方法来测量布局中的所有子孩子的宽高
加入之后我发现自定义的viewgroup可以正常显示子孩子了。
然后我们在xml布局下 不断加入多个子孩子 实现换行的效果
这边我是直接贴了,在编译的时候生成的效果。
每个子孩子的布局都是如下
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/str_b"
android:padding="5dp"
android:text="11111" />
区别只是text变了而已。
问题1:子孩子设置margin无效
上面我们好像就完成了这个自定义viewgroup了,但是当我为子孩子加入layout_margin属性的时候,会发现完全没有效果
对上面的textview加入属性
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="@drawable/str_b"
android:padding="5dp"
android:text="11111" />
那么我们就需要在绘制的时候考虑了下子孩子的margin设置之后,不就可以实现了
修改的onLayout
如下
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int x = 0, y = 0;
int maxL = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
int childW = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
int childH = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;
maxL = Math.max(maxL, childH);
if (x + childW > getMeasuredWidth()) {
x = 0;
y += maxL;
maxL = 0;
}
int cl = x;
int ct = y;
int cr = cl + childW;
int cb = ct + childH;
child.layout(cl, ct, cr, cb);
x += childW;
}
}
其中因为加入了MarginLayoutParams所以TestLayou需要实现generateLayoutParams(LayoutParams p)
、generateLayoutParams(AttributeSet attrs)
、generateDefaultLayoutParams()
这三个方法
代码如下
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
修改之后,布局效果变成如下
我们发现确实布局变了。但是好像margin和padding的效果出了点问题。
注,这边的padding是没问题的,由于textview布局的背景是绘制出来的,所以开始设置的5dp的padding是让布局显示正常用的,下面会提到!
我们可以发现,布局应该是正常了,但是为什么,填充背景设置了,整个view的布局呢?
回顾一下,我们可以知道绘制子孩子的位置用的是child.layout(cl, ct, cr, cb);
我们看一下代码
int cl = x;
int ct = y;
int cr = cl + childW;
int cb = ct + childH;
child.layout(cl, ct, cr, cb);
绘制的矩形起点x和y还有添加的宽度和高度都不对了,起点是因为没有加入margin的left和top,背景是因为绘制的范围被加上了margin的宽高,所以导致了绘制出错。
我们修改如下
int cl = x + params.leftMargin;
int ct = y + params.topMargin;
int cr = cl + child.getMeasuredWidth();
int cb = ct + child.getMeasuredHeight();
child.layout(cl, ct, cr, cb);
在运行就可以发现一切都正常了
上面布局中,有的是有加margin,有的是没有加。所以是正常的显示
问题2:对TestLayout的宽高设置无效
首先我对testlayout布局进行修改(加入android:layout_margin)
<com.gjn.viewdemo.TestLayout
android:id="@+id/testlayout"
android:background="@drawable/fl_b"
android:layout_margin="35dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
我们发现我们设置的是wrap_content,按view应该只能到最后的子孩子下方才对
然而实际是错误的
那么我们只能去看下onMeasure
方法是不是测量错误了。
我们点进measureChildren
方法
代码如下
/**
* Ask all of the children of this view to measure themselves, taking into
* account both the MeasureSpec requirements for this view and its padding.
* We skip children that are in the GONE state The heavy lifting is done in
* getChildMeasureSpec.
*
* @param widthMeasureSpec The width requirements for this view
* @param heightMeasureSpec The height requirements for this view
*/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
发现是循环了一遍全部子孩子并且调用了measureChild(child, widthMeasureSpec, heightMeasureSpec)
方法设置,那么在进入measureChild
看看
代码如下
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding.
* The heavy lifting is done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param parentHeightMeasureSpec The height requirements for this view
*/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
我们看到这个方法对子孩子的padding进行了设置,这么就说明确实上面的padding设置是有效的。
那么我们只能自己设置下onMeasure
方法来进行测量宽高的管理了
那么我们就删掉重写onMeasure
修改代码如下
@Override
protected void onMeasure(int widthMeasureSpec, int 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 count = getChildCount();
int x = 0;
int maxL = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
int childW = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
int childH = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;
maxL = Math.max(maxL, childH);
if (x + childW > measureWidth) {
width = measureWidth;
height += maxL;
x = 0;
maxL = 0;
}
x += childW;
//当子孩子还未超过可用宽度则先设置第一层的宽高
if (width < measureWidth){
width = x;
height = maxL;
}
}
setMeasuredDimension(
(measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
(measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height
);
}
我们可以看到,onMeasure的设置测量和onLayout差别不大,就是多了一个测量还未填充完毕的时候设置好宽高罢了。
最后一个setMeasuredDimension
设置是用来判断,TestLayout是否被固定写死了宽高。
加入代码之后我们在运行如下
问题3:设置Padding出错
由上面子孩子的问题,我们对TestLayout设置了padding
<com.gjn.viewdemo.TestLayout
android:id="@+id/testlayout"
android:background="@drawable/fl_b"
android:layout_margin="35dp"
android:padding="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
我们回顾上面的代码,发现我们的起始位置都是从0开始的,所以一直都没有对自身的padding进行过判断,所以我们需要修改2个方法,将开始被忽略的padding都加上。
修改后的代码如下
@Override
protected void onMeasure(int widthMeasureSpec, int 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 count = getChildCount();
int x = getPaddingLeft();
int maxL = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
int childW = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
int childH = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;
maxL = Math.max(maxL, childH);
if (x + childW > measureWidth) {
width = measureWidth;
height += maxL;
x = getPaddingLeft();
maxL = 0;
}
x += childW;
//当子孩子还未超过可用宽度则先设置第一层的宽高
if (width < measureWidth){
width = x + getPaddingLeft() + getPaddingRight();
height = maxL + getPaddingTop() + getPaddingBottom();
}
}
setMeasuredDimension(
(measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
(measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height
);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int x = getPaddingLeft(), y = getPaddingRight();
int maxL = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
int childW = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
int childH = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;
maxL = Math.max(maxL, childH);
if (x + childW > getMeasuredWidth()) {
x = getPaddingLeft();
y += maxL;
maxL = 0;
}
int cl = x + params.leftMargin;
int ct = y + params.topMargin;
int cr = cl + child.getMeasuredWidth();
int cb = ct + child.getMeasuredHeight();
child.layout(cl, ct, cr, cb);
x += childW;
}
}
在运行一下看下效果
全部代码
至此一段自定义viewgroup的学习就到这里了。还有就是和自定义view一样的需要设置自定义的属性这类参数,自定义属性蛮简单的,可以自行百度查阅。
TestLayout.java
package com.gjn.viewdemo;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/**
* Created by gjn on 2018/5/28.
*/
public class TestLayout extends ViewGroup {
public TestLayout(Context context) {
this(context, null);
}
public TestLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TestLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int 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 count = getChildCount();
int x = getPaddingLeft();
int maxL = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
int childW = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
int childH = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;
maxL = Math.max(maxL, childH);
if (x + childW > measureWidth) {
width = measureWidth;
height += maxL;
x = getPaddingLeft();
maxL = 0;
}
x += childW;
//当子孩子还未超过可用宽度则先设置第一层的宽高
if (width < measureWidth){
width = x + getPaddingLeft() + getPaddingRight();
height = maxL + getPaddingTop() + getPaddingBottom();
}
}
setMeasuredDimension(
(measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
(measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height
);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int x = getPaddingLeft(), y = getPaddingRight();
int maxL = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
int childW = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
int childH = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;
maxL = Math.max(maxL, childH);
if (x + childW > getMeasuredWidth()) {
x = getPaddingLeft();
y += maxL;
maxL = 0;
}
int cl = x + params.leftMargin;
int ct = y + params.topMargin;
int cr = cl + child.getMeasuredWidth();
int cb = ct + child.getMeasuredHeight();
child.layout(cl, ct, cr, cb);
x += childW;
}
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.gjn.viewdemo.MainActivity">
<com.gjn.viewdemo.TestLayout
android:id="@+id/testlayout"
android:background="@drawable/fl_b"
android:layout_margin="35dp"
android:padding="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="@drawable/str_b"
android:padding="5dp"
android:text="11111" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="@drawable/str_a"
android:padding="5dp"
android:text="2222" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="@drawable/str_b"
android:padding="5dp"
android:text="331233" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="@drawable/str_a"
android:padding="5dp"
android:text="331231231233" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/str_b"
android:padding="5dp"
android:text="33322222211" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/str_a"
android:layout_margin="10dp"
android:padding="5dp"
android:text="333" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/str_b"
android:padding="5dp"
android:text="3332222" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="@drawable/str_a"
android:padding="5dp"
android:text="333" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/str_b"
android:padding="5dp"
android:text="333" />
</com.gjn.viewdemo.TestLayout>
</FrameLayout>
str_a.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="10dp" />
<solid android:color="@android:color/holo_blue_bright" />
</shape>
str_b.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="10dp" />
<solid android:color="@android:color/darker_gray" />
</shape>
fl_b.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke android:width="1dp"
android:color="@android:color/black"/>
</shape>
总结
以前也写过一些自定义view,但是自定义viewgroup倒是第一次写,算是一个学习记录了!之后可能会写一些自定义view和viewgroup相关的吧!
资料
Android自定义ViewGroup(四、打造自己的布局容器)
自定义控件三部曲视图篇(二)——FlowLayout自适应容器实现