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

作者: 小鱼人爱编程 | 来源:发表于2021-10-31 22:11 被阅读0次

    前言

    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 此时以内容大小为准
        <ImageView
            android:id="@+id/iv"
            android:background="@color/colorAccent"
            android:src="@drawable/test2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
        </ImageView>
    
    //对应宽为:EXACTLY 以父控件给的大小为准
    // 对应高为:AT_MOST 以内容高为准
        <ImageView
            android:id="@+id/iv"
            android:background="@color/colorAccent"
            android:src="@drawable/test2"
            android:layout_width="100dp"
            android:layout_height="wrap_content">
        </ImageView>
    

    两者都设置了背景,便于直观观察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里使用这属性

            <ImageView
                android:adjustViewBounds="true"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/test_small"></ImageView>
    

    无论图片多大,控件就会多大,跟随图片的尺寸变化。此时图片不缩放,不平移。因此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-各种不同效果解析

        本文链接:https://www.haomeiwen.com/subject/lfeaaltx.html