问题描述:在SD卡上有一个名为cat的图片文件,文件的大小是510px*380px。将该文件以Bitmap的格式读入内存后,再进一步的将该Bitmap对象转为Drawable对象。主要代码如下:
Cursor c =
getContentResolver().query(
Images.Media.EXTERNAL_CONTENT_URI,
newString[]{Images.Media.DATA},
Images.Media.TITLE+"=?",
new String[]{"cat"},
null);
c.moveToNext();
String path = c.getString(0);
c.close();
Log.d("TAG", "DisplayMetrics.densityDpi是:"+getResources().getDisplayMetrics().densityDpi);
Bitmap b = BitmapFactory.decodeFile(path);
Log.d("TAG", "图像的尺寸是:"+b.getWidth()+"/"+b.getHeight());
Drawable d1 = BitmapDrawable.createFromPath(path);
Log.d("TAG", "d1的尺寸是:"+d1.getIntrinsicWidth()+"/"+d1.getIntrinsicHeight());
Drawable d2 = new BitmapDrawable(getResources(), b);
Log.d("TAG", "d2的尺寸是:"+d2.getIntrinsicWidth()+"/"+d2.getIntrinsicHeight());
程序运行在Genymotion模拟器上,模拟器的配置是:480px*800px,屏幕密度是240dpi
程序运行结果是:
DisplayMetrics.densityDpi是:240
图像的尺寸是:510/380
d1的尺寸是:340/253
d2的尺寸是:510/380
可以看到,d1和d2都是来自同一个Bitmap,但是两者的宽度和高度是不一样的。
d1和d2的不同之处在于d1是通过BitmapDrawable的createFromPath方法创建的,而d2是通过BitmapDrawable构造器创建的。
那么看一下使用createFromPath和使用构造器来有什么区别?
首先看一下createFromPath方法。该方法并不是BitmapDrawable的方法,而是BitmapDrawable继承自父类Drawable的方法:
Drawable/createFromPath方法:
public static Drawable createFromPath(String pathName) {
if (pathName ==null) {
return null;
}
Bitmap bm = BitmapFactory.decodeFile(pathName);
if (bm != null) {
returndrawableFromBitmap(null, bm, null, null, pathName);
}
return null;
}
crateFromPath方法使用 ① BitmapFactory的decodeFile方法获得外部存储上的Bitmap图像文件,然后利用 ② drawableFromBitmap方法从Bitmap转为drawable
①首先看获得Bitmap对象的过程,即BitmapFactory的decodeFile方法:
public static BitmapdecodeFile(String pathName) {
return decodeFile(pathName, null);
}
在decodeFile(String pathName)方法中调用重载的decodeFile(pathName,null)方法。
重载的decodeFile方法第二个参数是一个Options对象,这里意味着Options对象为null
public static Bitmap decodeFile(String pathName, Options opts){
Bitmap bm = null;
InputStreamstream = null;
try {
stream = newFileInputStream(pathName);
bm =decodeStream(stream, null, opts);
} catch(Exception e) {
/* do nothing.
If theexception happened on open, bm will be null.
*/
} finally {
if (stream !=null) {
try {
stream.close();
} catch(IOException e) {
// donothing here
}
}
}
return bm;
}
decodeFile返回的Bitmap是通过decodeStream方法获得的
注意,此时在decodeFile方法中调用decodeStream时,decodeStream方法中的第二个参数outPadding和第三个参数opts参数均为null
public static Bitmap decodeStream(InputStream is, RectoutPadding, Options opts) {
// we don't throwin this case, thus allowing the caller to only check
// the cache, andnot force the image to be decoded.
if (is == null) {
return null;
}
// we needmark/reset to work properly
if(!is.markSupported()) {
is = newBufferedInputStream(is, 16 * 1024);
}
// so we can callreset() if a given codec gives up after reading up to
// this manybytes. FIXME: need to find out from the codecs what this
// value shouldbe.
is.mark(1024);
Bitmap bm;
if (is instanceofAssetManager.AssetInputStream) {
bm =
nativeDecodeAsset(
((AssetManager.AssetInputStream)is).getAssetInt(),
outPadding, opts);
} else {
// pass sometemp storage down to the native code. 1024 is made up,
// but shouldbe large enough to avoid too many small calls back
// intois.read(...) This number is not related to the value passed
// tomark(...) above.
byte []tempStorage = null;
if (opts !=null) tempStorage = opts.inTempStorage;
if(tempStorage == null) tempStorage = new byte[16 * 1024];
bm =nativeDecodeStream(is, tempStorage, outPadding, opts);
}
if (bm == null&& opts != null && opts.inBitmap != null) {
throw newIllegalArgumentException(
"Problemdecoding into existing bitmap");
}
return finishDecode(bm, outPadding, opts);
}
decodeStream方法调用nativeDecodeStream或者nativeDecodeAsset获得Bitmap对象后,要调用finishDecode方法对获得的Bitmap对像进行一下处理,并将finishDecode方法的返回值作为decodeStream方法的返回值。需要注意的是,此时finishDecode方法的第二个参数outPadding和第三个参数opts均为null。
private static Bitmap finishDecode(Bitmap bm, Rect outPadding,Options opts) {
if (bm == null ||opts == null) {
return bm;
}
final int density= opts.inDensity;
if (density == 0){
return bm;
}
bm.setDensity(density);
final inttargetDensity = opts.inTargetDensity;
if (targetDensity== 0 ||
density ==targetDensity ||
density ==opts.inScreenDensity) {
return bm;
}
byte[] np =bm.getNinePatchChunk();
final booleanisNinePatch = np != null && NinePatch.isNinePatchChunk(np);
if (opts.inScaled|| isNinePatch) {
float scale =targetDensity / (float)density;
// TODO: Thisis very inefficient and should be done in native by Skia
final BitmapoldBitmap = bm;
bm =Bitmap.createScaledBitmap(
oldBitmap, (int)(bm.getWidth() * scale + 0.5f),
(int)(bm.getHeight() * scale + 0.5f), true);
oldBitmap.recycle();
if(isNinePatch) {
np =nativeScaleNinePatch(np, scale, outPadding);
bm.setNinePatchChunk(np);
}
bm.setDensity(targetDensity);
}
return bm;
}
因为此时finishDecode方法的第二个参数outPadding和第三个参数opts均为null,所以直接返回SD卡上原生尺寸的Bitmap对象,在这里不做任何的缩放处理。
在通过BitmapFactory的decodeFile成功获得指定路径位置的Bitmap对象后,接下来会调用drawableFromBitmap方法。在createFromPath方法中调用drawableFromBitmap方法时,第一个参数res,第三个参数np和第四个参数pad这三个参数的值均为null
private staticDrawable drawableFromBitmap(Resources res, Bitmap bm, byte[] np,
Rect pad,String srcName) {
if (np != null) {
return newNinePatchDrawable(res, bm, np, pad, srcName);
}
return new BitmapDrawable(res, bm);
}
因为res,np,pad这三个参数的值均为null,所以drawableFromBitmap方法会返回new BitmapDrawable(res, bm);
到这里已经变成了用BitmapDrawable构造器构建Drawable对象了。但是此时BitmapDrawable构造器中,第一个参数res的值为null。
public BitmapDrawable(Resources res, Bitmap bitmap) {
this(newBitmapState(bitmap), res);
mBitmapState.mTargetDensity = mTargetDensity;
}
构造器中会调用另一个重载版本的构造器,在重载的构造器中,会先将Bitmap对象包装为一个BitmapState对象。
BitmapState是BitmapDrawable的一个内部类,看一下它的代码:
final static class BitmapState extends ConstantState {
Bitmap mBitmap;
int mChangingConfigurations;
int mGravity =Gravity.FILL;
Paint mPaint =new Paint(DEFAULT_PAINT_FLAGS);
Shader.TileModemTileModeX = null;
Shader.TileMode mTileModeY= null;
int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
booleanmRebuildShader;
BitmapState(Bitmap bitmap) {
mBitmap =bitmap;
}
省略部分代码...
}
BitmapState中有一个Bitmap属性引用调用构造器时传入的Bitmap,另外还有一个属性mTargetDensity =DisplayMetrics.DENSITY_DEFAULT;即默认时,mTargetDensity的值为160。(DisplayMetrics类中定义了若干表示手机屏幕密度的常量值,其中DENSITY_DEFAULT的值为160)
接下来看一下重载版本的构造器,该构造器是private的
private BitmapDrawable(BitmapState state, Resources res) {
mBitmapState =state;
if (res != null){
mTargetDensity = res.getDisplayMetrics().densityDpi;
} else {
mTargetDensity = state.mTargetDensity;
}
setBitmap(state!= null ? state.mBitmap : null);
}
构造器首先给BitmapDrawable的mBitmapState属性赋值。赋的值就是利用Bitmap对象包装而成的那个BitmapState。接下来给mTargetDensity属性赋值,根据res是否为null,会赋予不同的值。此时因为第二个参数res是null,所以会将BitmapDrawable的mTargetDensity的属性值设置为BitmapState对象的mTargetDensity属性值,而之前已经说过BitmapState对象的mTargetDensity属性值为160,所以BitmapDrawable的mTargetDensity属性值也为160。
然后在这个构造器中会去调用setBitmap方法:
private void setBitmap(Bitmapbitmap) {
if (bitmap !=mBitmap) {
mBitmap =bitmap;
if (bitmap !=null) {
computeBitmapSize();
} else {
mBitmapWidth = mBitmapHeight = -1;
}
invalidateSelf();
}
}
如果传入的参数为null,则mBitmapWidth和mBitmapHeight这两个BitmapDrawable的值为-1;如果传入的参数不为null,则调用computeBitmapSize()方法计算尺寸。这个方法不用传入参数,所以计算是根据BitmapDrawable的mBitmap属性来展开的。
private void computeBitmapSize() {
mBitmapWidth =mBitmap.getScaledWidth(mTargetDensity);
mBitmapHeight =mBitmap.getScaledHeight(mTargetDensity);
}
方法很简单,调用Bitmap的getScaledWidth和getScaledHeight两个方法分别为BitmapDrawable的mBitmapWidth和mBitmapHeight两个属性赋值。需要注意的是,方法的参数使用的是BitmapDrawable的mTargetDensity,当前这个值为160。
/**
* Convenience methodthat returns the width of this bitmap divided
* by the densityscale factor.
*
* @paramtargetDensity The density of the target canvas of the bitmap.
* @return The scaledwidth of this bitmap, according to the density scale factor.
*/
public intgetScaledWidth(int targetDensity) {
return scaleFromDensity(getWidth(), mDensity, targetDensity);
}
根据方法的说明就可以知道,getScaledWidth方法的就是将调用该方法的那个Bitmap对象的宽度除以一个缩放系数后,得到一个新的值来返回。具体的计算过程在scaleFromDensity(getWidth(), mDensity, targetDensity)方法中完成。
这里三个参数分别是调用者Bitmap对象的宽度,Bitmap对象的mDensity属性值和传入的targetDensity即160。
mDensity是Bitmap对象的一个属性,mDensity的属性值是:
int mDensity = sDefaultDensity = getDefaultDensity();
private static volatile int sDefaultDensity = -1;
static int getDefaultDensity() {
if(sDefaultDensity >= 0) {
return sDefaultDensity;
}
sDefaultDensity =DisplayMetrics.DENSITY_DEVICE;
return sDefaultDensity;
}
初始时,sDefaultDensity的值等于-1,所以在getDefaultDensity方法中会让sDefaultDensity的值为DisplayMetrics.DENSITY_DEVICE;也就是当前设备的屏幕密度值。然后sDefaultDensity再将值赋给mDensity属性。我们现在程序的运行环境手机屏幕密度是240,所以mDensity的值就是240。
所以,在调用scaleFromDensity方法时,三个参数的数值将是图片的宽度,当前设备的屏幕密度240和160。
static public int scaleFromDensity(int size, int sdensity, inttdensity) {
if (sdensity ==DENSITY_NONE || sdensity == tdensity) {
return size;
}
// Scale bytdensity / sdensity, rounding up.
return ((size *tdensity) + (sdensity >> 1)) / sdensity;
}
如果,当前屏幕的分辨率(即第二个参数的值)恰好也等于160,那么图片的宽度将不做任何处理直接返回。如果当前屏幕的分辨率不是160,那么就要计算出一个缩放参数,缩放参数为(160/屏幕实际密度)。我们当前屏幕的实际密度是240,那么此时缩放系数就是0.75。然后用图片的实际宽度*缩放系数+0.5,即对实际宽度*缩放系数得到的值四舍五入。图片的实际宽度是510,经过计算后得到的mBitmapWidth的值是510*0.75,四舍五入为383。
getScaledHeight方法与getScaledWidth方法的方法体是一样的,唯独的却别是这一次传入的是图片的高度值而不是宽度值,计算后得到的结果是380*0.75,四舍五入为285。
通过调用setBitmap方法之后,我们得到了经过缩放参数计算后的图像宽度值和高度值,分别存放在mBitmapWidth属性和mBitmapHeight属性中。
再回头看一下BitmapDrawable(Resources res, Bitmap bitmap) 构造器,随着this(new BitmapState(bitmap), res);执行完毕,构造器还要反过来为mBitmapState的mTargetDensity赋值,值是BitmapDrawable的mTargetDensity的属性值。
public BitmapDrawable(Resources res, Bitmap bitmap) {
this(newBitmapState(bitmap), res);
mBitmapState.mTargetDensity = mTargetDensity;
}
至此,通过调用createFromPath方法,从一个存储在外部存储的文件中得到一个Drawable的过程就完全结束了。此时的Drawable对象是一个BitmapDrawable对象。所以调用该Drawable对象的getIntrinsicWidth和getIntrinsicHeight时调用的并不是Drawable的这两个方法而是BitmapDrawable的这两个方法。这两个方法在BitmapDrawable中进行了重写:
public int getIntrinsicWidth() {
return mBitmapWidth;
}
public int getIntrinsicHeight() {
return mBitmapHeight;
}
可以看到getIntrinsicWidth和getIntrinsicHeight返回的是mBitmapWidth和mBitmapHeight,而这两个值是Bitmap图像经过了缩放系数处理后得到的值。
现在,不通过createFromPath方法来创建Drawable对象,而是直接利用构造器来创建,即:
Drawable d2 = new BitmapDrawable(getResource(),bitmap);
此时构造器的res参数不为null了,那么执行过程与使用createFromPath是不一样的。
看一下重载版本的BitmapDrawable构造器:
privateBitmapDrawable(BitmapState state, Resources res) {
mBitmapState =state;
if (res != null){
mTargetDensity = res.getDisplayMetrics().densityDpi;
} else {
mTargetDensity = state.mTargetDensity;
}
setBitmap(state!= null ? state.mBitmap : null);
}
此时res不为null了,mTargetDensity的值将是DisplayMetrics().densityDpi。densityDpi会取DENSITY_LOW(120),ENSITY_MEDIUM(160)或DENSITY_HIGH(240)中的一个最接近用户手机实际屏幕密度的值。我们的程序获得的densityDpi的值就是240,这个值恰好也是我模拟器屏幕的密度。
此时再去执行setBitmap的时候,在computeBitmapSize时调用scaleFromDensity的时候,此时第二个参数sDensity(来自mDensity属性)和第三个参数tDensity(来自BitmapDrawable的mTargetDensity)的值是一样的,它们的值都是240。这时Bitmap就不会执行缩放操作了,这也就意味着BitmapDrawable的mBitmapWidth和mBitmapHeight的属性值就是Bitmap对象的width和height。这样再去调用Drawable的getIntrinsicWidth和getIntrinsicHeight得到的宽度和高度值就是与Bitmap就是一致的。