Android drawable微技艺,你所不知道的drawable的那些细节

Android drawable微技巧,你所不知道的drawable的那些细节

转载请注明出处:http://blog.****.net/guolin_blog/article/details/50727753
好像有挺久时间没更新博客了,最近我为了准备下一个系列的博客,也是花了很长的时间研读源码。很遗憾的是,下一个系列的博客我可能还要再过一段时间才能写出来,那么为了不至于让大家等太久,今天就给大家更新一篇单篇的文章,讲一讲Android drawable方面的微技巧。


话说微技巧这个词也是我自己发明的,因为drawable这个东西相信大家天天都在使用,每个人都再熟悉不过了,之所以叫微技巧就是对于这个我们再熟悉不过的技术,可能还有一些你所不知道的细节,那今天我们就来一起探究一下这些微小的细节吧。
大家都知道,在Android项目当中,drawable文件夹都是用来放置图片资源的,不管是jpg、png、还是9.png,都可以放在这里。除此之外,还有像selector这样的xml文件也是可以放在drawable文件夹下面的。
但是如果你现在使用Android Studio来新建一个项目,你会发现有如下的目录结构:

Android drawable微技艺,你所不知道的drawable的那些细节

嗯?怎么会有这么多mipmap开头的文件夹,而且它们的命名规则和drawable文件夹很相似,也是hdpi、mdpi、xhdpi等等,并且里面还真是放的图片,难道Android项目中放置图片的位置已经改了?
对于刚刚从Eclipse转向Android Studio的开发者们可能会对mipmap文件夹感到陌生,其实不用担心,我们平时的编程习惯并不需要发生任何改变,因为mipmap文件夹只是用来放置应用程序的icon的,仅此而已。那么在此之前,我们都是把应用程序的icon图标和普通的图片资源一起放到drawable文件夹下的,这样看上去就会比较杂乱,有的时候想从一堆的图片资源里面找icon半天也找不到,而文件一多也就容易出现漏放的情况,但恰恰Android是极度建议我们在每一种分辨率的文件夹下面都放一个相应尺寸的icon的,因此将它们独立出来专门放到mimap文件夹当中就很好地解决了这个问题。
另外,将icon放置在mipmap文件夹还可以让我们程序的launcher图标自动拥有跨设备密度展示的能力,比如说一台屏幕密度是xxhdpi的设备可以自动加载mipmap-xxxhdpi下的icon来作为应用程序的launcher图标,这样图标看上去就会更加细腻。
关于建议使用mipmap的原文可以参阅这篇文章:Getting Your Apps Ready for Nexus 6 and Nexus 9, 当然你还是要科学上网的。
除此之外,对于每种密度下的icon应该设计成什么尺寸其实Android也是给出了最佳建议,icon的尺寸最好不要随意设计,因为过低的分辨率会造成图标模糊,而过高的分辨率只会徒增APK大小。建议尺寸如下表所示:

密度 建议尺寸
mipmap-mdpi 48 * 48
mipmap-hdpi 72 * 72
mipmap-xhdpi 96 * 96
mipmap-xxhdpi 144 * 144
mipmap-xxxhdpi 192 * 192

然后我们引用mipmap的方式和之前引用drawable的方式是完全一致的,在资源中就使用@mipmap/res_id,在代码就使用R.mipmap.res_id。比如AndroidManifest.xml中就是这样引用ic_launcher图标的:

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
    </activity>
</application>

好的,关于mimap的内容就讲这么多,它并不是本篇文章的重点,接下来我们来真真正正看一些drawable的微技巧。


首先我准备了一张270*480像素的图片:

Android drawable微技艺,你所不知道的drawable的那些细节

将图片命名为android_logo.png,然后把它放在drawable-xxhdpi文件夹下面。为什么要放在这个文件夹下呢?是因为我的手机屏幕的密度就是xxhdpi的。那么怎么才能知道自己手机屏幕的密度呢?你可以使用如下方法先获取到屏幕的dpi值:

float xdpi = getResources().getDisplayMetrics().xdpi;
float ydpi = getResources().getDisplayMetrics().ydpi;

其中xdpi代表屏幕宽度的dpi值,ydpi代表屏幕高度的dpi值,通常这两个值都是近乎相等或者极其接近的,在我的手机上这两个值都约等于403。那么403又代表着什么意思呢?我们直接参考下面这个表格就知道了:

dpi范围 密度
0dpi ~ 120dpi ldpi
120dpi ~ 160dpi mdpi
160dpi ~ 240dpi hdpi
240dpi ~ 320dpi xhdpi
320dpi ~ 480dpi xxhdpi
480dpi ~ 640dpi xxxhdpi

从表中可以看出,403dpi是处于320dpi到480dpi之间的,因此属于xxhdpi的范围。
图片放好了之后,下面我在布局文件中引用这张图片,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
   >

    <ImageView
        android:id="@+id/image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/android_logo"
        />

</LinearLayout>

在ImageView控件中指定加载android_logo这张图,并把ImageView控件的宽高都设置成wrap_content,这样图片有多大,我们的控件就会有多大。
现在运行一下程序,效果如下所示:

Android drawable微技艺,你所不知道的drawable的那些细节

由于我的手机分辨率是1080*1920像素的,而这张图片的分辨率是270*480像素的,刚好是手机分辨率的四分之一,因此从上图中也可以看出,android_logo图片的宽和高大概都占据了屏幕宽高的四分之一左右,大小基本是比较精准的。
到目前为止一切都挺顺利的,不是吗?下面我们尝试做点改变,将android_logo.png这张图移动到drawable-xhdpi文件夹下,注意不是复制一份到drawable-xhdpi文件夹下,而是将图片移动到drawable-xhdpi文件夹下,然后重新运行一下程序,效果如下图所示:
Android drawable微技艺,你所不知道的drawable的那些细节

嗯?怎么感觉图片好像变大了一点,是错觉吗?
那么我们再将这张图移动到drawable-mdpi文件夹下试试,重新运行程序,效果如下图所示:
Android drawable微技艺,你所不知道的drawable的那些细节

这次肯定不是错觉了,这实在是太明显了,图片被放大了!
那么为什么好端端的一张图片会被自动放大呢?而且这放大的比例是不是有点太过份了。其实不然,Android所做的这些缩放操作都是有它严格的规定和算法的。可能有不少做了很多年Android的朋友都没去留意过这些缩放的规则,因为这些细节太微小了,那么本篇的微技巧探索里面,我们就来把这些细节理理清楚。
首先解释一下图片为什么会被放大,当我们使用资源id来去引用一张图片时,Android会使用一些规则来去帮我们匹配最适合的图片。什么叫最适合的图片?比如我的手机屏幕密度是xxhdpi,那么drawable-xxhdpi文件夹下的图片就是最适合的图片。因此,当我引用android_logo这张图时,如果drawable-xxhdpi文件夹下有这张图就会优先被使用,在这种情况下,图片是不会被缩放的。但是,如果drawable-xxhdpi文件夹下没有这张图时, 系统就会自动去其它文件夹下找这张图了,优先会去更高密度的文件夹下找这张图片,我们当前的场景就是drawable-xxxhdpi文件夹,然后发现这里也没有android_logo这张图,接下来会尝试再找更高密度的文件夹,发现没有更高密度的了,这个时候会去drawable-nodpi文件夹找这张图,发现也没有,那么就会去更低密度的文件夹下面找,依次是drawable-xhdpi -> drawable-hdpi -> drawable-mdpi -> drawable-ldpi。
总体匹配规则就是这样,那么比如说现在终于在drawable-mdpi文件夹下面找到android_logo这张图了,但是系统会认为你这张图是专门为低密度的设备所设计的,如果直接将这张图在当前的高密度设备上使用就有可能会出现像素过低的情况,于是系统自动帮我们做了这样一个放大操作。
那么同样的道理,如果系统是在drawable-xxxhdpi文件夹下面找到这张图的话,它会认为这张图是为更高密度的设备所设计的,如果直接将这张图在当前设备上使用就有可能会出现像素过高的情况,于是会自动帮我们做一个缩小的操作。所以,我们可以尝试将android_logo这张图移动到drawable-xxxhdpi文件夹下面将会得到这样的结果:
Android drawable微技艺,你所不知道的drawable的那些细节

