第6章 Android应用资源

第6章 Android应用资源

本章要点

  • Android 应用的资源及其作用
  • Android应用资源的存储方式
  • 在XML布局文件中使用资源
  • 在Java程序中使用资源
  • 使用字符串资源
  • 使用颜色资源
  • 使用尺寸资源
  • 使用数组资源
  • 使用图片资源
  • 使用各种Drawable资源
  • 使用原始XML资源
  • 使用布局资源
  • 使用菜单资源
  • 使用样式和主题资源
  • 使用属性资源
  • 使用原始资源
  • 为Android应用提供国际化资源
  • 自适应不同屏幕的资源

经过前面的介绍,相信读者对Android应用已有了大致的了解。如果从物理存在形式来分,Android 应用的源文件大致可分为如下三大类:

  1. 界面布局文件: XML文件,文件中每个标签都对应于相应的View标签。
  2. 程序源文件: 应用中的Activity、Service、BroadcastReceiver、ContentProvider四大组件都是由Java或Kotlin源代码来实现的。
  3. 资源文件: 主要以各种XML文件为主,还可包括*.png、.jpg、.gif 图片资源。

在传统开发中,初学者很容易犯一个错误:直接在Java或Kotlin源代码中使用如"crazyit.org"、“hello"这样的字符串,或者直接使用123、0.9这样的数值,而且不添加任何注释。过了一段时间后,即使自己再去看原来写的程序代码,一时之间,也无法理解其中"crazyit.org”、"hello"字符串,123、0.9等数值的含义。这种方式就大大增加了程序的维护成本。这种直接在代码中定义的123、0.9等数值,也被称为“魔术数值”(就像表演魔术一样,其他人都搞不懂)。

为了改善这种情况,有经验的开发者会专门定义一个或多个接口或类,然后在其中以常量的形式来定义程序中用到的所有字符串、数值等,这些常量的名称十分明确,如ResultSet.TYPE_FORWARD_ONLY,相信有经验的读者一看到这个常量就大致明白了它的含义。这样的方式就可以很好地提高程序的可维护性。

使用接口或类的形式来定义程序中用到的字符串、数值,虽然已经部分提高了程序的解耦,但后期维护、进一步开发时,开发人员还得去“代码海”中打捞那些定义字符串常量、数值常量的位置,因此还有可以提高的地方。

Android 应用对这种字符串常量、数值常量的定义做了进一步改进:Android允许把应用中用到的各种资源,如字符串资源、颜色资源、数组资源、菜单资源等都集中放到/res/目录中定义,应用程序则直接使用这些资源中定义的值。

在Android 应用中,除了/res/目录用于存放资源之外,assets目录也用于存放资源。一般来说,assets目录下存放的资源代表应用无法直接访问的原生资源,应用程序需要通过AssetManager以二进制流的形式来读取资源。而/res/目录下的资源,Android SDK会在编译该应用时,自动在R.java文件中为这些资源创建索引,程序可直接通过R资源清单类进行访问。

前面介绍的很多示例都是直接将字符串值写在界面布局文件或Activity代码中的,实际上那并不是一种好的方式。只是前面还未详细介绍Android应用的资源,为了避免读者产生畏难心理,并未使用资源文件而已。

6.1 应用资源概述

Android应用资源可分为两大类:

  1. 无法通过R资源清单类访问的原生资源,保存在assets目录下。
  2. 可通过R资源清单类访问的资源,保存在/res/目录下。

大部分时候提到Android应用资源时,往往都是指位于/res/目录下的应用资源,Android SDK会在编译该应用时在R类中为它们创建对应的索引项。

6.1.1 资源的类型及存储方式

Android要求在/res/目录下用不同的子目录来保存不同的应用资源,表6.1大致显示了Android不同资源在/res/目录下的存储方式。

表6.1 Android 应用资源的存储

目录存放的资源
/res/animation/存放定义属性动画的XML文件
/res/anim/存放定义补间动画的XML文件
/res/color/存放定义不同状态下颜色列表的XML文件
/res/drawable/存放适应不同屏幕分辨率的各种位图文件(如*.png、.9.png、.jpg、*.gif等)。此外,也可能编译成各种Drawable对象的XML文件。
/res/mipmap/主要存放适应不同屏幕分辨率的应用程序图标,以及其他系统保留的Drawable资源
/res/layout/存放各种用户界面的布局文件
/res/menu/存放为应用程序定义各种菜单的资源,包括选项菜单、子菜单、上下文菜单资源
/res/raw/存放任意类型的原生资源(比如音频文件、视频文件等)。在Java或Kotlin代码中可通过调用Resources对象的openRawResource(int id)方法来获取该资源的二进制输入流。实际上,如果应用程序需要使用原生资源,也可把这些原生资源保存到assets目录下,然后在应用程序中使用AssetManager来访问这些资源。
/res/values/存放各种简单值的XML文件。这些简单值包括字符串值、整数值、颜色值、数组等。这些资源文件的根元素都是<resources>,为该<resources>元素添加不同的子元素则代表不同的资源。
/res/xml/存放任意的原生XML文件。这些XML文件可以在Java或Kotlin代码中使用Resources.getXML()方法进行访问。

提示:

  • /res/drawable/res/mipmap子目录都可针对不同的分辨率建立对应的子目录,比如drawable-ldpi(低分辨率)、drawable-mdpi(中等分辨率)、drawable-hdpi(高分辨率)、drawable-xhdpi(超高分辨率)、drawable-xxhdpi(超超高分辨率)等子目录。这种做法可以让系统根据屏幕分辨率来选择对应子目录下的图片。如果开发时为所有分辨率的屏幕提供的是同一张图片,则可直接将该图片放在drawable目录下。

6.1.2 使用资源

在Android应用中使用资源可分为在Java或Kotlin代码和XML文件中使用资源,其中Java或Kotlin程序用于为Android应用定义四大组件,而XML文件则用于为Android应用定义各种资源。

1. 在源程序中使用资源清单项

由于Android SDK会在编译应用时在R类中为/res/目录下所有资源创建索引项,因此在Java或Kotlin代码中访问资源主要通过R类来完成。其完整的语法格式为:

[<package_name>.]R.<resource_type>.<resource_name>
  • <resource_type>:R类中代表不同资源类型的子类,例如string代表字符串资源。
  • <resource_name>:指定资源的名称。该资源名称可能是无后缀的文件名(如图片资源),也可能是XML资源元素中由android:name属性所指定的名称。
  • <package_name>:指定R类所在包,实际上就是使用全限定类名。当然,如果在源程序中导入R类所在包,就可以省略包名。

例如如下代码片段:

// 从drawable资源中加载图片,并设为该窗口的背景
window.setBackgroundDrawableResource(R.drawable.back);

// 从string资源中获取指定字符串资源,并设置该窗口的标题
window.setTitle(resources.getText(R.string.main_title));

// 获取指定的TextView组件,并设置该组件显示string资源中的指定字符串资源
TextView msg = findViewById(R.id.msg);
msg.setText(R.string.hello_message);
2. 在源代码中访问实际资源

R资源清单类为所有的资源都定义了一个资源清单项,但这个清单项只是一个int类型的值,并不是实际的资源对象。在大部分情况下,Android应用的API允许直接使用int类型的资源清单项代替应用资源。

但有些时候,程序也需要使用实际的Android资源,为了通过资源清单项来获取实际资源,可以借助于Android提供的Resources类。

提示:
笔者把Resources类称为“Android资源访问总管家”,Resources类提供了大量的方法来根据资源清单ID获取实际资源。

Resources主要提供了如下两类方法:

  • getXxx(int id):根据资源清单ID来获取实际资源。
  • getAssets():获取访问/assets/目录下资源的AssetManager对象。

ResourcesContext调用getResources()方法来获取。

下面的代码片段示范了如何通过Resources获取实际字符串资源。

// 直接调用Activity的getResources()方法来获取Resources对象
Resources res = getResources();
// 获取字符串资源
String mainTitle = res.getText(R.string.main_title).toString();
// 获取Drawable资源
Drawable logo = res.getDrawable(R.drawable.logo);
// 获取数组资源
int[] arr = res.getIntArray(R.array.books);
3. 在XML文件中使用资源

