这节课是 Android 开发(入门)课程 的第二部分《多屏幕应用》的第二节课,导师依然是 Katherine Kuan 和 Jessica Lin,这节课完成了 Miwok App 的以下几点内容:
- Learn about how to store a list of words in the app.(Data Structure: Array, ArrayList)
- Display a list of words.
- Display a list of English/Miwok word pairs.
- Add the words from all the remaining categories.
关键词:数组 (Array),列表 (ArrayList),while & for 循环,ListView 与 ArrayAdapter 实现视图回收,自定义类 (ArrayAdapter)
Array(数组)
数组 (Array) 可以保存一系列变量,并使之保持一定的顺序,就像有七个格子的药盒,数组可以理解成长度固定的容器,每一格存储一个值,所有值必须是相同类型的 (Java is a strongly typed language)。
整个数组有一个名字,数组中的每个单元称为其元素 (element),通过其数值位置 (numerical position,即 indices(索引)) 来访问元素。
// 创建数组:数据类型[] 数组名 = new 数据类型[数组长度];
int[] shoeSizeAvailable = new int[3];
// 数组赋值:数组名[索引号] = 值;
// 注意要输入正确的数据类型
shoeSizeAvailable[0] = 5;
// 数组取值:数组名[索引号];
shoeSizeAvailable[0];
// 获取数组的长度
shoeSizeAvailable.length;
在 Android Studio 中,日志 (Log) 按重要/紧急程度分为 verbose(Log.v) → debug(Log.d) → information(Log.i) → warning(Log.w) → error(Log.e),可以通过不同的 Log 语句打印对应等级的日志信息。
ArrayList(列表)
相比长度固定的 Array,ArrayList(列表)可通过添加和移除元素的指令动态调整大小。与数组不同,ArrayList 是一个类,其元素是对象,所以 ArrayList 只能通过 method 来存取对象(若要存储原始类型数据 (Primitive) 要用到对象封装类 (Object Grabbers))以及其他操作。
// 创建 ArrayList:ArrayList<对象数据类型> 名称 = new ArrayList<对象数据类型>();
ArrayList<String> musicLibrary = new ArrayList<String>();
// 添加和移除 ArrayList 的元素:使用 add 和 remove method 实现
musicLibrary.add(“Thriller”);
// ArrayList 名称.add(索引号, 添加的字符串);
musicLibrary.add(0, “Blue Suede Shoes”);
// 移除索引号为 2 的元素后,索引号为 3 及以上的元素补上,ArrrayList 的大小减一
musicLibrary.remove(2);
// ArrayList 取值:使用 get method 实现
musicLibrary.get(0);
// 获取 ArrayList 的大小:使用 size method 实现
musicLibrary.size();
查看 Android 文档,可以知道 ArrayList 可溯源至 List 接口,关系链为 ArrayList ← AbstractList ← List,如下图所示。

