高级自定义View系列六;ViewGroup的标准范式

作者:波澜步惊
链接:https://www.jianshu.com/p/d906745f160c

前言

做程序开发,基础很重要。同样是拧螺丝人家拧出来的可以经久不坏,你拧出来的遇到点风浪就开始颤抖,可见基本功的重要性。此系列,专门收录一些看似基础,但是没那么简单的小细节,同时提供权威解决方案。喜欢的同志们点个赞就是对我最大的鼓励!先行谢过!

网上可能有一些其他文章,提供了解决方案,但是要么就是没有提供可运行demo,要么就是demo不够纯粹,让人探索起来受到其他代码因素的影响,无法专注于当前这个知识点(比如,我只是想了解Activity的生命周期,你把生命周期探究的过程混入到一个很复杂的大杂烩Demo中,让人一眼就没有了阅读Demo代码的欲望),所以我觉得有必要做一个专题,用最纯粹的方式展示一个的解决方案.

高级自定义view系列文章,都为学员波澜步惊在学习Android高级进阶课程的过程中学习的收获和笔记,如果你从事Android开发3-5年,处于瓶颈期需要寻找突破和进阶方向。那么我们一定会你有帮助和启发

高级自定义View系列思维脑图;

正文

高级ViewGroup是高级开发者的必备技能,而且也有固定套路,标准范式。

效果

 

要实现的效果很清晰,就是一行比一行缩进一定距离,并且考虑marginpadding.

标准套路

注:这里不考虑viewGroup自绘背景onDraw神马的。

  1. 重写onMeasure
  2. 在参数int值 int widthMeasureSpec,int heightMeasureSpec 中,分离出sizemode
  3. 遍历子view,把宽高信息传递给子view,让子view完成自己对自己的测量
  4. 利用子view测量之后的宽高,计算viewGroup的宽高
  5. 将计算出来的ViewGroup宽高保存起来
  1. 重写onLayout
  2. 遍历子view,计算子viewlefttoprightbottom
  3. 将计算出来的left,top,right,bottom 用child.layout(left, top, right, bottom);设置给子view

关键代码

自定义ViewGroup

package study.hank.com.customviewgroup;

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

/**
 * 实现自己的ViewGroup,效果为:
 * 所有子view,一律以纵向往下排布,每行一个的规律放置。
 * 并且,每放置一个,下一个就往右边锁紧一定距离。
 */
public class MyViewGroup extends ViewGroup {

    private static int OFFSET = 10;// 注意,源码中直接出现的距离,都是以px为单位,只有在定义了单位为dp的距离的时候,才是dp

    public MyViewGroup(Context context) {
        this(context, null);
    }

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