可以看到,现在图片的宽和高都达到不手机屏幕的四分之一,说明图片确实是被缩小了。
另外,刚才在介绍规则的时候提到了一个drawable-nodpi文件夹,这个文件夹是一个密度无关的文件夹,放在这里的图片系统就不会对它进行自动缩放,原图片是多大就会实际展示多大。但是要注意一个加载的顺序,drawable-nodpi文件夹是在匹配密度文件夹和更高密度文件夹都找不到的情况下才会去这里查找图片的,因此放在drawable-nodpi文件夹里的图片通常情况下不建议再放到别的文件夹里面。
图片被放大的原因现在我们已经搞清楚了,那么接下来还有一个问题,就是放大的倍数是怎么确定的呢?很遗憾,我没有找到相关的文档记载,但是我自己总结出了一个规律,这里跟大家分享一下。
还是看一下刚才的 dpi范围-密度 表格:

dpi范围 密度
0dpi ~ 120dpi ldpi
120dpi ~ 160dpi mdpi
160dpi ~ 240dpi hdpi
240dpi ~ 320dpi xhdpi
320dpi ~ 480dpi xxhdpi
480dpi ~ 640dpi xxxhdpi

可以看到,每一种密度的dpi范围都有一个最大值,这个最大值之间的比例就是图片会被系统自动放大的比例。
口说无凭,下面我们来通过实例验证一下,修改布局文件中的代码,如下所示:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
   >

    <ImageView
        android:id="@+id/image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/android_logo"
        />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="获取图片宽高"
        android:onClick="buttonClick"
        />

</LinearLayout>

可以看到,我们添加了一个按钮,并给按钮注册了一个点击事件。然后在MainActivity中处理这个点击事件:

public class MainActivity extends AppCompatActivity {

    ImageView imageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        imageView = (ImageView) findViewById(R.id.image);
    }


    public void buttonClick(View view) {
        Toast.makeText(this, "图片宽度:" + imageView.getWidth(), Toast.LENGTH_SHORT).show();
        Toast.makeText(this, "图片高度:" + imageView.getHeight(), Toast.LENGTH_SHORT).show();
    }
}

这里在点击事件中分别获取图片的宽和高并使用Toast提示出来。代码修改这么多就可以了,然后将图片移动到drawable-mdpi文件夹下。
下面我们来开始分析,mdpi密度的最高dpi值是160,而xxhdpi密度的最高dpi值是480,因此是一个3倍的关系,那么我们就可以猜测,放到drawable-mdpi文件夹下的图片在xxhdpi密度的设备上显示会被放大3倍。对应到android_logo这张图,原始像素是270*480,放大3倍之后就应该是810*1440像素。下面运行程序,效果如下图所示:

Android drawable微技艺,你所不知道的drawable的那些细节

验证通过。我们再来试验一次,将图片移动到drawable-xxxhdpi目录下。xxxhdpi密度的最高dpi值是640,480是它的0.75倍,那么我们就可以猜测,放到drawable-xxxdpi文件夹下的图片在xxhdpi密度的设备上显示会被缩小至0.75倍。270*480的0.75倍应该是202.5*360,由于像素不支持小数点,那么四舍五入就应该是203*360像素。重新运行程序,效果如下图所示:
Android drawable微技艺,你所不知道的drawable的那些细节

再次验证通过。如果你有兴趣的话可以使用其它几种dpi的drawable文件夹来试一试,应该都是适配这套缩放规则的。这样我们就把图片为什么会被缩放,以及具体的缩放倍数都搞明白了,drawable相关的细节你已经探究的非常细微了。
不过本篇文章到这里还没结束,下面我准备讲一讲我们在实际开发当中会遇到的场景。根据Android的开发建议,我们在准备图片资源时尽量应该给每种密度的设备都准备一套,这样程序的适配性就可以达到最好。但实际情况是,公司的UI们通常就只会给一套图片资源,想让他们针对每种密度的设备都设计一套图片资源,并且还是按照我们上面讲的缩放比例规则来设计,就有点想得太开心了。没错,这个就是现实情况,那么在这种情况下,我们应该将仅有的这一套图片资源放在哪个密度的文件夹下呢?
可以这样来分析,根据我们刚才所学的内容,如果将一张图片放在低密度文件夹下,那么在高密度设备上显示图片时就会被自动放大,而如果将一张图片放在高密度文件夹下,那么在低密度设备上显示图片时就会被自动缩小。那我们可以通过成本的方式来评估一下,一张原图片被缩小了之后显示其实并没有什么副作用,但是一张原图片被放大了之后显示就意味着要占用更多的内存了。因为图片被放大了,像素点也就变多了,而每个像素点都是要占用内存的。
我们仍然可以通过例子来直观地体会一下,首先将android_logo.png图片移动到drawable-xxhdpi目录下,运行程序后我们通过Android Monitor来观察程序内存使用情况:
Android drawable微技艺,你所不知道的drawable的那些细节

可以看到,程序所占用的内存大概稳定在19.45M左右。然后将android_logo.png图片移动到drawable-mdpi目录下,重新运行程序,结果如下图所示:
Android drawable微技艺,你所不知道的drawable的那些细节

现在涨到23.40M了,占用内存明显增加了。如果你将图片移动到drawable-ldpi目录下,你会发现占用内存会更高。
通过这个例子同时也验证了一个问题,我相信有不少比较有经验的Android程序员可能都遇到过这个情况,就是当你的项目变得越来越大,有的时候加载一张drawable-hdpi下的图片,程序就直接OOM崩掉了,但如果将这张图放到drawable-xhdpi或drawable-xxhdpi下就不会崩掉,其实就是这个道理。
那么经过上面一系列的分析,答案自然也就出来了,图片资源应该尽量放在高密度文件夹下,这样可以节省图片的内存开支,而UI在设计图片的时候也应该尽量面向高密度屏幕的设备来进行设计。就目前来讲,最佳放置图片资源的文件夹就是drawable-xxhdpi。那么有的朋友可能会问了,不是还有更高密度的drawable-xxxhdpi吗?干吗不放在这里?这是因为,市面上480dpi到640dpi的设备实在是太少了,如果针对这种级别的屏幕密度来设计图片,图片在不缩放的情况下本身就已经很大了,基本也起不到节省内存开支的作用了。


好的,关于drawable微技巧方面的探索我们就讲到这里,本篇文章中也是集合了不少我平时的工作经验总结,以及通过做试验所得出的一些结论,相信还是可以给大家带来不少帮助的。后面我会抓紧时间继续准备新系列的内容,敬请期待。

关注我的微信公众号,第一时间获得博客的更新提醒,更有很多其它的技术信息分享给大家
扫一扫下方二维码或搜索微信号guolin_blog即可关注:
Android drawable微技艺,你所不知道的drawable的那些细节