当定义XML资源文件时,其中的XML元素可能需要指定不同的值,这些值就可设置为已定义的资源项。在XML代码中使用资源的完整语法格式为:

@[<package_name>:]<resource_type>/<resource_name>
  • <package_name>:指定资源类所在应用的包。如果所引用的资源和当前资源位于同一个包下,则<package_name>可以省略。
  • <resource_type>:R类中代表不同资源类型的子类。
  • <resource_name>:指定资源的名称。该资源名称可能是无后缀的文件名(如图片资源),也可能是XML资源元素中由android:name属性所指定的名称。

下面将会对各种资源分别进行详细阐述。

6.2 字符串、颜色、尺寸资源

字符串资源、颜色资源、尺寸资源,它们对应的XML文件都将位于/res/values/目录下,它们默认的文件名以及在R类中对应的内部类如表6.2所示。

表6.2 字符串、颜色、尺寸资源表

资源类型资源文件的默认名对应于R类中的内部类的名称
字符串资源/res/values/strings.xmlR.string
颜色资源/res/values/colors.xmlR.color
尺寸资源/res/values/dimens.xmlR.dimen

6.2.1 颜色值的定义

Android中的颜色值是通过红(Red)、绿(Green)、蓝(Blue)三原色以及一个透明度(Alpha)值来表示的,颜色值总是以井号(#)开头,接下来就是Alpha-Red-Green-Blue的形式。其中Alpha值可以省略,如果省略了Alpha值,那么该颜色默认是完全不透明的。

Android颜色值支持常见的4种形式:

  • #RGB:分别指定红、绿、蓝三原色的值(只支持0~f这16级颜色)来代表颜色。
  • #ARGB:分别指定红、绿、蓝三原色的值(只支持0f这16级颜色)及透明度(只支持0f这16级透明度)来代表颜色。
  • #RRGGBB:分别指定红、绿、蓝三原色的值(支持00~ff这256级颜色)来代表颜色。
  • #AARRGGBB:分别指定红、绿、蓝三原色的值(支持00ff这256级颜色)以及透明度(支持00ff这256级透明度)来代表颜色。

在这四种形式中,A、R、G、B都代表一个十六进制的数,其中A代表透明度,R代表红色数值,G代表绿色数值,B代表蓝色数值。

6.2.2 定义字符串、颜色、尺寸资源文件

字符串资源文件位于/res/values/目录下,字符串资源文件的根元素是<resources>,该元素里每个<string>子元素定义一个字符串常量,其中<string>元素的name属性指定该常量的名称,<string>元素开始标签和结束标签之间的内容代表字符串值,如以下代码所示:

<string name="hello">Hello World, ValuesResTest!</string>

如下文件是该示例的字符串资源文件:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">字符串、颜色、尺寸资源</string>
    <string name="c1">F00</string>
    <string name="c2">0F0</string>
    <string name="c3">00F</string>
    <string name="c4">0FF</string>
    <string name="c5">F0F</string>
    <string name="c6">FF0</string>
    <string name="c7">07F</string>
    <string name="c8">70F</string>
    <string name="c9">F70</string>
</resources>

颜色资源文件位于/res/values/目录下,颜色资源文件的根元素是<resources>,该元素里每个<color>子元素定义一个颜色常量,其中<color>元素的name属性指定该颜色的名称,<color>元素开始标签和结束标签之间的内容代表颜色值,如以下代码所示:

<!--定义一个颜色,名称为c1,颜色为红色-->
<color name="c1">#F00</color>

如下文件是该示例的颜色资源文件:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="c1">#F00</color>
    <color name="c2">#0F0</color>
    <color name="c3">#00F</color>
    <color name="c4">#0FF</color>
    <color name="c5">#F0F</color>
    <color name="c6">#FF0</color>
    <color name="c7">#07F</color>
    <color name="c8">#70F</color>
    <color name="c9">#F70</color>
</resources>

尺寸资源文件位于/res/values/目录下,尺寸资源文件的根元素是<resources>,该元素里每个<dimen>子元素定义一个尺寸常量,其中<dimen>元素的name属性指定该尺寸的名称,<dimen>元素开始标签和结束标签之间的内容代表尺寸值,如以下代码所示:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 定义 Gridview组件中每个单元格的宽度、高度 -->
    <dimen name="spacing">8dp</dimen>
    <dimen name="cell_width">60dp</dimen>
    <dimen name="cell_height">66dp</dimen>
    <dimen name="title_font_size">18sp</dimen>
</resources>

上面三份资源文件分别定义了字符串、颜色、尺寸资源,应用程序接下来既可在XML文件中使用这些资源,也可在Java或Kotlin代码中使用这些资源。

6.2.3 使用字符串、颜色、尺寸资源

正如前面所介绍的,在XML文件中使用资源按如下语法格式:

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

下面程序的界面布局中大量使用了前面定义的资源:

<?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"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <!-- 使用字符串资源、尺寸资源 -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="@string/app_name"
        android:textSize="@dimen/title_font_size" />

    <!-- 定义一个GridView组件,使用尺寸资源中定义的长度来指定水平间距、垂直间距 -->
    <GridView
        android:id="@+id/grid01"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:numColumns="3"
        android:horizontalSpacing="@dimen/spacing"
        android:verticalSpacing="@dimen/spacing" />
</LinearLayout>

上面程序中的代码就是使用字符串资源、尺寸资源的代码。

在Java或Kotlin代码中使用资源按如下语法格式:

[<package_name>.]R.<resource_type>.<resource_name>

下面的Activity代码同时使用了上面定义的三种资源:

public class MainActivity extends Activity {
    // 使用字符串资源
    int[] textIds = new int[]{R.string.c1, R.string.c2, R.string.c3, R.string.c4, R.string.c5, R.string.c6, R.string.c7, R.string.c8, R.string.c9};
    // 使用颜色资源
    int[] colorIds = new int[]{R.color.c1, R.color.c2, R.color.c3, R.color.c4, R.color.c5, R.color.c6, R.color.c7, R.color.c8, R.color.c9};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        GridView grid = findViewById(R.id.grid01);
        // 创建一个BaseAdapter对象
        BaseAdapter ba = new BaseAdapter() {
            // 重写该方法,该方法返回的View 将作为GridView的每个格子
            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                TextView tv;
                if (convertView == null) {
                    tv = new TextView(MainActivity.this);
                } else {
                    tv = (TextView) convertView;
                }
                Resources res = MainActivity.this.getResources();
                // 使用尺寸资源来设置文本框的高度、宽度
                tv.setWidth((int) res.getDimension(R.dimen.cell_width));
                tv.setHeight((int) res.getDimension(R.dimen.cell_height));
                // 使用字符串资源设置文本框的内容
                tv.setText(textIds[position]);
                // 使用颜色资源来设置文本框的背景色
                tv.setBackgroundResource(colorIds[position]);
                tv.setTextSize

(res.getInteger(R.integer.font_size));
                return tv;
            }
        };
        // 为GridView 设置Adapter
        grid.setAdapter(ba);
    }
}

上面程序中的代码分别使用了前面定义的字符串资源、颜色资源和尺寸资源。运行上面的程序,将可以看到如图6.1所示的界面。

图6.1 使用字符串、颜色、尺寸资源

与定义字符串资源类似的是,Android也允许使用资源文件来定义boolean常量。例如,在/res/values/目录下增加一个bools.xml文件,该文件的根元素也是<resources>,根元素内通过<bool>子元素来定义boolean常量,示例如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <bool name="is_male">true</bool>
    <bool name="is_big">false</bool>
</resources>

一旦在资源文件中定义了如上所示的资源文件之后,在Java或Kotlin代码中按如下语法格式访问:

[<package_name>.]R.bool.<bool_name>

在XML文件中按如下格式即可访问资源:

@[<package_name>:]bool/<bool_name>

例如,为了在Java代码中获取指定boolean变量的值,可通过如下代码来实现:

Resources res = getResources();
boolean isMale = res.getBoolean(R.bool.is_male);

