10.2 使用资源

Android应用中的资源可以在XML资源文件中使用,也可以使用Java代码访问,而且系统中也自带了很多资源。本节将详细介绍这些资源的的各种访问方法。

10.2.1 生成资源类文件(R.java)

就像Map一样,任何资源都需要通过一个简单的值(key)来获得。而这个key就存在于本节要介绍的R类中。

R类(位于R.java文件中)是一个普通的Java类,R.java是由系统自动生成的(在Android工程的gen目录中),并不需要由开发人员维护。当res目录中的资源发生变化时(可能是添加新的资源,也可能是修改了某些资源的名字),ADT会使用res目录中的资源来同步R类。在解释R类之前,我们先来看一个简单R类。

package mobile.android.first;

public final class R {

  // 数组资源

  public static final class array {

    public static final int incall_refuse_mode_entries=0x7f090001;

    public static final int incall_refuse_mode_entry_values=0x7f090002;

  }

  // 颜色资源

  public static final class color {

    public static final int divider=0x7f060000;

    public static final int translucent_background=0x7f060001;

  }

  // 图像资源

  public static final class drawable {

    public static final int about=0x7f020000;

    public static final int face=0x7f020078;

  }

  // 定义控件、菜单时指定的ID

  public static final class id {

    public static final int body_linearlayout=0x7f0b004e;

    public static final int btnCancel=0x7f0b0007;

    public static final int btnChat=0x7f0b0075;

  }

  // 布局资源

  public static final class layout {

    public static final int about=0x7f030000;

    public static final int add_black_list=0x7f030001;

    public static final int add_email=0x7f030002;

  }

  // 字符串资源

  public static final class string {

    public static final int app_name=0x7f070001;

    public static final int busy_sound=0x7f070018;

  }

}

从这段代码可以看出,R类中包含了几个内嵌类,而且这几个内嵌类的类名似曾相识。根据这几个类可以得出一个结论:所有的资源对应的索引(Key)都是被封装在R类的内嵌类中的,而且大多数资源(除了values资源)对应的内嵌类都是以资源目录名作为类名的,如drawable、layout。

再看看R类中的每一个内嵌类,在这些类中都定义了若干个被定义为final的int类型变量Java中没有常量的概念,但被声明成final的变量可以被认为是常量,为了方便,本书会直接将声明为final的变量称为常量。。实际上,这里的每一个常量(被声明为final的变量)都对应于一个资源。在编译Android应用时,系统会自动将这些常量与相应的资源一一对应。因此,我们可以直接使用这些常量来引用资源。

也许看到这,读者会产生一些更深入的问题:这些常量以及常量值是怎么来的呢?这些常量值是按着某些规则指定的,还是随便定义的?

首先强调一点,这些常量是由系统自动生成的,这是毋庸至疑的。在理论上,这些常量可以是任何int类型的值,但常量值必须对于当前的应用程序是唯一的。也就是说,当前常量的值不能与其他常量的值和系统资源对应的变量的值重复。不过我们一般并不需要管这些常量是如何取值的,只需要了解如何使用它们即可。

从表面上看,是ADT根据res目录中的资源自动生成R.java文件的,但实际上,ADT是通过aapt(Android Asset Packaging Tool )命令来生成R.java文件的。aapt.exe文件(Mac OS X和Linux的相应命令也叫aapt,但没有文件扩展名)位于<Android SDK安装目录>/platform-tools目录中。如果想手工生成R.java文件,可以在Windows控制台中输入如下的命令。

aapt package -f -m -J d:\ -S res -I D:\sdk\android-sdk-windows_new\platforms

\android-12\android.jar -M AndroidManifest.xml

对于aapt命令至少应了解如下几点。

生成R.java文件需要在aapt后面加package或p。

-f命令行参数表示如果R.java文件已存在,会使用新的R.java文件覆盖旧的R.java文件。

-m命令行参数表示会在-J命令行参数指定的路径中生成由AndroidManifest.xml文件指定的包目录,而这个AndroidManifest.xml文件由-M命令行参数指定。

在执行上面的命令之前要确保当前目录有包含资源的res目录。读者可以将Android工程目录中的res目录单独复制到其他任何目录下,然后在res所在的目录执行上面的命令即可。

