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

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

前言

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

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

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

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

高级自定义View系列六;ViewGroup的标准范式_第1张图片

正文

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

效果

高级自定义View系列六;ViewGroup的标准范式_第2张图片

 

要实现的效果很清晰,就是一行比一行缩进一定距离,并且考虑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值
     * 

* 如果考虑margin和padding的话,那具体指的就是 子view的margin,和自身的padding,不要弄错了。 * *

* 自身的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




    

    

    

    

    

    

    

    

    

    



神坑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。

持续更新 关注不迷路哦

你可能感兴趣的:(自定义View)