Android自定义View之ViewGroup

Android自定义View基础及绘制流程
Android自定义View工作原理关于measure、layout、draw详解
前面两篇文章介绍了android关于View的绘制流程和原理,下面会列举一些简单的自定义View帮助大家更容易理解掌握。

通过前面的介绍自定义ViewGroup一般重写onMeausre和onLayout两个方法。这里回忆一下为何重写这两个方法?

当measure事件和layout事件传递到我们布局的ViewGroup时,会调用onMeasure和onLayout方法,这两个方法交由具体的实现类实现。参考LinearLayout,其onMeasure和onLayout循环遍历了子元素,并且又调用了子元素的measure和layout方法,这样来完成ViewTree的遍历。

我们在Android自定义View工作原理关于measure、layout、draw详解中介绍了自定义ViewGroup的以下结论:

  • onMeasure 测量本身和测量子元素,测量子View根据实际情况调用不同方法。一般调用系统测量子元素的方法比如measureChildWithMargins、measureChild,如果子元素只有一层也可以调用view.measure
  • onLayout 子元素的位置计算,本身的位置计算已在layout当中完成。

那么,我们根据以上结论来实现一个简单的流式布局

先实现一个简单的效果,不考虑margin,padding,换行等。
每个控件换一行,向右移动相对上一个控件宽度的距离。
在这里插入图片描述
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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

<com.example.mygroupview.MyViewGroup
    android:background="@color/design_default_color_background"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:text="我是控件1"
        android:background="@color/design_default_color_secondary"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:text="我是控件2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text="我是控件3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text="我是控件4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text="我是控件5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:layout_marginTop="20dp"
        android:text="我是控件2111111111111"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text="我是控件33333333"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text="我是控件4444444"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text="我是控件555555555555555555"
        android:layout_marginLeft="15dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</com.example.mygroupview.MyViewGroup>

</LinearLayout>

实现步骤:

1.重写onMeasure和onLayout,
2.onMeasure里面测量子元素。遍历出所有子元素,根据ViewGroup的宽高测量规格(也就是子元素的父控件),和子元素的layoutParams调用子元素的measure。
3.测量自身尺寸。如果自身有具体尺寸,返回具体尺寸。否则宽返回所有子元素宽之和,高返回所有子元素高之和。
4.重写onLayout方法,确定子元素位置。

package com.example.mygroupview;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

public class MyViewGroup extends ViewGroup {

    public MyViewGroup(Context context) {
        super(context);
    }

    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //初步测量自身 主要是为了初始化 一些东西避免后续报错
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childCount = getChildCount();

        //测量子View尺寸
        for(int i =0;i<childCount;i++){
            View view = getChildAt(i);
            ViewGroup.LayoutParams lp = view.getLayoutParams();
            int childWidthSpec = getChildMeasureSpec(widthMeasureSpec,0,lp.width);
            int childHeightSpec = getChildMeasureSpec(heightMeasureSpec,0,lp.height);
            view.measure(childWidthSpec,childHeightSpec);
        }
        //测量自身尺寸
        int width = 0;
        int height = 0;
        switch (widthMode) {
            case MeasureSpec.EXACTLY:
                width = widthSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                for (int i = 0; i < childCount; i++) {
                    width +=  getChildAt(i).getMeasuredWidth();
                }
                break;
            default:
                break;
        }

        switch (heightMode){
            case MeasureSpec.EXACTLY:
                height =heightSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                for (int i = 0; i < childCount; i++) {
                    View child = getChildAt(i);
                    height += child.getMeasuredHeight();
                }
                break;
            default:
                break;
        }
        //保存自身的尺寸
        setMeasuredDimension(width,height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int left = 0;
        int top = 0;
        int right = 0;
        int bottom = 0;

        int childCount = getChildCount();
        for(int i = 0; i < childCount; i++){
            View child = getChildAt(i);

            right = left + child.getMeasuredWidth();
            bottom = top + child.getMeasuredHeight();
            child.layout(left,top,right,bottom);
            left  +=child.getMeasuredWidth();
            top += child.getMeasuredHeight();
        }
    }

}


其实列举这个,主要是大家要思考为何要重写这两个方法?这两个方法要做什么事情?调用子元素的测量方法的时候我们需要如何传值?
再回忆下Android自定义View工作原理关于measure、layout、draw详解

