Android-自定义view

九章 等级 357 0 0

要自定义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,然后按照自己需要的逻辑去排列位置。

收藏
评论区

相关推荐

面试问到烂的MVC、MVP以及MVVM
MVP 什么是MVP? 在了解MVP之前可以先观察MVC的架构模式。 MVC中三个组成部分:1. View,即视图中的各个控件;2. Controller
Android开发艺术探索-第三章-View的事件体系
3.1 View的基础知识 位置参数 top、left、right、bottom,在3.0之后增加了x、y、translationX、translationY.这里的所有参数都是相对其父布局来说的. 下面是具体的含义表示 (https://imghelloworld
微信小程序modal
首先创建一个组件component,组件命名可以为modal modal.wxml的内容为 <view class'modalmask' wx:if'{{show}}' bindtap'clickMask' <view class'modalcontent' <scrollview scrolly class'mainc
微信小程序轮播图
实现效果 wxml代码 <view style"height:20rpx;"</view <view class"swiper"
android view 常用的6种 View 的滑动方法
View 的滑动是Android 实现自定义控件的基础,实现View 滑动有很多种方法,在这里主要讲解6 种滑动方法,分别是layout()、offsetLeftAndRight()与offsetTopAndBottom()、LayoutParams、动画、scollTo 与scollBy,以及Scroller。   View 的滑动是Android
Python Django开发 经验技巧总结(一)
1.前后台的数据传递view HTML:使用Django模版views.py代码:pythonfrom django.shortcuts import renderdef main_page(request): data 1,2,3,4 return render(request, 'index.html', {'data
Python Django开发 异常及解决办法(一)
1.ValueError: The view didn't return an HttpResponse object. It returned None instead该错误表明views.py中没有return一个返回值给前端。解决办法:检查 return HttpResponse()是否错位或者是否缺失。 2.NoReverseMat
前端面试题自检 Vue 网络 浏览器 性能优化部分
框架Vue MVVM是什么?ModelViewViewModel , Model 表示数据模型层。view 表示视图层, ViewModel 是 View 和 Model 层的桥梁,数据绑定到 viewModel 层并自动渲染到页面中,视图变化通知 viewModel 层更新数据。 Vue 的生命周期
「小程序 — 云开发」搜索跳转
样式如图:在home.wxml中 js< confirmtype:键盘的右下角按钮显示'搜素'bindconfirm:按下键盘'搜索'按钮bindinput:在输入框输入过程中触发事件bindtap:点击搜索图标<view class"search" <input type"text" placeholder"搜索菜品" con
Android-自定义view
要自定义view,都知道有3个方法需要重写:onMeasure、onLayout、onDraw。而且这三个方法的执行是按顺序的。 生命周期image.png 实际开发中,比较多的自定义都是具体实现一个view的子类,实现viewgroup的子类比较少,两者基本相似,区别就是view需要实现onMeasure、onLayout、onDraw三个方法,而vie
最新最全的 Vue 面试题 ➕详解答案
前言本文整理了高频出现的 Vue 相关面试题并且附带详解答案 难度分为简单 中等 困难 三种类型 大家可以先不看答案自测一下自己的 Vue 水平哈 整理不易 如果觉得本文有帮助 记得点赞三连哦 十分感谢! 简单 1 MVC 和 MVVM 区别 MVCMVC 全名是 Model View Controller,是模型(model)-视图(view)-控制器
Android输入系统(一)输入事件传递流程和InputManagerService的诞生
Android框架层 Android输入系统 Android框架层本文首发于微信公众号「刘望舒」 前言很多同学可能会认为输入系统是不是和View的事件分发有些关联,确实是有些关联,只不过View事件分发只能算是输入系统事件传递的一部分。这个系列讲的输入系统主要是我们不常接触的,但还是需要去了解的那部分。 1. 输入事件传递流程的组成部分输入系统是外界与And
Qt简单使用表格
接在Qt简单登录后https://www.helloworld.net/p/4enJFnZUQAC8G 添加新文件 使用的组件table weight和 table view头文件c++ifndef MANAGEMENTHdefine MANAGEMENTHinclude <QWidgetinclude <QMouseEventnamespace Ui cl
深圳社招大厂面试分享
先了解清楚,看准了再下手。 3月1号高铁到达深圳,到今天第九天了,四家大公司二家中小型公司,有几家已经面了几轮,下周还要面,挂了几家,不过目前已经选择了一家。总结一下找工作这段时间的经历,问得最多的是自定义 View 基本每家都问,问 View 的绘制流程,自定义View的步骤,有时会涉及到细节比如 PhoneWindow 实例是在哪个类哪个方法中实例化的,
Android程序员面试必备的知识点,深入分析
由于涉及到的面试题较多导致篇幅较长,我根据这些面试题所涉及到的常问范围总结了并做出了一份学习进阶路线图​​​​​​​及面试题答案免费分享给大家,文末有免费领取方式! View面试专题1. View的滑动方式2. View的事件分发机制3. View的加载流程4. View的measure layout 和 draw流程5. 自定义view需要注意的