与定义字符串资源类似,Android也允许使用资源文件来定义整型常量。例如,在/res/values/目录下增加一个integers.xml文件,该文件的根元素也是<resources>,根元素内通过<integer>子元素来定义整型常量,示例如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="my_size">32</integer>
    <integer name="book_numbers">12</integer>
</resources>

在Java或Kotlin代码中按如下语法格式访问:

[<package_name>.]R.integer.<integer_name>

在XML文件中按如下格式即可访问资源:

@[<package_name>:]integer/<integer_name>

例如,为了在Java代码中获取指定整型变量的值,可通过如下代码来实现:

Resources res = getResources();
int mySize = res.getInteger(R.integer.my_size);

6.3 数组(Array)资源

在之前的示例中,数组是在Java源代码中定义的。然而,Android并不推荐在程序源代码中定义数组,因为Android允许通过资源文件来定义数组资源。

Android采用位于/res/values/目录下的arrays.xml文件来定义数组资源。定义数组时,XML资源文件的根元素是<resources>,该元素内可包含如下三种子元素:

  • <array>:定义普通类型的数组,例如Drawable数组。
  • <string-array>:定义字符串数组。
  • <integer-array>:定义整型数组。

在资源文件中定义了数组资源后,可以在Java或Kotlin程序中通过如下形式来访问资源:

[<package_name>.]R.array.<array_name>

在XML文件中则可通过如下形式进行访问:

@[<package_name>:]array/<array_name>

为了在Java或Kotlin程序中访问到实际数组,Resources类提供了如下方法:

  • String[] getStringArray(int id):根据资源文件中字符串数组资源的名称来获取实际的字符串数组。
  • int[] getIntArray(int id):根据资源文件中整型数组资源的名称来获取实际的整型数组。
  • TypedArray obtainTypedArray(int id):根据资源文件中普通数组资源的名称来获取实际的普通数组。TypedArray代表一个通用类型的数组,该类提供了getXxx(int index)方法来获取指定索引处的数组元素。

以下是示例的数组资源文件:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 定义一个Drawable数组 -->
    <array name="plain_arr">
        <item>@color/c1</item>
        <item>@color/c2</item>
        <item>@color/c3</item>
        <item>@color/c4</item>
        <item>@color/c5</item>
        <item>@color/c6</item>
        <item>@color/c7</item>
        <item>@color/c8</item>
        <item>@color/c9</item>
    </array>
    <!-- 定义字符串数组 -->
    <string-array name="string_arr">
        <item>@string/c1</item>
        <item>@string/c2</item>
        <item>@string/c3</item>
        <item>@string/c4</item>
        <item>@string/c5</item>
        <item>@string/c6</item>
        <item>@string/c7</item>
        <item>@string/c8</item>
        <item>@string/c9</item>
    </string-array>
    <!-- 定义书籍字符串数组 -->
    <string-array name="books">
        <item>疯狂Java讲义</item>
        <item>疯狂前端开发讲义</item>
        <item>疯狂Android讲义</item>
    </string-array>
</resources>

在定义了上面的数组资源之后,既可在XML文件中使用这些数组资源,也可在Java程序中使用这些数组资源。例如,如下界面布局文件中定义了一个ListView组件,并将android:entries属性值指定为一个数组。

界面布局文件代码如下:

<?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"
    android:gravity="center_horizontal"
    android:orientation="vertical">
    <!--省略其他组件定义 -->
    <!-- 定义ListView组件,使用了数组资源 -->
    <ListView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:entries="@array/books" />
</LinearLayout>

接下来在Java程序中使用资源文件中定义的数组,程序代码如下:

public class MainActivity extends Activity {
    // 获取系统定义的数组资源
    private String[] texts;
    private TypedArray icons;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        texts = getResources().getStringArray(R.array.string_arr);
        icons = getResources().obtainTypedArray(R.array.plain_arr);

        // 创建一个BaseAdapter对象
        BaseAdapter ba = new BaseAdapter() {
            @Override
            public int getCount() {
                return texts.length;
            }

            @Override
            public Object getItem(int position) {
                return null;
            }

            @Override
            public long getItemId(int position) {
                return 0;
            }

            // 重写该方法,该方法返回的View将作为GridView的每个格子
            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                TextView tv;
                if (convertView == null) {
                    tv = new TextView(MainActivity.this);
                } else {
                    tv = (TextView) convertView;
                }
                Resources res = MainActivity.this.getResources();
                // 使用尺寸资源来设置文本框的高度、宽度
                tv.setWidth((int) res.getDimension(R.dimen.cell_width));
                tv.setHeight((int) res.getDimension(R.dimen.cell_height));
                // 使用字符串资源设置文本框的内容
                tv.setText(texts[position]);
                // 使用颜色资源来设置文本框的背景色
                tv.setBackground(icons.getDrawable(position));
                tv.setTextSize(20f);
                return tv;
            }
        };

        GridView grid = findViewById(R.id.grid01);
        // 为GridView设置Adapter
        grid.setAdapter(ba);
    }
}

上面程序中的代码就是使用数组资源的关键代码。运行上面的程序,将看到如图6.2所示的结果。

图6.2 使用数组资源

6.4 使用Drawable资源

Drawable资源是Android应用中使用最广泛且灵活的资源之一。它不仅可以直接使用诸如.png.jpg.gif.9.png等图片格式,还可以通过多种XML文件定义资源。这些资源文件会被系统编译成Drawable类的对象,因此它们也被称为Drawable资源。

通常,Drawable资源保存在/res/drawable目录下,或者针对不同的屏幕分辨率保存在/res/drawable-ldpi/res/drawable-mdpi/res/drawable-hdpi/res/drawable-xhdpi等子目录下。

6.4.1 图片资源

图片资源是最简单的Drawable资源。将.png.jpg.gif等格式的图片放入/res/drawable-xxx目录下后,Android SDK会在编译应用时自动加载这些图片,并在R资源清单类中生成对应的资源索引。

注意: Android要求图片资源的文件名必须符合Java或Kotlin标识符的命名规则,否则Android SDK无法为该图片在R类中生成资源索引。

在Activity类中使用图片资源时,可以通过如下语法格式来访问该资源:

[<package_name>.]R.drawable.<file_name>

在XML文件中则按如下语法格式来访问该资源:

@[<package_name>:]drawable/<file_name>

要在程序中获得实际的Drawable对象,可以使用Resources类提供的getDrawable(int id)方法,根据资源的ID获取实际的Drawable对象。

6.4.2 StateListDrawable资源

StateListDrawable用于组织多个Drawable对象。当使用StateListDrawable作为目标组件的背景或前景图片时,StateListDrawable对象所显示的Drawable对象会根据目标组件的状态自动切换。

定义StateListDrawable对象的XML文件的根元素为<selector>,该元素可以包含多个<item>元素,这些元素可指定如下属性:

  • android:colorandroid:drawable:指定颜色或Drawable对象。
  • android:state_xxx:指定一个特定状态。

以下是一个简单的StateListDrawable XML文件示例:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 指定获得焦点时的颜色 -->
    <item android:state_focused="true" android:color="#144" />
    <!-- 指定失去焦点时的颜色 -->
    <item android:state_focused="false" android:color="#ccf" />
</selector>

StateListDrawable<item>元素支持多种状态,常见的状态如表6.3所示。

实例:高亮显示正在输入的文本框

在这个实例中,我们将使用StateListDrawable资源来动态改变文本框的文字颜色。通过定义一个Drawable资源文件,可以在文本框获取焦点时显示一种颜色,失去焦点时显示另一种颜色。

定义的Drawable资源文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 指定获得焦点时的颜色 -->
    <item android:state_focused="true" android:color="#144" />
    <!-- 指定失去焦点时的颜色 -->
    <item android:state_focused="false" android:color="#ccf" />
</selector>

接下来,在界面布局文件中使用该Drawable资源:

<?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"
    android:orientation="vertical">
    <!-- 使用 StateListDrawable资源 -->
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="@drawable/my_image" />
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="@drawable/my_image" />
</LinearLayout>

这个程序运行时,当用户在文本框中输入内容时,文字颜色会动态改变。