系统通过performTraversals依次调用performMeasure、performLayout、performDraw方法,最终会调用顶级View的measure,layout,draw完成顶级View的测量。
顶级View就是decorview,为一个FrameLayout,一般情况下里面是一个LinearLayout,这个LinearLayout又包含两个FrameLayout,分别显示标题和内容,其内容就是我们的setContentView。
measure,layout方法如何从顶级decorview–FrameLayout传递到LinearLayout再传递到我们的View?

FrameLayout的measure会调用onMeasure,onMeasure里面会测量自身并遍历子元素,调用子元素的measure方法测量子View。所以就调用到了LinearLayout的meausre方法,而LinearLayout.measure()又会调用LinearLayout.onMeasure方法,在其onMeasure里面又会测量自身并遍历子元素调用子元素的measure方法。这样measure方法就从顶级View传递到了我们的View,对于layout基本也是这个过程。

  • ViewGroup的measure和layout方法为final不可重写,它们分别调用了onMeasure和onLayout方法,所以重写onMeasure,和onLayout方法。LinearLayout和FrameLayout都是ViewGroup,它们是如何重写onMeasure和onLayout的?其实总结起来就是我们本文开篇所说的,测量自身和子元素,为子元素确定位置。我们自己重写这两个方法要完成的事情也是这样。

接着上面的例子,扩展下如果加入margin和padding值考虑下换行,实现下面简单的流式布局

在这里插入图片描述

还是按照上述思路去思考,需要先确定子元素和本身宽高,再确定子元素的位置。子元素的测量要考虑margin等值,ViewGroup的宽高计算不一样了,对于每个子元素的layout的四个点也需要考虑换行。计算过程确实复杂了很多,但是原理是相通的。其实真正看源码明白了onMeasure,measure,onLayout,layout做的事情后,对于ViewGroup这两个方法的重写基本都是大同小异,无非就是计算过程根据实际需求的复杂度不同。

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

<com.example.mygroupview.MyViewGroup1
    android:background="@color/design_default_color_background"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:text="我是控件1"
        android:layout_marginLeft="10dp"
        android:paddingLeft="15dp"
        android:background="@color/design_default_color_secondary"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:text="我是控件2"
        android:layout_marginLeft="10dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text="我是控件3"
        android:layout_marginTop="10dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text="我是控件4"
        android:layout_marginLeft="12dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text="我是控件5"
        android:layout_marginLeft="15dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:text="我是控件2111111111111"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text="我是控件33333333"
        android:layout_marginLeft="30dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text="我是控件4444444"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text="我是控件555555555555555555"
        android:layout_marginLeft="15dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</com.example.mygroupview.MyViewGroup1>

</LinearLayout>
package com.example.mygroupview;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

public class MyViewGroup1 extends ViewGroup {

    private String TAG = "MyViewGroup1";

    public MyViewGroup1(Context context) {
        super(context);
    }