61楼qq_27844985昨天 18:22
郭神,图片放在也有自己的dpi吧。你例子中的图片正好是你手机的四分之一大小是不是因为图片的dpi与你手机的dpi相差不多?
Re: sinyu8908073小时前
回复qq_27844985n图片只有像素,dpi是根据屏幕分辨率和屏幕尺寸计算出来的,和图片没有关系。图片在我手机上占四分之一是因为我特意去剪了这样像素的一个尺寸。
60楼wang838283昨天 16:22
啦啦啦
59楼shakespeare001昨天 15:32
一二一,一二一,立定,向--大神看--齐!
58楼zx0506昨天 13:34
叼的一笔啊
57楼wangtao0921昨天 11:08
嗯,学习了。很好。
56楼Evan123mg昨天 10:51
好!
55楼u012692322昨天 10:21
感谢解惑
54楼LouisZhoun昨天 09:55
那平时我们的图片必须要分大小放在对应文件夹下面吗?如果放在同一个文件夹下面不影响吧,平时都放一个文件夹下面的。
53楼u010287009昨天 09:21
郭神,你说“mipmap文件夹只是用来放置应用程序的icon的,仅此而已”,但是我看好多项目在AS中都不用drawable了,而是把图片分别放到对应的mipmap中去了,这样做是错的么?
Re: sinyu890807昨天 09:36
回复u010287009n我认为这样做是错的,因为Google官方博客上只说明使用mipmap放置icon,从来没提到过把所有图片都放在这里。而我在stackoverflow上找到的答案也都是说mipmap只是用来放icon的。
Re: u010331406昨天 09:51
回复sinyu890807n是这样的:nnhttps://github.com/android-cn/android-discuss/issues/114
52楼wangdong20昨天 08:28
这都是以前我很困惑的细节,今天郭大神终于把它拿出来讲讲,对我帮助很大
51楼jkkjklmn前天 16:49
郭神,如果此处不是wrap_content,而是match_parent或者给一个固定的宽高呢?
Re: sinyu890807前天 17:44
回复jkkjklmnn这种情况我也做了试验,内存还是一样会涨,感觉应该是把图片仍然放大,但是在绘制的时候限制了图片的大小展示而已。
Re: jkkjklmn前天 18:05
回复sinyu890807n如果是固定宽高的话内存也会涨,但是我感觉没关系吧?因为如果UI给多套图的话,每套图的尺寸也是按比例来放大或者缩小的啊,比方mdpi中的图示100*100,那么xxhdpi中UI给的图片就应该是300*300吧?占用的内存也是mdpi中的9倍,是不是这样?
Re: xuejinwei0530昨天 18:57
回复jkkjklmnn按照这么说,内存是一样的,但放在第分辨率drawable下,高分辨率加载时候图片相应放大会模糊的,用户体验就不好了。而且还的为系统执行放大买单。
Re: jkkjklmn昨天 20:04
回复xuejinwei0530所以的话,我可以认为只是因为图片放大会模糊缩小不会模糊,而不是因为会占用过多的内存而把图片放在drawable-xxhdpi下,是这样吗?
50楼ltxxx321425前天 16:49
谢郭神,Mark一下,回去学习
49楼u012582495前天 16:46
牛逼
48楼malinkang1989前天 16:38
如果在320*480的160dpi ~ 240dpi设备上,屏幕上放置一张存放在hdpi的文件夹80*120的图片,刚好占用1/4,如果放到xxhdip目录下需要尺寸为160*240的图片,图片占用的内存也会增大,我们手动放大和Android系统自动放大有何区别?
47楼u012147465前天 16:31
mark
46楼ws_lm前天 16:06
好文
45楼qq_33884900前天 16:05
请大神多更新点studio方面知识,感觉编译的有些错误好难网上找
44楼liujun3512159前天 16:03
郭霖的每篇文章挺有价值的,通俗易懂,真不错
43楼mngya001前天 16:03
好,膜拜。
42楼xiangwang2015前天 15:26
占座
41楼dachaoxuexi前天 15:23
学习中
40楼u010287009前天 14:44
回复sinyu890807n 好的,谢谢郭神指点。
39楼garywlx前天 14:35
android:src="@mipmap/android_logo"
38楼J_Demon前天 13:50
还在看你的书,酷我天气了。对我这种初学者来说很有帮助。
37楼talent_zhl前天 13:41
啦啦啦。。。
36楼l19890125前天 11:51
学习了 多谢
35楼qq_34412345前天 11:32
终于等到你
34楼dgs960825前天 11:26
郭神请收下我的膝盖~
33楼qq_28533013前天 11:25
我们的项目没有做适配,只是把图片资源都放在了xhdpi下,现在感觉正合适
32楼dahaidadavid前天 11:23
哈哈,如期而至,好东西啊!
31楼u010331406前天 11:21
[code=java]n不错不错n[/code]
30楼LDW5800前天 11:20
沙发
29楼wavever前天 11:15
郭神还是6
28楼diyangxia前天 11:14
学习了
27楼ForAd前天 11:09
接地气好
26楼Adusta前天 11:05
前排
25楼qq849177475前天 11:04
前排膜拜郭神
24楼yujunlong3919前天 10:19
其中xdpi代表屏幕宽度的dpi值,ydpi代表屏幕高度的api值,郭神应该打错字母了
Re: sinyu890807前天 10:59
回复yujunlong3919n已修正,多谢
23楼a378881925前天 10:14
这篇文章讲解的非常细致:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2016/0116/3874.html
22楼zhengkaihong前天 10:07
赞郭神
21楼cxf537前天 10:06
我郭神讲的还是6啊,实用性很强
20楼kb1989520前天 10:04
你为什么这么牛逼。
19楼szyangzhen前天 09:53
郭兄,对于你所讲的图片等应放在 xxdpi里面所引用的例证我有些疑问啊,如果图片都放在xxdpi中,美工做图片的时候,相应的图片尺寸都会做的比较大啊,同样占内存多啊,并且apk还很大,求指导
Re: sinyu890807前天 09:57
回复szyangzhenn是的,但是你不得不兼容xxhdpi的情况,因为现在xxhdpi的手机非常多,快有一小半以上的手机都是这个密度的了,我手上的三个手机试了一下全部都是xxhdpi的,所以即使这样图片会比较大,但你还是要适配这个密度的手机,总不能你只兼容小屏手机了吧。
Re: szyangzhen前天 10:02
回复sinyu890807ok,明白了
18楼yayun0516前天 09:48
http://www.imooc.com/learn/484 这个视频有做过这个实验
17楼heroxuetao前天 09:47
顶楼主
16楼L2011303476前天 09:46
郭神,有个疑问,xxhdpi文件夹是API16才加上的,但是实践发现即使把一张图片放在xxhdpi里面,在API14的设备上还是能够显示的,请问对这一块有没有做过研究?是系统源码自动做了兼容吗?
15楼hello_json前天 09:43
前排
14楼weinierfei前天 09:36
郭神好,问一个关于svg的问题哈,svg转为xml后,放在哪个文件夹下,求指导?
Re: tanranran前天 09:36
回复weinierfein我是放在drawble
13楼anerle2012前天 09:36
好文~
12楼cwt8805前天 09:32
这儿有一篇好文作为补充n安卓分辨率的相关知识 http://leoray.leanote.com/post/android-resolution
11楼BestMafen前天 09:29
郭大侠,我还以为你闭关修炼了。郭神出品,必属精品啊!此处给你82分,剩下的18分给你666
10楼Easy_v5前天 09:28
666
9楼sinat_32902703前天 09:19
顶,看君博客一篇,胜读大学四年啊,感谢郭大神!!!
8楼qq_22770457前天 09:20
前排!
7楼u012934230前天 09:19
又学会一个微技巧!
6楼legend12300前天 09:16
good good study and day day up
5楼qq_24916671前天 09:13
可是android studio上不是只有一个drawble文件夹吗?
Re: sinyu890807前天 09:14
回复qq_24916671n自己建。。。。
Re: ForAd前天 09:15
回复sinyu890807google 这里又偷懒
Re: qq_24916671前天 09:16
回复sinyu890807哦哦。。。。。学习了!谢谢大神
4楼mfkauko前天 09:11
学习了。
3楼qq_26646565前天 09:10
前排
2楼XTASK前天 09:09
沙发2
1楼basic_voice前天 09:08
涨知识了