课程 2: 数据,列表,循环和自定义类
这节课是 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 语句检查,是一个很好的编程习惯;
- 良好的注释是必备的习惯,能让代码更加容易理解和以后的使用。