【Android_View】ImageView源码简析笔记(二)

来源:互联网 发布:语音录入软件 编辑:程序博客网 时间:2024/06/06 00:36

ImageView源码简析


ImageView的测量–onMeasure()

总所周知,对于继承自View的控件来说,最重要的就是测量、布局与绘制三个步骤。当然,因为毕竟不是ViewGroup,当中很少考虑的子控件的位置问题,所以布局(Layout)的作用相对较小。下面,我们就一起来看看ImageView的上述几个步骤。


1、知识准备

在查看ImageView的源码之前,我们先一起简单的回顾一下有关Measure的3个知识点。

1.1 啥叫MeasureSpec

答:我们一般称之为【测量规格】。
首先,贴上google官方文档。
当然,这里是源码。嗯,仅仅是View的一个内部类。。。

public static class MeasureSpec {        private static final int MODE_SHIFT = 30;        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;        /** @hide */        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})        @Retention(RetentionPolicy.SOURCE)        public @interface MeasureSpecMode {}        //定义3种【测量模式】        public static final int UNSPECIFIED = 0 << MODE_SHIFT;          public static final int EXACTLY     = 1 << MODE_SHIFT;        public static final int AT_MOST     = 2 << MODE_SHIFT;       //根据提供的尺寸[size]和模式[mode] 创建 [测量规格]。        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,@MeasureSpecMode int mode) {            if (sUseBrokenMakeMeasureSpec) {                return size + mode;            } else {                return (size & ~MODE_MASK) | (mode & MODE_MASK);            }        }        //如果测量模式为UNSPECIFIED,直接返回0.        public static int makeSafeMeasureSpec(int size, int mode) {            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {                return 0;            }            return makeMeasureSpec(size, mode);        }        //获取测量模式        // MODE_MASK = 11 00000000000(11后跟30个0),原理是用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。        public static int getMode(int measureSpec) {            return (measureSpec & MODE_MASK);        }        //获取测量值的大小        public static int getSize(int measureSpec) {            return (measureSpec & ~MODE_MASK);        }        //测量值 的 增量自适应        static int adjust(int measureSpec, int delta) {            final int mode = getMode(measureSpec);            int size = getSize(measureSpec);            if (mode == UNSPECIFIED) {                // 不需要适配 UNSPECIFIED 模式.                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);        }        //输出测量规格 包含 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();        }    }

官方对这个类的定义是:

A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode.

很明显,
* MeasureSpec封装了从父View传递给子View的布局要求。
* 每个MeasureSpec既包含表示宽度Width也包含高度Height。
* 每一个MeasureSpec由size(大小)和mode(模式)组成。
本质上,MeasureSpec是一个32位的int型数据。其中,其高2位表示mode,即【测量模式】,低30位表示size,即【测量值的大小】。

测量值的大小不需要多说,我们一起看看这个【测量模式】:

Mode Summary UNSPECIFIED 未指定模式,即父布局不限制子View的大小。多用于系统内部控件的测量。 EXACTLY 精确模式,对应xml文件中的”match_parent”属性,即父布局会精确地测量出相应的数值。 AT_MOST 最大模式,对应xml文件中的”warp_parent”属性,即父布局会指定测量的最大值,这是子View最终的大小。

需要在再次强调一点的是,这个任何View的MeasureSpec的测量模式,是受父布局影响的。
当然,有一种情况可以看作是例外。当子View在定义宽、高时直接应用了某些具体的数值,例如20dp、30px…,此时此刻无论父布局的【测量模式】是什么,最终子View的【测量值】都会直接无视之,而直接设为先前的定义值(20dp、30px…)。
其实这仅仅是定义指出的一种特殊情况,也是官方定义的合情合理的实际。


一篇很强大的大神的总结:奉上:大神原文


1.2 LayoutParams又是个啥

