您的位置:首页 > 其它

利用不同的方法将同一个Bitmap转为Drawable时,两个Drawable的高度和宽度不一致的原因分析

2015-07-07 14:48 615 查看
问题描述:在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就是一致的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: