内容简介:我们平时在使用ImageView,当设置宽高为wrap_content的时候,设置bitmap,有没有想过一个问题,那就是大小究竟是如何计算的,平时说的那些density又和最终显示的图片大小有什么关系呢。本着严谨的态度,我开始了探索源码解读的不归路上。本次实验所用测试机density为420。我们首先来解码一张bitmap(ic_launcher大小为144 * 144),代码如下:打印结果是{height: 126 --- width: 126},那么这个数值是怎么来的呢。我们进入decodeRe
我们平时在使用ImageView,当设置宽高为wrap_content的时候,设置bitmap,有没有想过一个问题,那就是大小究竟是如何计算的,平时说的那些density又和最终显示的图片大小有什么关系呢。本着严谨的态度,我开始了探索源码解读的不归路上。
过程
本次实验所用测试机density为420。我们首先来解码一张bitmap(ic_launcher大小为144 * 144),代码如下:
val options = BitmapFactory.Options() val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options) Log.d("Bitmap", "{height: ${bitmap.height} --- width: ${bitmap.width}}") 复制代码
打印结果是{height: 126 --- width: 126},那么这个数值是怎么来的呢。我们进入decodeResource一看究竟,
public static Bitmap decodeResource(Resources res, int id, Options opts) { validate(opts); Bitmap bm = null; InputStream is = null; try { final TypedValue value = new TypedValue(); is = res.openRawResource(id, value); bm = decodeResourceStream(res, value, is, null, opts); } catch (Exception e) { /* do nothing. If the exception happened on open, bm will be null. If it happened on close, bm is still valid. */ } finally { try { if (is != null) is.close(); } catch (IOException e) { // Ignore } } if (bm == null && opts != null && opts.inBitmap != null) { throw new IllegalArgumentException("Problem decoding into existing bitmap"); } return bm; } 复制代码
bitmap是decodeResourceStream产生的,那我们接着往下看,
@Nullable public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value, @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) { validate(opts); if (opts == null) { opts = new Options(); } if (opts.inDensity == 0 && value != null) { final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density != TypedValue.DENSITY_NONE) { opts.inDensity = density; } } if (opts.inTargetDensity == 0 && res != null) { opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } return decodeStream(is, pad, opts); } 复制代码
可以看到,如果options.inDensity等于0,这里会对options做赋值操作,inDensity指的是图片资源所在资源文件夹的density,即xhdpi这些文件对应的density,inTargetDensity是指目标的density即手机屏幕dpi,在这个实验中,资源的原始density是480,目标density是420。赋值操作之后,我们继续往下看。
@Nullable public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding, @Nullable Options opts) { // we don't throw in this case, thus allowing the caller to only check // the cache, and not force the image to be decoded. if (is == null) { return null; } validate(opts); Bitmap bm = null; Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap"); try { if (is instanceof AssetManager.AssetInputStream) { final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset(); bm = nativeDecodeAsset(asset, outPadding, opts); } else { bm = decodeStreamInternal(is, outPadding, opts); } if (bm == null && opts != null && opts.inBitmap != null) { throw new IllegalArgumentException("Problem decoding into existing bitmap"); } setDensityFromOptions(bm, opts); } finally { Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS); } return bm; } 复制代码
这里做的是调用native方法进行解码,具体就不往下看。但是我们掐指一算和本着直觉来对大小计算,原始大小是144,解码大小是126,inDensity是480,inTargetDensity是420,相信看到这里,聪明的读者很快就可以算出来了,没错,126 = 144 * 420 / 480, 也就是说 targetSize = rawSize * targetDensity / rawDensity,其实也很好理解,就是对图片进行缩放,缩放的依据就是为了适应当前手机的density。那可以对图片解码的大小做修改吗?当然可以,代码献上:
val options = BitmapFactory.Options() options.inTargetDensity = 480 val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options) Log.d("Bitmap", "{height: ${bitmap.height} --- width: ${bitmap.width}}") 复制代码
打印结果是{height: 144 --- width: 144},按照上面的公式计算即可得到这个结果,其实我们就是把目标density做了修改,从而影响bitmap的解码过程。我们接着修改options,这一次如下:
val options = BitmapFactory.Options() options.inDensity = 240 options.inTargetDensity = 480 val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options) Log.d("Bitmap", "{height: ${bitmap.height} --- width: ${bitmap.width}}") 复制代码
心算一下,就知道结果是288。这一次我们是通过修改图片资源的density影响了bitmap的解码产生的大小。 那么ImageView的大小是否和bitmap的一致呢,二话不说上代码跑起来:
val options = BitmapFactory.Options() val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options) Log.d("Bitmap", "{height: ${bitmap.height} --- width: ${bitmap.width}}") image_view.setImageBitmap(bitmap) image_view.viewTreeObserver.addOnPreDrawListener { Log.d("ImageView", "{height: ${image_view.height} --- width: ${image_view.width}}") true } 复制代码
结果还真的是一样的,都是126,但是这样还不够,改下options参数试一下, inTargetDensity 改为 480,你猜结果怎么着,bitmap是144,imageview是126,咦这么神奇。老实看代码去吧。从setImageBitmap入手,如下:
public void setImageBitmap(Bitmap bm) { // Hacky fix to force setImageDrawable to do a full setImageDrawable // instead of doing an object reference comparison mDrawable = null; if (mRecycleableBitmapDrawable == null) { mRecycleableBitmapDrawable = new BitmapDrawable(mContext.getResources(), bm); } else { mRecycleableBitmapDrawable.setBitmap(bm); } setImageDrawable(mRecycleableBitmapDrawable); } 复制代码
可以看到实际上内部是把bitmap装进BitmapDrawable,继续往下看:
public void setImageDrawable(@Nullable Drawable drawable) { if (mDrawable != drawable) { mResource = 0; mUri = null; final int oldWidth = mDrawableWidth; final int oldHeight = mDrawableHeight; updateDrawable(drawable); if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) { requestLayout(); } invalidate(); } } 复制代码
关键代码是updateDrawable,除此之外,还会进行新旧宽高的判断,决定是否重新requestLayout。查看updateDrawable代码,
private void updateDrawable(Drawable d) { if (d != mRecycleableBitmapDrawable && mRecycleableBitmapDrawable != null) { mRecycleableBitmapDrawable.setBitmap(null); } boolean sameDrawable = false; if (mDrawable != null) { sameDrawable = mDrawable == d; mDrawable.setCallback(null); unscheduleDrawable(mDrawable); if (!sCompatDrawableVisibilityDispatch && !sameDrawable && isAttachedToWindow()) { mDrawable.setVisible(false, false); } } mDrawable = d; if (d != null) { d.setCallback(this); d.setLayoutDirection(getLayoutDirection()); if (d.isStateful()) { d.setState(getDrawableState()); } if (!sameDrawable || sCompatDrawableVisibilityDispatch) { final boolean visible = sCompatDrawableVisibilityDispatch ? getVisibility() == VISIBLE : isAttachedToWindow() && getWindowVisibility() == VISIBLE && isShown(); d.setVisible(visible, true); } d.setLevel(mLevel); mDrawableWidth = d.getIntrinsicWidth(); mDrawableHeight = d.getIntrinsicHeight(); applyImageTint(); applyColorMod(); configureBounds(); } else { mDrawableWidth = mDrawableHeight = -1; } } 复制代码
关键的有几处,一处是drawable的赋值,另外一处是
mDrawableWidth = d.getIntrinsicWidth(); mDrawableHeight = d.getIntrinsicHeight(); configureBounds(); 复制代码
对drawable的宽高进行赋值,然后重新调整bound的大小,configureBounds方法代码较多,这里先摘抄最重要的一部分,
final int dwidth = mDrawableWidth; final int dheight = mDrawableHeight; mDrawable.setBounds(0, 0, dwidth, dheight); 复制代码
到这里就水落石出了,ImageView的宽高由上面d.getIntrinsicWidth(),d.getIntrinsicHeight()决定,所以破案的关键就在于这两个方法,走,看源码去,由于这里drawable的实现类是BitmapDrawable,所以需要查看BitmapDrawable的实现方法,如下
@Override public int getIntrinsicWidth() { return mBitmapWidth; } @Override public int getIntrinsicHeight() { return mBitmapHeight; } 复制代码
好的,离胜利不远了,查看mBitmapWidth赋值,
private void computeBitmapSize() { final Bitmap bitmap = mBitmapState.mBitmap; if (bitmap != null) { mBitmapWidth = bitmap.getScaledWidth(mTargetDensity); mBitmapHeight = bitmap.getScaledHeight(mTargetDensity); } else { mBitmapWidth = mBitmapHeight = -1; } } 复制代码
保持微笑:blush:,离结果又近了一步,
public int getScaledHeight(int targetDensity) { return scaleFromDensity(getHeight(), mDensity, targetDensity); } /** * @hide */ static public int scaleFromDensity(int size, int sdensity, int tdensity) { if (sdensity == DENSITY_NONE || tdensity == DENSITY_NONE || sdensity == tdensity) { return size; } // Scale by tdensity / sdensity, rounding up. return ((size * tdensity) + (sdensity >> 1)) / sdensity; } 复制代码
到这里就又恍然大悟了,原来绘制到ImageView的bitmapDrawable会对bitmap再进行一次缩放,缩放的比例还是inDensity,targetDensity,只不过这里的inDensity是bitmap的density,如果options没有做设置,bitmap的density即为图片资源文件夹的density,在这里是480,那targetDensity又是多少呢,找到BitmapDrawable赋值的地方,代码如下:
state.mTargetDensity = Drawable.resolveDensity(r, 0); static int resolveDensity(@Nullable Resources r, int parentDensity) { final int densityDpi = r == null ? parentDensity : r.getDisplayMetrics().densityDpi; return densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi; } 复制代码
这里很明显可以得到 targetDensity等于设备的density,即420。说到这里,是不是有种柳暗花明又一村的感觉呢,因为这和bitmap的默认缩放配置是一样的,虽然我们修改了bitmap的缩放配置,但是并没有影响到bitmapDrawable的配置,所以BitmapDrawable的大小为 144 * 420 / 480 = 126。 看到这里,聪明的读者A肯定可以想到,既然不能修改BitmapDrawable的targetDensity, 那么我通过修改options的inDensity不就可以修改图片大小了吗,恭喜你,答对了,
val options = BitmapFactory.Options() options.inDensity = 240 options.inTargetDensity = 480 val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options) Log.d("Bitmap", "{height: ${bitmap.height} --- width: ${bitmap.width}}") image_view.setImageBitmap(bitmap) image_view.viewTreeObserver.addOnPreDrawListener { Log.d("ImageView", "{height: ${image_view.height} --- width: ${image_view.width}}") true } 复制代码
铛铛铛,小学数学问题,结果是256,因为分母少了二分之一,所以相当于变成两倍。看到这里,读者A肯定觉得自己很聪明,一切都在自己掌握当中, 但是too young too naive,其实可以修改BitmapDrawable的targetDensity,代码献上,
val options = BitmapFactory.Options() val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options) Log.d("Bitmap", "{height: ${bitmap.height} --- width: ${bitmap.width}}") val bitmapDrawable = BitmapDrawable(resources, bitmap) bitmapDrawable.setTargetDensity(480) image_view.setImageDrawable(bitmapDrawable) image_view.viewTreeObserver.addOnPreDrawListener { Log.d("ImageView", "{height: ${image_view.height} --- width: ${image_view.width}}") true } 复制代码
什么,还想要结果,这么简单的问题。
好吧,偷偷告诉你,其实结果是144。
总结
- 对于Bitmap,大小等于 rawSize * targetDensity / rawDensity,targetDensity是目标的density, rawDensity是原始资源的density,当然这两个值都可以通过options进行修改,其实从这里也可以看出图片资源放在适合的资源夹的重要性,如果图片资源放的文件夹density太小,会导致解码的bitmap放大,从而导致内存增加,毕竟解码之后的面积变大了,单位面积的占用内存又不变。
- 对于ImageView,我们可以知道,即使我们对bitmap进行了缩放,在内存的drawable又会重新进行缩放,以用来适应实际大小。缩放比例我们还是可以通过targetDensity,inDensity修改进行控制的。
- 好的,这一次的分享就到此结束了,喜欢的点个赞呗,或者大家讨论讨论。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
哥德尔、艾舍尔、巴赫
[美] 侯世达 / 严勇、刘皓明、莫大伟 / 商务印书馆 / 1997-5 / 88.00元
集异璧-GEB,是数学家哥德尔、版画家艾舍尔、音乐家巴赫三个名字的前缀。《哥德尔、艾舍尔、巴赫书:集异璧之大成》是在英语世界中有极高评价的科普著作,曾获得普利策文学奖。它通过对哥德尔的数理逻辑,艾舍尔的版画和巴赫的音乐三者的综合阐述,引人入胜地介绍了数理逻辑学、可计算理论、人工智能学、语言学、遗传学、音乐、绘画的理论等方面,构思精巧、含义深刻、视野广阔、富于哲学韵味。 中译本前后费时十余年,......一起来看看 《哥德尔、艾舍尔、巴赫》 这本书的介绍吧!