因此,ArrayList 是 List 的一个具象类(List 的其它子类有 LinkedList、Stack、Vector 等),ArrayList 可以使用 List 的 method,例如 add(E e) 和 abstract E remove(int index),留意到 add 的输入数据类型是 E 以及 remove 的返回值类型也是 E,这是 Java 的泛型类型 (Generic Type) 参数,常见的有以下几种。
- E - element
- K - key
- N - number
- T - type
- V - value
- S, U, V, etc - 2nd, 3rd, 4th types.
泛型类型参数与抽象类和接口的概念类似,它是参数化的数据类型,在具体实现时需要指定数据类型,例如 add(E e) 表示处理的是数据集合的元素 (element),它可以是任何非原始数据类型 (如 String)。
因此,ArrayList 也是一种泛型类,其元素可以是自定义对象。也就是说,下面 ArrayList 的元素数据类型 String 可以换成任何自定义对象,在 Miwok App 就是 Word 自定义类。
ArrayList<String> musicLibrary = new ArrayList<String>();
ArrayList<Word> words = new ArrayList<Word>();
使用 Java 添加和设置 Views 。
-
从 API 26 开始,findViewById 返回值类型为 T (A view with given ID if found, or null otherwise),所以不再需要 cast findViewById 的返回值类型;以前 findViewById 返回值类型直接为 View。
LinearLayout rootView = (LinearLayout) findViewById(R.id.rootView); -
在 XML 定义的 View 无需在 Java 中定义。
TextView method 的输入参数为 Context,包括应用主题和其他环境信息。
在从 Context 延伸出 (extends) 的类 (Application, Activity, Service, IntentService classes) ,可以使用getApplicationContext()、getContext()、getBaseContext()、this来获取 context。
若在不含 class extends from Context 的自定义类中,需要传入Context context才行TextView wordView = new TextView(this); -
注意 setText 的输入数据类型
wordView.setText(“some texts”); -
使用 addView method 向 rootView 添加一个 View
rootView.addView(wordView);
while & for 循环语句
- while 循环语句
Setup counter variable;
while(Condition) {
Instruction;
Update counter variable;
}
对于 while 循环语句,在设置计数器变量后,进入 while 循环;首先判断 Condition 是否为真,若真则进入循环执行 Instruction,记得更新计时器变量;执行完后再次判断 Condition,若假则跳出循环。Update counter variable 的简写语句有
index++; // index = index + 1;
index--; // index = index - 1;
index += 3; // index = index + 3;
- for 循环语句
for(Setup counter variable; Condition; Update counter variable;) {Instruction;}
对于 for 循环语句,工作流程与 while 循环相同,不过它将三处代码集合到一个小括号内。
for(String variable: arrays) 专用于遍历数据的所有元素。
ListView 与 ArrayAdapter 实现视图回收
由于内存是非常宝贵的资源,所以 App 要有有效的内存策略:视图回收,即重复使用屏幕上不在可见的视图(以单行为单位,包括 ViewGroups,例如一个 Horizontal 的 LinearLayout),即无需重新创建视图,直接改变 Views 的内容,如 TextView 的 Text,ImageView 的 Image。
这里有一个 Scrap Pile(不可见的视图的存放区)的概念,放入 Scrap Pile 的视图称为 Scrap View,这些视图在修改数据后,会作为新出现的视图显示在屏幕上。
ListView、GridView、RecycleView 等视图都可以与 ArrayAdapter 实现视图回收,这里介绍 ListView 与 ArrayAdapter 的例子。
ListView 由 ArrayAdapter 提供支持 (powered by),没有 ArrayAdapter 的话 ListView 只是一个空容器,ArrayAdapter 会决定在屏幕上显示的数据集。
具体的工作流程如下。
ListView 向 ArrayAdapter 询问 Array 有几个元素,ArrayAdapter 会查询 (getView);
ListView 对 ArrayAdapter 发送当前 Array 的索引位置,ArrayAdapter 查看 Array 的数据,并向 ListView 说明如何显示列表;
当屏幕上显示完全后,ListView 停止向 ArrayAdapter 寻求更多的列表项,此时显示在屏幕上的视图才会创建;用户划动屏幕,一些视图将不再出现,这些 Scrap Views 会放到 Scrap Pile 中,需要显示新的列表项时 Scrap Views 会返回到 ArrayAdapter 中,此时 ListView 会请求要显示位置的视图以及之前显示过的视图(在 Scrap Pile 中的 Reusable View),ArrayAdapter 就把数据放入显示过的视图中,并把重新使用的视图放到新显示的视图中。
这就实现了整个视图回收的过程。