    public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        OFFSET = Utils.dip2px(10, context);//这里直接把它变成10dp
    }

    /**
     * 为了在121行 获取LayoutParam时 得到的是MarginLayoutParam,这里要重写此方法
     *
     * @param attrs
     * @return
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    /**
     * 注意,这两个参数,是自身的MeasureSpec int值
     * <p>
     * 如果考虑margin和padding的话,那具体指的就是 子view的margin,和自身的padding,不要弄错了。
     *
     * <p>
     * 自身的margin是自己的父去使用的,子view的padding是子View自己的。
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //获得自身的padding,它最终影响自身的可绘制区域
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();

        //从MeasureSpec中分离出size和mode
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //3、把宽高限制信息传给子,让子完成自己的测量
        final int childCount = getChildCount();
        View child;
        for (int i = 0; i < childCount; i++) {
            child = getChildAt(i);
            if (child.getVisibility() != GONE) {//如果子可见,才执行测量
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
                final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height);
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }

        //获取子测量之后的尺寸,然后根据子的尺寸决定 自己的尺寸
        int width = 0, height = 0;
        switch (widthMode) {//自身的测量mode
            case MeasureSpec.EXACTLY:
                width = widthSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                int xOffset = 0;
                for (int i = 0; i < childCount; i++) {
                    child = getChildAt(i);
                    //由于我这里加了一个缩进,所以要计算缩进之后的距离
                    //child的左右margin也要计算在内,一起参与viewGroup的测量
                    int widthAddOffsetAddPadding = i * OFFSET + child.getMeasuredWidth() + paddingLeft + paddingRight;
                    MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                    xOffset += lp.leftMargin + lp.rightMargin;
                    int widthAddOffsetAddPaddingAddChildMargin = widthAddOffsetAddPadding + xOffset;
                    width = Math.max(width, widthAddOffsetAddPaddingAddChildMargin);//这里考虑到缩进也有可能是负数,但是自身的宽度不能容不下子,所以要max
                }
                break;
            default:
                break;
        }

        switch (heightMode) {//自身的测量mode
            case MeasureSpec.EXACTLY:
                height = heightSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                for (int i = 0; i < childCount; i++) {
                    child = getChildAt(i);
                    height += child.getMeasuredHeight() + paddingTop + paddingBottom;
                }
                break;
            default:
                break;
        }

        //保存自身宽高
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int childCount = getChildCount();
        final int paddingLeft = getPaddingLeft();
        final int paddingTop = getPaddingTop();

        int topOffset = 0;//top偏移量
        int leftOffset = 0;//left偏移量
        for (int i = 0; i < childCount; i++) {//布局,就是要确定子的左上右下
            View child = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            int left = leftOffset + lp.leftMargin + paddingLeft;//为什么在这里获取的paddingLeft是空的?
            int right = left + child.getMeasuredWidth();
            int top = topOffset + paddingTop + lp.topMargin;
            int bottom = top + child.getMeasuredHeight();

            child.layout(left, top, right, bottom);

            topOffset += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            leftOffset += OFFSET + lp.leftMargin;//左偏移量也计算在内
        }

    }

    static class Utils {

        /**
         * 屏幕dip值转换为像素值
         *
         * @param dipValue 屏幕dip值
         * @return int 屏幕像素值
         */
        public static int dip2px(float dipValue, Context activity) {
            return (int) (dipValue * getScreenDensity(activity) + 0.5f);
        }

        private static float getScreenDensity(Context activity) {
            try {
                return activity.getResources().getDisplayMetrics().density;
            } catch (Exception e) {
                return 1;
            }
        }
    }
}

xml

<?xml version="1.0" encoding="utf-8"?>
<study.hank.com.customviewgroup.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:background="@color/colorAccent"
    android:padding="0dp"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:background="@color/colorPrimaryDark"
        android:text="margin=10" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimaryDark"
        android:padding="10dp"
        android:text="padding=10" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimaryDark"
        android:text="没有padding,margin" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimaryDark"
        android:text="没有padding,margin" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimaryDark"
        android:text="没有padding,margin" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimaryDark"
        android:text="没有padding,margin" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimaryDark"
        android:text="没有padding,margin" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimaryDark"
        android:text="没有padding,margin" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimaryDark"
        android:text="没有padding,margin" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:background="@color/colorPrimaryDark"
        android:text="margin=10" />

</study.hank.com.customviewgroup.MyViewGroup>

神坑QA(新手不一定搞得清概念,老手可能容易忘记)

1 、如何处理自定义ViewGroup里的margin和padding?

答:所有ViewGroup的margin处理,都是子view的margin属性值;所有padding处理,都是viewGroup自身的。padding,可直接通过getPaddingLeft()这种方法处理,然后在测量和布局的时候考虑进去。Margin,则必须遍历子View,拿到子view的MaginLayouParam,再获得leftMargin这种属性,最后在测量和布局时考虑进去。

2、为什么有时候子view的LayoutParam强转成MarginLayoutParam会报错?

因为没有重写generateLayoutParam方法。这个方法将会对子view的attr进行封装,最后的结果就是,改变子view的LayoutParam类型,变为MarginLayoutParam。

持续更新 关注不迷路哦

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值