ImageView-scaleType-各种不同效果解析

前言

ImageView是Android最基础的控件之一,通过ImageView我们能够展示各式各样的图片,对其原理的研究有助于我们更好的使用它。
通过本篇文章,你将了解到:

1、ImageView 如何确定view的尺寸
2、ImageView "adjustViewBounds" 怎么用
3、ImageView "scaleType" 理解与运用
4、ImageView 和Drawable异同

ImageView 尺寸的确定

ImageView继承自View,我们知道View的尺寸最终是在onMeasure方法里确定,看看ImageView对该方法有没有做处理:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //省略
        int w;
        int h;

        if (mDrawable == null) {
            mDrawableWidth = -1;
            mDrawableHeight = -1;
            w = h = 0;
        } else {
            //mDrawableWidth 为ImageView内容宽、高
            w = mDrawableWidth;
            h = mDrawableHeight;
            if (w <= 0) w = 1;
            if (h <= 0) h = 1;
        }
        if (resizeWidth || resizeHeight) {
            //关于adjustViewBounds 处理
            //省略
        } else {
            w += pleft + pright;
            h += ptop + pbottom;

            //寻找较大值,确保能够容纳
            w = Math.max(w, getSuggestedMinimumWidth());
            h = Math.max(h, getSuggestedMinimumHeight());

            //widthMeasureSpec/heightMeasureSpec 是父控件为ImageView分配的大小
            //w、h为内容的大小
            //该方法是结合父控件给的大小与内容的大小,最终算出View真正需要的大小
            //具体规则如下:
            //1、如果父控件给的测量模式是:EXACTLY,那么ImageView将采取spec里的值
            //2、如果父控件给的测量模式是:AT_MOST,那么ImageView将会采取内容的值。这里还需要注意的是
            //如果内容的大小超过父控件的给大小,那么限制最终的大小不超过父控件给的值
            widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
            heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
        }

        //最终、将计算后的尺寸保存到mMeasuredWidth/mMeasuredHeight里,ImageView测量完成
        setMeasuredDimension(widthSize, heightSize);
    }

ImageView重写了onMeasure方法。从上面可以看出,ImageView尺寸取决于内容的大小与父控件的大小,来看看不同组合对ImageView大小的影响。

小例子

首先选取一张图片test2.jpg,并放置在Drawable/nodpi目录下(读取图片原始尺寸,不进行压缩,对此想了解的请移步:Android 屏幕分辨率适配)。
该图片长宽分别为:592*258(单位像素)

image.png

//对应AT_MOST 此时以内容大小为准
    
    

//对应宽为:EXACTLY 以父控件给的大小为准
// 对应高为:AT_MOST 以内容高为准
    
    

两者都设置了背景,便于直观观察ImageView的尺寸变化(当然更精确的比较是打印ImageView大小)。看看效果:

image.png

image.png

对比上面两图,只是更改了"layout_width"属性,产生的效果却是不同。
总结:当ImageView使用“wrap_content”时,其尺寸取决于内容的大小

ImageView scaleType 属性

上面讲述了ImageView尺寸是如何确定的,但是如果给ImageView设置固定宽高,而图片尺寸与之不一样,该怎么确定图片在ImageView上的展示呢?ImageView提供了“scaleType”属性来定制不同的展示方式。

    public enum ScaleType {
        MATRIX      (0),

        FIT_XY      (1),

        FIT_START   (2),

        FIT_CENTER  (3),

        FIT_END     (4),

        CENTER      (5),

        CENTER_CROP (6),

        CENTER_INSIDE (7);
    }

来看看设置不同scaleType的效果。


image.png

图片尺寸是:182 * 538(px)
ImageView尺寸是:100 * 100 (dp) ,测试设备密度是2.75 ,换算作像素是:275 * 275 px
关于dp与px请参考:Android 屏幕分辨率适配
为了更直观看出控件尺寸与图片尺寸,给控件尺寸加了红色背景。
可以看出图片的宽小于控件的宽,图片的高大于控件的高。
分别来看看各个模式下展示表现,每个控件上都有标明对应的scaleType。

1、原图展示在屏幕最下方,此时图片没有缩放。
2、matrix:图片没有缩放,按照正常布局(matrix如果设置了变换,则可能会有缩放、平移等操作),从左上角开始展示,超出部分不显示。
3、fitXY:图片非等比例缩放,把图片宽高限制在控件内并且充满控件的四周。
4、fitStart:图片等比例缩放,把图片的宽高限制在控件内,缩放规则:两边都需要缩放到控件内,至少有一边与控件的某边齐平。缩放后,从左上角开始展示。
5、fitCenter:缩放规则同fitStart,只是缩放后,图片居中展示。
6、fitEnd:缩放规则同fitStart,只是缩放后,从右下角开始展示。
7、center:不缩放,图片居中展示。
8、centerCrop:等比例缩放,缩放规则:图片长宽都都需要大于等于控件长宽。居中展示。
9、centerInside:等比例缩放,缩放规则,图片长宽有一边大于控件尺寸,则缩放,两边都需要缩放到控件内。如果图片长宽都小于控件尺寸,则不缩放。不管是否缩放,都居中展示。

