Android中Drawable适配介绍

        Android可以运行在各种不同屏幕大小和密度的设备上,并且为不同的设备提供一致的开发环境。在不同设备上显示时,Android可以调整应用的UI悴适配不同的屏幕。此外,Android也提供了一些API可以针对不同的设备屏幕大小和密度来控制UI的显示。虽然Android可以针对不同的APP进行缩放和调整以适配不同的屏幕,但是我们仍然需要针对不同的屏幕大小和密度在资源和代码上进行优化来提升用户体验,而针对开者来说主要是layout和drawable文件目录下的文件,这里主要是介绍一下drawable的适配。

1. 基础知识

屏幕大小

  设备屏幕的物理尺寸,是指屏幕的对角线长度,通常以inch为单位,1inch = 2.54 cm。

分辨率

  屏幕物理像素的总数,一般以横向像素×纵向像素表示分辨率。一般对设备屏幕的适配并不以分辨率为准,而是按照Android确定的广义的屏幕的大小和分辨率组为标准来适配不同的资源的。

屏幕密度(dpi)

  屏幕上一个物理区域的像素数量,通常称为dpi(dotsper inch)。Android根据不同的dpi将Android设备分成多个显示级别,具体如下:

A set of six generalized densities(6类广义密度):

· ldpi (low) ~120dpi

·mdpi(medium) ~160dpi

·hdpi(high) ~240dpi

·xhdpi(extra-high) ~320dpi

·xxhdpi(extra-extra-high) ~480dpi

·xxxhdpi(extra-extra-extra-high) ~640dpi


Fig.1 Android如何将实际的大小和密度映射为广义的大小和密度

       不同于屏幕尺寸和屏幕分辨率,这两个值是我们可以直接得到的,而屏幕密度则需要通过计算来得到。如下图,对于一个分辨率为1200*1920,屏幕尺寸为7inch的设备来说,其实际屏幕密度按如下来计算:


根据勾股定理,计算出斜对角线的像素个数为:2264,再用2264除以7即得屏幕密度为323。

    实际屏幕密度就是我们自己算出来的密度,这个密度代表了屏幕真实的细腻程度,如上述例子中的323dpi就是实际屏幕密度,说明这块屏幕每寸有323个像素。 7英寸1200×1920屏幕的实际屏幕密度是323,5英寸1200×1920屏幕的实际屏幕密度是452,而相同分辨率的4.5英寸屏幕的实际屏幕密度是503。因此,屏幕实际密度将会出现很多数值,呈现严重的碎片化。而密度又是安卓屏幕将界面进行缩放显示的依据,那么安卓是如何适配这么多屏幕的呢? 其实,每部安卓手机屏幕都有一个初始的固定密度,这些数值是120、160、240、320、480等,这些就是android为不同设备设定的系统密度。得到实际密度以后,一般会选择一个最近的密度作为系统密度,系统密度是出厂预置的,如440dpi的系统密度就是和它最接近的480dpi;如果是330dpi的设备,它的系统密度就是320dpi。但是,现在很多手机不一定会选择这些值作为系统密度,而是选择实际的dpi作为系统密度,这就导致了很多手机的dpi也不是在这些值内。

屏幕方向

   以用户视角来看,分为水平(landscape)和垂直(portrait)两个方向

密度无关的像素(dp, Density-independent)

   一种虚拟的像素,使用这个单位后,在不同屏幕密度上显示的大小相同。在Android中,将屏幕密度为160dpi的中密度设备屏幕作为基准屏幕,那么dp与px的换算公式为:px =dp * (dpi / 160),那么不同屏幕密度的设备按照比例进行换算,结果如下表所示:


Tab.1 不同dpi与基准屏幕的比例关系

  根据上表,从px到dp的角度来说明一下这种比例关系:比如设计APP的icon,为了让App的icon在不同的屏幕密度设备显示相同,就必须要icon在屏幕中占据相同的dp。那么对于不同的屏幕密度(MDPI、HDPI、XHDPI、XXHDPI和XXXHDPI)应按照2:3:4:6:8 的比例进行缩放。比如说一个icon的尺寸为48x48dp,这表示在 MDPI的屏幕上其实际尺寸应为48x48px,在 HDPI的屏幕上其实际大小是 MDPI的 1.5 倍 (72x72 px),在 XDPI的屏幕上其实际大小是 MDPI的 2 倍(96x96 px),依此类推。

比例无关的像素(sp, scale-independent pixels)

  与dp类似,主要用于字体显示,可以在设置里面调节字号的时候,文字会随之改变。当安卓系统字号设为“普通”时,sp与px的尺寸换算和dp与px是一样的。

2. 获取设备的上述属性

        可以用如下代码来获得上述属性的值,代码如下:

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

       setContentView(R.layout.activity_main);

       DisplayMetrics displayMetrics = getResources().getDisplayMetrics();

       float density =displayMetrics.density;//屏幕的逻辑密度

       int densityDpi= displayMetrics.densityDpi;//屏幕密度dpi

       //屏幕高度的像素

       int heightPixels = displayMetrics.heightPixels;

       //屏幕宽度的像素

       int widthPixels= displayMetrics.widthPixels;

       //字体的放大系数

       floatscaledDensity = displayMetrics.scaledDensity;       float xdpi = displayMetrics.xdpi;//宽度方向上的dpi

       float ydpi =displayMetrics.ydpi;//高度方向上的dpi

       Log.i(TAG,"density= " + density);

       Log.i(TAG,"densityDpi= " + densityDpi);

       Log.i(TAG,"scaledDensity= " + scaledDensity);

       Log.i(TAG,"Screenresolution = " + widthPixels +"×" +heightPixels);

       Log.i(TAG,"xdpi= " + xdpi);

       Log.i(TAG,"ydpi= " + ydpi);

}

以letv S2为测试设备,显示结果如下:


如上的测试结果中,大家可能对density比较疑惑,我们可以从代码里看下针对denisity的计算:

//DisplayInfo.java

private void getMetricsWithSize(DisplayMetrics outMetrics, CompatibilityInfo compatInfo,

            Configuration configuration, intwidth, int height) {

            outMetrics.densityDpi= outMetrics.noncompatDensityDpi = logicalDensityDpi(480);

            outMetrics.density= outMetrics.noncompatDensity =

            //DisplayMetrics.DENSITY_DEFAULT_SCALE = 1/160

           logicalDensityDpi* DisplayMetrics.DENSITY_DEFAULT_SCALE;

        outMetrics.scaledDensity= outMetrics.noncompatScaledDensity =outMetrics.density;

        ……

}

因此density其实是相对于基准(160dpi)的比例,即Tab.1中的0.75,1,1.5,2,3,4。

3. 为什么要做drawable适配

        为不同大小和密度的屏幕适配不同的drawable,除了为提升用户体验外,还有另外一个目的,那就是节省内存。

        首先从用户体验上来说。如果APP需要适配多种屏幕密度且部分UI元素需要保持大小,那么针对这些UI元素则需要设计成密度无关。如下两副图分别展示了是否将UI元素设计成密度无关的效果。


Fig.2 不支持密度无关,在low, medium和high-density屏幕上显示的效果


Fig.3 支持密度无关,在low, medium和high-density屏幕上显示的效果

  可以在布局中将那些密度无关的UI元素的大小设为以dp为单位或直接指定为 wrap_content。针对图片,由于伸缩会导致模糊或pixelated bitmaps,因此需要针对不同屏幕密度的设备提供不同分辨率的图片。

  其次,从节省内存方面来说(这里的内存是指RAM)。为了验证这个其真实性,我们通过实验来证实。实验中选择一个大小为540*490的图片test.png并将S2作为测试设备,采用如下测试代码:

//布局文件

<FrameLayoutxmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:paddingBottom="@dimen/activity_vertical_margin"

    android:paddingLeft="@dimen/activity_horizontal_margin"

    android:paddingRight="@dimen/activity_horizontal_margin"

    android:paddingTop="@dimen/activity_vertical_margin"

    tools:context=".MainActivity">

 

    <ImageView

        android:id="@+id/imageView"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:src="@drawable/test"/>

 

    <TextView

        android:id="@+id/text"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:textSize="30dp"/>

FrameLayout>

//测试代码

mImageView = (ImageView)findViewById(R.id.imageView);

mTextView = (TextView)findViewById(R.id.text);

mImageView.post(new Runnable() {

@Override

    publicvoid run() {

        BitmapDrawable drawable = (BitmapDrawable)mImageView.getDrawable();

        StringBuildersb =newStringBuilder(300);       

        Bitmap bitmap = drawable.getBitmap();

       sb.append("bitmap_width="+bitmap.getWidth()+",bitmap_height="+bitmap.getHeight()).append("\n")

            .append("bitmap_size="+bitmap.getByteCount()).append("\n")

            .append("imageview_width="+mImageView.getWidth()+",imageview_heigth="+mImageView.getHeight()).append("\n")

            .append("imageview_scaletype="+mImageView.getScaleType());

      mTextView.setText(builder.toString());

    }

});

        由于Letv S2的屏幕密度为480,对于到Tab.1中刚好为xxhdpi,因此分别将test.png放在drawable-xxhdip\drawable-xhdpi\drawable-hdpi\

drawable-mdpi\drawable-ldpi,并记录下测试结果如下表:


Tab.2 将test.png放在不同屏幕密度文件夹下的测试结果

      从Tab.2中可以看出,在同一手机上可以得出如下结论:

  • 同一张图片,放在不同目录下,会生成不同大小的bitmap。bitmap的长度和宽度越大,占用的内存就越大。
  • 同一张图片,放在不同的drawable目录下(从drawable-lpdi到drawable-xxhpdi)在同一手机上占用的内存越来越小。
  • 图片在硬盘上占用的大小,与在内存中占用的大小完全不一样。

     现在从代码角度来分析上述三个结论的原因:在ImageView.java的构造函数中设置ImageView的Drawable的代码如下:

Drawable d =a.getDrawable(com.android.internal.R.styleable.ImageView_src);

if (d != null) {

   setImageDrawable(d);

}

我们通过跟踪a.getDrawable()最终到达BitmapFactory.decodeResou rceStream()函数中,在该函数中打印RuntimeException,我们可以详细确定BitmapFactory.DecodeResourceStream()的详细调用流程如下:

04-0708:22:24.311  6632  6632 D decodeResourceStream:value.density=480, opts.inDensity=0,opts.inTargetDensity=0

04-0708:22:24.311  6632  6632 D decodeResourceStream:java.lang.RuntimeException

04-0708:22:24.311  6632  6632 D decodeResourceStream:    atandroid.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:426)

04-0708:22:24.311  6632  6632 D decodeResourceStream:    atandroid.graphics.drawable.Drawable.createFromResourceStream(Drawable.java:1080)

04-0708:22:24.311  6632  6632 D decodeResourceStream:    atandroid.content.res.Resources.loadDrawableForCookie(Resources.java:2637)

04-0708:22:24.311  6632  6632 D decodeResourceStream:    atandroid.content.res.Resources.loadDrawable(Resources.java:2542)

04-0708:22:24.311  6632  6632 D decodeResourceStream:    atandroid.content.res.TypedArray.getDrawable(TypedArray.java:870)

04-0708:22:24.311  6632  6632 D decodeResourceStream:    atandroid.widget.ImageView.(ImageView.java:152)

04-0708:22:24.311  6632  6632 D decodeResourceStream:    atandroid.widget.ImageView.(ImageView.java:140)

04-0708:22:24.311  6632  6632 D decodeResourceStream:    atandroid.widget.ImageView.(ImageView.java:136)

getDrawable()通过decodeResourceStream()获得图片资源的Bitmap,并通过该Bitmap构造BitmapDrawable最终返回Drawable对象。通过对代码的分析,我们发现对drawable的适配是在decodeResourceStream()完成的,具体代码如下:

//BitmapFactory.java

public static Bitmap decodeResourceStream(Resources res, TypedValue value,

            InputStream is, Rect pad, Options 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; //The pixel density to use for the bitmap(drawable-)

       }

     }

     if(opts.inTargetDensity == 0 && res != null) {

            opts.inTargetDensity =res.getDisplayMetrics().densityDpi; //480

     }


     returndecodeStream(is, pad, opts);//开始解码并返回Bitmap

}

可就看到该方法主要对opts进行赋值,结果是opts.inDenisit=480,opts.inTargetDenisity=480,然后调用decodeStream()进行解码,接工会来分析decodeStream()的代码:

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {

    // we don't throw in this case, thusallowing the caller to only check

    // the cache, and not force the image to bedecoded.

    if (is == null) {

        return null;

    }


    Bitmap bm = null;


    Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS,"decodeBitmap");

    try {

        if (is instanceofAssetManager.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 newIllegalArgumentException("Problem decoding into existing bitmap");

        }


        setDensityFromOptions(bm,opts);

    } finally {

       Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);

    }


    return bm;

}

drawable中的bitmap资源是通过decodeStreamInternal()函数进行解析的,该函数最终调用native函数doDecode()进行解码,其代码如下:

//BitmapFactory.cpp

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobjectoptions) {

    ……

    if(env->GetBooleanField(options, gOptions_scaledFieldID)) {

        // The pixel density to use for the bitmap(drawable-?对应的bitmap密度)

        const int density =env->GetIntField(options, gOptions_densityFieldID);

        //获得tagetDensit(480)

        const int targetDensity = env->GetIntField(options,gOptions_targetDensityFieldID);

        const int screenDensity =env->GetIntField(options, gOptions_screenDensityFieldID);

        if (density != 0 && targetDensity!= 0 && density != screenDensity) {

             scale = (float)targetDensity / density; //求出bitmap的缩放倍数

        }

    }

    ……

    //解码出来的bitmap的原始宽度和高度

    int scaledWidth =decodingBitmap.width();

    int scaledHeight = decodingBitmap.height();


    if(willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {

        scaledWidth = int(scaledWidth* scale + 0.5f);//缩放后的宽

        scaledHeight = int(scaledHeight * scale+ 0.5f);//绽放后的高

    }

    ……

    if(willScale) {

      const float sx = scaledWidth / float(decodingBitmap.width());//重新计算宽度的绽放倍数

         const float sy = scaledHeight /float(decodingBitmap.height());//重新计算高度的绽放倍数

    ……

       canvas.scale(sx, sy);//缩放画布

      canvas.drawARGB(0x00, 0x00, 0x00, 0x00);

      canvas.drawBitmap(decodingBitmap, 0.0f,0.0f, &paint);//画出图像

    }

}

        代码中的density和targetDensity均是通过JNI获取,前者是opts.inDensity,targetDensity实际上是opts.inTargetDensity也就是DisplayMetrics 的densityDpi。由此可见,将图片放在不同的drawable目录工会导致其所占用的内存大小不一的原因是由于在绘制drawable时将其绽放了scale倍。

        图片的像素、分辨率和尺寸的关系是:分辨率 =像素 ÷尺寸,在对图片进行缩放的时候,分辨率是不变的,变化的是像素和尺寸。如果将图片放大,就会有白色像素插值填充到原有像素中,从而导致图片的清晰度下降;而缩小图片时,会通过对图片像素进行重采样生成缩略图,将会增加图片的平滑度和清晰度。从上边的代码中的我们也可以得出如下结论:scale = (float) targetDensity / density;

  •  如果APP只适配固定屏幕密度的设备,那么将bitmap资源放在对应的drawable目录下其所占用内存是最小的,否则会导致图片缩放,从而增加计算量或导致图片模糊。
  • 如果APP需要适配多种屏幕密度,对于每个屏幕密度做出一套bitmap并放置于相应的drawable目录下是最好的;否则,建议做一套xxhdpi的bitmap放置于drawable-xxhdpi目录下,虽然这样会增加APP的大小,但是对于用户体验来说,不会导致bitmap模糊。

4. Android提供的配置限定符

        Android为系统适配资源提供了一些配置限定符,配置限定符是一个字符串,可以添加到资源目录的命名中,从而指定该目录中的资源为可以为哪种屏幕配置使用。使用方式如下:

  •  -,其中resources_name是标准的资源名称,如layout 或drawable,qualifier的说明见Tab.3。


Tab.3 不同屏幕配置的配置限定符及其说明

        从Android 3.2开始后,Android为平版电脑引入了一种新资源限定符:通过指定资源需要的最小的空间,如sw600dp,当屏幕的可用宽度最小为600dp时才可使用该资源。


Tab.4 新的资源配置限定符(Android 3.2)

5. 最佳实践

  •  对于APP的layout需要考虑:
  1. 是否适合小屏幕
  2. 是否针对大屏幕进行优化以充分利用大屏幕的空间
  3. 是否针对横屏或竖屏进行优化

     而对于bitmap需要考虑:

  1. 是否需要针对不同的广义屏幕密度提供不同的bitmap文件(.png/.jpg/.gif/.9.png)
  2. 在定义的6种屏幕密度中,按3:4:6:8:12:16对bitmap进行缩放。
  • 定义layout尺寸时,多使用wrap_content/match_parent或dp单位,因为可以确保在当前设备屏幕上给view一个合适的大小。对于文本的大小,建议使用sp作为单位,可以保证文本的大小随着系统的设置而变化。
  • 不要在代码中使用硬编码的像素值。出于性能和代码简洁性的原因,Android中在表示大小和坐标位置时都使用像素作为标准单位,这也就意味着view的大小基于当前设备屏幕密度计算出来并采用像素作为单位来表示的。因此,如果在代码中使用了硬编码的像素值,这势必缺乏对不同设备屏幕密度的考虑。
  • 不要使用AbsoluteLayout,建议使用RelativeLayout替代。在AbsoluteLayout中,强制使用固定的位置来而已子view,这会影响APPUI在不同设备屏幕上用户体验,这也就是AbsoluteLayout在Android1.5(API Level 3)被丢弃的原因。
  • 使用大小和指定密度的资源。如果想精确的控制APPUI在不同屏幕上的显示,可以调整指定配置的资源目录中的layout和drawables。

6. 参考文献

1.    https://developer.android.com/guide/practices/screens_support.html#DeclaringTabletLayouts

2.    http://blog.csdn.net/wrg_20100512/article/details/51295317

 

更多相关文章

  1. Android(安卓)屏幕元素层次结构
  2. Android软键盘弹出,界面整体上移的问题
  3. Android图片的固定大小显示
  4. android ICS4.0.3 改变默认字体大小
  5. Android(安卓)设定横屏,禁止屏幕旋转,Activity重置
  6. android用DroidDraw实现可视化UI编程
  7. Android屏幕保持常亮的三种方法
  8. Android(安卓)设定横竖屏,屏幕旋转导致Activity重置问题
  9. Android(安卓)如何从屏幕底部向上滑出一个view

随机推荐

  1. android 4.2 源码在64位Ubuntu编译
  2. android布局中容易混淆的几个属性
  3. Android学习一:Hello World
  4. 《Android第一行代码》first reading 十
  5. android aidl iBinder理解
  6. [置顶] Android之ContextMenu的使用方法
  7. Android之网络通信·Web通讯
  8. android Q open failed: EACCES (Permiss
  9. [Android] 开发资料收集:多媒体开发
  10. android 动画详解(一)