300字范文,内容丰富有趣,生活中的好帮手!
300字范文 > 自定义控件从入门到轻生之---解锁新姿势

自定义控件从入门到轻生之---解锁新姿势

时间:2022-11-10 12:36:42

相关推荐

自定义控件从入门到轻生之---解锁新姿势

所有blog局限于博主水平有限,很多不足之处大家可以指出共同探讨进步。

尊重原创转载请注明:From 倪大叶/renyi0109 侵权必究!虽然我不知道具体怎么究,但是看大家都这么写我也就这么写吧

上次说到View的事件分发从根本原则上清楚了几条事件分发的法……..好好好,上次是说到我侄儿拿了一道初一得数学题让我解惑一下,先不说这道题的本身,本着程序员看问题从本质出发的角度,我觉得就算我给他把答案写出来他下次再碰到类似的题或者同类诡异的题肯定还是做不出来,不能举一反三讲来何用?所以我本着从源头上解决的思想告诉他: 以后碰到这种题,写个解字可以得一分…

上次讲的事件分发是在现有View层面上的玩意儿,今天我们倒叙回去看看一个View的从无到有是怎么生成的也就是常说的View绘制流程。在这之前先大概讲讲Android下的”View 框架”

Android四大组件各司其职,前有Activity卖弄风骚,后有service暗度陈仓,再有BroadCastReceiver左右逢源,最后有ContentProvider不知道在干嘛,而Activity正是负责前台接待客户的,所以我们就从Activity开始讲讲View绘制流程的框架(这部分并不是这篇blog的重点,所以就不贴源码了)虽然Activity是负责和用户交互和显示视图的但其实Activity并不直接控制它们,而是由他内部一个叫window的哥们来负责的,每个Activity在attach的时候会创建一个唯一的window对象并把自己作为接口回调注册给window,以用于window将一些Activity关心的玩意儿丢回来,比如上一篇说到的触摸事件就是由window丢给Activity的。我们要想在Activity显示什么布局,直接在onCreate中调用setContentView就可以了,这个方法其实就是调用了window的setContentView,window内部会把我们设置的布局文件装载到一个叫DecorView的东西上,这个DecorView就是我们能看到的手机画面的根视图,它是一个FrameLayout里面只有一个Linearlayout子View,Linearlayout又分为titleView(就是title布局,actionBar和普通title就是显示在这里)和ContentView(这就是显示我们设置布局的地方了),这时候敏感的哥们可能会问那状态栏呢?状态栏和我们应用可没什么关系,它其实是一个系统级的应用,最高优先级显示在手机上。到这儿虽然已经把我们想显示的视图设置给了DecorView,但是这时候decorView和window其实还没有关联起来,要将DecorView添加到window上才能真正的显示出来,这个添加过程就要靠一个叫ViewRootImpl的类,说到这个类就吊了,基本所有View处理都是它来完成的,比如我们接下来要讲的measure layout draw的发起源头都是在这个类, ViewRootImpl可以看作是DecorView和window连接的纽带,DecorView添加到window的大概流程 windowManager(window的管理器,负责操作window的)->windowManagerGobal(addView) ->ViewRootImpl 最后就是ViewRootImpl的setView来完成添加,而这个方法是一个IPC过程,添加的最终实现是一个叫Session的远程实现类。都讲到这里了 就顺便再提一下VIewRootImpl的performTraversals方法,接下来要讲到的View的测量啊布局什么的源头都是由该方法发起的

