课程 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 musicLibrary = new ArrayList();
// 添加和移除 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 musicLibrary = new ArrayList();
ArrayList words = new ArrayList();
复制代码

使用 Java 添加和设置 Views 。

  1. 从 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);
    复制代码
  2. 在 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);
    复制代码
  3. 注意 setText 的输入数据类型

     wordView.setText(“some texts”);
    复制代码
  4. 使用 addView method 向 rootView 添加一个 View

     rootView.addView(wordView);
    复制代码

while & for 循环语句

  1. 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;
复制代码
  1. 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 会决定在屏幕上显示的数据集。

具体的工作流程如下。

  1. ListView 向 ArrayAdapter 询问 Array 有几个元素,ArrayAdapter 会查询 (getView);

  2. ListView 对 ArrayAdapter 发送当前 Array 的索引位置,ArrayAdapter 查看 Array 的数据,并向 ListView 说明如何显示列表; 当屏幕上显示完全后,ListView 停止向 ArrayAdapter 寻求更多的列表项,此时显示在屏幕上的视图才会创建;

  3. 用户划动屏幕,一些视图将不再出现,这些 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

"http://schemas.android.com/apk/res/android"
   android:id="@+id/list"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical" />
复制代码
  1. 列表方向由 android:orientation 设置,下划线样式由 android:dividerandroid:dividerHeight 设置。注意如果设置了 android:divider(颜色),那也要同时设置 android:dividerHeight(宽度),否则下划线消失。
  2. 把 ListView 添加到 XML 时,Android Studio 预览会出现列表内容,但实际上 App 中不存在内容。

ArrayAdapter

ArrayAdapter itemsAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, words);
复制代码
  1. 创建 ArrayAdapter,String 为元素的数据类型;
  2. ArrayAdapter 的构造函数有三个输入参数,Context、Resource(Layout)、List(对象列表);
  • this ← Context
  • android.R.layout.simple_list_item_1 是 Android 预定义的一个 XML,是一个 TextView;如果要显示更多内容,要将 Resource 指定到自定义的一个 Layout
  • List object 需要输入列表对象,它是 ArrayAdapter 的数据来源
  1. ArrayAdapter 也是泛型类,其元素不仅可以是 String,也可以是自定义数据类型对象
ListView listView = (ListView) findViewById(R.id.list);
复制代码
  1. 找到 ListView 的视图层级。
listView.setAdapter(itemsAdapter);
复制代码
  1. 连接 ListView 和 ArrayAdapter;
  2. setAdapter 是 ListAdapter 的 method,通过 Android 文档查得关系链,ArrayAdapter(Concrete Class) ← BaseAdapter(Abstract Class) ← ListAdapter(Interface)

自定义对象(Word)和自定义类(WordAdapter)

正如前面说到的,ArrayAdapter 是泛型类,其元素可以是自定义数据类型对象,所以针对 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 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 表示 WordAdapter 继承 ArrayAdapter 的行为
public class WordAdapter extends ArrayAdapter {

   /**
    * 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 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,有几个点可分享。

  1. 将 ReportCard 自定义类的域设置为 public,使其可外部访问;
  2. 内部变量常以 m 开头,如 mCategorymGrade,method 名及其形参没必要在名字前加 m
  3. override toString method 来自定义 return 值;同时将数据以可读的字符串形式显示出来,方便检查和调试;
  4. 在设置分数前先用 if/else 语句检查,是一个很好的编程习惯;
  5. 良好的注释是必备的习惯,能让代码更加容易理解和以后的使用。

你可能感兴趣的:(课程 2: 数据,列表,循环和自定义类)