ScaleType作为一个枚举类型,一共定义了八个模式
八个ScaleType,其实可以分为三个类型:
-
以FIT_开头的4种,它们的共同点是都会对图片进行缩放。
-
以CENTER_开头的三种,它们的共同点是居中显示,图片的中心点会与ImageView的中心点重叠。
-
MATRIX,不改变原图的大小,从ImageView的左上角开始绘制,超出的位置做剪切处理;原图小于ImageView则只绘制原图大小,其他地方空白。
本文的分析先进行实战猜测,根据实际结果猜测一个结论,最后看源码进行验证猜测的结论,所以猜测的结论不一定正确,如果不想看猜测过程,可以直接跳到源码分析环节,直接找到正确的结论。
FIT_开头的四种类型
-
FIT_START(保持原图片宽高比例)
把原图按照比例放大或缩小,显示在ImageView的start(左部/上部),具体是怎么缩放,和图片的宽高有关系。
当图片宽度大于高度时,按照宽度放大/缩小,图片宽度放大/缩小到ImageView的宽度,高度按同样比例放大/缩小,显示在ImageView的上半部分。
当图片高度大于宽度时,按照高度放大/缩小,图片高度放大/缩小到ImageView的高度,宽度按同样比例放大/缩小,显示在ImageView的左半部分。
-
FIT_END(保持原图片宽高比例)
FIT_END和FIT_START类似,FIT_START是左/上部,FIT_END是右/下部。
-
FIT_CENTER(默认模式,保持原图片宽高比例)
把原图按照比例放大或缩小,显示在ImageView的center(中部),具体是怎么缩放,和图片的宽高有关系
当图片宽度大于高度时,按照宽度放大/缩小,图片宽度放大/缩小到ImageView的宽度,高度按同样比例放大/缩小,显示在ImageView的中间。
当图片高度大于宽度时,按照高度放大/缩小,图片高度放大/缩小到ImageView的高度,宽度按同样比例放大/缩小,显示在ImageView的中间。
- FIT_XY(不保持原图片宽高比例)
把图片放大/缩小到ImageView的大小,不保持原图片比例,简单粗暴。
小结
对于FIT_START,FIT_END,FIT_CENTER来说,图片的宽和高哪个更接近ImageView对应的宽和高,哪个(宽/高)就先缩放到ImageView的大小,然后另一个按前面的比例进行缩放,会保持图片的原宽高比例,而且一定会显示完整的图片,就是图片的所有部分,不会造成放大或缩小后只显示图片的一部分内容;FIX_XT就是简单粗暴的缩放到ImageView的大小,不会保持图片的原宽高比例,但是也会显示图片的所有内容。
所以FIT_开头的方式都能显示图片的所有内容。
PS:显示图片的所有内容意思就是,把图片上所有东西都显示出来,有些放大/缩小只是放大/缩小一部分内容,比如放大上面那个图片的打电脑的女生,然后放大到充满ImageView,但是我们就看不到LeetCode这段文字了,这样就不能显示图片的所有内容。
CENTER_开头的三种类型(缩放中心点是图片的中心点)
-
CENTER_CROP
本来以为缩放也会和宽高有关,但是看到下面这个图就猜不出具体是什么关系了,只能在后面的源码分析里面看一下是怎么缩放的。
-
CENTER_INSIDE(保持原图片宽高比例)
以原图正常显示为目的,如果原图大小大于ImageView的size,就按照比例缩小原图的宽高,直到较长的一端刚好能显示在ImageView中,居中显示在ImageView中。如果原图size小于ImageView的size,则不做处理居中显示图片。如下图所示:
-
CENTER
保持原图的大小,显示在ImageView的中心。当原图的size大于ImageView的size时,多出来的部分被截掉。
MATRIX
不改变原图的大小,从ImageView的左上角开始绘制,超出的位置做剪切处理。
源码分析
Matrix (0), //Scale the image using {@link Matrix.ScaleToFit#FILL}.
FIT_XY (1), //Scale the image using {@link Matrix.ScaleToFit#START}.
FIT_START (2), //Scale the image using {@link Matrix.ScaleToFit#START}.
FIT_CENTER (3), //Scale the image using {@link Matrix.ScaleToFit#CENTER}.
FIT_END (4), //Scale the image using {@link Matrix.ScaleToFit#END}.
CENTER (5), //Center the image in the view, but perform no scaling.
CENTER_CROP (6), /**
* Scale the image uniformly (maintain the image's aspect ratio) so
* that both dimensions (width and height) of the image will be equal
* to or larger than the corresponding dimension of the view
* (minus padding). The image is then centered in the view.
*/
CENTER_INSIDE (7) /**
* Scale the image uniformly (maintain the image's aspect ratio) so
* that both dimensions (width and height) of the image will be equal
* to or less than the corresponding dimension of the view
* (minus padding). The image is then centered in the view.
*/
设置完ScaleType之后,具体的实现细节在ImageView的configureBounds()方法中(android-29)
private void configureBounds() {
if (mDrawable == null || !mHaveFrame) {
return;
}
final int dwidth = mDrawableWidth; //图片宽度
final int dheight = mDrawableHeight;//图片高度
final int vwidth = getWidth() - mPaddingLeft - mPaddingRight; //ImageView宽度
final int vheight = getHeight() - mPaddingTop - mPaddingBottom;//ImageView高度
final boolean fits = (dwidth < 0 || vwidth == dwidth)
&& (dheight < 0 || vheight == dheight);
if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) { //FIX_XY
/* If the drawable has no intrinsic size, or we're told to
scaletofit, then we just fill our entire view.
*/
mDrawable.setBounds(0, 0, vwidth, vheight); //FIT_XY最简单粗暴
mDrawMatrix = null;
} else {
// We need to do the scaling ourself, so have the drawable
// use its native size.
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) { //图片宽高与ImageView一致
// 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 { //FIT_START,FIT_END,FIT_CENTER
// Generate the required transform.
mTempSrc.set(0, 0, dwidth, dheight);
mTempDst.set(0, 0, vwidth, vheight);
mDrawMatrix = mMatrix;
mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
}
}
}
- CENTER
// Center bitmap in view, no scaling.
mDrawMatrix = mMatrix;
mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f),
Math.round((vheight - dheight) * 0.5f));
mDrawMatrix矩阵平移,其实就是将图片移到ImageView的中间位置,展示图片的中间区域。直接进行平移。
- CENTER_CROP
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));
dwidth/dheight>vwidth/vheight(图片的宽高比大于ImageView的宽高比)等价于dwidth * vheight > vwidth * dheight ,也就是说ImageView和图片高度比小于ImageView和图片的宽度比,这时候取vheight/deight进行图片缩放,就能保证图片宽度在进行同等比例缩放的时候,图片宽度大于或等于ImageView的宽度,因为(vheight/dheight)* dwidth>vwidth。也解释了注释。先缩放,再平移。所以该模式和图片的宽高比以及ImageView的宽高比有关。
- CENTER_INSIDE
mDrawMatrix = mMatrix;
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);
从scale = Math.min((float) vwidth / (float) dwidth, (float) vheight / (float) dheight); 可以看出该模式与(ImageView和图片的高度比)以及(ImageView和图片的宽度比)有关,哪个小scale就等于它。最后再进行缩放平移。
- 当ImageView设置了四种ScaleType后,ImageView采用Matrit.ScaleToFit来进行图片的展示。
} else { //FIT_START,FIT_END,FIT_CENTER
// Generate the required transform.
mTempSrc.set(0, 0, dwidth, dheight);
mTempDst.set(0, 0, vwidth, vheight);
mDrawMatrix = mMatrix;
mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
@UnsupportedAppUsage
private static Matrix.ScaleToFit scaleTypeToScaleToFit(ScaleType st) {
// ScaleToFit enum to their corresponding Matrix.ScaleToFit values
return sS2FArray[st.nativeInt - 1];
}
private static final Matrix.ScaleToFit[] sS2FArray = {
Matrix.ScaleToFit.FILL,
Matrix.ScaleToFit.START,
Matrix.ScaleToFit.CENTER,
Matrix.ScaleToFit.END
};
/**
* Controlls how the src rect should align into the dst rect for setRectToRect().
*/
public enum ScaleToFit {
/**
* Scale in X and Y independently, so that src matches dst exactly. This may change the
* aspect ratio of the src.
*/
FILL(0),
/**
* Compute a scale that will maintain the original src aspect ratio, but will also ensure
* that src fits entirely inside dst. At least one axis (X or Y) will fit exactly. START
* aligns the result to the left and top edges of dst.
*/
START(1),
/**
* Compute a scale that will maintain the original src aspect ratio, but will also ensure
* that src fits entirely inside dst. At least one axis (X or Y) will fit exactly. The
* result is centered inside dst.
*/
CENTER(2),
/**
* Compute a scale that will maintain the original src aspect ratio, but will also ensure
* that src fits entirely inside dst. At least one axis (X or Y) will fit exactly. END
* aligns the result to the right and bottom edges of dst.
*/
END(3);
// the native values must match those in SkMatrix.h
ScaleToFit(int nativeInt) { //关键方法
this.nativeInt = nativeInt;
}
final int nativeInt;
}
FILL:相当于ScaleType.FIX_XY,图片的高度和宽度有独立的缩放比例。图片宽度的缩放比例为vwidth/dwidth,高度的缩放比例为vheight/dheight。
下面是这篇网上一篇文章的对FIT_START,FIT_CENTER,FIT_END这三种类型的结论,但是并没有给出源码的分析,我按照它的结论测试了几个用例,发现并不对。所以我打算从源码角度来进行一下分析。
另外说一下,这篇文章写得还是很好的,前面的几个模式结合源码进行图文分析的很不错。
错误结论:
FIT_START,FIT_CENTER,FIT_END采用的缩放策略一样,如果dheight> dwidth,则使用vwidth/dwidth作为缩放比例,反之使用vheight/dheight作为缩放比例。
正确分析:
从源码我们可以看到FIT_START,FIT_CENTER,FIT_END这三个模式都调用了setRectToRect()
public boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf) {
if (dst == null || src == null) {
throw new NullPointerException();
}
return nSetRectToRect(native_instance, src, dst, stf.nativeInt);
}
//而nSetRectToRect是一个nativie方法
@FastNative
private static native boolean nSetRectToRect(long nObject,
RectF src, RectF dst, int stf);
最后我们找到framework层里面的真正实现
/**
33 * Delegate implementing the native methods of android.graphics.Matrix
34 *
35 * Through the layoutlib_create tool, the original native methods of Matrix have been replaced
36 * by calls to methods of the same name in this delegate class.
37 *
38 * This class behaves like the original native implementation, but in Java, keeping previously
39 * native data into its own objects and mapping them to int that are sent back and forth between
40 * it and the original Matrix class.
41 *
42 * @see DelegateManager
43 *
44 */
public final class Matrix_Delegate {
...
...
...
532 @LayoutlibDelegate
533 /*package*/ static boolean nSetRectToRect(long native_object, RectF src,
534 RectF dst, int stf) {
535 Matrix_Delegate d = sManager.getDelegate(native_object);
536 if (d == null) {
537 return false;
538 }
539
540 if (src.isEmpty()) {
541 reset(d.mValues);
542 return false;
543 }
544
545 if (dst.isEmpty()) {
546 d.mValues[0] = d.mValues[1] = d.mValues[2] = d.mValues[3] = d.mValues[4] = d.mValues[5]
547 = d.mValues[6] = d.mValues[7] = 0;
548 d.mValues[8] = 1;
549 } else {
550 float tx, sx = dst.width() / src.width();
551 float ty, sy = dst.height() / src.height();
552 boolean xLarger = false;
553
554 if (stf != ScaleToFit.FILL.nativeInt) { //FIT_START,FITA_END,FIT_CENTER
555 if (sx > sy) {
556 xLarger = true;
557 sx = sy;
558 } else {
559 sy = sx;
560 }
561 }
562
563 tx = dst.left - src.left * sx;
564 ty = dst.top - src.top * sy;
565 if (stf == ScaleToFit.CENTER.nativeInt || stf == ScaleToFit.END.nativeInt) {
566 float diff;
567
568 if (xLarger) {
569 diff = dst.width() - src.width() * sy;
570 } else {
571 diff = dst.height() - src.height() * sy;
572 }
573
574 if (stf == ScaleToFit.CENTER.nativeInt) {
575 diff = diff / 2;
576 }
577
578 if (xLarger) {
579 tx += diff;
580 } else {
581 ty += diff;
582 }
583 }
584
585 d.mValues[0] = sx;
586 d.mValues[4] = sy;
587 d.mValues[2] = tx;
588 d.mValues[5] = ty;
589 d.mValues[1] = d.mValues[3] = d.mValues[6] = d.mValues[7] = 0;
590
591 }
592 // shared cleanup
593 d.mValues[8] = 1;
594 return true;
595 }
}
如果要了解矩阵的原理和变化,可以看看下面这篇博文
Matrix本质上是一个如下图所示的矩阵:
Matrix 提供了如下几个操作:
-
缩放(Scale) 对应 MSCALE_X 与 MSCALE_Y
-
位移(Translate) 对应 MTRANS_X 与 MTRANS_Y
-
错切(Skew) 对应 MSKEW_X 与 MSKEW_Y
-
旋转(Rotate) 旋转没有专门的数值来计算,Matrix 会通过计算缩放与错切来处理旋转。
FIT_START的分析:
float tx, sx = dst.width() / src.width();
551 float ty, sy = dst.height() / src.height();
552 boolean xLarger = false;
if (stf != ScaleToFit.FILL.nativeInt) { //FIT_START,FITA_END,FIT_CENTER,所以下面这段实 //际上是FIT_START
555 if (sx > sy) {
556 xLarger = true;
557 sx = sy;
558 } else {
559 sy = sx;
560 }
561 }
562
563 tx = dst.left - src.left * sx;
564 ty = dst.top - src.top * sy;
if (stf == ScaleToFit.CENTER.nativeInt
|| stf == ScaleToFit.END.nativeInt) { //FITA_END,FIT_CENTER
···
d.mValues[0] = sx;
586 d.mValues[4] = sy;
587 d.mValues[2] = tx;
588 d.mValues[5] = ty;
589 d.mValues[1] = d.mValues[3] = d.mValues[6] = d.mValues[7] = 0;
可以看出当(图片和ImageView的宽度比)大于(图片和ImageView的高度比)时,缩放比等于图片和ImageView的宽度比,反之也成立,x轴平移的距离为dst.left - src.left * sx,y轴平移的距离为dst.top - src.top * sy,因为矩阵的0,4位置是缩放的,2,5位置是平移。
FIT_END,FIT_CENTER的分析:与FIT_START一样,缩放比也是和图片和Imageview的宽度比(高度比)相关,只是位移做了一些调整,这里就不细究了。
结论
我们的猜测结论是错误的,因为我们的测试的例子不能够覆盖所有场景,所以导致得出的结论有偏差,只适应于一定的范围,所以,看源码,看源码,看源码!
FIT_START,FIT_END,FIT_CENTER的缩放比都和图片与ImageView的宽度比(高度比)相关,然后在进行位移,图片会保持原有的比例。
FIT_XY,直接缩放到ImageView的宽高,会破坏原有的图片比例。
CENTER,其实就是将图片移到ImageView的中间位置,展示图片的中间区域。直接进行平移,不缩放,多余的进行剪裁。
CENTER_CROP,先缩放,再平移。该模式和图片的宽高比以及ImageView的宽高比有关,而不是图片和ImageView的宽度比(高度比),注意区别。
CENTER_INSIDE,该模式与(ImageView和图片的高度比)以及(ImageView和图片的宽度比)有关,哪个小scale就等于它。最后再进行缩放平移。
MATRIX,不改变原图的大小,从ImageView的左上角开始绘制,超出的位置做剪切处理。
扩展:自定义一种图片缩放模式
我们可以设计一种模式,让图片显示在底部中间,如果图片超过ImageView的大小,那就剪裁掉多余部分,保留底部。
效果图如下:无论ImageView多高多宽,图片永远显示在底部中间,多余的剪裁掉。
代码如下:
//设置初始矩阵值
Matrix matrix = new Matrix();
float matrixValues[] = { 1f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 1f };
matrix.setValues(matrixValues);
//获取ImageView宽高
vWidth = img_test3.getWidth();
vHeight = img_test3.getHeight();
//获取图片固有宽高,与源码的获取方式相同,一定要使用该方式
dWidth = img_test3.getDrawable().getIntrinsicWidth();
dHeight = img_test3.getDrawable().getIntrinsicHeight();
// BitmapFactory.Options options = new BitmapFactory.Options();
// BitmapFactory.decodeResource(getResources(),R.drawable.ic_test_3,options);
// //获取图片的宽高,这个方式不行,注意!
// dHeight = options.outHeight;
// dWidth = options.outWidth;
//对图片进行平移
float tx3 = Math.round((vWidth - dWidth) * 1 * 0.5);
float ty3 = Math.round((vHeight - dHeight) * 1);
matrix.setTranslate(tx3,ty3);
img_test3.setImageMatrix(matrix);
带有个需要注意的地方,就是ImageView要设置scaleType方式为matrix,这样才能生效。源码如下:
if (ScaleType.MATRIX == mScaleType) {
// Use the specified matrix as-is.
if (mMatrix.isIdentity()) {
mDrawMatrix = null;
} else {
mDrawMatrix = mMatrix;
}
参考文章
Android Matrix详解:www.jianshu.com/p/5e30db034…
ImageView之ScaleType详解及拓展:juejin.cn/post/684490…
ImageView的ScaleType原理及效果分析:www.jianshu.com/p/fe5d2e3fe…