关闭

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

712人阅读 评论(0) 收藏 举报

问题描述:在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就是一致的。

 

 

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:71528次
    • 积分:1432
    • 等级:
    • 排名:千里之外
    • 原创:58篇
    • 转载:84篇
    • 译文:0篇
    • 评论:16条
    文章分类
    最新评论