我们知道子View的MeasureSpec受到【父布局的MeasureSpec】 与 【自身LayoutParams】的双重影响。前面看完了MeasureSpec,那么接下来我们在看一看LayoutParams。
LayoutParams是ViewGroup中的一个静态内部类。源码太长,这里就不贴出了。

public static class LayoutParams {        ...}

官方注释的第一句是:子View通过LayoutParams参数告诉它的父布局自己的【布局意愿】和【期望的布局方式】。再简单点,就是告诉它的布局老爹,想要怎么被放置以及自己的宽高。
这个布局意愿实际上包括了三个方面:
* 布局的位置
* 宽
* 高

这个LayoutParams实际上我们大家经常用到,只不过隐藏的太深,没感觉。。。
举个栗子:
我们在xml文件里新建控件的时候,一般都是这样的:

<?xml version="1.0" encoding="utf-8"?><LinearLayout    xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <com.example.mario.myviewdemo.view.myPathView        android:id="@+id/demo_view"        android:layout_height="match_parent"        android:layout_width="match_parent"/></LinearLayout>

是不是很眼熟?是不是经常用??是不是觉得也没啥特别的呢???
那么再多看一眼啊:
为什么在具体控件的定义当中,我们还要写上【layout_height】和【layout_width】呢?
前缀–layout,他是不是很显眼!
是的。在我们定义的xml文件中,所有前缀为layout的属性,实际上都不属于当前View而直接属于LayoutParams。比如说这些:
layout举例
在上述例子当中,我们的父控件是LinearLayout,因此子View中所有的layout_XXX属性,都要考虑LinearLayout的特点,同样的道理,如果保持其余不变的前提下只更改父布局的类型,如RelativeLayout等,那么子View最终显示的时候还需要考虑RelativeLayout的特点。
好了,相应的细节我们在以后的文章中再来分析。


1.3 那么合起来呢

有了MeasureSpec和LayoutParams,我们的子VIew就比较容易的测量出自己的宽高了。


粗略的回顾了一下相关的基础知识与定义,具体的解释我们会在以后的文章中再详谈。接下来我们开始对本次的主角进行“解剖”吧。


2、onMeasure()

2、1 综述

我们来看ImageView中onMeasure()方法的全部代码:

@Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        resolveUri();        int w;        int h;        //视图内容期望的宽高比(不包含边界)        float desiredAspect = 0.0f;        //是否允许改变view's width        boolean resizeWidth = false;        //是否允许改变view's height        boolean resizeHeight = false;        //通过传入的宽、高的【测量规格】获取相应的【测量模式】        final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);        final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);        if (mDrawable == null) {            // 如果当前显示的图像资源为空,则固有尺寸设为0            mDrawableWidth = -1;            mDrawableHeight = -1;            w = h = 0;        } else {            //不为空则设置为当前图像的宽、高            w = mDrawableWidth;            h = mDrawableHeight;            //容错--不能为0            if (w <= 0) w = 1;            if (h <= 0) h = 1;            // 调整视图的边界以匹配资源图像的宽高比。            if (mAdjustViewBounds) {            //如果当前测量模式 != EXACTLY(精确测量模式)==> 宽高可变                resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;                resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;                //计算宽高比                desiredAspect = (float) w / (float) h;            }        }        //距离周围的padding值        final int pleft = mPaddingLeft;        final int pright = mPaddingRight;        final int ptop = mPaddingTop;        final int pbottom = mPaddingBottom;        //宽高的测量值        int widthSize;        int heightSize;        if (resizeWidth || resizeHeight) {            //如果我们到达这里,这意味着我们要调整大小以匹配            //可绘制长宽比,我们至少可以自由地改变一个维度值。            // 给定我们可以约束的最大可能宽度            widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec);            // 给定我们可以约束的最大可能高度            heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec);            if (desiredAspect != 0.0f) {                // 计算获得实际的宽高比(注意减掉边距)                final float actualAspect = (float)(widthSize - pleft - pright) /(heightSize - ptop - pbottom);                //如果两者的绝对值相差大于0.0000001                if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {                    boolean done = false;                    // 调整 width 获得合适的height以符合宽高比                    if (resizeWidth) {                        int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) +pleft + pright;                     // 如果height固定,则允许width超过其原始估计值                        if (!resizeHeight && !sCompatAdjustViewBounds) {                            widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec);                        }                        //重新设置width                        if (newWidth <= widthSize) {                            widthSize = newWidth;                            done = true;                        }                    }                    // 如果,没有重新设置Width,则调整Height以适应宽高比,步骤同上。                    if (!done && resizeHeight) {                        int newHeight = (int)((widthSize - pleft - pright) / desiredAspect) +  ptop + pbottom;                        if (!resizeWidth && !sCompatAdjustViewBounds) {                            heightSize = resolveAdjustedSize(newHeight, mMaxHeight, heightMeasureSpec);                        }                        if (newHeight <= heightSize) {                            heightSize = newHeight;                        }                    }                }            }        } else {            //如果既【不想保留长宽比】又【不允许更改宽高】            w += pleft + pright;            h += ptop + pbottom;            //借助最小建议宽、高            w = Math.max(w, getSuggestedMinimumWidth());            h = Math.max(h, getSuggestedMinimumHeight());            //求得最终的测量结果            widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);            heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);        }        //"告诉"父布局,最终的测量结果。        setMeasuredDimension(widthSize, heightSize);    }

