Android 自定义View学习(十)——View的测量方法onMeasure()学习

学习资料


在整个系列的第一篇Android自定义View学习(一)——准备中,简单学习来了测量涉及到的onMeasure()方法,本篇进行补充学习。由于目前水平还很低,测量过程涉及到的源码没有能力去分析,只是记录一下学习怎么调用方法,很多地方都是"知其然而不知其所以然",不能对调用的方法本身原理进行说明。爱哥是从源码进行分析,写的非常好,可以去深入学习爱哥的博客


1. onMeasure() 测量方法 <p>

在之前的学习中,很多地方都用到了这个方法,而且几乎每次使用,代码都是不变的,套路是固定的

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

    int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, 300);
    } else if (wSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, hSpecSize);
    } else if (hSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(wSpecSize, 300);
    }
}

偶尔做出的改动便是300这个数值

这个方法内,主要是对于MeasureSpec.AT_MOST这个模式,也就是针对在布局xml文件控件的宽高写wrap_content时的处理。无论是MeasureSpec.EXACTLY(match_parent)还是MeasureSpec.AT_MOST,这两种模式都是根据当前控件以及所在的父控件大小共同来确定的。

如果不进行重写这个方法,直接引用这个当前的自定义控件,无论是wrap_content还是match_parent都会沾满全屏,原因下面尝试分析


1.1 尝试分析原因<p>

UI界面架构图

在一个Activity中,在onCreate()方法中,都会调用setContentView(@LayoutRes int layoutResID)这样一个方法,layoutResId就是Activityxml布局文件。

例如,在MainActivity中,setContentView(R.layout.main_activity)。在R.layout.main_activity中,最外层的布局往往会是一个LinearLayout(或者RelativeLayout)。之前认为这个最外层的布局就是一个MainActivity的根布局,而对应的LinearLayout(或者RelativeLayout)便是父容器。但根据上面的图可以知道,MainActivity的最外层布局是添加到了ContentView中,这个ContentView可以通过LinearLayout.getParent()获得,返回的结果是一个idandroid:id="@android:id/content"FramLayout,这个android:id="@android:id/content才是Activity的根布局,R.layout.main_activity包含在其中

除了ContentView外,手机屏幕上展示的还有ContentView上面的TitleViewTitleView便是ActionBar或者5.0ToolBar。也就是说,屏幕真正可见的根ViewDecorView

每个Activity都包含有一个Window对象,通常是PhoneWindow。当ActivityonCreate()方法中调用了setContentView()方法后,ActivityManagerService会回调onResume()方法,系统会把整个DecorView添加进PhoneWindow


上面大概叙述了一下UI界面架构图的关系。DecorView的大小,无论什么时候,默认都是全屏的。而DecorVew包含两部分,TitleViewContentView,除去TitleView,屏幕可见的就是ContentViewContentView包含了Activityxml布局文件中最外层Layout,自定义View则是在这个Layout中。而整个Activity的绘制过程是从最外层的DecorView开始的,由外向内。最终确定自定义View的大小,需要根据由MeasureSpec.getMode(widthMeasureSpec)得到的SpecMode当做判断依据来进行确定

  • AT_MOST 最大值模式,
    对应 LayoutParams.WARP_CONTENT。由父容器指定了一个可用大小SpecSize,自定义View的大小不能超过这个SpecSize值,而这个SpecSize也不会超过窗口的值
  • EXACTLY 精准模式
    对应 LayoutParams.MATCH_PARENT或者固定大小值。父容器已经检测出View所需大小的值SpecSize,最终大小也就是窗口的大小或者固定值。

一开始,很不理解warp_content,怎么就对应的是最大值模式,而平常经常用到这个模式的TextView或者Button中,理解为包裹内容。那是因为在TextView中,都已经做了针对处理,之后使得这个模式才具备包裹内容的特性 。这个wrap_content模式本身并不是已经具备包裹内容的特性


如果不对上面的onMeasure()方法进行重写,无论是warp_content还是match_parentSpecSzie的大小都是窗口的大小,也就是自定义View所在的布局大小,而布局的宽高一般默认是match_parent,显示在屏幕上就是DecorView中的ContentView的大小。

重写了onMeasure()方法,在AT_MOST也就是wrap_content时,确定了300,也就是父容器给指定了SpecSize300px


不晓得整个叙述清不清楚,回头以后自己还能不能看明白。。。

虽然指定了wrap_content的大小,但还会有问题,padding并未处理


2. 处理Padding <p>

先根据上面的onMeasure()方法,创建一个针对可以处理warp_content过的PaddingView

代码很简单,就是绘制一个Bitmap,以Bitmap的宽高作为wrap_content最大值

public class PaddingView extends View {
    private Paint mPaint;

    public PaddingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    /**
     * 初始化
     */
    private void init() {
        //画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.parseColor("#FF4081"));
    }

