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

学习资料

  • Android开发艺术探索
  • 爱哥的自定义控件系列

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


1. onMeasure() 测量方法

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

@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 尝试分析原因

Android 自定义View学习(十)——View的测量方法onMeasure()学习_第1张图片
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

先根据上面的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);
    }
}

布局文件:



    


Android 自定义View学习(十)——View的测量方法onMeasure()学习_第2张图片
以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);
}
Android 自定义View学习(十)——View的测量方法onMeasure()学习_第3张图片
paddig=50dp

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


2.1 使用Matrix进行优化

主要是利用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);
    }
}

Android 自定义View学习(十)——View的测量方法onMeasure()学习_第4张图片
Matrix进行优化

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

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


3. 最后

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

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

共勉 : )

你可能感兴趣的:(Android 自定义View学习(十)——View的测量方法onMeasure()学习)