内容简介:作者:fishforest链接:https://www.jianshu.com/p/273e99e3d1b7
code小生 一个专注大前端领域的技术平台 公众号回复 Android
加入安卓技术群
作者:fishforest
链接:https://www.jianshu.com/p/273e99e3d1b7
声明:本文已获 fishforest
授权发表,转发等请联系原作者授权
前言
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(单位像素)
//对应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大小)。看看效果:
对比上面两图,只是更改了"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的效果。
图片尺寸是:182 * 538(px) ImageView尺寸是:100 * 100 (dp) ,测试设备密度是2.75 ,换算作像素是:275 * 275 px
关于dp与px请参考:Android 屏幕分辨率适配 https://www.jianshu.com/p/72e6e1e83b96
为了更直观看出控件尺寸与图片尺寸,给控件尺寸加了红色背景。可以看出图片的宽小于控件的宽,图片的高大于控件的高。分别来看看各个模式下展示表现,每个控件上都有标明对应的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限制的尺寸进行绘制
若有疑问,欢迎评论留言。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- CSS 实现各种 Loading 效果附带解析
- html5 canvas实现背景鼠标连线动态效果代码解析
- jQuery效果—雪花飘落
- jQuery效果—雪花飘落
- SlidingMenu实现侧滑效果
- 鼠标悬停动画效果
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。