    /**
     * 绘制
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制底色 辅助用
        canvas.drawColor(Color.CYAN);
        //绘制Bitmap
        final float left = 0;
        final float top = 0;
        canvas.drawBitmap(mBitmap, left, top, null);
    }

    /**
     * 测量
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        
        int resultWidth = wSpecSize;
        int resultHeight = hSpecSize;

        if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
            resultWidth = mBitmap.getWidth();
            resultHeight = mBitmap.getHeight();
        } else if (wSpecMode == MeasureSpec.AT_MOST) {
            resultWidth = mBitmap.getWidth();
            resultHeight = hSpecSize;
        } else if (hSpecMode == MeasureSpec.AT_MOST) {
            resultWidth = wSpecSize;
            resultHeight = mBitmap.getHeight();
        }
        // 取Bitmap宽高和窗口宽高的较小的一个
        resultWidth = Math.min(resultWidth, wSpecSize);
        resultHeight = Math.min(resultHeight, hSpecSize);
        setMeasuredDimension(resultWidth, resultHeight);
    }
}

布局文件:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.szlk.customview.custom.PaddingView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true" />

</RelativeLayout>
以Bitmap的宽高作为wrap_content

测试的图片有点巧合,宽度和手机屏幕差不多

简单修改代码, 在布局文件加入android:padding="50dp",发现并没有效果。因为测量的时候,并没有加入padding的大小的考虑。

onMeasure()onDraw()进行简单修改:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    int resultWidth = wSpecSize;
    int resultHeight = hSpecSize;

    if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
        resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
        resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
    } else if (wSpecMode == MeasureSpec.AT_MOST) {
        resultWidth =  mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
        resultHeight = hSpecSize;
    } else if (hSpecMode == MeasureSpec.AT_MOST) {
        resultWidth = wSpecSize;
        resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
    }
     // 取Bitmap宽高和窗口宽高的较小的一个
    resultWidth = Math.min(resultWidth, wSpecSize);
    resultHeight = Math.min(resultHeight, hSpecSize);
    setMeasuredDimension(resultWidth, resultHeight);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //绘制底色 辅助用
    canvas.drawColor(Color.CYAN);
    //绘制Bitmap
    final float left = getPaddingLeft();
    final float top = getPaddingTop();
    canvas.drawBitmap(mBitmap, left, top, null);
}
paddig=50dp

padding的效果是有了,可照片太大了,控件resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight()的大小,超过了手机屏幕宽度,导致控件内容显示不全。可以考虑使用Matrix进行优化


2.1 使用Matrix进行优化 <p>

主要是利用Matrix.setRectToRect()方法,不了解的同学,可以去Android 自定义View学习(八)——Matrix知识学习看一下 : )

public class PaddingView extends View {
    private Bitmap mBitmap;
    private Matrix mMatrix ;
    private RectF srcRectF;
    private RectF dstRectF;
    public PaddingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    /**
     * 初始化
     */
    private void init() {
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
        mMatrix = new Matrix();
        //截取用来显示的Bitmap有效区域
        srcRectF = new RectF(0,0,mBitmap.getWidth(),mBitmap.getHeight());
        //显示Bitmap的底板
        dstRectF = new RectF();
    }



    /**
     * 绘制
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制底色 辅助用
        canvas.drawColor(Color.CYAN);
        //绘制Bitmap
        final float left = getPaddingLeft();
        final float top = getPaddingTop();
        //设置Bitmap的缩放模式
        mMatrix.setRectToRect(srcRectF,dstRectF, Matrix.ScaleToFit.FILL);
        //利用后乘平移对开始绘制位置进行改变 这里不可以使用setTranslate()
        mMatrix.postTranslate(left,top);
        canvas.drawBitmap(mBitmap, mMatrix, null);
    }

    /**
     * 测量
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        //将最终结果宽高设置为窗口大小 这样省的判断MeasureSpec.EXACTLY模式的情况
        int resultWidth = wSpecSize;
        int resultHeight = hSpecSize;

        if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
            resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
            resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
        } else if (wSpecMode == MeasureSpec.AT_MOST) {
            resultWidth =  mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
            resultHeight = hSpecSize;
        } else if (hSpecMode == MeasureSpec.AT_MOST) {
            resultWidth = wSpecSize;
            resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
        }
        //取Bitmap宽高和窗口宽高的较小的一个
        resultWidth = Math.min(resultWidth, wSpecSize);
        resultHeight = Math.min(resultHeight, hSpecSize);
        //确定dstRectF的范围
        dstRectF.left= dstRectF.top = 0;
        dstRectF.right = resultWidth-getPaddingRight()-getPaddingLeft();
        dstRectF.bottom = resultHeight-getPaddingBottom()-getPaddingTop();
        setMeasuredDimension(resultWidth, resultHeight);
    }
}

Matrix进行优化

虽然图片有些变形,这样无论多大的图片都可以填充满整个控件。有点类似ImageViewandroid:scaleType="centerCrop"这个属性,当然想要完全实现这个属性的特点,还需要对srcRectF进行优化,截取Bitmap的中间正方形部分来显示就可以了

Margin外边距,封装在LayoutParams内交由父容器统一处理


3. 最后 <p>

主要是学习1.1原因分析那里。理解UI界面架构图,这个图看起来并不复杂,但涉及的东西却非常多。算是先了解一下View的工作原理,View最复杂的过程就是测量。

本人很菜,有错误,请指出

共勉 : )

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容