大部分文章对scaleType解释止步于此,看过容易忘记,主要是原理没有展开将讲述,接下来我们从源码角度进行深入分析,让记忆更深刻。

ImageView ScaleType 源码实现

ScaleType实现在ImageView configureBounds方法里,该方法在layout之后生效。

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

        //图片尺寸
        final int dwidth = mDrawableWidth;
        final int dheight = mDrawableHeight;

        //控件尺寸
        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 || ImageView.ScaleType.FIT_XY == mScaleType) {
            //将drawable尺寸设置为与控件尺寸一致
            //最终会将图片绘制到drawable设置的大小区域
            mDrawable.setBounds(0, 0, vwidth, vheight);
            mDrawMatrix = null;
        } else {
            //将drawable尺寸设置为与图片尺寸一致
            //最终的会将图片绘制到drawable设置的大小区域
            mDrawable.setBounds(0, 0, dwidth, dheight);

            if (ImageView.ScaleType.MATRIX == mScaleType) {
                //使用matrix
                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) {
                    //这个判断不是那么直观,换个方式看
                    //dwidth * vheight > vwidth * dheight
                    //dwidth / vwidth > dheight / vheight
                    //vwidth / dwidth <= vheight / 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 {
                    //与centerCrop模式相反,这里的判断是:如果控件/图片宽比例 小于 其高的比例
                    //那么缩放比例采用宽的比例,也就是较小值的比例。
                    //此种模式下,缩放后图片的宽高都不能超过控件宽高
                    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);

                //先缩放,沿着默认的点(0,0)
                mDrawMatrix.setScale(scale, scale);
                //再平移,使得图片居中
                mDrawMatrix.postTranslate(dx, dy);
            } else {
                // 剩下的模式包括:
                //fitStart
                //fitEnd
                //fitCenter
                mTempSrc.set(0, 0, dwidth, dheight);
                mTempDst.set(0, 0, vwidth, vheight);

                mDrawMatrix = mMatrix;
                //重点是此
                mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
            }
        }
    }

上看代码有注释,应该比较详细了。接下来看看
mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));方法:

    enum ScaleToFit {
        kFill_ScaleToFit,
        kStart_ScaleToFit,  //对应java层fit_Start
        kCenter_ScaleToFit, //对应java层fit_Center
        kEnd_ScaleToFit,    //对应java层fit_End
    };
    bool SkMatrix::setRectToRect(const SkRect& src, const SkRect& dst, ScaleToFit align) {
        if (src.isEmpty()) {
            this->reset();
            return false;
        }

        if (dst.isEmpty()) {
            sk_bzero(fMat, 8 * sizeof(SkScalar));
            fMat[kMPersp2] = 1;
            this->setTypeMask(kScale_Mask | kRectStaysRect_Mask);
        } else {
            //dst 表示控件尺寸
            //src 表示图片尺寸
            //先算宽、高比例
            SkScalar    tx, sx = dst.width() / src.width();
            SkScalar    ty, sy = dst.height() / src.height();
            bool        xLarger = false;

            //对应fit
            if (align != kFill_ScaleToFit) {
                //依然是熟悉的配方,取比例比较小值进行缩放,缩放规则同java层的center_inside模式
                if (sx > sy) {
                    xLarger = true;
                    sx = sy;
                } else {
                    sy = sx;
                }
            }

            tx = dst.fLeft - src.fLeft * sx;
            ty = dst.fTop - src.fTop * sy;
            if (align == kCenter_ScaleToFit || align == kEnd_ScaleToFit) {
                SkScalar diff;

                if (xLarger) {
                    //算出平移量,注意是整个x偏移
                    diff = dst.width() - src.width() * sy;
                } else {
                    diff = dst.height() - src.height() * sy;
                }

                if (align == kCenter_ScaleToFit) {
                    //如果是fit_center模式,则算出居中偏移量 SkScalarHalf=diff/2
                    diff = SkScalarHalf(diff);
                }

                if (xLarger) {
                    //如果是以高的比例缩放,那么需要对x方向进行平移
                    //照上面的计算,如果是fit_center模式,那么diff已经是平分过了
                    //如果是fit_end模式,那么将偏移到平齐右下方
                    tx += diff;
                } else {
                    ty += diff;
                }
            } else {
                //如果是fit_start,那么不对diff作操作
            }

            //最后进行缩放+平移
            this->setScaleTranslate(sx, sy, tx, ty);
        }
        return true;
    }