通过使用StateListDrawable,不仅可以让文本框里的文字颜色随文本框状态的改变而切换,还可以让按钮的背景图片随按钮状态的改变而切换。StateListDrawable的功能非常灵活,它可以让各种组件的背景、前景随状态的改变而切换。

6.4.3 LayerDrawable资源

LayerDrawable类似于StateListDrawable,它也可以包含一个Drawable数组。系统会按照这些Drawable对象的数组顺序绘制它们,索引最大的Drawable对象将会被绘制在最上面。

定义LayerDrawable的XML文件

定义LayerDrawable对象的XML文件的根元素为<layer-list>,该元素可以包含多个<item>元素,这些元素可指定如下属性:

  • android:drawable:指定作为LayerDrawable元素之一的Drawable对象。
  • android:id:为该Drawable对象指定一个标识。
  • android:bottom | android:top | android:left | android:right:用于指定一个长度值,用于将该Drawable对象绘制到目标组件的指定位置。

以下是一个简单的LayerDrawable XML文件示例:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 指定一个Drawable元素 -->
    <item android:id="@android:id/background"
          android:drawable="@drawable/grow" />
</layer-list>

实例:定制拖动条的外观

在这个实例中,我们将使用LayerDrawable来定制SeekBar的外观。通过定义一个LayerDrawable资源文件,可以改变SeekBar轨道的外观及其已完成部分的Drawable对象。

定义如下的Drawable资源文件:

<?xml version="1.0" encoding="UTF-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 定义轨道的背景 -->
    <item android:id="@android:id/background"
          android:drawable="@drawable/grow" />
    <!-- 定义轨道上已完成部分的外观 -->
    <item android:id="@android:id/progress"
          android:drawable="@drawable/ok" />
</layer-list>

另一个LayerDrawable对象定义如下:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <bitmap android:gravity="center"
                android:src="@drawable/ic_logo" />
    </item>
    <item android:left="40dp" android:top="40dp">
        <bitmap android:gravity="center"
                android:src="@drawable/ic_logo" />
    </item>
    <item android:left="80dp" android:top="80dp">
        <bitmap android:gravity="center"
                android:src="@drawable/ic_logo" />
    </item>
</layer-list>

上面的代码定义了三个“层叠”在一起的Drawable对象。接下来,在界面布局中使用my_bar.xml定义的Drawable对象来改变SeekBar的外观,并通过ImageView显示layer_logoDrawable组件。

布局文件如下:

<?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"
    android:orientation="vertical">
    <!-- 定义一个拖动条,并改变轨道外观 -->
    <SeekBar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="100"
        android:progressDrawable="@drawable/my_bar" />
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/layer_logo" />
</LinearLayout>

该程序的代码无需任何改变,只需加载并显示上述界面布局文件即可。运行该程序,将看到如图6.3所示的界面。

图6.3 使用LayerDrawable资源

6.4.4 ShapeDrawable资源

ShapeDrawable 用于定义一个基本的几何图形(如矩形、圆形、线条等)。定义 ShapeDrawable 的 XML 文件的根元素是 <shape> 元素,该元素可以指定以下属性:

  • android:shape=["rectangle" | "oval" | "line" | "ring"]:指定定义哪种类型的几何图形。

定义 ShapeDrawable 对象的完整语法格式如下:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape=["rectangle" | "oval" | "line" | "ring"]>
    <!-- 定义几何图形的四个角的弧度 -->
    <corners
        android:radius="integer"
        android:topLeftRadius="integer"
        android:topRightRadius="integer"
        android:bottomLeftRadius="integer"
        android:bottomRightRadius="integer" />
    <!-- 定义使用渐变色填充 -->
    <gradient
        android:centerX="integer"
        android:angle="integer"
        android:centerY="integer"
        android:centerColor="integer"
        android:endColor="color"
        android:gradientRadius="integer"
        android:startColor="color"
        android:type=["linear" | "radial" | "sweep"]
        android:usesLevel=["true" | "false"] />
    <!-- 定义几何形状的内边距 -->
    <padding
        android:left="integer"
        android:top="integer"
        android:right="integer"
        android:bottom="integer" />
    <!-- 定义几何形状的大小 -->
    <size
        android:width="integer"
        android:height="integer" />
    <!-- 定义使用单种颜色填充 -->
    <solid
        android:color="color" />
    <!-- 定义为几何形状绘制边框 -->
    <stroke
        android:width="integer"
        android:color="color"
        android:dashWidth="integer"
        android:dashGap="integer" />
</shape>

实例:椭圆形、渐变背景的文本框

在这个实例中,我们将使用 ShapeDrawable 资源为文本框指定背景,并实现各种形状和渐变效果。

首先,定义一个简单的矩形 ShapeDrawable 资源文件:

<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <!-- 设置填充颜色 -->
    <solid android:color="#fff" />
    <!-- 设置四周的内边距 -->
    <padding android:left="7dp"
        android:top="7dp"
        android:right="7dp"
        android:bottom="7dp" />
    <!-- 设置边框 -->
    <stroke android:width="3dp" android:color="#ff0" />
</shape>

接下来定义一个带有渐变效果的 ShapeDrawable 资源:

<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <!-- 定义填充渐变颜色 -->
    <gradient
        android:startColor="#FFFF0000"
        android:endColor="#80FF00FF"
        android:angle="45" />
    <!-- 设置内填充 -->
    <padding android:left="7dp"
        android:top="7dp"
        android:right="7dp"
        android:bottom="7dp" />
    <!-- 设置圆角矩形 -->
    <corners android:radius="8dp" />
</shape>

最后,定义一个椭圆形的 ShapeDrawable 资源:

<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <!-- 定义填充渐变颜色 -->
    <gradient
        android:startColor="#f00"
        android:endColor="#00f"
        android:type="sweep"
        android:angle="45" />
    <!-- 设置内填充 -->
    <padding android:left="7dp"
        android:right="7dp"
        android:top="7dp"
        android:bottom="7dp" />
</shape>

在定义了上面的 ShapeDrawable 资源之后,接下来在界面布局文件中使用这些资源作为文本框的背景:

<?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"
    android:orientation="vertical">
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/my_shape_1" />
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/my_shape_2" />
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/my_shape_3" />
</LinearLayout>

运行程序,将看到如图6.4所示的界面,显示了带有不同 ShapeDrawable 背景的文本框。

图6.4 使用 ShapeDrawable资源

6.4.5 ClipDrawable资源

ClipDrawable 代表从其他位图上截取的一个“图片片段”。在 XML 文件中定义 ClipDrawable 对象使用 <clip> 元素,该元素的语法格式如下:

<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="drawable_resource"
    android:clipOrientation="horizontal_or_vertical"
    android:gravity="alignment" />

上面的语法格式中可指定如下三个属性:

  • android:drawable:指定截取的源 Drawable 对象。
  • android:clipOrientation:指定截取方向,可以设置为水平截取或垂直截取。
  • android:gravity:指定截取时的对齐方式。

使用 ClipDrawable 对象时可以调用 setLevel(int level) 方法来设置截取的区域大小。当 level 为 0 时,截取的图片片段为空;当 level 为 10000 时,截取整张图片。

实例:徐徐展开的风景

本实例展示了如何通过 ClipDrawable 对象实现图片徐徐展开的效果。程序先定义如下 ClipDrawable 对象:

<?xml version="1.0" encoding="UTF-8"?>
<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:clipOrientation="horizontal"
    android:drawable="@drawable/shuangta"
    android:gravity="center" />

上面的程序控制从中间开始截取图片,截取方向为水平截取。接下来通过一个定时器来定期修改 ClipDrawable 对象的 level,实现图片徐徐展开的效果。相关代码如下:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ImageView imageView = findViewById(R.id.image);
        final ClipDrawable drawable = (ClipDrawable) imageView.getDrawable();

        class MyHandler extends Handler {
            @Override
            public void handleMessage(Message msg) {
                if (msg.what == 0x1233) {
                    drawable.setLevel(drawable.getLevel() + 200);
                }
            }
        }

        final Handler handler = new MyHandler();
        final Timer timer = new Timer();

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                Message msg = new Message();
                msg.what = 0x1233;
                handler.sendMessage(msg);
                if (drawable.getLevel() >= 10000) {
                    timer.cancel();
                }
            }
        }, 0, 300);
    }
}