aapt命令需要使用Android SDK中的android.jar文件,因此,要使用-I命令行参数指定android.jar文件的具体位置。android.jar文件在Android SDK中可能存在多个,读者可以选择相应Android版本的android.jar文件。

如果只是生成R.java文件,AndroidManifest.xml文件包含顶层标签<manifest>以及相应的属性即可,aapt会获取<manifest>标签的package属性值作为要生成的包目录的目录名。例如,package属性值为mobile.android,那么执行上面的命令行,会在当前目录生成mobile/android目录。也就是说在mobile目录中包含了一个android目录,在android目录中包含了生成的R.java文件。

注意

R类中的常量可以有多种称谓,例如,资源ID、资源索引、key等。为了统一,在本书中将这些常量统称为资源ID。

10.2.2 从XML文件中访问资源

我们可以在XML资源文件(布局资源、菜单资源等)中某个标签的属性值中引用资源。例如,下面定义了一个按钮控件,其中android:text属性值引用了一个字符串资源。

<Button

  android:layout_width="fill_parent"

  android:layout_height="wrap_content"

  android:text="@string/ok" />

在属性中引用资源的语法如下:

@[<package_name>:]<resource_type>/<resource_name>

其中<package_name>、<resource_type>和<resource_name>的含义如下。

<package_name>:R类的package。如果R类的package与AndroidManifest.xml文件中定义的package相同,可以不指定package。但如果引用系统资源,就需要使用package了,例如,@android:string/copy。

<resource_type>:R类的子类名称,如drawable、string、id等。

<resource_name>:资源文件名(不包扩展名)或XML资源文件中某个标签的android:name属性的值,也就是R类中相应子类的常量名。

某些属性(如android:src)必须引用资源ID才可以使用,但大多数属性可以使用属性值或资源ID。下面是在res/values/strings.xml文件中定义的资源。

<?xml version="1.0" encoding="utf-8"?>

<resources>

  <color name="opaque_red">#f00</color>

  <string name="hello">Hello!</string>

</resources>

可以使用下面的代码引用color和string资源。

<?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" />

在这种情况下,我们不需要指定package,但如果引用系统的资源,就需要指定package,代码如下:

<?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" />

如果Android应用需要本地化,建议属性值应尽可能使用资源ID,至少字符串资源要这样做。如果不使用资源ID引用资源,就需要为每个布局资源文件、菜单资源文件建立本地化资源目录,而这些资源文件的区别仅仅是显示的字符串、图像不同。如果使用资源ID引用资源,就只需要对所引用的资源进行本地化即可,而各个国家、语言所使用的布局资源文件、菜单资源文件可以共享,这样更有利于程序的维护。

10.2.3 使用Java代码访问资源

在Java代码中可以直接使用资源ID来访问资源,例如,下面的代码访问了布局资源、ID资源和字符串资源。

// 使用布局资源

setContentView(R.layout.activity_main);

// 使用ID资源

TextView textView = (TextView)findViewById(R.id.textview);

// 使用字符串资源

textView.setText(R.string.hello);

使用资源ID引用资源时应注意如下几点。

在Android SDK中有很多方法既可以接收资源ID,又可以接受实际的值,例如,setText方法的参数值可以是资源ID形式的R.string.hello,又可以是字符串形式的"hello world"。所以在传递整型值时,这些方法都会将参数值当成是资源ID。因此在设置数字形式的文本时,需要将数字转换为字符串形式。

在某些时候引用资源时可能会出现找不到资源ID的错误提示,其实这也不是什么严重的错误。由于ADT的缘故,在使用Key Assist时可能会自动加上“import android.R;”。而如果在使用R类时不加上package,其实使用的是系统的R类(android.R),只要将“import android.R;”去掉,或在R类前面加上package即可。

如何想对资源更进一步地操作,可以通过Context. getResources方法获取封装资源的相应对象或值。例如下面是在窗口类中的一段代码:

// 获取封装布局资源文件(main.xml)的XmlResourceParser对象,可以当成普通的XML文件处理

XmlResourceParser parser = getResources().getLayout(R.layout.main);

// 读取/res/raw/product.sqlite文件,并返回InputStream对象,可以将product.sqlite文件

// 当成字节流处理

InputStream is = getResources().openRawResource("product.sqlite");