1、可以看出fit_start、fit_center、fit_end缩放规则与center_inside类似,只是center_inside图片宽、高其一大于控件宽、高才生效。
2、对于图片的缩放最终都会落实到matrix变换。ImageView scaleType也是Matrix经典运用的具体体现。需要注意的是,这里的Matrix scale都是基于左上角(0,0)做变换的。关于Matrix变换请移步Android Matrix 不再疑惑
3、scaleType只是改变图片的展示方式,并没有减少或者增大图片的内存占用。
4、scaleType做的工作实际上就是如何让内容在控件上做不同的展示,这里面的思想运用也比较多,比如做视频播放器时,如何让视频填充高或者宽,并居中播放。

scaleType默认值

    private void initImageView() {
        mMatrix = new Matrix();
        mScaleType = ScaleType.FIT_CENTER;

        if (!sCompatDone) {
            final int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
            sCompatAdjustViewBounds = targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1;
            sCompatUseCorrectStreamDensity = targetSdkVersion > Build.VERSION_CODES.M;
            sCompatDrawableVisibilityDispatch = targetSdkVersion < Build.VERSION_CODES.N;
            sCompatDone = true;
        }
    }

可以看出scaleType默认值是FIT_CENTER模式。

ImageView "adjustViewBounds" 怎么用

scaleType是控件尺寸不变,图片适应控件的尺寸。那么控件的尺寸能否随着图片的尺寸变化呢?答案是可以的,就是通过adjustViewBounds,顾名思义。
我们都知道控件的尺寸确定是在onMeasure方法里,前面我们分析ImageView onMeasure方法时,省略了一部分代码:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        if (mDrawable == null) {
            // If no drawable, its intrinsic size is 0.
            mDrawableWidth = -1;
            mDrawableHeight = -1;
            w = h = 0;
        } else {
            //是否设置了"AdjustViewBounds"属性
            if (mAdjustViewBounds) {
                //是否需要重新计算宽高,如果父类给的测量模式不是EXACTLY,则需要重新计算
                //如果是EXACTLY,则控件尺寸都是固定的,没必要重新计算
                resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
                resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;

                //图片 宽/高比例
                desiredAspect = (float) w / (float) h;
            }
        }


        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);

                //如果控件宽高与图片宽高比不一致
                if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {

                    boolean done = false;

                    if (resizeWidth) {
                        //重新计算宽度
                        //计算方式:按照实际的图片比例,用控件的高*比例得到控件新的宽
                        //也就是按照图片的宽高比来约束控件的比例
                        int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) +
                                pleft + pright;

                        if (!resizeHeight && !sCompatAdjustViewBounds) {
                            widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec);
                        }

                        if (newWidth <= widthSize) {
                            //重新计算出来的宽<原本的控件宽,说明约束成功,否则继续约束高
                            //如果大于,说明宽的约束不合适
                            widthSize = newWidth;
                            done = true;
                        }
                    }

                    // Try adjusting height to be proportional to width
                    if (!done && resizeHeight) {
                        int newHeight = (int)((widthSize - pleft - pright) / desiredAspect) +
                                ptop + pbottom;

                        // Allow the height to outgrow its original estimate if width is fixed.
                        if (!resizeWidth && !sCompatAdjustViewBounds) {
                            heightSize = resolveAdjustedSize(newHeight, mMaxHeight,
                                    heightMeasureSpec);
                        }

                        if (newHeight <= heightSize) {
                            heightSize = newHeight;
                        }
                    }
                }
            }
        } else {
            //省略
        }

        setMeasuredDimension(widthSize, heightSize);
    }

在xml里使用这属性

        

无论图片多大,控件就会多大,跟随图片的尺寸变化。此时图片不缩放,不平移。因此adjustViewBounds属性和scaleType属性是两种不同的作用,前者是控制控件的大小,后者是控制图片的显示大小。

ImageView 和 Drawable异同

在演示scaleType属性的时候,ImageView引用了BitmapDrawable,而BitmapDrawable持有Bitmap对象,最终将Bitmap展示在view上。
大体流程如下:

ImageView->onDraw()->Drawable->draw()->canvas.drawXXX();

我们知道View需要展示到屏幕上,最终得在onDraw()方法里调用canvas的系列方法,比如:

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(bitmap, null, new Rect(0, 0, 100, 258), paint);
    }

如果在onDraw里的绘制有通用的部分可以展示,那么可以提出来,如:

    private void draw(Canvas canvas) {
        canvas.drawBitmap(bitmap, null, new Rect(0, 0, 100, 258), paint);
        canvas.drawXX();
    }
}

那么在View的onDraw()方法里只需要调用公共部分draw()方法就可以实现不同的效果。实际上Drawable就是这么使用的,我们把一些通用效果封装为不同的Drawable,在View里持有Drawable对象,最终在View的onDraw()里调用Drawable的draw()方法。
Drawable需要的两个要素

1、通过setBounds()设置drawable尺寸
2、重写draw()方法,该方法里使用drawable限制的尺寸进行绘制

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Java

你可能感兴趣的:(ImageView-scaleType-各种不同效果解析)