Android-自定义view

九章
• 阅读 1502

要自定义view,都知道有3个方法需要重写:onMeasure、onLayout、onDraw。而且这三个方法的执行是按顺序的。

生命周期

Android-自定义view

image.png

实际开发中,比较多的自定义都是具体实现一个view的子类,实现viewgroup的子类比较少,两者基本相似,区别就是view需要实现onMeasure、onLayout、onDraw三个方法,而viewgroup必需实现onLayout(当然你要实现另2个也是可以的)。

构造函数

自定义view也需要构造函数,而且看很多别人写的view,构造函数都有3个,那他们的作用是啥?

class MyView : View {

    /**
     * 代码使用
     */
    public constructor(context: Context) : super(context) {

    }

    /**
     * 布局是会调用这个方法,xml中达到预览的效果
     */
    public constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {

    }

    /***
     * 多一个主题参数,可以单独设置主题
     */
    public constructor(context: Context, attrs: AttributeSet, defStyleAtr: Int) : super(context, attrs, defStyleAtr) {

    }
} 
onMeasure函数

先了解view和viewGroup的结构,其实是树形结构。

Android-自定义view

image.png

当执行onMeasure方法时,会自上而下地遍历这个view是否有子view或者子viewGroup,通过层层递归,先算出最下层的view的尺寸,再往上计算上一层的尺寸。

MeasureSpec

Android-自定义view

944365-0cf0a1ffd083cad1.png

MeasureSpec是View的内部类,里面主要的有mode和size,由一个int类型变量来表示,前2位表示mode测量模式,后30位表示size测量大小。
mode:
UNSPECIFIED:不对view进行限制,系统去调用
EXACTLY:有确定值,例如宽100dp,数值多大绘制多大,允许超出屏幕
AT_MOST:最大值,常用的matchParent,就是最大值为屏幕
源码:

public static class MeasureSpec {
        // 进位大小 = 2的30次方
        // int的大小为32位,所以进位30位 = 使用int的32和31位做标志位
        private static final int MODE_SHIFT = 30;
        // 运算遮罩:0x3为16进制,10进制为3,二进制为11
        // 3向左进位30 = 11 00000000000(11后跟30个0)  
        // 作用:用1标注需要的值,0标注不要的值。因1与任何数做与运算都得任何数、0与任何数做与运算都得0
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        // UNSPECIFIED的模式设置:0向左进位30 = 00后跟30个0,即00 00000000000
        // 通过高2位
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        // EXACTLY的模式设置:1向左进位30 = 01后跟30个0 ,即01 00000000000
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        // AT_MOST的模式设置:2向左进位30 = 10后跟30个0,即10 00000000000
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        /**
          * makeMeasureSpec()方法
          * 作用:根据提供的size和mode得到一个详细的测量结果吗,即measureSpec
          **/ 
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) { 
            // measureSpec = size + mode;此为二进制的加法 而不是十进制
            // 设计目的:使用一个32位的二进制数,其中:32和31位代表测量模式(mode)、后30位代表测量大小(size)
            // 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100  
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        /**
         * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
         * will automatically get a size of 0. Older apps expect this.
         *
         * @hide internal use only for compatibility with system widgets and older apps
         */
        @UnsupportedAppUsage
        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        /**
          * getMode()方法
          * 作用:通过measureSpec获得测量模式(mode)
          **/    
        @MeasureSpecMode
        public static int getMode(int measureSpec) {

            // 即:测量模式(mode) = measureSpec & MODE_MASK;  
            // MODE_MASK = 运算遮罩 = 11 00000000000(11后跟30个0)
            //原理:保留measureSpec的高2位(即测量模式)、使用0替换后30位
            // 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值
            return (measureSpec & MODE_MASK);
        }

         /**
          * getSize方法
          * 作用:通过measureSpec获得测量大小size
          **/  
        public static int getSize(int measureSpec) {
                // size = measureSpec & ~MODE_MASK;  
               // 原理类似上面,即 将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size
            return (measureSpec & ~MODE_MASK);
        }

        static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            int size = getSize(measureSpec);
            if (mode == UNSPECIFIED) {
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(size, UNSPECIFIED);
            }
            size += delta;
            if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
            }
            return makeMeasureSpec(size, mode);
        }

        /**
         * Returns a String representation of the specified measure
         * specification.
         *
         * @param measureSpec the measure specification to convert to a String
         * @return a String with the following format: "MeasureSpec: MODE SIZE"
         */
        public static String toString(int measureSpec) {
            int mode = getMode(measureSpec);
            int size = getSize(measureSpec);

            StringBuilder sb = new StringBuilder("MeasureSpec: ");

            if (mode == UNSPECIFIED)
                sb.append("UNSPECIFIED ");
            else if (mode == EXACTLY)
                sb.append("EXACTLY ");
            else if (mode == AT_MOST)
                sb.append("AT_MOST ");
            else
                sb.append(mode).append(" ");

            sb.append(size);
            return sb.toString();
        }
    } 
如何算MeasureSpec大小