// 直接从字符串资源中获取字符串

String str = (String)getResources().getText(R.string.hello);

使用Context. getResources方法还可以获取很多资源,这些内容会在讲到相应的资源时再介绍。

10.2.4 Java反射技术与枚举资源

源代码目录:src/ch10/EnumResource

众所周知,res目录中的资源只能通过资源ID引用,但如果要成批引用资源该怎么办呢?例如,在res/drawable目录中有10个图像文件,文件名为face1.png、face2.png、……face10.png,而且这些图像有可能增加或减少,例如,可能会变成30个图像文件,最后一个图像文件名为face30.png。程序的要求是扫描这些图像文件,并将其装载到同样数量的ImageView控件中。当然,方法肯定很容易想到,就是一个一个图像地引用,例如,R.drawable.face1、R.drawable.face2、……R.drawable.face10。不过这样做不仅需要编写大量的代码,而且当图像的数量增加或减少时还需要改动代码,非常麻烦。因此本节将介绍一种更方便地引用资源文件的方法,这种方法利用了Java的反射技术,可以直接通过资源文件名访问res目录中的资源。如果可以直接使用资源名称引用资源,就可以通过循环自动访问资源了(对face后面的数字进行循环)。

既然R类中的常量都对应于一个资源,所以资源名实际上就是常量名,因此可以直接通过Java反射技术获取指定常量的值,也就是资源ID。由于R类中有很多子类,可以只获取指定子类中的常量,也可以在所有子类中搜素常量,因为这些常量的值在R类中都是唯一的,不过常量名不一定唯一,例如,布局资源ID与菜单资源ID可以同名,但分属两个不同的子类:R.layout和R.menu。

下面的代码实现了从R类中所有的子类搜索指定常量名(资源ID)的功能,首先需要枚举R类中所有的子类,然后在每个子类中搜索指定的常量名,如果找到第1个满足条件的常量,返回相应的常量值(资源ID),如果未找到返回-1。因此本例不能搜索同名不同类型的常量,感兴趣的读者可以改进本程序,以使其更完美。

源代码文件:src/ch10/EnumResource/src/mobile/android/enumresource/EnumResourceActivity.java

public class EnumResourceActivity extends Activity

{  

  // 用于显示资源ID(十六进制)

  private TextView mResourceId;

  // 用于输入资源名称

  private EditText mResourceName;

  @Override

  protected void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_enum_resource);

    mResourceId = (TextView) findViewById(R.id.textview_resource_id);

    mResourceName = (EditText) findViewById(R.id.edittext_resource_name);

  }

  // 通过资源名称获取资源ID,name参数表示资源名称

  private int getResourceId(String name)

  {

    // 获取R类中所有的的内嵌类

    Class[] resourceClasses = R.class.getClasses();

    // 扫描R类中所有的内嵌类

    for (Class resourceClass : resourceClasses)

    {

      try

      {

        // 在当前内嵌类中搜素name指定的常量,如果不存在该常量,抛出异常

        Field field = resourceClass.getField(name);

        // 搜索到name指定的常量后,获取常量值,由于资源ID都是静态常量,所以对象传入null

        int resourceID = field.getInt(null);

        // 返回查到的资源ID

        return resourceID;

      }

      catch (Exception e)

      {

      }

    }

    // 常量不存在,返回-1

    return -1;

  }

  // “根据资源名获取资源ID”按钮的单击事件方法

  public void onClick_ResourceId(View view)

  {

    // 获取要检索的资源名称

    String resourceName = String.valueOf(mResourceName.getText());

    // 检索指定的资源名称,并返回相应的资源ID

    int resourceID = getResourceId(resourceName);

    // 如果未找到资源,在TextView控件中显示“资源不存在“

    if(resourceID == -1)

      mResourceId.setText("资源不存在");

    else // 找到资源后,输出十六进制的资源ID

      mResourceId.setText("0x" + Integer.toHexString(resourceID));

  }

}

现在运行程序,在文本框中输入一个资源名称,例如,app_name,单击“根据资源名获取资源ID”按钮会在按钮上方输出相应的资源ID(十六进制),如图10-11所示。在输入资源名称时要注意不能包括“R.id”。

 

▲图10-11 通过资源名查询资源ID