运行上面的程序,将看到如图6.5所示的效果,图片将从中间向两侧徐徐展开,类似于一个图片进度条的效果。

图6.5 使用 ClipDrawable资源

通过这种 ClipDrawable 实现的图片进度条,可以在用户界面中实现视觉效果更好的进度指示功能。

6.4.6 AnimationDrawable 资源

AnimationDrawable 代表一个逐帧动画,这种动画通过逐帧切换不同的图片来实现动态效果。Android 既支持传统的逐帧动画,也支持补间动画(通过平移、缩放、旋转等变换实现)。本节将介绍如何定义和使用 AnimationDrawable 资源。

定义 AnimationDrawable 资源

在 Android 中,可以使用 XML 文件定义一个逐帧动画,其根元素为 <animation-list>,该元素可以包含多个 <item> 元素,每个 <item> 元素指定一帧图片及其显示时间。定义的动画资源文件应该放在 /res/drawable 目录下。

XML 资源文件示例:

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item android:drawable="@drawable/frame1" android:duration="50"/>
    <item android:drawable="@drawable/frame2" android:duration="50"/>
    <item android:drawable="@drawable/frame3" android:duration="50"/>
    <item android:drawable="@drawable/frame4" android:duration="50"/>
</animation-list>

上面的资源文件定义了一个逐帧动画,其中 android:oneshot 属性决定动画是否只播放一次。android:drawable 属性指定每一帧的图片资源,android:duration 属性指定每帧显示的时间(单位为毫秒)。

在 Java 代码中使用 AnimationDrawable

在 Java 或 Kotlin 代码中,可以通过以下步骤来使用 AnimationDrawable 资源:

  1. 在布局文件中定义一个 ImageView,用于显示动画。
  2. 在代码中使用 ImageViewgetDrawable() 方法获取 AnimationDrawable 对象。
  3. 调用 start() 方法开始播放动画,调用 stop() 方法停止动画。

示例代码:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ImageView imageView = findViewById(R.id.image);
        imageView.setBackgroundResource(R.drawable.my_animation);

        AnimationDrawable animationDrawable = (AnimationDrawable) imageView.getBackground();
        animationDrawable.start();
    }
}

在上面的代码中,首先通过 setBackgroundResource() 方法将定义的动画资源设置为 ImageView 的背景,然后获取该背景资源并强制转换为 AnimationDrawable 类型,最后通过 start() 方法开始播放动画。

补间动画

补间动画是通过平移、缩放、旋转、渐变等变换来实现动画效果的。可以通过在 XML 中定义 <set> 元素以及 <alpha><scale><translate><rotate> 等子元素来设置动画效果。

定义补间动画

定义补间动画的 XML 文件如下:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/linear_interpolator"
    android:duration="5000">
    <scale
        android:fromXScale="1.0"
        android:toXScale="1.4"
        android:fromYScale="1.0"
        android:toYScale="0.6"
        android:pivotX="50%"
        android:pivotY="50%"
        android:fillAfter="true"
        android:duration="2000"/>
    <translate
        android:fromXDelta="10"
        android:toXDelta="130"
        android:fromYDelta="30"
        android:toYDelta="-80"
        android:duration="2000"/>
</set>

上面的 XML 文件定义了一个动画资源,其中包含了缩放和位移两种变换。通过设置 android:fillAfter="true",动画结束后将保留最终的状态。

在 Java 代码中使用上述定义的动画资源:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ImageView imageView = findViewById(R.id.image);
        Animation animation = AnimationUtils.loadAnimation(this, R.anim.my_anim);
        imageView.startAnimation(animation);
    }
}

上述代码中,通过 AnimationUtils.loadAnimation() 方法加载动画资源,并调用 startAnimation() 方法来启动动画。

结果展示

运行以上代码,ImageView 中的图片将根据定义的补间动画效果进行缩放和平移,直到动画结束。补间动画可用于实现丰富的动画效果,提升用户界面的交互性和动态表现。

总结

AnimationDrawable 和补间动画是 Android 中两种重要的动画资源类型,它们可以在用户界面中实现动态的视觉效果。逐帧动画适用于简单的图片切换,而补间动画则可以用来实现复杂的图形变换。通过合理使用这些动画资源,可以为 Android 应用程序提供更加生动和吸引人的用户体验。

本来Android的API文档中说明可以在<alpha…/>、<scale…/> 、<translate…/> 、<rotate…/>等元素中指定android:fillAfter为true来实现这个效果,但实际上要为<set…/>设置android:fillAfter为true 才可以。详始品类图6.6 使用AnimationDrawable资源

6.5 属性动画 (Property Animation) 资源

Animator 代表一个属性动画,是 Android 动画系统的一部分。它主要通过操作对象的属性来实现动画效果。Animator 本身是一个抽象类,常用的子类包括 AnimatorSetValueAnimatorObjectAnimatorTimeAnimator。在本节中,我们将介绍如何定义属性动画资源,并通过简单的实例展示如何使用这些资源。

定义属性动画的 XML 文件

在 Android 中,可以使用 XML 文件来定义属性动画,这些文件通常位于 /res/animator 目录下。定义属性动画的 XML 文件可以以以下三个元素中的任意一个作为根元素:

  • <set.../>: 定义一个 AnimatorSet 对象,用于组合多个动画。
  • <objectAnimator.../>: 定义一个 ObjectAnimator 动画,直接操作对象的属性。
  • <animator.../>: 定义一个 ValueAnimator 动画,允许开发者自定义动画的属性变化过程。

下面是属性动画的 XML 文件的一般语法格式:

<?xml version="1.0" encoding="utf-8"?>
<set android:ordering="together | sequentially">
    <objectAnimator
        android:propertyName="string"
        android:duration="int"
        android:valueFrom="float | int | color"
        android:valueTo="float | int | color"
        android:startOffset="int"
        android:repeatCount="int"
        android:repeatMode="repeat | reverse"
        android:valueType="intType | floatType"
        android:interpolator="[@package:]anim/interpolator"/>
    
    <animator
        android:duration="int"
        android:valueFrom="float | int | color"
        android:valueTo="float | int | color"
        android:startOffset="int"
        android:repeatCount="int"
        android:repeatMode="repeat | reverse"
        android:valueType="intType | floatType"
        android:interpolator="[@package:]anim/interpolator"/>
</set>
示例:不断渐变的背景色

这个示例将使用属性动画控制组件的背景色不断渐变。以下是定义的属性动画 XML 文件,它位于 /res/animator 目录下:

color_anim.xml

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:propertyName="backgroundColor"
    android:duration="3000"
    android:repeatCount="infinite"
    android:repeatMode="reverse"
    android:valueFrom="#FF8080"
    android:valueTo="#8080FF"
    android:valueType="intType" />

在上面的 XML 文件中,<objectAnimator> 定义了一个 ObjectAnimator 对象,用于控制组件的 backgroundColor 属性,动画持续时间为 3000 毫秒,并且会无限次重复,重复模式为反向播放 (reverse),即颜色从 valueFrom 渐变到 valueTo 后,再从 valueTo 渐变回 valueFrom

在 Java 代码中使用属性动画

接下来,我们在 Activity 中加载并应用这个动画资源:

MainActivity.java

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        LinearLayout container = findViewById(R.id.container);
        container.addView(new MyAnimationView(this));
    }

    public class MyAnimationView extends View {
        public MyAnimationView(Context context) {
            super(context);
            // 加载动画资源
            ObjectAnimator colorAnim = (ObjectAnimator) AnimatorInflater.loadAnimator(
                    MainActivity.this, R.animator.color_anim);
            colorAnim.setEvaluator(new ArgbEvaluator());
            // 对该 View 本身应用属性动画
            colorAnim.setTarget(this);
            // 开始动画
            colorAnim.start();
        }
    }
}

