咱们平时在使用ImageView,当设置宽高为wrap_content的时候,设置bitmap,有没有想过一个问题,那就是大小到底是如何计算的,平时说的那些density又和最终显示的图片大小有什么关系呢。本着严谨的态度,我开始了探索源码解读的不归路上。bash
本次实验所用测试机density为420。咱们首先来解码一张bitmap(ic_launcher大小为144 * 144),代码以下:app
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一看究竟,ide
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。赋值操做以后,咱们继续往下看。ui
@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。那能够对图片解码的大小作修改吗?固然能够,代码献上:this
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,这一次以下:spa
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的一致呢,二话不说上代码跑起来:code
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入手,以下:server
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;
}
}
复制代码
保持微笑😊,离结果又近了一步,
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。