if (lp.horizontalWeight > 0.0f) {width += (int) ((mWidth - width) lp.horizontalWeight);//生成子View宽的测量规格 这个MeasureSpec是整个measure的核心数据类,子View的onMeasure方法中的测量规格都是由父元素给的,//至于有什么用后面讲测量的时候再细说,而这个规格就是在这里生成的,当然这只是生成的起点,每层View拿到这个规格都有可能会根据//自己的需要做一些修改再传给子ViewchildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,MeasureSpec.EXACTLY);measureAgain = true;}if (lp.verticalWeight > 0.0f) {height += (int) ((mHeight - height) * lp.verticalWeight);//同上childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);measureAgain = true;}if (measureAgain) {if (DEBUG_LAYOUT) Log.v(TAG,"And hey let's measure once more: width=" + width+ " height=" + height);//发起测量,正式开始走View的测量逻辑performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);}//发起布局 performLayout(lp, desiredWindowWidth, desiredWindowHeight);//发起绘制performDraw();

上面说的这些只是最最最最范阔的介绍,里面有很多细节值得大家去研究学习,当然要真的弄懂上面这一套View原理框架并不是一件容易的事,好了扯了这么多该吃正餐了

首先先跳出程序的思维来看下生活中如果我们要画出一张图需要哪什么步骤

1.首先不管要画什么东西得先有张纸

2.有了纸你得知道你要画得图像大概有多大

3.你要把这个图像画在什么位置

4.你用什么画,怎么画,画什么

完成上面几步基本就可以画出一个View了,那我们把这几步映射到Android的View绘制里来, 纸就是我们的canvas,这个东西不用关心系统已经给我们准备好了而且也必须画在系统给我们准备的这张纸上才能显示出来,画多大就是View的测量方法measure,画在哪就是layout方法,画什么就是draw方法

onMeasure

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {....if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||....if (cacheIndex < 0 || sIgnoreMeasureCache) {// measure ourselves, this should set the measured dimension flag back//这里就是调用onMeasure的地方onMeasure(widthMeasureSpec, heightMeasureSpec);mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;} else {long value = mMeasureCache.valueAt(cacheIndex);// Casting a long to int drops the high 32 bits, no mask neededsetMeasuredDimensionRaw((int) (value >> 32), (int) value);mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;}....mOldWidthMeasureSpec = widthMeasureSpec;mOldHeightMeasureSpec = heightMeasureSpec;mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension}

可能有人还不是很清楚measure和onMeasure的区别,其实看看measure的源码就能看出是一个测量的控制中心,比如一些测量执行的判断,状态的记录等等,当然实际的测量onMeasure也是由measure来负责控制的,还有各位老板注意看这是一个final方法,final!!所以不要再问为什么测量不能复写measure方法,同理 layout draw都是Android系统已经写好的,不用你关心也轮不到你关心,提供给我们能做的就是 onMeasure这几个方法,说白了 measure是测量的逻辑控制,而onMeasure是具体的测量实现

在江湖上流传着一个说法,一个View的大小实际是由父View和自身共同决定的,View本身对自己大小的期望就是来自我们在代码或者xml文件中给它设置的宽高,而父View对儿子得期望就是measuren的两个int参数,这两个参数由父亲传给儿子,儿子又传给孙子,孙子再传给曾孙….一直传到买不起房找不了老婆没办法生儿子那一代.. 那么肯定有没注意看的事儿逼会问那顶级父View的参数又是谁传给他的呢?上面已经讲了有个叫ViewRootImpl的上帝用performTraversals方法去生成这两个int规则,至于大小就是屏幕大小。然后由它传给顶级父View。 这两个int 高2位为mode,低30位为size,至于是什么意思就要先讲讲今天的配角

MeasureSpec

//提取期望int型低30位为大小int specSize = MeasureSpec.getSize(measureSpec)//提取期望int型中高两位为modeint specMode = MeasureSpec.getMode(measureSpec)

这个mode有三种类型: MeasureSpec.EXACTLY , MeasureSpec.AT_MOST , MeasureSpec.UNSPECIFIED(这个我到现在都不知道有什么鸟用,我们用不到就不讲了,想来应该是系统级控件会用到吧)

MeasureSpec.EXACTLY:

父View已经测量出子View的大小,你就直接用我给你的size就行,你自己设置的大小别用了

MeasureSpec.AT_MOST:

父View不强制要求子View多大,子View可以根据自己的期望来设置大小,但是前提是不能超过我给的Size大小

下面是View源码的onMeasure逻辑

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//注意这个方法才是设置有效宽高的方法,前面说的一大堆的都是规则是理论,极端点你可以完全抛弃//系统的这一套规则,直接用这个方法随便设置什么都可以,当然一般人不会这么做的setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}//根据父期望和自己的size来确定最终的size大小public static int getDefaultSize(int size, int measureSpec) {int result = size;int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);switch (specMode) {case MeasureSpec.UNSPECIFIED:result = size;break;case MeasureSpec.AT_MOST:case MeasureSpec.EXACTLY:result = specSize;break;}return result;}

View只是很简单的实现了最基本的onMeasure逻辑,基本所有View在继承View的同时都会复写自己逻辑的onMeasure方法,每个View基本都不一样,在这我就不挑某一个特定View来看源码了,但是为了让大家更深刻的理解”父子共同决定View的大小”,我们自己简单写个onMeasure逻辑

//childSize: 子View自己希望的大小(来自xml文件中设置或者代码)//parentMeasureSpec: 父View对子View的期望public int getMeasureHeightSize(int childSize, int parentMeasureSpec) {int result ;//提取父View期望modeint specMode = MeasureSpec.getMode(measureSpec);//提取父View期望大小int specSize = MeasureSpec.getSize(measureSpec);switch (specMode) {case MeasureSpec.UNSPECIFIED://父View对子View不做限制//子View想要多大就多大result = size;break;case MeasureSpec.AT_MOST: //父View不对子View做具体要求,但是不能大过父View给的最大值//如果子View想要的大小没有超过父View的限定,那么直接用子View想要的大小,如果超过了,只能取到specSize这个限定值result = size <= specSize? size : specSize;break;case MeasureSpec.EXACTLY://父View已经测量出子View的精确大小//直接使用父View给的大小就好result = specSize;break;}return result;}

这只是很简单的一个小例子,实际中要考虑的很多,比如获取自身期望值得时候还需要加上padding,或者一些特有的属性对值的影响,这里我们已经看到子View是怎么根据父View的期望和自身的设置值来最终确定大小的。我们来思考一个问题,这个父View的期望是依据什么得来的呢?有人会说不是ViewRootImpl传下来的,然后再一层一层传下去的吗?是这样没错,可是ViewRootImpl只是简单将满屏宽高作为规则向下传递,我上面说过这个规则每层View拿到他都可能根据自己的实际情况做修改再传递给子View,我们在View里没看到类似的代码是因为只有在ViewGroup系列类下才有合成childMesureSpec的逻辑。在我们ViewGroup中有几个方法是用来计算合成对子View规则的相关方法,我们依次看看

//遍历所有child,调用measureChild去合成对子View的测量规则protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {final int size = mChildrenCount;final View[] children = mChildren;for (int i = 0; i < size; ++i) {final View child = children[i];if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {measureChild(child, widthMeasureSpec, heightMeasureSpec);}}} //测量子Viewprotected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {//获取child的 layoutParams ,里面包括了child对自己大小的一个设置信息final LayoutParams lp = child.getLayoutParams();//调用getChildMeasureSpec,根据父View的期望和子View希望值来合成对子View的规则final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp.width);final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom, lp.height);//将合成的规则传递给子View去测量child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}主要合成对子View期望规则的方法就是getChildMeasureSpec,我们进去详细看看这个方法public static int getChildMeasureSpec(int spec, int padding, int childDimension) {//提取父View的mode和sizeint specMode = MeasureSpec.getMode(spec);int specSize = MeasureSpec.getSize(spec);//父View的期望size首选得先减去该View的padding,才是实际给内容的大小,这里做个小于0限制int size = Math.max(0, specSize - padding);//定义对子View规则的 size和modeint resultSize = 0;int resultMode = 0;//根据父View的mode分别进行合成switch (specMode) {// Parent has imposed an exact size on uscase MeasureSpec.EXACTLY: 父View有准确的大小要求if (childDimension >= 0) { //如果子View大小这只为一个精确值(childDimension大于0为精确数字,match_parent 和wrap_content都小于0)//直接用子View设置的值为规则的大小,mode肯定就为EXACTLY了resultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {//如果为填满父View//直接用父View的size,因为父View的大小是确定的所以子mode也是精确的 EXACTLYresultSize = size;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.WRAP_CONTENT) {//如果为包裹内容// 也用父View的size,因为父View的值是精确的,所以子View的大小不能超过这个限定resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;// Parent has imposed a maximum size on uscase MeasureSpec.AT_MOST: //父View的mode为限定一个大小//这里条件和上面一样,就直接看里面的生成了if (childDimension >= 0) {//直接用子View设置的值,mode也就为 EXACTLY了resultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {//用父View给的大小,因为父View大小被限定了,所以子View也跟着被限定resultSize = size;resultMode = MeasureSpec.AT_MOST;} else if (childDimension == LayoutParams.WRAP_CONTENT) {//用父View给的大小,这时候父View是最大值限定,子View同样不能超过父View,也同样做最大值限定resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;// 这个模式就不多分析了,和上面是一样的case MeasureSpec.UNSPECIFIED:if (childDimension >= 0) {// Child wants a specific size... let him have itresultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size... find out how big it should// beresultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;resultMode = MeasureSpec.UNSPECIFIED;} else if (childDimension == LayoutParams.WRAP_CONTENT) {// Child wants to determine its own size.... find out how// big it should beresultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;resultMode = MeasureSpec.UNSPECIFIED;}break;}return MeasureSpec.makeMeasureSpec(resultSize, resultMode);}

看到这里大家应该都清楚了onMeasure的测量逻辑以及测量规格的生成了吧,测量的东西差不多就是这些了,接下来看看onLayout

onLayout

我们已经测量出View的大小,接下来就该确定它应该所呆得位置了,layout和measure就有点不同了,比如measure是在父View和自身期望下共同决定大小的,但是最终还是View自己去调用setMeasure方法设置宽高,也就是说最终决定权还是在View本身,上面说过极端情况下你甚至可以完全不管父View的期望直接设置宽高都是可行的,而layout就没有这个特权了,该放在那他只能提意见(xml文件设置或者代码),最终决定权在父View,我们看下View中的onLayout:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}

是一个空实现,onLayout和onMeasure也有点不同,我们说过measure是测量控制,onMeasure是实现具体设置宽高,这里onLayout其实只是用来控制子View该摆放的位置,具体摆放设置的代码却是layout作为实现,既然能作为父View那么绝逼是个ViewGroup吧,再去看下它里面的onLayout:

protected abstract void onLayout(boolean changed,int l, int t, int r, int b);

这特么不止是空实现而是一个抽象方法了,也就是说任何ViewGroup系列的子类都必须实现onLayout方法来摆放子View的位置并且这个layout工作系统不会再像onMeasure这样帮你做好基本的逻辑了,你要自定义一个新控件就必须自己去实现onLayout来摆放子View。 所以像常用的 RelativeLayout,Linearlayout什么的都是必然有自己的onLayout逻辑,这也是他们的核心功能逻辑, 就不单独挑一个出来看了,我们来自己简单的自定义一个玩一下:

/*** 就简单的写个每个View占一排,类似竖直的LinearLayout*/@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { /* * 如果有子元素才需要的layout位置*/ if (getChildCount() > 0) { // 用来记录已被占有的高度 int tempHeight = 0; // 遍历子View并对其进行定位布局 for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); /*** 让child进行布局,这layout的四个参数 left right top bottom 一会下面画张图介绍* 布局的参考坐标原点为父View的左上起点,因为是垂直布局,每次起点Y坐标会下移已经布局* 的高度*/ child.layout(0, mutilHeight, child.getMeasuredWidth(), child.getMeasuredHeight() + mutilHeight); // 改变高度倍增值 tempHeight += child.getMeasuredHeight(); } } }

好了一个自制Linearlayout就完了,你是不是觉得原来linearlayout太简单了?你去看看Linearlayout的源码会发现这特么不是坑爹吗,其实吧这例子就是一个引子,实际的控件还需要考虑子View的margin,或者一些特定的属性(比如 RelativeLayout的 在某元素下面,在父元素底部等等),还有父View本身的内边距啥的。很多很多需要考虑的,所以可以看到系统每个容器View都有大量非常严谨的layout代码来确保正确的放置View,所以要写一个完全自定义的容器view并不是一件容易的事,这里就不这么详细的去做了,因为那些都是一些逻辑处理了,我们只介绍基本的原理,大家自己可以下去想个控件出来练练手

onDraw

draw的代码会有点多就不贴代码了,主要在我们自己绘制以前系统会先做一些绘制,比如背景啊,滚动条啊,edge啥的,对我们来说知道就行。 而onDraw方法也是一个空实现,具体要画的内容完全交给子View自己去实现, onDraw方法会有一个canvas参数,这个玩意儿就是我们上面说的画布了,我们所有画的东西都必须画到这个画布上才能显示出来,而且没错这个canvas也是ViewRootImpl创建并且传下来的。好了 我觉得draw已经讲完了 哈哈。。。 其实draw可以说是绘制流程最简单的也可以说是最复杂的,它完全放权给View自己爱咋画咋画,就像有两个人给了他们两张纸,一个就画了个圆所以在这看来draw和简单,另一个画了一幅美女出浴图在这看来draw就很难很复杂很有意思,所以这个方法没什么好说的,能说的就是画的过程和用的工具。但是那就是一个庞大的话题了,不在这系列blog讨论范围之内了 ,感兴趣的自己去研究吧

好了就讲到这,下一篇会基于这两篇blog的内容做一个实用的小例子,当我正想关闭编辑器的时候我那侄儿又跑过来了。。我看着他手上并没有拿试卷课本啥的心里暗松一口气。。。 “叔叔,我听说你玩英雄联盟很厉害,能不能带带我” 呼~~ 果然不是问我怪题了,还好还好。”这个主要得多练,你现在还小要好好学习不能花时间在玩游戏上,你好好学习期末考好了假期我带你玩!”,”真的?好,我白金1上砖石上了几次都没上去,这下有叔叔终于好了!”,”额。。。。。。。”,我想着我黄金3的账号又陷入了沉思。。。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。