在上面的代码中:

  1. MyAnimationView 是一个自定义的 View,在 onCreate() 方法中被添加到 LinearLayout 中。
  2. 使用 AnimatorInflater.loadAnimator() 方法加载定义好的动画资源文件,并转换为 ObjectAnimator 对象。
  3. 通过 setEvaluator() 方法设置 ArgbEvaluator,该评估器将颜色值从 valueFrom 逐渐变为 valueTo
  4. 使用 setTarget() 方法将动画应用于 MyAnimationView 本身。
  5. 最后,通过 start() 方法启动动画。

运行该程序后,你将看到背景颜色在两个定义的颜色之间不断渐变,带来动态的视觉效果。

总结

属性动画是 Android 提供的一个强大而灵活的动画系统,它通过改变对象的属性值来创建动画效果。通过 XML 文件定义和 Java 代码加载,开发者可以方便地在应用中实现复杂的动画效果。这个系统不仅可以改变视图的属性,还可以应用于任何对象,使得 Android 应用程序的界面更加生动和互动。

6.6 使用原始XML资源

在某些时候,Android应用有一些初始化的配置信息、应用相关的数据资源需要保存,一般推荐使用XML文件来保存它们,这种资源就被称为原始XML资源。下面介绍如何定义、获取原始XML资源。

6.6.1 定义原始XML资源

原始XML资源一般保存在 /res/xml/ 路径下,当使用Android Studio创建Android应用时,/res/ 目录下并没有包含xml子目录,开发者应该自行手动创建xml子目录。

接下来Android应用对原始XML资源没有任何特殊的要求,只要它是一份格式良好的XML文档即可。

一旦成功地定义了原始XML资源,接下来在XML文件中就可通过如下语法格式来访问它。

@[<package_name>:] xml/file_name

在Java 或Kotlin代码中则按如下语法格式来访问。

[<package_name>.]R.xml.<file_name>

为了在Java 或Kotlin程序中获取实际的XML文档,可以通过Resources的如下两个方法来实现。

  • XmlResourceParser getXml(int id): 获取XML文档,并使用一个XmlPullParser来解析该XML文档,该方法返回一个解析器对象(XmlResourceParser是XmlPullParser的子类)。
  • InputStream openRawResource(int id): 获取XML文档对应的输入流。

大部分时候都可以直接调用getXml(int id)方法来获取XML文档,并对该文档进行解析。

Android 默认使用内置的Pull解析器来解析XML文件。

6.6.2 使用原始XML文件

下面为示例程序添加一个原始的XML文件,将该XML文件放到 /res/xml 目录下。该XML文件的内容很简单,如下所示。

books.xml

<?xml version="1.0" encoding="UTF-8"?>
<books>
    <book price="109.0" 出版日期="2008年">疯狂Java讲义</book>
    <book price="108.0" 出版日期="2009年">轻量级 Java EE企业应用实战</book>
    <book price="79.0" 出版日期="2009年">疯狂前端开发讲义</book>
</books>

接下来就可以在Activity中获取该XML资源,并解析该XML资源中的信息了。Activity程序如下。

MainActivity.java

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 获取按钮,并为其设置点击事件监听器
        Button bn = findViewById(R.id.bn);
        bn.setOnClickListener(view -> {
            // 获取XML资源的解析器
            XmlResourceParser xrp = getResources().getXml(R.xml.books);
            StringBuilder sb = new StringBuilder();
            try {
                // 解析XML文档
                while (xrp.getEventType() != XmlResourceParser.END_DOCUMENT) {
                    // 遇到开始标签
                    if (xrp.getEventType() == XmlResourceParser.START_TAG) {
                        String tagName = xrp.getName();
                        // 处理 <book> 标签
                        if ("book".equals(tagName)) {
                            String price = xrp.getAttributeValue(null, "price");
                            String pubDate = xrp.getAttributeValue(null, "出版日期");
                            sb.append("价格: ").append(price).append(" 出版日期: ").append(pubDate).append(" 书名: ");
                            sb.append(xrp.nextText()).append("\n");
                        }
                    }
                    // 获取下一个解析事件
                    xrp.next();
                }
                // 显示解析结果
                TextView show = findViewById(R.id.show);
                show.setText(sb.toString());
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
}

上面的程序中包含一个按钮和一个文本框,当用户单击该按钮时,程序将会解析指定XML文档,并把文档中的内容显示出来。

6.7 使用布局 (Layout) 资源

从我们学习第一个Android应用开始,已经开始接触Android的Layout资源了,因此此处不会详述Android的Layout资源的知识,只是对Layout资源进行简单的归纳。

Layout资源文件应放在 /res/layout/ 目录下,Layout资源文件的根元素通常是各种布局管理器,比如 LinearLayoutTableLayoutFrameLayout 等,接着在该布局管理器中定义各种View组件即可。

一旦在Android项目中定义了Layout资源,接下来在XML文件中就可通过如下语法格式来访问它。

@[<package_name>:]layout/<file_name>

在Java 或Kotlin代码中则按如下语法格式来访问。

[<package_name>.]R.layout.<file_name>

6.8 使用菜单 (Menu) 资源

前面已经介绍过Android的菜单支持,并分别介绍了如何使用Java代码来实现菜单和使用XML资源文件来定义菜单。

实际上,Android 推荐使用XML资源文件来定义菜单,这样将会提供更好的解耦。由于前面介绍过如何使用XML资源文件定义菜单,因此此处不再详细介绍菜单资源文件的内容,只是对其进行简单的归纳。

Android 菜单资源文件放在 /res/menu 目录下,菜单资源的根元素通常是 <menu> 元素,<menu> 元素无须指定任何属性。

一旦在Android项目中定义了菜单资源,接下来在XML文件中就可通过如下语法格式来访问它。

@[<package_name>:]menu/<file_name>

在Java 或Kotlin代码中则按如下语法格式来访问。

[<package_name>.]R.menu.<file_name>

6.9 使用样式 (Style) 和主题 (Theme) 资源

样式和主题资源都用于对Android应用进行“美化”,只要充分利用Android应用的样式和主题资源,开发者就可以开发出各种风格的Android应用。

6.9.1 样式资源

在Android开发中,经常需要对某个类型的组件指定大致相似的格式,比如字体、颜色、背景色等。如果每次都为View组件重复指定这些属性,无疑会有大量的工作量,而且不利于项目后期的维护。为了提高效率,Android提供了样式资源的功能。

样式资源文件放在 /res/values/ 目录下,样式资源文件的根元素是 <resources> 元素,该元素内可包含多个 <style> 子元素,每个 <style> 子元素定义一个样式。 <style> 元素指定如下两个属性:

  • name:指定样式的名称。
  • parent:指定该样式所继承的父样式。当继承某个父样式时,该样式将会获得父样式中定义的全部格式。当然,当前样式也可以覆盖父样式中指定的格式。

<style> 元素内可包含多个 <item> 子元素,每个 <item> 子元素定义一个格式项。例如:

<resources>
    <style name="style1">
        <item name="android:textSize">20sp</item>
        <item name="android:textColor">#00d</item>
    </style>
    <style name="style2" parent="style1">
        <item name="android:background">#fee6</item>
        <item name="android:padding">8dp</item>
        <item name="android:textColor">#000</item>
    </style>
</resources>

在上面的样式资源中定义了两个样式,其中第二个样式继承了第一个样式,并且覆盖了父样式中的 textColor 属性。

一旦定义了样式资源之后,可以在XML资源中按如下语法格式来使用样式:

@[<package name>:]style/<file name>
6.9.2 主题资源

与样式资源非常相似,主题资源的XML文件通常也放在 /res/values/ 目录下,主题资源的XML文件同样以 <resources> 元素作为根元素,同样使用 <style> 元素来定义主题。

主题与样式的区别主要体现在:

  • 主题不能作用于单个的View组件,而是对整个应用中的所有Activity起作用,或对指定的Activity起作用。
  • 主题定义的格式应该是改变窗口外观的格式,例如窗口标题、窗口边框等。

例如,定义如下主题资源:

<style name="CrazyTheme">
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowFullscreen">true</item>
    <item name="android:windowFrame">@drawable/window_border</item>
    <item name="android:windowBackground">@drawable/star</item>
</style>

在定义了主题之后,可以在Java代码中使用该主题,例如:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setTheme(R.style.CrazyTheme);
    setContentView(R.layout.activity_main);
}

大部分时候,在 AndroidManifest.xml 文件中为指定应用或指定Activity应用主题更加简单。如果想让应用中全部窗口使用该主题,只需要为 <application> 元素添加 android:theme 属性即可:

<application android:theme="@style/CrazyTheme">
</application>

如果只想让某个Activity拥有这个主题,可以修改 <activity> 元素,通过 android:theme 指定主题即可。

Android中还提供了几种内置的主题资源,这些主题可以通过查询 Android.R.style 类来查看。例如,如果希望某个Activity使用对话框风格的窗口,可以这样定义:

<activity android:theme="android:Theme.Material.Dialog">
</activity>

与样式类似,Android主题同样支持继承。例如:

<style name="CrazyTheme" parent="android:Theme.Material.Dialog">
</style>

上面定义的 CrazyTheme 主题继承了 android:Theme.Material.Dialog 主题,在此基础上可以添加或覆盖某些属性来实现自定义主题。

6.10 属性 (Attribute) 资源

在开发自定义View组件时,如果需要让组件支持在XML布局文件中指定属性,那么可以通过属性资源来实现。这种属性资源的定义和管理能够帮助开发者更方便地控制自定义View组件的外观和行为。

属性资源文件也放在 /res/values/ 目录下,文件的根元素是 <resources>,该元素包含以下两个子元素:

  • <attr>:用于定义一个属性。
  • <declare-styleable>:用于定义一个可声明的对象,每个 styleable 对象是一组 attr 属性的集合。

在定义属性资源后,开发者可以在自定义组件的构造函数中通过 AttributeSet 对象来获取这些属性。

例如,假设开发了一个默认带动画效果的图片组件,该组件需要一个额外的 duration 属性来控制动画的持续时间。首先需要在属性资源文件中定义该属性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 定义一个属性 -->
    <attr name="duration" format="integer"/>
    <!-- 定义一个 styleable 对象来组合多个属性 -->
    <declare-styleable name="AlphaImageView">
        <attr name="duration"/>
    </declare-styleable>
</resources>

在定义了上述属性资源后,可以在自定义的 AlphaImageView 组件中获取并使用该属性,例如:

public class AlphaImageView extends ImageView {
    private static final int SPEED = 300;
    private int alphaDelta;
    private int curAlpha = 0;
    private Timer timer;

    public AlphaImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AlphaImageView);
        int duration = typedArray.getInt(R.styleable.AlphaImageView_duration, 0);
        typedArray.recycle();
        alphaDelta = 255 * SPEED / duration;
        timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                curAlpha += alphaDelta;
                if (curAlpha >= 255) {
                    curAlpha = 255;
                    timer.cancel();
                }
                postInvalidate();
            }
        }, 0, SPEED);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        this.setImageAlpha(curAlpha);
        super.onDraw(canvas);
    }
}