一眼略过前面的代码,WQ,是不是感觉代码略长呢?没事,我们一起慢慢分析。

首先传入的形参widthMeasureSpec和heightMeasureSpec分别用来描述宽度测量规范和高度测量规范。通过前面的回顾,我们也知道,这个测量规范采用了一个int值来表示,分为两个部分:mode+size即高两位表示mode(测量模式),其余30位表示size(测量大小)。(再次“化肥”一遍…)


2.2 第一个方法

可以看到,方法体内部首先执行的是resolveUri()方法。我们点击进去,发现代码如下:

 private void resolveUri() {        if (mDrawable != null) {            return;        }        if (getResources() == null) {            return;        }        Drawable d = null;        if (mResource != 0) {            try {                d = mContext.getDrawable(mResource);            } catch (Exception e) {                Log.w(LOG_TAG, "Unable to find resource: " + mResource, e);                // Don't try again.                mUri = null;            }        } else if (mUri != null) {            d = getDrawableFromUri(mUri);            if (d == null) {                Log.w(LOG_TAG, "resolveUri failed on bad bitmap uri: " + mUri);                // Don't try again.                mUri = null;            }        } else {            return;        }        updateDrawable(d);    }

如名称所示,这个方法主要是根据Uri实现图片加载的。可以看到,在在判断完成相关变量是否为空后,resolveUri()方法主要做了两件事,一是在成员变量mUri不为空的情况下,调用getDrawableFromUri(mUri)方法加载图像,获得Drawable对象。二是将获得的对象设为ImageView的显示资源。
首先,getDrawableFromUri(mUri)代码如下:

private Drawable getDrawableFromUri(Uri uri) {        final String scheme = uri.getScheme();        if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {            try {                // Load drawable through Resources, to get the source density information                ContentResolver.OpenResourceIdResult r =                        mContext.getContentResolver().getResourceId(uri);                return r.r.getDrawable(r.id, mContext.getTheme());            } catch (Exception e) {                Log.w(LOG_TAG, "Unable to open content: " + uri, e);            }        } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)                || ContentResolver.SCHEME_FILE.equals(scheme)) {            InputStream stream = null;            try {                stream = mContext.getContentResolver().openInputStream(uri);                return Drawable.createFromResourceStream(sCompatUseCorrectStreamDensity                        ? getResources() : null, null, stream, null);            } catch (Exception e) {                Log.w(LOG_TAG, "Unable to open content: " + uri, e);            } finally {                if (stream != null) {                    try {                        stream.close();                    } catch (IOException e) {                        Log.w(LOG_TAG, "Unable to close content: " + uri, e);                    }                }            }        } else {            return Drawable.createFromPath(uri.toString());        }        return null;    }

可以看到,在这个方法中,一共有两处可以返回Drawable资源。
一是通过InputStream,即Drawable.createFromResourceStream()处;
一是通过URI,即Drawable.createFromPath()处。
这两个方法都是Drawable类中的方法,前者应用了输入流,后者应用了统一资源标识符。
对于

Drawable.createFromResourceStream(Resources res, TypedValue value,InputStream is, String srcName)

方法,此处应用了Resources资源加载,我们回看上一篇文章中的initImageView()方法会发现这样一句

sCompatUseCorrectStreamDensity = targetSdkVersion > Build.VERSION_CODES.M;

即当前编译环境 > Android 6.0(23)时,赋值号左侧的成员变量会为true,否则为false。

而在本方法中,可以知道如果当前的系统版本号低于23,Resource会被设定成null,反之如果当前的系统版本号高于23,则会通过getResources()获取resource对象后进行设置。注意此处调用的是View中的方法而不是Drawable类自有的方法,代码如下:

 /**     * Returns the resources associated with this view.     *     * @return Resources object.     */    public Resources getResources() {        return mResources;    }

如果获得的Drawable对象不为空,则继续调用updateDrawable()方法,将其赋值给成员变量mDrawable。很明显,这就是当前ImageView将要显示的图像。


对于第二种方式,通过源码可以发现,主要调用了Drawable类中的createFromPath(String pathName)方法,很明显,这里是通过将Uri转换为文件名称后,在内部再次调用了Drawable类中的drawableFromBitmap()方法获取一个Drawable对象,调用关系如下:
首先是createFromPath方法

public static Drawable createFromPath(String pathName) {        ...            Bitmap bm = BitmapFactory.decodeFile(pathName);            if (bm != null) {                return drawableFromBitmap(null, bm, null, null, null, pathName);         ...    }

紧接着是内部调用的drawableFromBitmap()方法:

private static Drawable drawableFromBitmap(Resources res, Bitmap bm, byte[] np, Rect pad, Rect layoutBounds, String srcName) {        if (np != null) {            return new NinePatchDrawable(res, bm, np, pad, layoutBounds, srcName);        }        return new BitmapDrawable(res, bm);    }

通过上述方法得到Drawable对象后,再调用updateDrawable()方法,将得到的Drawable设置为ImageView的显示资源,即赋值给成员变量mDrawable。代码如下:

private void updateDrawable(Drawable d) {        if (d != mRecycleableBitmapDrawable && mRecycleableBitmapDrawable != null) {            mRecycleableBitmapDrawable.setBitmap(null);        }        boolean sameDrawable = false;        if (mDrawable != null) {            sameDrawable = mDrawable == d;            mDrawable.setCallback(null);            unscheduleDrawable(mDrawable);            if (!sCompatDrawableVisibilityDispatch && !sameDrawable && isAttachedToWindow()) {            //传入false,设置mDrawable不可见以及不强制重新启动(绘制)                mDrawable.setVisible(false, false);            }        }        //Drawable赋值        mDrawable = d;        if (d != null) {            d.setCallback(this);            //设置布局方向            d.setLayoutDirection(getLayoutDirection());            if (d.isStateful()) {            //设置Drawable状态                d.setState(getDrawableState());            }            if (!sameDrawable || sCompatDrawableVisibilityDispatch) {                final boolean visible = sCompatDrawableVisibilityDispatch                        ? getVisibility() == VISIBLE                        : isAttachedToWindow() && getWindowVisibility() == VISIBLE && isShown();//通过之前的InitImageView()方法可以知道,如果当前系统小于24,则sCompatDrawableVisibilityDispatch变量为true,在此情况下,局部变量visable的值仅与当前控件的VISABLE属性有关系。而当系统版本大于24时,则需要综合考虑【view是否已经依赖于窗口Window】、【当前Window的可见情况】以及【所有当前视图全部父View的可见度】。                d.setVisible(visible, true);            }            d.setLevel(mLevel);            mDrawableWidth = d.getIntrinsicWidth();            mDrawableHeight = d.getIntrinsicHeight();            applyImageTint();            applyColorMod();            configureBounds();        } else {            mDrawableWidth = mDrawableHeight = -1;        }    }

applyImageTint()的代码如下所示,我们可以知道,这个方法主要是用来设置与着色Tint有关的属性。分别代表了绘制到屏幕之前Drawable的颜色状态列表与色调模式。
很明显主要有两个:TintList与TintMode。
其中mDrawableTintList与mDrawableTintMode都是在ImageView的构造器中赋初值,具体代码可以查阅ImageView源码解析笔记(一)。

private void applyImageTint() {        if (mDrawable != null && (mHasDrawableTint || mHasDrawableTintMode)) {            mDrawable = mDrawable.mutate();            if (mHasDrawableTint) {                mDrawable.setTintList(mDrawableTintList);            }            if (mHasDrawableTintMode) {                mDrawable.setTintMode(mDrawableTintMode);            }            // The drawable (or one of its children) may not have been            // stateful before applying the tint, so let's try again.            if (mDrawable.isStateful()) {                mDrawable.setState(getDrawableState());            }        }    }

applyColorMod()代码如下。可以看到,本方法主要是设置Drawable的颜色过滤器、图像混合模式、透明度。

 private void applyColorMod() {        if (mDrawable != null && mColorMod) {            mDrawable = mDrawable.mutate();            if (mHasColorFilter) {                mDrawable.setColorFilter(mColorFilter);            }            mDrawable.setXfermode(mXfermode);            mDrawable.setAlpha(mAlpha * mViewAlphaScale >> 8);        }    }

上述方法中还需要特别注意的一点是

mDrawable = mDrawable.mutate();

这一句。其中mutate方法代码如下:

public @NonNull Drawable mutate() {        return this;    }

通过此方法获得的Drawable对象是【可变的】。这意味着这个对象可以接受设置各种属性,并且保证不会与其余Drawable对象【共享状态】且这种【可变】状态不可逆。


我们再来看updateDrawable中执行的最后一个方法configureBounds()。该方法属于ImageView,源代码如下:

private void configureBounds() {        if (mDrawable == null || !mHaveFrame) {            return;        }        //这里的值指的是Drawable的值,与imageview无关        final int dwidth = mDrawableWidth;        final int dheight = mDrawableHeight;        //这里指的是ImageView的宽、高        final int vwidth = getWidth() - mPaddingLeft - mPaddingRight;        final int vheight = getHeight() - mPaddingTop - mPaddingBottom;        final boolean fits = (dwidth < 0 || vwidth == dwidth)                && (dheight < 0 || vheight == dheight);        if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {            //裁掉Drawable的超出的边界,以Imageview的宽高赋值            mDrawable.setBounds(0, 0, vwidth, vheight);            mDrawMatrix = null;        } else {            mDrawable.setBounds(0, 0, dwidth, dheight);            if (ScaleType.MATRIX == mScaleType) {                // Use the specified matrix as-is.                if (mMatrix.isIdentity()) {                    mDrawMatrix = null;                } else {                    mDrawMatrix = mMatrix;                }            } else if (fits) {                // The bitmap fits exactly, no transform needed.                mDrawMatrix = null;            } else if (ScaleType.CENTER == mScaleType) {                // Center bitmap in view, no scaling.                mDrawMatrix = mMatrix;                mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f),                                         Math.round((vheight - dheight) * 0.5f));            } else if (ScaleType.CENTER_CROP == mScaleType) {                mDrawMatrix = mMatrix;                float scale;                float dx = 0, dy = 0;                if (dwidth * vheight > vwidth * dheight) {                    scale = (float) vheight / (float) dheight;                    dx = (vwidth - dwidth * scale) * 0.5f;                } else {                    scale = (float) vwidth / (float) dwidth;                    dy = (vheight - dheight * scale) * 0.5f;                }                mDrawMatrix.setScale(scale, scale);                mDrawMatrix.postTranslate(Math.round(dx), Math.round(dy));            } else if (ScaleType.CENTER_INSIDE == mScaleType) {                mDrawMatrix = mMatrix;                float scale;                float dx;                float dy;                if (dwidth <= vwidth && dheight <= vheight) {                    scale = 1.0f;                } else {                    scale = Math.min((float) vwidth / (float) dwidth,                            (float) vheight / (float) dheight);                }                dx = Math.round((vwidth - dwidth * scale) * 0.5f);                dy = Math.round((vheight - dheight * scale) * 0.5f);                mDrawMatrix.setScale(scale, scale);                mDrawMatrix.postTranslate(dx, dy);            } else {                // 创建所需要的转换                mTempSrc.set(0, 0, dwidth, dheight);                mTempDst.set(0, 0, vwidth, vheight);                mDrawMatrix = mMatrix;                mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));            }        }    }

首先通过方法名,我们可以知道,这个方法主要是“配置边界信息”。而实际上,在本方法中主要是操作了mDrawMatrix矩阵,便于以后在onDraw()方法中使用。
可以看到:如果当前drawable没有固定的尺寸或者缩放模式为fit_XY时,我们需要做的是直接将试图充满整个View(即左上坐标为(0,0),右下角坐标为(view的宽高-边界长度))。
除此之外,便需要自己做相应的缩放操作以适应图片原生的宽高。很明显,因为要缩放,所以这里多次调用了缩放矩阵:

private Matrix mMatrix;

在得到需要缩放的坐标矩阵后,我们将mMatrix赋值给mDrawMatrix,然后调用mDrawMatrix的相应方法,如setScale()、setTranslate()等进行矩阵操作以达到相应的图像转换要求。
这里有一个大前提:

ImageView中所有的图像转换是基于【图像矩阵】进行的。

接下来:
我们来看一个多次被调用的方法:setBounds()。首先需要明确的是:这是Drawable类中的方法。

 public void setBounds(int left, int top, int right, int bottom) {        Rect oldBounds = mBounds;        if (oldBounds == ZERO_BOUNDS_RECT) {            oldBounds = mBounds = new Rect();        }        if (oldBounds.left != left || oldBounds.top != top ||                oldBounds.right != right || oldBounds.bottom != bottom) {            if (!oldBounds.isEmpty()) {                invalidateSelf();            }            //重新设置边界            mBounds.set(left, top, right, bottom);            onBoundsChange(mBounds);        }    }

官方的注释告诉我们,这个方法主要是为Drawable绘制一个边界矩形,并且是在Drawable的draw()方法被调用时绘制。可以看到,当原来的边界矩形坐标与当前传进的坐标不相同时,随即调用invalidateSelf()放方法进行重绘,然后将当前的边界矩形的坐标设置为传入值。
注意,mBounds是Drawable类中的成员变量,

private Rect mBounds = ZERO_BOUNDS_RECT;  

而赋值号的右侧为该类之前定义好的一个Rect坐标:

private static final Rect ZERO_BOUNDS_RECT = new Rect();

还有要注意的一个点是onBoundsChange(mBounds)方法。

protected void onBoundsChange(Rect bounds) {        // Stub method.    }

注意这个方法是需要在子类中Override(复写)的。本身不具有相应的实现。


截止到这里,我们一起大概的看完了onMeasure()中执行的第一个方法—resolveUri()。接下来继续吧:

2.2其他方法

计算可以获得的最大(宽、高)尺寸:resolveAdjustedSize()方法:

private int resolveAdjustedSize(int desiredSize, int maxSize,                                   int measureSpec) {        int result = desiredSize;        //根据测量规格,获取测量值与测量模式        final int specMode = MeasureSpec.getMode(measureSpec);        final int specSize =  MeasureSpec.getSize(measureSpec);        switch (specMode) {            case MeasureSpec.UNSPECIFIED:                /* 未指定模式。父布局明确子View可以获得想要的尺寸,且只要不超过最大值则不做限制               */               //只要不超过最大值就可以                result = Math.min(desiredSize, maxSize);                break;            case MeasureSpec.AT_MOST:                // 最大不超过测量尺寸                result = Math.min(Math.min(desiredSize, specSize), maxSize);                break;            case MeasureSpec.EXACTLY:                // 没有选择,强行指定为测量值的大小                result = specSize;                break;        }        return result;    }

可以看到,传入的形参分别为【期望得到的测量值】【最大值】【测量规格】。这个方法,主要是根据传入形参的不同,最终根据不同的测量模式,获取不同的测量值,测量模式的介绍见【前文的知识回顾】。

获取最小建议宽度。注意这个方法是VIew类中的方法。

protected int getSuggestedMinimumWidth() {        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());    }

注意返回值中Drawable类中的getMinimumWidth()方法:

public int getMinimumWidth() {        final int intrinsicWidth = getIntrinsicWidth();        return intrinsicWidth > 0 ? intrinsicWidth : 0;    }

注意:上述方法返回此Drawable建议的最小宽度。
如果视图使用此Drawable作为背景,则建议View至少使用此值作为其宽度。如果此Drawable没有建议的最小宽度,则返回0。

再来看:resolveSizeAndState()方法:

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {        final int specMode = MeasureSpec.getMode(measureSpec);        final int specSize = MeasureSpec.getSize(measureSpec);        final int result;        switch (specMode) {            case MeasureSpec.AT_MOST:                if (specSize < size) {                    result = specSize | MEASURED_STATE_TOO_SMALL;                } else {                    result = size;                }                break;            case MeasureSpec.EXACTLY:                result = specSize;                break;            case MeasureSpec.UNSPECIFIED:            default:                result = size;        }        return result | (childMeasuredState & MEASURED_STATE_MASK);    }

我们看看该方法的形参列表:

Name Summary Size How big the view wants to be measureSpec Constraints imposed by the parent. childMeasuredState Size information bit mask for the view’s children

很明显,三个参数分别代表了view的尺寸、测量规格、子View的测量状态。
通过代码很明显可以看出,最终的返回值result实际上就是传入的参数size在不同测量模式Mode下的结果。


最后,需要特别注意的是,ImageView在完成自身的宽、高测量后,必须调用继承得到的父视图View的setMeasuredDimension()方法,通知自己需要设置的宽高。

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {        boolean optical = isLayoutModeOptical(this);        if (optical != isLayoutModeOptical(mParent)) {            Insets insets = getOpticalInsets();            int opticalWidth  = insets.left + insets.right;            int opticalHeight = insets.top  + insets.bottom;            measuredWidth  += optical ? opticalWidth  : -opticalWidth;            measuredHeight += optical ? opticalHeight : -opticalHeight;        }        setMeasuredDimensionRaw(measuredWidth, measuredHeight);    }
This method must be called by {@link #onMeasure(int, int)} to store the measured width and measured height. Failing to do so will trigger an exception at measurement time

通过上述源码注释,我们可以知道,这个方法是【必须调用】的,否则会触发IllegalStateException(无效状态)异常。


2.3小结

好了,到这里我们也算初步的对ImageView.onMeasure()有了一点自己的简介,那么趁热打铁,再来回顾一下整个测量过程:

Step Summary S1 借助resolveUri()方法,通过Uri加载Drawable S2 根据【测量规格】,获得【测量尺寸】【测量模式】 S3 根据实际获取预期的长宽比desiredAspect S4 如果宽和高可以改变,则根据预期的宽高比,结合实际,调整相应的宽度与高度以适配。否则,借助最小建议宽高SuggestedMinimumWidth,求得宽度与高度。 S5 通知父布局最终的测量结果。
原创粉丝点击