[AS3.0.1]自定义ViewGroup的学习,写一个FlowLayout布局

前言

关于上篇的[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的getMeasuredHeightgetMeasuredWidth都是0,这样绘制当然就是空白的。

之后我们就是要在获取子孩子的宽高之前,先让其有值。我们就去重写onMeasure方法
代码如下

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

我加入了一个measureChildren方法来测量布局中的所有子孩子的宽高
加入之后我发现自定义的viewgroup可以正常显示子孩子了。
然后我们在xml布局下 不断加入多个子孩子 实现换行的效果

图1

这边我是直接贴了,在编译的时候生成的效果。

每个子孩子的布局都是如下

<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);
    }

修改之后,布局效果变成如下

图2

我们发现确实布局变了。但是好像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);

在运行就可以发现一切都正常了
img3

上面布局中,有的是有加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应该只能到最后的子孩子下方才对
然而实际是错误的
img4

那么我们只能去看下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是否被固定写死了宽高。

加入代码之后我们在运行如下
img5

问题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">

img6

我们回顾上面的代码,发现我们的起始位置都是从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;
        }
    }

在运行一下看下效果
img7


全部代码

至此一段自定义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自适应容器实现

猜你喜欢

转载自blog.csdn.net/g777520/article/details/80483782