目前为止,可以把 ListView 和 ArrayAdapter 分成 User Interface 和 Data Model 两部分来看,所以存在同一个 ArrayAdapter 关联不同的 ListView 或 GridView 或 Spinner 仍可工作的情况,这就是适配器模式。
下面来看 ListView 与 ArrayAdapter 的代码实例。
ListView
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />
- 列表方向由
android:orientation设置,下划线样式由android:divider和android:dividerHeight设置。注意如果设置了android:divider(颜色),那也要同时设置android:dividerHeight(宽度),否则下划线消失。 - 把 ListView 添加到 XML 时,Android Studio 预览会出现列表内容,但实际上 App 中不存在内容。
ArrayAdapter
ArrayAdapter<String> itemsAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, words);
- 创建 ArrayAdapter,String 为元素的数据类型;
- ArrayAdapter 的构造函数有三个输入参数,Context、Resource(Layout)、List<T>(对象列表);
-
this← Context -
android.R.layout.simple_list_item_1是 Android 预定义的一个 XML,是一个 TextView;如果要显示更多内容,要将 Resource 指定到自定义的一个 Layout -
List<T>object 需要输入列表对象,它是 ArrayAdapter 的数据来源
- ArrayAdapter<T> 也是泛型类,其元素不仅可以是 String,也可以是自定义数据类型对象
ListView listView = (ListView) findViewById(R.id.list);
- 找到 ListView 的视图层级。
listView.setAdapter(itemsAdapter);
- 连接 ListView 和 ArrayAdapter;
- setAdapter 是 ListAdapter 的 method,通过 Android 文档查得关系链,ArrayAdapter(Concrete Class) ← BaseAdapter(Abstract Class) ← ListAdapter(Interface)
自定义对象(Word)和自定义类(WordAdapter)
正如前面说到的,ArrayAdapter<T> 是泛型类,其元素可以是自定义数据类型对象,所以针对 Miwok App 要显示一组两个单词的需求,我们要自定义一个对象输入 ArrayAdapter。自定义对象有 state 和 method,所有这些结合在一起叫作封装 (Encapsulation),外部可以调用内部 method,但不关心内部的工作原理。
在包名 (com.example.android.miwok)右键选择 new → Java Class,输入类名,点击完成即可新建一个 Java Class 文件。自定义类 Word 的代码如下。
public class Word {
// 变量要声明为 private
private String mDefaultTranslation;
private String mMiwokTranslation;
// 构造函数:名称必须与类名完全一致(包括大小写),无返回值(但需要标 void)
// 访问修饰符为 public 说明外部类可访问
public Word(String defaultTranslation, String miwokTranslation) {
mDefaultTranslation = defaultTranslation;
mMiwokTranslation = miwokTranslation;
}
// getter methods,声明为 public
public String getDefaultTranslation() {
return mDefaultTranslation;
}
public String getMiwokTranslation() {
return mMiwokTranslation;
}
// 一般要有 setter methods
}
在完成自定义对象 Word 后,先输入到 ArrayList 中,代码如下。
ArrayList<Word> words = new ArrayList<>();
words.add(new Word("one", "lutti"));
words.add(new Word("two", "otiiko"));
words.add(new Word("three", "tolookosu"));
完成这个步骤,还不能直接将 words 传入 ArrayAdapter,因为前面说到,ArrayAdapter 的构造函数有三个输入参数,第二个参数为资源,默认为一个 TextView (simple_list_item_1.xml 就是一个 TextView),如果要显示多个 Views 就要 override gerView(),所以要创建一个 ArrayAdapter 的子类 WordAdapter,代码如下。
// 类名添加 extends ArrayAdapter<Word> 表示 WordAdapter 继承 ArrayAdapter 的行为
public class WordAdapter extends ArrayAdapter<Word> {
/**
* This is our own custom constructor (it doesn't mirror a superclass constructor).
* The context is used to inflate the layout file, and the list is the data we want
* to populate into the lists.
*
* @param context The current context. Used to inflate the layout file.
* @param words A List of Word objects to display in a list
*/
public WordAdapter(Context context, ArrayList<Word> words) {
// Here, we initialize the ArrayAdapter's internal storage for the context and the list.
// the second argument is used when the ArrayAdapter is populating a single TextView.
// Because this is a custom adapter for two TextViews, the adapter is not
// going to use this second argument, so it can be any value. Here, we used 0.
super(context, 0, words);
}
// 选择菜单 Code → Override Methods 或快捷键 cmd+O 来快速生成一个override method
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// Check if the existing view is being reused, otherwise inflate the view
View listItemView = convertView;
if (listItemView == null) {
listItemView = LayoutInflater.from(getContext()).inflate(
R.layout.list_item, parent, false);
}
// Get the {@link Word} object located at this position in the list
Word currentWord = getItem(position);
// Find the TextView in the list_item.xml layout with the ID version_name
TextView miwokTextView = listItemView.findViewById(R.id.miwok_text_view);
// Get the version name from the current Word object and
// set this text on the name TextView
miwokTextView.setText(currentWord.getMiwokTranslation());
// Find the TextView in the list_item.xml layout with the ID version_number
TextView defaultTextView = listItemView.findViewById(R.id.default_text_view);
// Get the version number from the current Word object and
// set this text on the number TextView
defaultTextView.setText(currentWord.getDefaultTranslation());
// Return the whole list item layout (containing 2 TextViews)
// so that it can be shown in the ListView
return listItemView;
}
}
Tips
1. 对于 Android 的命名空间,除了 AndroidNS 外,还有 toolsNS 提供了 Designtime Layout Attributes ,即在设计时辅助显示,但在实际运行 (Runtime) 时忽略的属性。
2. 在 GitHub 上按 T 键可以激活 file finder 功能,直接输入关键字即可查找文件。
3. 留意 GitHub README.md 里面的 Licenses 内容,查看该项目是否允许修改和再发布。
完成第二节课后,我做了第五个实战项目:ReportCard 成绩单,项目托管在我的 GitHub 上,主要应用了这节课学习的自定义 Java Class,详细介绍我写在 GitHub 的 README 上。App 的效果如下:

这只是 Demo App,没有提供输入成绩的接口,但总成绩是自动计算的。主要知识点在于自定义了一个 Java 类 ReportCard,有几个点可分享。
- 将 ReportCard 自定义类的域设置为 public,使其可外部访问;
- 内部变量常以
m开头,如mCategory、mGrade,method 名及其形参没必要在名字前加m; - override toString method 来自定义 return 值;同时将数据以可读的字符串形式显示出来,方便检查和调试;
- 在设置分数前先用 if/else 语句检查,是一个很好的编程习惯;
- 良好的注释是必备的习惯,能让代码更加容易理解和以后的使用。