是利用getChildMeasureSpec方法

/**
  * 源码分析:getChildMeasureSpec()
  * 作用:根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
  * 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
  **/

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  

         //参数说明
         * @param spec 父view的详细测量值(MeasureSpec) 
         * @param padding view当前尺寸的的内边距和外边距(padding,margin) 
         * @param childDimension 子视图的布局参数(宽/高)

            //父view的测量模式
            int specMode = MeasureSpec.getMode(spec);     

            //父view的大小
            int specSize = MeasureSpec.getSize(spec);     

            //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)   
            int size = Math.max(0, specSize - padding);  

            //子view想要的实际大小和模式(需要计算)  
            int resultSize = 0;  
            int resultMode = 0;  

            //通过父view的MeasureSpec和子view的LayoutParams确定子view的大小  

            // 当父view的模式为EXACITY时,父view强加给子view确切的值
           //一般是父view设置为match_parent或者固定值的ViewGroup 
            switch (specMode) {  
            case MeasureSpec.EXACTLY:  
                // 当子view的LayoutParams>0,即有确切的值  
                if (childDimension >= 0) {  
                    //子view大小为子自身所赋的值,模式大小为EXACTLY  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  

                // 当子view的LayoutParams为MATCH_PARENT时(-1)  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    //子view大小为父view大小,模式为EXACTLY  
                    resultSize = size;  
                    resultMode = MeasureSpec.EXACTLY;  

                // 当子view的LayoutParams为WRAP_CONTENT时(-2)      
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    //子view决定自己的大小,但最大不能超过父view,模式为AT_MOST  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                }  
                break;  

            // 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)  
            case MeasureSpec.AT_MOST:  
                // 道理同上  
                if (childDimension >= 0) {  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                }  
                break;  

            // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
            // 多见于ListView、GridView  
            case MeasureSpec.UNSPECIFIED:  
                if (childDimension >= 0) {  
                    // 子view大小为子自身所赋的值  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    // 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0  
                    resultSize = 0;  
                    resultMode = MeasureSpec.UNSPECIFIED;  
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0  
                    resultSize = 0;  
                    resultMode = MeasureSpec.UNSPECIFIED;  
                }  
                break;  
            }  
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
        } 

总结其实就是:

Android-自定义view

944365-6088d2d291bbae09.png

一定要注意的是getChildMeasureSpec(int spec, int padding, int childDimension),spec是父控件的预给值,不代表真实大小,padding就不解释了,childDimension是子控件的大小值。
也就是说,当计算子view时,是由这个控件的自身大小,还有父控件的预给值共同决定;当计算的是viewGroup时,同样需要父类的预给值,还跟自己子类的大小值有关。例如viewpager实现的banner:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int height = 0;
        Log.d(TAG, "onMeasure: getChildCount: " + getChildCount());
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
//            child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED));
            ViewGroup.LayoutParams lp = child.getLayoutParams();
            int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
            int childHightSpec = getChildMeasureSpec(heightMeasureSpec, 0 ,lp.height);
            child.measure(childWidthSpec, childHightSpec);

            int h = child.getMeasuredHeight();
            if (h > height) {
                height = h;
            }
            Log.d(TAG, "onMeasure: "  + h + " height: " + height);
        }
        heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
} 
getWidth和getMeasureWidth

getWidth:onLayout之后,才有值
getMeasureWidth:onMeasure之后,才有值

onLayout

这里其实就是根据onMeasure算好的大小,把控件放在画布上,需要注意的也就上面的getWidth和getMeasureWidth的时机。

onDraw

自定义view:利用canvas paint matrix clip rect animation path(贝塞尔) line text绘制等来绘制。
自定义viewgroup:需要遍历它拥有的子view,然后按照自己需要的逻辑去排列位置。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
Android自定义ViewGroup:onMeasure与onLayout(1)
Android自定义ViewGroup:onMeasure与onLayout(1)Android自定义一个ViewGroup,需要重写ViewGrouo里面的两个最重要的回调函数onMeasure()与onLayout()。如果开发者自己摆脱Android为我们做好的几套布局(如常见的线1性布局、相对布局、帧布局等等),往底层实现vi
可莉 可莉
3年前
2019年Android岗位BAT等大厂面试题,希望对新的一年的你有所帮助
2019年Android岗位BAT等大厂面试题知识点小结2019年了搜集了很多面试题,希望能对大家有所帮助1.View的绘制流程;自定义View如何考虑机型适配;自定义View的事件分发机制;View和ViewGroup分别有哪些事件分发相关的回调方法;自定义View如何提供获取View属
Stella981 Stella981
3年前
Android中不规则形状View的布局实现
在Android中不管是View还是ViewGroup,都是方的!方的!方的!而对于非方形的,Android官方并没有给出非常好的解决方案.有的无非就是自定义View了.然而自定义View非常麻烦,需要重写很多方法,而且稍微不注意可能就会丧失一些特性或者造成一些Bug.而且即便是自定义View,其实那个自定义View还是方的!!!,自定义V
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
9个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这