在上面的代码中,通过获取 AlphaImageViewduration 属性,计算了图片透明度的变化幅度,并通过定时器动态改变图片的透明度。

在使用自定义组件 AlphaImageView 时,可以在XML布局文件中为它指定 duration 属性,代码如下:

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

    <!-- 使用自定义组件,并指定属性资源文件中定义的属性 -->
    <org.crazyit.res.AlphaImageView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:src="@drawable/javaee"
        crazyit:duration="8000" />
</LinearLayout>

在运行该程序时,可以看到图片从透明逐渐变为完全显示的效果,这就是通过 duration 属性实现的。

6.11 使用原始资源

除了前面介绍的各种XML文件和图片文件外,Android应用可能还需要用到其他类型的资源,如声音文件等。声音资源对于许多应用来说非常重要,选择合适的音效可以大大提升应用的用户体验。

Android的原始资源可以放在以下两个地方:

  1. /res/raw/ 目录下:Android SDK会处理该目录下的资源,并在R清单类中为这些资源生成索引项。
  2. /assets/ 目录下:该目录下的资源是更彻底的原始资源,应用程序需要通过AssetManager来管理这些资源。

访问原始资源:

  • 对于放在 /res/raw/ 目录下的资源,Android SDK会在R清单类中生成一个索引项。可以在XML文件中通过以下语法格式访问这些资源:

    @[<package_name>:]raw/file_name
    

    在Java或Kotlin代码中,可以通过以下语法格式访问:

    [<package_name>.]R.raw.<file_name>
    

    这样,Android应用可以非常方便地访问 /res/raw/ 目录下的原始资源。

  • 对于放在 /assets/ 目录下的资源,Android应用需要使用 AssetManager 来访问。AssetManager 提供了以下两种常用方法:

    • InputStream open(String fileName): 根据文件名获取原始资源的输入流。
    • AssetFileDescriptor openFd(String fileName): 根据文件名获取原始资源的 AssetFileDescriptor,它代表了原始资源的描述,应用程序可以通过 AssetFileDescriptor 来获取原始资源。

示例:

以下是一个播放声音文件的示例程序。程序中定义了两个按钮:一个用于播放 /res/raw/ 目录下的声音文件,另一个用于播放 /assets/ 目录下的声音文件。

首先,将 bomb.mp3 文件放入 /res/raw/ 目录,并将 shot.mp3 文件放入 /assets/ 目录。Android SDK会自动处理 /res/raw/ 目录下的资源,并在R清单类中为它生成一个索引项 R.raw.bomb

public class MainActivity extends Activity {
    private MediaPlayer mediaPlayer1;
    private MediaPlayer mediaPlayer2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 直接根据声音文件的ID来创建MediaPlayer
        mediaPlayer1 = MediaPlayer.create(this, R.raw.bomb);

        // 获取应用的AssetManager
        AssetManager am = getAssets();
        try {
            // 获取指定文件对应的AssetFileDescriptor
            AssetFileDescriptor afd = am.openFd("shot.mp3");
            mediaPlayer2 = new MediaPlayer();
            // 使用MediaPlayer加载指定的声音文件
            mediaPlayer2.setDataSource(afd.getFileDescriptor());
            mediaPlayer2.prepare();
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 获取第一个按钮,并为它绑定事件监听器
        Button playRaw = findViewById(R.id.playRaw);
        playRaw.setOnClickListener(view -> mediaPlayer1.start()); // 播放声音

        // 获取第二个按钮,并为它绑定事件监听器
        Button playAsset = findViewById(R.id.playAsset);
        playAsset.setOnClickListener(view -> mediaPlayer2.start()); // 播放声音
    }
}

在这个示例中,mediaPlayer1 用于播放 /res/raw/ 目录下的声音文件,而 mediaPlayer2 则利用 AssetManager 来播放 /assets/ 目录下的声音文件。通过这种方式,可以灵活地处理和播放不同位置的声音文件。

6.12 国际化

引入国际化的目的是为了提供自适应、更友好的用户界面。程序国际化指的是同一个应用在不同语言、国家环境下,可以自动呈现出对应的语言等用户界面,从而提供更好的用户体验。

6.12.1 为 Android 应用提供国际化资源

为 Android 程序提供国际化资源非常方便,因为 Android 本身采用了 XML 资源文件来管理所有字符串消息。只要为各消息提供不同国家、语言对应的内容即可。通过前面的介绍我们知道,Android 应用使用 res/values/ 目录下的资源文件来保存程序中用到的字符串消息。为了给这些消息提供不同国家、语言的版本,开发者需要为 values 目录添加几个不同的语言国家版本。不同 values 文件夹的命名方式为:

values-语言代码-r国家代码

例如,如果希望应用支持简体中文和美式英语两种环境,则需要在 res/ 目录下添加 values-zh-rCNvalues-en-rUS 两个目录。

如果希望应用程序的图片也能随国家、语言环境改变,那么还需要为 drawable 目录添加几个不同的语言国家版本。不同 drawable 文件夹的命名方式为:

drawable-语言代码-r国家代码

如果还需要为 drawable 目录按分辨率提供文件夹,则可以在后面追加分辨率后缀,例如:

drawable-zh-rCN-mdpi
drawable-zh-rCN-hdpi
drawable-zh-rCN-xhdpi
drawable-en-rUS-mdpi
drawable-en-rUS-hdpi
drawable-en-rUS-xhdpi

接下来,可以分别在 values-en-rUSvalues-zh-rCN 目录下创建 strings.xml 文件。这些文件将包含不同语言的字符串资源。

例如:

\res\values-en-rUS\strings.xml(美式英语):

<resources>
    <string name="cancel">Cancel</string>
    <string name="ok">OK</string>
    <string name="msg">Hello, Android!</string>
</resources>

\res\values-zh-rCN\strings.xml(简体中文):

<resources>
    <string name="ok">确定</string>
    <string name="cancel">取消</string>
    <string name="msg">你好啊,可爱的小机器人!</string>
</resources>

在不同语言的国际化资源文件中,所有消息的 key 是相同的,只是在不同国家、语言环境下,消息资源 key 对应的 value 不同。

6.12.2 国际化 Android 应用

Android 的设计本身就是国际化的,当开发者在 XML 界面布局文件、Java 代码中加载字符串资源时,Android 的国际化机制就已经在起作用了。

例如,下面是一个界面布局文件:

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

    <TextView
        android:id="@+id/show"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="top"
        android:lines="2" />

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:src="@drawable/logo" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:gravity="center_horizontal">

        <!-- 两个按钮的文本都是通过消息资源指定的 -->
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/ok" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/cancel" />
    </LinearLayout>
</LinearLayout>

在上面的布局文件中,字符串内容并没有硬编码在布局文件中,而是通过资源文件中的字符串值加载,这时 Android 的国际化机制就会起作用。如果系统环境是简体中文,则加载 res/values-zh-rCN/strings.xml 文件中的字符串资源;如果是美式英语环境,则加载 res/values-en-rUS/strings.xml 文件中的字符串资源。

同样,在 Java 或 Kotlin 代码中也可以根据资源 ID 设置字符串内容,而不是以硬编码的方式设置为固定的字符串内容。例如:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView tvShow = findViewById(R.id.show);
        // 设置文本框所显示的文本
        tvShow.setText(R.string.msg);
    }
}

如果将手机设置为美式英语环境(通过 Android 系统的 Settings → System → Language & input → Select language → English (United States) 设置),运行程序将看到美式英语环境下的界面。

如果将手机设置为简体中文环境(通过 Android 系统的 Settings → System → Language & input → Language → 中文(简体) 设置),运行程序将看到简体中文环境下的界面。

通过国际化机制,Android 应用可以根据系统的语言环境自动加载相应的资源文件,从而实现界面语言和图片的自动切换。如果需要对程序标题等内容也进行国际化处理,只需为相应的字符串资源提供不同语言版本即可。

6.13 自适应不同屏幕的资源

开发Android应用的一个挑战是不同设备的屏幕尺寸和分辨率差异很大,而开发者希望应用能够在所有设备上运行良好。因此,开发Android应用时必须考虑如何让应用自适应不同的屏幕。

提示:相比之下,iOS设备的屏幕尺寸和分辨率较为固定,因此开发iOS应用时所需考虑的设备更少。

前面提到,Android默认将 drawable 目录(存放图片等Drawable资源的目录)分为 drawable-ldpidrawable-mdpidrawable-hdpidrawable-xhdpidrawable-xxhdpi 等子目录,正是为了适应不同分辨率的屏幕。

通常来说,屏幕资源需要考虑以下几个方面:

  • 屏幕尺寸:可分为 small(小屏幕)、normal(中等屏幕)、large(大屏幕)、xlarge(超大屏幕)四种。
  • 屏幕分辨率:可分为 ldpi(低分辨率)、mdpi(中等分辨率)、hdpi(高分辨率)、xhdpi(超高分辨率)、xxhdpi(超超高分辨率)五种。
  • 屏幕方向:可分为 land(横屏)和 port(竖屏)两种。

图6.13展示了不同屏幕尺寸、不同分辨率的通用说法。

为不同屏幕适配资源

为不同尺寸的屏幕设置用户界面时,每种用户界面总有一个最低的屏幕尺寸要求。上面这些通用说法中屏幕尺寸的最低分辨率是以 dp 为单位的。因此,定义界面布局时应尽量使用 dp 作为单位。

下面是不同屏幕尺寸所需的最低尺寸:

  • xlarge 屏幕:至少需要 960dp x 720dp
  • large 屏幕:至少需要 640dp x 480dp
  • normal 屏幕:至少需要 470dp x 320dp
  • small 屏幕:至少需要 426dp x 320dp

为了提供自适应不同屏幕的资源,可以采取以下措施:

  • 屏幕分辨率:为 drawable 目录增加后缀 ldpi(低分辨率)、mdpi(中等分辨率)、hdpi(高分辨率)、xhdpi(超高分辨率)、xxhdpi(超超高分辨率),分别为不同分辨率的屏幕提供资源。
  • 屏幕尺寸:为 layoutvalues 等目录增加后缀 smallnormallargexlarge,分别为不同尺寸的屏幕提供相应资源。

从Android 3.2开始,Android建议直接使用真实的屏幕尺寸来定义屏幕尺寸,例如:

  • sw<N>dp:屏幕尺寸至少宽 Ndp 才能使用该资源,例如 layout-sw600dp 表示设备屏幕的宽度大于或等于 600dp 时使用该目录下的布局资源。
  • w<N>dp:屏幕尺寸可用宽度为 Ndp 可使用该资源。
  • h<N>dp:屏幕尺寸可用高度为 Ndp 才能使用该资源。

提示:还可以为 layoutvalues 等目录增加后缀 landport,分别为横屏和竖屏提供相应的资源。

示例:适配不同屏幕的布局

以下是一个示例,在 layout 目录下定义了一个界面布局文件 activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:src="@drawable/a"/>
</RelativeLayout>

xhdpi 设备上(例如 768x1280320dpi),系统将会使用 res/drawable-xhdpi/ 目录下的图片。

xxhdpi 设备上(例如 1080x1920420dpi),系统将会使用 res/drawable-xxhdpi/ 目录下的图片。

示例:适配不同屏幕尺寸的布局

提供了两份布局文件,一份放在 layout-normal 目录下,一份放在 layout-large 目录下。

layout-normal 目录下的 activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="第一个按钮"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="第二个按钮"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="第三个按钮"/>
</LinearLayout>

layout-large 目录下的 activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="第一个按钮"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="第二个按钮"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="第三个按钮"/>
</LinearLayout>

在正常尺寸的屏幕(如 768x1280320dpi)上,系统将加载 layout-normal 目录下的 activity_main.xml 布局文件。

在大尺寸的屏幕(如 720x1280240dpi)上,系统将加载 layout-large 目录下的 activity_main.xml 布局文件。

这表明,屏幕大小不仅与分辨率有关,还与 dpi 有关。dpi 越高,屏幕越小,图像显示效果越细腻。因此,720x1280240dpi 的屏幕比 768x1280320dpi 的屏幕更大,故而Android在 720x1280240dpi 的屏幕上会选择 layout-large 目录下的 activity_main.xml 布局文件。

6.14 本章小结

通过使用资源文件,Android 应用可以将各种字符串、图片、颜色、界面布局等内容交由 XML 文件配置管理,从而避免在 Java 或 Kotlin 代码中直接定义这些内容,减少硬编码带来的问题。本章介绍了 Android 应用资源的存储方式与使用方式,同时详细讲解了字符串资源、颜色资源、尺寸资源、数组资源、图片资源、各种 Drawable 资源、原始 XML 资源、布局资源、菜单资源、样式和主题资源、属性资源、原始资源等多种资源文件的使用。

此外,本章还讨论了 Android 应用的国际化支持及其对不同分辨率屏幕的自适应处理。这些内容在实际开发中至关重要,读者需要认真掌握。

总之,Android 应用资源是实现应用高解耦设计的关键,学习和掌握这些内容将为开发灵活、高效的 Android 应用奠定坚实的基础。

  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值