本来这个应该在学习从零学Android(四)、适配不同的Android设备之前就该记录的,不过写着写着就写忘了- -!现在回头再单独来了解App中资源相关的知识。。
概述
在开发的过程中,将资源和代码分离总是一个好的习惯。在Android开发中我们提供多套资源去适配多种Android设备。为了兼容不同配置的Android设备,我们需要将资源放置到那些根据类型或者配置信息来创建的res/目录下的各种子目录中。
对于任何的资源来说,我们都可以为我们的App提供一套默认的资源和多套可选的资源。
默认资源就是指那些不管当前设备配置而使用或者没有匹配当前配置的代替资源时使用的资源。
代替资源是指那些你设计给一些特殊配置的设备用的资源。为了指定这些资源是代替资源,我们需要将这些资源放到那些和特定配置相匹配的res/目录中有特殊限定名称的子目录中。
比如我们会将默认的布局资源文件放置到res/layout/目录下,而将为设备竖屏时准备的布局资源放置到res/layout-land/目录下。Android系统会根据当前的系统配置去对应的资源目录中选择合适的资源使用。
下面有两张对比图:
第一张图展示的是只有默认资源而没有合适代替资源时,相同的布局资源在不同设备上展示的效果。第二张图则是给大屏设备加入代替资源时候的效果。
我们将从资源的提供、资源的使用、运行时配置改变的处理这三个方面详细学习App资源的知识。
1.资源的提供
1.1 资源分类
在Android项目开发时,我们需要将不同的资源放在我们项目res/目录下的对应的特殊的子目录中。比如说有这么一个 简单项目的资源目录结构:
如图中所示,res/目录下(子目录中)包含了所有的资源,一个图片资源,两个layout布局资源,另外mipmap/目录下放置的应用图标资源,还有一个字符串资源文件。这里每一个资源对应的目录名称都很重要,下面列出了res/目录下支持的子目录名称:
1.animator/目录: 这个目录是用来放置属性动画的XML资源文件的。
2.anim/目录: 这个文件是用来放置补间动画的XML资源文件的(同样属性动画资源也能放置在这个目录下,但是animator/目录是放置属性动画资源的首选目录,这样也更 容易区分两种动画资源)
3.color/目录: 用来放置状态颜色列表的XML资源文件夹。
4.drawable/目录: 用来放置图片资源(后缀为.png
, .9.png
, .jpg
, .gif
等)或者能编译成下面的几种类型的XML资源文件:①Bitmap资源 ②.9图片资源 ③状态列表资源 ④shape形状资源 ⑤动画图片资源 ⑥其它的图片资源 查看详情或者更多点击这里
5.mipmap/目录: 用来放置不同尺寸的应用图标的资源文件夹。
6.layout/目录: 用来放置布局资源的文件夹。
7.menu/目录: 用来放置应用菜单资源的文件夹,比如Options Menu,Context Menu 或者Sub Menu的资源。详情查看
8.raw/目录: 这个目录能放置任意类型的文件,而且它会保留这些文件的原始数据,要使用这个文件夹下的资源,我们需要一个它的一个输入流或者通过如R.raw.filename格式的资源的ID,去调用Resources.openRawResource()这个方法来打开这些资源。但是,如果你需要通过文件的原始文件名或者这个文件的文件路径去访问资源,那么你可以考虑将这类资源放着到assets/目录下,这个目录下的资源不会被编译,也不会在R文件中生成ID,所以你可以通过AssetManager去访问其中的资源。
9.values/目录: 这个目录下可以放置很多的简单XML资源文件,如下:①数组资源arrays.xml ②颜色资源colors.xml ③尺寸资源dimens.xml ④字符串资源strings.xml ⑤样式资源styles.xml 更多资源类型
10.xml/目录: 这个目录下保存的是那些可以在运行时通过Resources.getXML()来读取的任意的XML文件。
放在上面子目录中的资源全部属于默认资源。现在我们继续去了解下代替资源。
1.2 提供替代资源
几乎每一个应用都应该提供代替资源以支持那些特殊的配置设备。比如说我们应该给不同屏幕尺寸的设备提供不同大小的图片,给不同语言的设备提供不同的字符串资源,Android系统会在运行时去根据手机的配置动态加载合适的资源。
为了指定那些代替资源的配置,我们需要做如下两个事:
①在res/目录下创建一个子目录,这个子目录的命名形式如:<resources_name>-<config_qualifier>。其中的<resources_name>
是上面我们提到的十种资源子目录的名称,而<qualifier>
就是限定配置的名称,我们同样可以使用”-“连接多个限定名称(如果我们有多个限定符,那么我们需要保证这些限定符的顺序正确,不然这个资源目录就会被忽略,具体的资源参考下面链接中,官网表格的顺序)。这个配置限定符名称的常用可选项有下面这些:
1.屏幕像素密度DPI:取值有ldpi,mdpi,hdpi,xhdpi,xxhdpi,nodpi,tvdpi
2.屏幕方向:取值有port(竖屏),land(横屏)
3.屏幕大小:取值有small,normal,larger,xlarge
4.语言区域:取值为ISO国家代码,比如en,fr,en-rUS,fr-rFR
②将相应的代替资源放入到对应的子目录中,并且这些代替资源的命名必须和默认资源保持一致。如下有一些默认资源和代替资源:
res/ drawable/ icon.png background.png drawable-hdpi/ icon.png background.png
1.3 限定名称的规则
下面有一些关于使用配置限定符的一些规则:
①我们可以指定多个限定符,限定符之间由长划线“-”连接。比如:drawable-en-rUS-land目录下的资源是给US-English的竖屏设备用。
②限定符出现的顺序必须按照上面官网提供的表格中的顺序一致。比如:错误的资源限定:drawable-hdpi-port/ 正确的资源限定:drawable-port-hdpi/
③代替资源目录不能被嵌套,比如你不能这样去放置代替资源目录res/drawable/drawable-en/
.
④限定名称不区分大小写,资源编译器会在处理前将目录名称全部转换成小写,以避免在那些不区分大小写的文件系统中出现问题,文件名的大写是为了提高可读性而已。
⑤同一种限定符类型的取值只能出现一个,比如说你希望在西班牙和法国的设备上使用同一套drawable资源,你不能将这个代替资源目录命名为drawable-rES-rFR/,而是应该将它拆分为2个子目录,drawable-rES/和drawable-rFR/。但是,你不需要将同一套drawable资源复制粘贴到两个子目录中,这个时候你可以使用别名资源。
1.4 创建别名资源
当我们有一套资源同时作为多种配置的设备使用,同时你又不希望将这个资源作为默认资源使用,我们可以不用将相同的资源复制粘贴到多个代替资源目录中,相反,我们可以给默认资源目录下的资源去创建一个别名资源。(注意:不是所有的资源都可以使用别名资源机制,特别是动画,菜单,raw资源和那些在xml/目录下的未指定资源是不支持这个别名功能的)。
下面我们通过一个具体的例子来说明别名资源的使用:假设我们APP的应用图标名为icon.png,这个时候需要在English-Canadian(加拿大英文)和French-Canadian(加拿大法语)这两种设备上使用另外一个图片资源(假设名字为icon_ca.png,必须和icon.png名字不一样)作为应用图标。这个时候我们不用将icon_ca.png改名为icon.png然后复制到res/drawable-en-rCA/和res/drawable-fr-rCA/代替资源目录下,而是将icon_ca.png放置到默认drawable资源目录下,然后分别在res/drawable-en-rCA/和res/drawable-fr-rCA/代替资源目录下创建一个XML图片资源文件icon.xml,这个XML图片资源文件中使用<bitmap>元素标签来表示图片资源。这样我们就通过使用一个PNG图片资源icon_ca.png和两个指向这个图片资源的小的XML文件完成了我们的需求。其中的xml文件如下:
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/icon_ca" />
这样我们就给一个drawable资源创建了一个别名XML资源。然后我们把这个
icon.xml放入代替资源目录
res/drawable-en-rCA/中,这个时候我可以通过
R.drawable.icon去引用这个资源,同时它实际是默认资源目录
res/drawable/下的
R.drawable.icon_ca资源的别名资源。
我们再看看Layout布局资源的别名资源使用:给一个存在的layout布局资源创建一个别名资源,我们需要使用<include>元素标签,然后用<merge>标签将其包裹。如下:
<?xml version="1.0" encoding="utf-8"?>
<merge>
<include layout="@layout/main_ltr"/>
</merge>
假设我们将这个layout别名资源命名为
main.xml
,我们可以通过
R.layout.main
来引用它,同时它实际上是R.layout.main_ltr
资源的一个别名资源。
我们再看看Stirngs以及其它简单资源的别名使用:
给一个已经存在的字符串资源创建一个别名资源,只需要让这个别名资源使用这个字符串资源的ID去作为一个新的字符串即可,如:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="hello">Hello</string>
<string name="hi">@string/hello</string>
</resources>
颜色等其它简单资源同理可得。
1.5. Android系统是怎么选择最佳资源的?
我们先看官网给出的Android系统选择最佳资源的流程图,如下:
第一步:首先消除掉所有和设备配置相违背的资源目录。
第二步:根据官网提供的代替资源表格的资源限定出现顺序来选择一个最高优先级的限定符。
第三步:判断当前的资源目录是否有包含这个限定符的资源目录,如果没有,返回第二步,选择下一个限定符,如果有继续第四步。
第四步:消除那些不包含这个限定符的资源目录。
第五步:重复第二三四步,直到只有一个资源目录被留下,那么这个资源目录就是当前配置下最佳的资源目录。
虽然每一次资源的请求都是这么一个流程,但是系统进一步优化了一些方面。比如说:一旦当前的设备配置是已知的,它可能不匹配而直接消除一些代替资源目录。举个简单的例子:如果设备配置的语言是English("en"),那么那些包含了语言限定字符但是却不是”en“的资源目录不会被包含在检查池内,也就是说不会去匹配这类的资源目录(虽然那些没有语言限定符的资源目录还是包含在检查池内)。
当根据屏幕大小限定符去选资源时,如果没有想匹配的资源目录,系统会选择设计给那些比当前屏幕尺寸小的资源(比如说:一个large-size的屏幕可能会使用normal-size下的资源)。然而,如果唯一可用的资源目录比当前屏幕的尺寸大,系统将不会使用它。而如果没有相关的资源匹配,那么app就会崩溃(比如说:如果所有的layout布局资源是xlarge的限定资源,但是当前的设备是normal-size时,系统不会选择使用这些资源)。
注意:限定符的优先级比限定符的数量更重要。
2.资源的使用
当我们加入资源到我们的项目中时,我们可以通过这个资源对应的ID去引用它。所有的资源ID都是通过aapt工具自动生成,并且保存到项目的R文件中。当我们项目被编译的时候,aapt会帮我们生成R文件,它包含了所有res/目录下的资源的ID,并且对于每一种资源类型,都有一个内部类与之对应(比如和drawable资源对应的R.drawable),同时对于每一个资源都会有一个静态的整型数据如:R.drawable.icon
。这个整型就是资源ID,我们可以通过它去引用我们的资源。
一个资源ID总是由下面几个部分组成:
①资源类型:每一个资源都会被分配有一个类型,比如string
,drawable
和layout
②资源名字:它可以是一个不包括文件扩展名的文件名,如果是一个简单的资源类型比如:string,该值也可能是它在XML文件中对应的android:name属性的取值。
同样的,我们也有两种方式去获取引用资源:
①在代码中引用:使用R文件中的静态整型,如:R.string.hello,其中string是资源的类型,hello则是资源的名称。Android提供了很多的API帮助我们在代码中使用资源。下面小节中我们会详细学习到这些API。
②在XML文件中引用:在XML文件中引用资源同样和R文件中的资源ID有关。如:@string/hello,其中string是资源的类型,hello则是资源的名称。详细的使用在下面的小节中会讲到。
2.1 在代码中引用资源
在代码中,我们可以将资源ID作为方法参数来引用资源。比如说:我们可以通过setImageResource()
方法设置
使用ImageView
res/drawable/myimage.png
资源:
ImageView imageView = (ImageView) findViewById(R.id.myimageview);
imageView.setImageResource(R.drawable.myimage);
同样我们可以通过
getResources()方法获取到
Resources类的实例,然后使用
Resources类中的一些方法去检索获取资源。
下面是在代码中引用资源的语法:
[<package_name>.]R.<resource_type>.<resource_name>
<package_name>是应用包名,和普通类一样,当我们引入R类的时候,我们需要确定其包路径,所以这里可以省略。
<resource_type>是资源类型,对应了R类中的内部类。
<resource_name>是资源名称,它可以是一个不包括文件扩展名的文件名,如果是一个简单的资源类型比如:string,该值也可能是它在XML文件中对应的android:name属性的取值。
我们通过代码来学习具体的用法:
// Load a background for the current screen from a drawable resource
getWindow().setBackgroundDrawableResource(R.drawable.my_background_image) ;
// Set the Activity title by getting a string from the Resources object, because
// this method requires a CharSequence rather than a resource ID
getWindow().setTitle(getResources().getText(R.string.main_title));
// Load a custom layout for the current screen
setContentView(R.layout.main_screen);
// Set a slide in animation by getting an Animation from the Resources object
mFlipper.setInAnimation(AnimationUtils.loadAnimation(this,
R.anim.hyperspace_in));
// Set the text on a TextView object using a resource ID
TextView msgTextView = (TextView) findViewById(R.id.msg);
msgTextView.setText(R.string.hello_message);
2.2 在XML中引用资源
同样我们也可以在XML属性和元素中引用存在的资源。并且在创建XML布局文件的时候,我们经常会这么做。比如说,我们布局文件中与一个按钮,并且这个按钮引用了字符串资源:
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/submit" />
在XML中引用资源的语法如下:
@[<package_name>:]<resource_type>/<resource_name>
其中的<package_name>,<resource_type>,<resource_name>的含义和上面的一致。
假设我们有如下2个资源:一个字符串资源,一个颜色资源。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="opaque_red">#f00</color>
<string name="hello">Hello!</string>
</resources>
那么在XML文件中,我们可以这样去引用它们:
<?xml version="1.0" encoding="utf-8"?>
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:textColor="@color/opaque_red"
android:text="@string/hello" />
因为资源和XML文件在同一个包下,所以我们可以不去指定包名,但是如果我们去引用不同包的资源,比如系统的颜色资源,那么我们就要加入包名,如下:
<?xml version="1.0" encoding="utf-8"?>
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:textColor="@android:color/secondary_text_dark"
android:text="@string/hello" />
2.3 引用样式属性
样式属性资源允许我们引用当前应用的theme主题下的属性值。引用一个样式属性允许我们通过使用当前应用主题下的样式来自定义UI元素的外观样式,而不是通过硬编码指定一个具体的值。从本质上来说,引用样式属性就是”使用当前主题下的给这个属性定义样式“。(就是系统定制样式)
引用样式资源的语法和普通资源语法几乎一样,只需要将最前面的"@"符号换成“?”,然后其中的<resource_type>是可选的。如下:
<EditText id="text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textColor="?android:textColorSecondary"
android:text="@string/hello_world" />
这里,
android:textColor
属性自定了当前主题下的样式的属性名称。然后系统会使用
android:textColorSecondary
样式属性的值作为EditText控件
android:textColor的属性值。因为系统资源工具知道这种上下环境中需要什么属性资源,所以我们不用指定具体的资源类型。
2.4 使用平台资源
Android系统提供了很多的平台资源,比如样式资源,布局资源等,我们需要在引用时加上android包名。如下:
setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, myarray));
3.运行时配置改变的处理
一些设备在运行时它的配置可以发生改变,比如:屏幕方向改变、系统语言改变或者键盘的可用性改变等。当这些变化发生时,Android系统会将我们当前正在运行的Activity销毁重建(即调用完onDestroy()后,再调用onCreate()),这种重新创建的行为通过系统自动加载适合当前设备配置的代替资源让我们的APP适应新的配置。对于重建Activity后的处理,最重要的就是恢复Activity被销毁前的状态,所以我们需要借助onSaveInstanceState()方法在Activity被销毁前保存Activity的状态信息,然后在重建时借助onCreate()方法或者onRestoreInstanceState()方法来恢复Activity的状态。
在我们去重建Activity的时候,我们可能会遇到一些状况,比如说在恢复一些重要数据的时候花费代价很大,甚至导致不好的用户体验。这个时候我们有2个选择:
【1】保留原来Activity的状态数据,即允许Activity在配置改变时重新创建,同时保存状态数据,携带这些数据到新的Activity页面中。
【2】自己处理配置改变,即禁止系统在设备配置发生改变时销毁重建我们的Activity,而是我们自己当设备配置改变时,收到一个回调信号,以便我们手动地去更新我们的Activity。
3.1 保留原来Activity的状态数据
在重建我们Activity的时候,我们可能需要去恢复大量的数据、重新建立网络连接或者一些其它的麻烦操作,这个时候或许一个配置改变导致的重建操作就是一次很差的用户体验。另外,由于在onSaveInstanceState()用来保存数据的Bundle在设计之初就不是用来携带那种大数据对象(比如Bitmap图片对象)的,所以有时候我们甚至无法恢复这些数据,同时它携带数据必须是能被序列化和反序列化的,这会导致消耗大量的内存,以致我们的App运行缓慢。在这种情况下,我们可以通过保留一个Fragment的对象,来减轻我们恢复Activity的负担。这个Fragment中可以保留那些有状态的对象。
在配置改变导致系统销毁重建我们的Activity的时候,我们标记过的那些Fragment不会被销毁,所以我们可以添加如下的Fragment到我们的Activity中,用来保存那些有状态的对象。
为了在设备配置发生改变时,保存那些有状态的对象到Fragment中,我们需要:
【1】继承自Fragment,并且声明那些我们需要保存的有状态的对象。
【2】在Fragment创建时调用setRetainInstance(boolean)方法。
【3】添加Fragment到我们的Activity中。
【4】在Activity重新创建时,使用FragmentManager重新恢复Fragment。
比如,如下去定义我们的Fragment:
public class RetainedFragment extends Fragment {
// data object we want to retain
private MyDataObject data;
// this method is only called once for this fragment
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
public void setData(MyDataObject data) {
this.data = data;
}
public MyDataObject getData() {
return data;
}
}
注意:虽然我们可以保存任何的数据,但是我们绝对不能将任何与Activity绑定的数据传递出去,比如一个Drawable对象,一个Adapter对象,一个View或者任何一个和一个Context联系在一起的对象。如果你这么做了,这会泄露原来Activity中所有的View视图和其中的资源。(资源泄露也就意味着我们的应用会一直持有这些资源,导致gc(垃圾回收器)不能回收这些资源,这也就意味着我们失去了许多的内存)。为了避免这种情况,我们要注意如下几个点:
1. 不要让生命周期长的对象引用activity context,即保证引用activity的对象要与activity本身生命周期是一样的
2. 对于生命周期长的对象,可以使用application context
3. 避免非静态的内部类,尽量使用静态类,避免生命周期问题,注意内部类对外部对象引用导致的生命周期变化
然后我们通过FragmentManager将上面的Fragment加入到我们的Activity中,并且在Activity重建时,通过它去恢复原来的数据:
public class MyActivity extends Activity {
private RetainedFragment dataFragment;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(“data”);
// create the fragment and data the first time
if (dataFragment == null) {
// add the fragment
dataFragment = new DataFragment();
fm.beginTransaction().add(dataFragment, “data”).commit();
// load the data from the web
dataFragment.setData(loadMyData());
}
// the data is available in dataFragment.getData()
...
}
@Override
public void onDestroy() {
super.onDestroy();
// store the data in the fragment
dataFragment.setData(collectMyLoadedData());
}
}
在上面这个例子中,在
onCreate()添加了一个Fragment,重建时又恢复了它的引用,
onDestroy()中则更新了Fragment中保留的有状态的数据。
3.2 自己处理配置改变
如果在配置改变时,我们不需要去更新我们的Activity或者在性能上跟不上,有一定的性能限制,我们就可以通过配置改变时,禁止系统销毁重建我们的Activity,而是自己去处理这个变化,以便避免重建导致的问题。
注意:当我们选择自己去处理配置改变这一行为时,系统不会帮我们自动去使用那些合适的代替资源(系统会更新最新的Resouece,但是不会自动去使用这些Resourece),所以在使用合适的代替资源这一方面会变得很难。所以这个技术一般是当你必须避免重建Activity时才采用的最后一招,一般不建议应用这么做。
为了声明自己处理配置改变,我们需要在应用配置文件AndroidManifest.XML的<activity>
标签下配置android:configChanges
属性,这个属性的值就是你希望自己处理的配置。最常见的取值一般是屏幕方向"orientation"和键盘可用性"keyboardHidden"。我们可以设置多个值,通过|
字符连接。如下:
<activity android:name=".MyActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/app_name">
这样,当发现上面声明的配置变化时,我们的Activity不会被销毁重建,而是回调
onConfigurationChanged()方法。这个方法传递了一个
Configuration对象,其中包含了设备的最新配置信息。这时我们就可以确定设备的配置,并且通过改变UI界面资源来做一些合适的改变。当这个方法被调用的时候,我们Activity的资源对象
Resources也已经被更新了,它会根据当前新的配置信息新的资源。所以我们能轻易地重置我们的UI对象,而不用去重新创建Activity。如下我们可以检查当前的屏幕方向:
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Checks the orientation of the screen
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
}
}
这里的Configuration
对象展示了当前的配置信息,而不是当前改变的配置。大多数时候,我们不用关心配置是怎么发生了改变,我们只需要简单地去给当前的配置重新分配所有的代替资源。比如说,由于Resources
对象已经更新了,我们可以使用setImageResource()
和当前新配置下的合适的资源重置任何的ImageView。
注意:从Android3.2(API level 13)开始,当设备切换横竖屏的时候,屏幕大小属性也会发生变化。因此,如果在API 13 或者更高的版本上,如果我们希望禁止横竖屏切换导致Activity销毁重建,我们还需要在android:configChanges中添加"screenSize"
属性值,如下:android:configChanges="orientation|screenSize"。使用API等级12或者以下的版本时,我们只能自己去处理运行时配置改变,因为API level 12和12以下的版本,配置改变不会去导致Activity销毁重建。
在一些配置发生改变时,如果我们不需要更新Activity,我们可以不用去实现onConfigurationChanged()。这样我们在配置改变前被使用的资源,在配置改变后仍然在使用,我们仅仅是避免了Activity的销毁重建。但是,我们的应用难免遇到各种各样的配置改变,导致Activity被销毁重建,所以我们一般不能考虑使用这种技术来逃避正常流程下的保留和恢复数据。