ImageView 源码分析 (图片和view的适配裁剪问题)

我们平时使用 ImageView 的场景比较多,不管是从资源、本地或是网络中获取图片,然后设置到 ImageView 中,适配图片这个问题绕不过去,比如 ImageView 的宽高比例和图片的宽高比例不一致,怎么办?是从图片居中剪切、从上往下剪切还是全部显示到 ImageView 中,周围留有黑边?如果获取的图片很大,为了优化内存,我们需要选择图片的比例来缩小,这时候要根据 ImageView 的宽高比例来取舍等等,这是为什么呢?如果 ImageView 本身宽度是固定的,图片宽度与 ImageView 的宽适配,想让它的高度随着图片的高度变化而变化,能做到吗
?这一切都需要从 ImageView 的源码说起,先看测量的方法

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        resolveUri();
        int w; int h; float desiredAspect = 0.0f;
        boolean resizeWidth = false; boolean resizeHeight = false;

        final int widthSpecMode = View.MeasureSpec.getMode(widthMeasureSpec);
        final int heightSpecMode = View.MeasureSpec.getMode(heightMeasureSpec);
        // 重点零
        if (mDrawable == null) {
            mDrawableWidth = -1;
            mDrawableHeight = -1;
            w = h = 0;
        } else {
            w = mDrawableWidth;
            h = mDrawableHeight;
            if (w <= 0) w = 1;
            if (h <= 0) h = 1;
            // 重点一
            if (mAdjustViewBounds) {
                resizeWidth = widthSpecMode != View.MeasureSpec.EXACTLY;
                resizeHeight = heightSpecMode != View.MeasureSpec.EXACTLY;
                desiredAspect = (float) w / (float) h;
            }
        }

        int pleft = mPaddingLeft;
        int pright = mPaddingRight;
        int ptop = mPaddingTop;
        int pbottom = mPaddingBottom;

        int widthSize;
        int heightSize;
        // 重点二
        if (resizeWidth || resizeHeight) {
            ...
        } else {
            // 重点2.2
            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);
    }

这个是简化后的代码,resolveUri() 中是根据 setImageResource(int resId) 或 setImageURI(Uri uri) 方法接收 id 或 uri,根据这两个属性获取到 Drawable,然后调用 updateDrawable(Drawable d) 方法设置图片,这个稍后会介绍。首先获取 ImageView 的宽和高的模式,然后判断 mDrawable 是否为null,如果为null,则 w = h = 0,如果不为null,则把Drawable的宽高赋值给w和h,如果这两个值不是正数,则修正为1,此时假设 mAdjustViewBounds 为 false,先忽略 重点一,则 重点二 也可以忽略,直接走到了 重点2.2; w 和 h 在本身基础上加上对应的 padd 的值,如果 ImageView 设置了最小宽度和高度,还要再做个比较,然后通过 resolveSizeAndState() 方法来计算出它需要的宽和高,最后通过setMeasuredDimension() 方法来设置 ImageView 本身的宽高值给父容器。


此时我们基本可以确定一个简单的 ImageView 的宽和高是怎么确定的,我们再来看看绘制的方法

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mDrawable == null) {
            return; // couldn't resolve the URI
        }
        if (mDrawableWidth == 0 || mDrawableHeight == 0) {
            return;     // nothing to draw (empty bounds)
        }
        if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
            mDrawable.draw(canvas);
        } else {
            int saveCount = canvas.getSaveCount();
            canvas.save();
            if (mCropToPadding) {
                final int scrollX = mScrollX; final int scrollY = mScrollY;
                canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
                        scrollX + mRight - mLeft - mPaddingRight, scrollY + mBottom - mTop - mPaddingBottom);
            }
            canvas.translate(mPaddingLeft, mPaddingTop);
            if (mDrawMatrix != null) {
                canvas.concat(mDrawMatrix);
            }
            mDrawable.draw(canvas);
            canvas.restoreToCount(saveCount);
        }
    }

简单点,基本上就是 mDrawable.draw(canvas) 这行代码,原来 ImageView 显示都是通过 Drawable 来绘制图形的;canvas.clipRect() 是用来裁剪的,意思是只在裁剪的区域内绘制,超出的部分不绘制。这是是把 Drawable 给绘制出来了,但 Drawable 的大小显示区域是哪里控制的呢? 难道 ImageView 的的绘制就这么完了? 当然不是,我们知道在 View 的 layout() 方法中会调用 setFrame() 方法,我们看一下 ImageView 中的

    @Override
    protected boolean setFrame(int l, int t, int r, int b) {
        boolean changed = super.setFrame(l, t, r, b);
        mHaveFrame = true;
        configureBounds();
        return changed;
    }

它里面调用了 configureBounds() 方法,如果再看看 setImageBitmap(Bitmap bm)、setImageIcon(Icon icon) 等方法,发现最终都是调用 setImageDrawable() 方法,进而调用 updateDrawable(Drawable d) 方法,在这个方法中,会调用 configureBounds() 方法,那么这个方法是干嘛用的呢? 我们平时通过设置 ScaleType 的类型,来控制 ImageView 中图片显示的样式,或居中、或拉伸铺满View,就是在这个方法中控制的。

    private void configureBounds() {
        if (mDrawable == null || !mHaveFrame) {
            return;
        }

        int dwidth = mDrawableWidth;
        int dheight = mDrawableHeight;
        int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
        int vheight = getHeight() - mPaddingTop - mPaddingBottom;

        boolean fits = (dwidth < 0 || vwidth == dwidth) && (dheight < 0 || vheight == dheight);

        if (dwidth <= 0 || dheight <= 0 || ImageView.ScaleType.FIT_XY == mScaleType) {
            mDrawable.setBounds(0, 0, vwidth, vheight);
            mDrawMatrix = null;
        } else {
            mDrawable.setBounds(0, 0, dwidth, dheight);
            if (ImageView.ScaleType.MATRIX == mScaleType) {
                if (mMatrix.isIdentity()) {
                    mDrawMatrix = null;
                } else {
                    mDrawMatrix = mMatrix;
                }
            } else if (fits) {
                mDrawMatrix = null;
            } else if (ImageView.ScaleType.CENTER == mScaleType) {
                mDrawMatrix = mMatrix;
                mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f), Math.round((vheight - dheight) * 0.5f));
            } else if (ImageView.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 (ImageView.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));
            }
        }
    }