    public MyViewGroup1(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyViewGroup1(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    private int lineWidth;//每一行的宽
    private int viewGroupWidth;//ViewGroup的宽
    private int lineHeight;
    private int viewGroupHeight;
    private List<List<View>> views = new ArrayList<>();
    private List<View> lineViews = new ArrayList<>();//每一行的View
    private List<Integer> heights = new ArrayList<>();//记录每行的高度

    private void init() {
        views.clear();
        lineViews.clear();
        lineWidth = 0;
        lineHeight = 0;
        viewGroupWidth = 0;
        heights.clear();
        viewGroupHeight = 0;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //初步测量自身 主要是为了初始化 一些东西避免后续报错
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        init();
        // 计算子View限制信息
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childCount = getChildCount();

        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            //测量子View宽高
            measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
            //计算每行的View主要是为了测量父View宽高
            int childWidth = childView.getMeasuredWidth();
            int childheight = childView.getMeasuredHeight();
            MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();

            if (lineWidth + childWidth + lp.leftMargin + lp.rightMargin > widthSize - getPaddingRight() - getPaddingLeft()) {
                //换行的时候清空记录每行的list,并把它加入两层list的views
                views.add(lineViews);
                lineViews = new ArrayList<>();
                viewGroupWidth = Math.max(viewGroupWidth, lineWidth);
                viewGroupHeight += lineHeight;
                heights.add(lineHeight);
                lineHeight = 0;
                lineWidth = 0;
            }
            // 不换行
            lineViews.add(childView);
            lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
            lineHeight = Math.max(lineHeight, childheight + lp.topMargin + lp.bottomMargin);

            if (i == childCount - 1) {
                viewGroupWidth = Math.max(viewGroupWidth, lineWidth);
                viewGroupHeight += lineHeight;
                heights.add(lineHeight);
                views.add(lineViews);
            }


        }
        //测量自身尺寸
        int width = 0;
        int height = 0;
        switch (widthMode) {
            case MeasureSpec.EXACTLY:
                width = widthSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                width = viewGroupWidth;
                break;
            default:
                break;
        }

        switch (heightMode) {
            case MeasureSpec.EXACTLY:
                height = heightSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:

                height = viewGroupHeight;

                break;
            default:
                break;
        }
        setMeasuredDimension(width, height);

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {


        int left = 0;
        int top = 0;


        for (int i = 0; i < views.size(); i++) {
            List<View> lineViews = views.get(i);
            lineHeight = heights.get(i);//行高
            // 遍历当前行
            for (int j = 0; j < lineViews.size(); j++) {


                View child = lineViews.get(j);
                // 该child的LayoutParams
                MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                //上一个child的LayoutParams
                MarginLayoutParams lastLp = null;
                View lastChild = null;
                if (j != 0) {
                    lastChild = lineViews.get(j - 1);
                    lastLp = (MarginLayoutParams) lastChild.getLayoutParams();
                }

                if (j == 0) {
                    left = lp.leftMargin;
                } else {
                    //上一次的左边+上个的宽+上一个的右边距+这一个的左边距
                    left = left + lastChild.getMeasuredWidth() + lastLp.rightMargin + lp.leftMargin;
                }


                Log.d(TAG, "第" + i + "行第" + j + "个 left:" + left + ",right:" + (left + child.getMeasuredWidth()) + ",top:" + (top + lp.topMargin )+ ",buttom:" + (top + lp.topMargin + child.getMeasuredHeight()));
                child.layout(left, top + lp.topMargin, left + child.getMeasuredWidth(), top + lp.topMargin + child.getMeasuredHeight());

            }

            top += lineHeight;
            left = 0;
        }


    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLayoutParams(getContext(), attrs);
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
        return new MyLayoutParams(lp);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MyLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

    public static class MyLayoutParams extends MarginLayoutParams {

        public MyLayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        public MyLayoutParams(int width, int height) {
            super(width, height);
        }

        public MyLayoutParams(LayoutParams lp) {
            super(lp);
        }
    }


}

关于layoutParams的坑
这里注意下有个layoutParams的坑,计算子元素宽高需要考虑margin的情况下,我们可以调用系统给我们提供的measureChildWithMargins。传入的值是子元素父控件的宽高规格和子元素的layoutParams。那么这个layoutParams还能不能用ViewGroup的layoutParams呢?
遗憾的是这样传入会报错,需要传入MarginLayoutParams。

我们来看看measureChildWithMargins源码:

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

通过上述可以看到,需要传入的是MarginLayoutParams 。如果我们不重写
generateLayoutParams方法,通过View的添加过程addView可以看到,默认传入的是ViewGroup.LayoutParams。

  public void addView(View child, int index) {
        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }
        LayoutParams params = child.getLayoutParams();
        if (params == null) {
            params = generateDefaultLayoutParams();
            if (params == null) {
                throw new IllegalArgumentException(
                        "generateDefaultLayoutParams() cannot return null");
            }
        }
        addView(child, index, params);
    }
    
 protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

以上只是一个简单的流式布局,里面还有很多需要优化的地方,比如超出屏幕如果没有放在scrollView里面无法滑动等。可结合滑动冲突系列文章处理Android滑动冲突解决方案内外部拦截法及原理
本文主要是帮助大家加深理解View的measure和layout的过程,这里就不做介绍了。
最后这里记录一个github上面的一个流式布局地址: https://github.com/google/flexbox-layout

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值