dwidth 和 dheight 是 Drawable 的宽和高,vwidth 和 vheight 是 ImageView 的可绘制区域的宽和高,fits 这个属性基本可以忽略不计;我们先看第一个if判断,如果drawable 没有大小或者 mScaleType 属性设置的是 FIT_XY 类型,则 把 vwidth 和 vheight 作为 setBounds() 设定它的边界的参数值,同时把 Matrix 矩阵对象置空;除了这种if情况外,其他的设置都是把 dwidth 和 dheight 作为参数设置到 setBounds() 方法中,如果 mScaleType 设置的是 MATRIX,则从左上角起始的矩阵区域开始绘制(自定义矩阵的除外);mScaleType 为 CENTER 时,(vwidth - dwidth) * 0.5f 计算的是view和drawable的宽的差值的一半,setTranslate() 则是设置了矩阵位移的值,只是位移;CENTER_CROP 中,会判断drawable的宽高比例与View的宽高比的大小,然后计算缩放值即位移值,它也是居中显示,但是会缩放图片,选取居中的展示;CENTER_INSIDE 则是有点不同,若图片高宽均小于控件高宽,则不进行缩放只进行偏移,若大于控件宽高,也是缩放,但是以较小值作为缩放的倍数; 网上有篇文章介绍的具体显示效果挺不错的,地址为  https://www.jianshu.com/p/32e335d5b842。

说了半天,还没说到如果宽度固定,如何让图片的高度去自适应的问题。configureBounds() 方法其实就是限定 Drawable 的绘制区域,Drawable 是在 ImageView 中的 onDraw()方法中通过 mDrawable.draw(canvas) 绘制的,决定图片样式大小的还是与 getWidth() 及 mDrawableWidth 有关系,mDrawableWidth 是固定的,那么我们改变 getWidth() 的值就行了。重新看 onMeasure() 方法,我们忽略的代码  重点一  和   重点二

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        boolean resizeWidth = false; boolean resizeHeight = false;
        final int widthSpecMode = View.MeasureSpec.getMode(widthMeasureSpec);
        final int heightSpecMode = View.MeasureSpec.getMode(heightMeasureSpec);
        ...
            w = mDrawableWidth;
            h = mDrawableHeight;
            // 重点一
            if (mAdjustViewBounds) {
                resizeWidth = widthSpecMode != View.MeasureSpec.EXACTLY;
                resizeHeight = heightSpecMode != View.MeasureSpec.EXACTLY;
                desiredAspect = (float) w / (float) h;
            }
        int pleft = mPaddingLeft;int pright = mPaddingRight; int ptop = mPaddingTop;int pbottom = mPaddingBottom;
        int widthSize; int heightSize;
        // 重点二
        if (resizeWidth || resizeHeight) {
            widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec);
            heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec);
            // 重点2.1
            if (desiredAspect != 0.0f) {
                float actualAspect = (float)(widthSize - pleft - pright) / (heightSize - ptop - pbottom);
                if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {
                    boolean done = false;
                    if (resizeWidth) {
                        int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) + pleft + pright;
                        if (!resizeHeight && !mAdjustViewBoundsCompat) {
                            widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec);
                        }
                        if (newWidth <= widthSize) {
                            widthSize = newWidth;
                            done = true;
                        }
                    }
                    if (!done && resizeHeight) {
                        int newHeight = (int)((widthSize - pleft - pright) / desiredAspect) + ptop + pbottom;
                        if (!resizeWidth && !mAdjustViewBoundsCompat) {
                            heightSize = resolveAdjustedSize(newHeight, mMaxHeight,heightMeasureSpec);
                        }
                        if (newHeight <= heightSize) {
                            heightSize = newHeight;
                        }
                    }
                }
            }
        } else {
            ...
        }
        setMeasuredDimension(widthSize, heightSize);
    }

mAdjustViewBounds 这个属性可以通过xml或是set方法来设置,只要它设置为true,就可以自适应了。 重点一 中,如果宽和高的模式不是固定的,那么就需要重新计算,同时计算出 drawable的宽和高的比例值 desiredAspect; 重点二 中,根据drawable的宽和高,获取了新的值 widthSize 和 heightSize,如果 desiredAspect 值不为0,actualAspect这个值是把 padd 值去掉后的宽高比,如果两者误差超过 0.0000001,则重新计算。如果宽度需重新计算,则以高度为准,然后根据比例算出一个新的宽 newWidth,此时如果高度不需要重新计算,则根据 resolveAdjustedSize() 方法算出 widthSize,然后做个比较,如果新的宽比较小,则用新的值,高度不变,用 drawable 获取到的高; 像我们上面举的例子,宽固定,高去自适应,测会根据drawable的宽高比去计算出需要的高度,然后比较一下大小,和计算宽度一个道理。 这里需要注意一点,设置 mAdjustViewBounds 后ScaleType 会变为 FIT_CENTER 模式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值