文章目錄
- ListView
- 內置類型的簡單運用
- 定制數據類型
- 提升效率
- 點擊事件
- RecyclerView
- 布局管理器
- 點擊事件
ListView
內置類型的簡單運用
由于手機屏幕空間有限,能夠一次性在屏幕上顯示的內容不多,當我們的程序有大量數據需要顯示的時候就可以借助 ListView 來實現。
布局文件 listview_layout.xml
:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><ListViewandroid:id="@+id/list_view"android:layout_width="match_parent"android:layout_height="match_parent" /></androidx.constraintlayout.widget.ConstraintLayout>
活動文件 ListViewTest.java
:
public class ListViewTest extends AppCompatActivity {private String[] data = {"Apple","Banana","Orange","Watermelon","Pear","Grape","Pineapple","Strawberry","Cherry", "Mango","Apple","Banana","Orange","Watermelon","Pear","Grape","Pineapple","Strawberry","Cherry","Mango"};// 數據無法直接傳遞給 ListView@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.listview_layout);ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data);// 借助適配器傳遞數據ListView listView = (ListView)findViewById(R.id.list_view);listView.setAdapter(adapter);}
}
- ListView 子項布局的
id
為android.R.layout.simple_list_item_1
,其是一個 Android 內置的布局文件,內容只有一個TextView
,可以用于簡單地顯示一段文本。 - ArrayAdapter 構造函數 :
- 參數一:當前 Context
- 參數二:ListView 子項布局的
id
,此例中為上面提到的TextView
- 參數三:適配布局的數據
運行結果:
定制數據類型
如果想要每個水果的名字都對應一張圖片,那么內置的 String 類型就無法滿足需求了,因此需要自定義一個 Fruit 類:
package com.example.activitytest;public class Fruit {private String name;private int imageId;public Fruit(String name, int imageId){this.name = name;this.imageId = imageId;}public String getName(){return name;}public int getImageId(){return imageId;}
}
為 ListView
的子項指定一個自定義布局,在 layout
目錄下新建 fruit_item.xml
:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="wrap_content"android:layout_height="wrap_content"><ImageViewandroid:id="@+id/fruit_image"android:layout_width="300dp"android:layout_height="200dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintVertical_bias="0.0" /><TextViewandroid:id="@+id/fruit_name"android:layout_width="wrap_content"android:layout_height="wrap_content"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="@+id/fruit_image"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/fruit_image" /></androidx.constraintlayout.widget.ConstraintLayout>
- ImageView:顯示圖片。
- TextView :顯式名稱,文字居中。
接下來參考書上的內容創建一個自定義適配器:
public class FruitAdapter extends ArrayAdapter<Fruit> {private List<Fruit> fruitList; // 數據private int resourceId; // 子項xml布局文件public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects){super(context, textViewResourceId, objects);resourceId = textViewResourceId;fruitList = objects;}// 重寫getView方法,加載每個處于屏幕內的子項時調用public View getView(int position, View convertView, ViewGroup parent){Fruit fruit = getItem(position); // 獲取當前的 Fruit 實例,還有一種寫法:fruitList.get(position),從list中獲取單個節點元素View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);fruitImage.setImageResource(fruit.getImageId());TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);fruitName.setText(fruitList.get(position).getName());return view;}
}
構造函數 FruitAdapter 本質上還是雷同于被它
extends
的 ArrayAdapter構造函數,參數分析:
- context:指定當前上下文。
- textViewResourceId:ListView 子項布局的
id
- objects:適配布局的數據
加載每個處于屏幕內的子項時調用的 getView,由于我們是重寫已有函數,因此參數是固定的:
- position:當前 Item 在屏幕中的位置,通常搭配
getItem()
返回一個子View以獲取當前實例。 - convertView:緩存視圖 View,如果非null,則直接再次對 convertView 復用,否則才創建新的 View。
- parent:Item 的 View 的父視圖,用處的話私以為就是為了充當 inflate 的第二個參數。
LayoutInflater 是根據 Layout XML文件 來生成 對應 View 對象 的系統服務。一般用它之作一件事: inflate(加載布局)。
inflate 方法:
public View inflate (int resource, ViewGroup root, boolean attachToRoot)
- resource:要加載的布局對應的資源 id
- root:在參數一對應布局外部嵌套一個父布局,如果不需要則為 null。
- attachToRoot:是否為加載的布局添加一個 root 的外層容器
- 為 true 時將子布局添加到父布局中并保存子布局的 layout 配置
- 為 false 時表示只讓在子布局中聲明的 layout 屬性生效,但不將子布局添加到父布局中,此時想要將子布局添加到父布局中需要調用 addView() 方法。
上述代碼中參數三就被賦值為 false
,這是因為 ListView 繼承自 AdapterView,繼承了 AdapterView 的控件是不支持 AddView()
方法的,因為一旦 resource 對應的 View 有了父布局,就不能再被添加到 ListView 中了。而參數三為 true
時會自動調用 AddView()
方法(關于inflate參數的相關信息詳見本文)。
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) { // attachToRoot 為 true 時自動調用 addView 方法root.addView(temp, params);
}
關于 LayoutInflater 與 inflate 可參考本文。
設置顯示的圖片和文字
- 通過
findViewById
方法分別獲取到 ImagView 和 TextView 的實例 - 分別調用它們的
setImageResource
和setText
方法來設置顯示的圖片和文字 - 最后將布局返回
最后修改活動文件 ListViewTest.java
,以自定義適配器為 extends ArrayAdapter<Fruit>
的情況為例:
public class ListViewTest extends AppCompatActivity {private List<Fruit> fruitList = new ArrayList<>();@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.listview_layout);initFruits(); // 初始化數據集// 借助適配器傳遞數據FruitAdapter adapter = new FruitAdapter(this, R.layout.fruit_item, fruitList);// FruitAdapter adapter = new FruitAdapter(this, fruitList);ListView listView = (ListView)findViewById(R.id.list_view);listView.setAdapter(adapter);}private void initFruits(){Fruit apple = new Fruit("Apple", R.drawable.ic_launcher_background);fruitList.add(apple);Fruit banana = new Fruit("Banana", R.drawable.ic_launcher_foreground);fruitList.add(banana);Fruit orange = new Fruit("Orange", R.drawable.cmy1);fruitList.add(orange);Fruit Watermelon = new Fruit("Watermelon", R.drawable.cmy2);fruitList.add(Watermelon);Fruit pear = new Fruit("Pear", R.drawable.cmy3);fruitList.add(pear);Fruit grape = new Fruit("Grape", R.drawable.cmy4);fruitList.add(grape);}
}
提升效率
- 在 FruitAdapter 的
getView
方法中每次都通過 LayoutInflater 與 inflate 將布局重新加載了一遍, 當 ListView 快速滾動的時候就會成為性能的瓶頸。而之前提到getView
方法中的convertView
參數可以緩存 View,因此可以利用該參數來提高效率。 getView
方法中我們每次都要通過findViewById
方法創建控件fruit_image
和fruit_name
的實例。不如用一個內部類ViewHolder
來對控件實例進行緩存。
public View getView(int position, View convertView, ViewGroup parent){Fruit fruit = (Fruit) getItem(position);ViewHolder viewHolder;if(convertView == null){convertView = LayoutInflater.from(context).inflate(resourceId, parent, false);viewHolder = new ViewHolder();viewHolder.fruitImage = convertView.findViewById(R.id.fruit_image);viewHolder.fruitName = convertView.findViewById(R.id.fruit_name);convertView.setTag(viewHolder);}else{viewHolder = (ViewHolder) convertView.getTag();}viewHolder.fruitImage.setImageResource(fruit.getImageId());viewHolder.fruitName.setText(fruit.getName());return convertView;}class ViewHolder{ImageView fruitImage;TextView fruitName;}
總而言之,之前每次加載屏幕外的 子項 時都需要進行前文提到的兩種操作,但在進行改進之后,只有第一次加載(比如往下滑屏幕)的時候執行 convertView == null
的代碼才需進行前文的兩項操作,之后加載時(比如滑到底了再往上滑)就是 convertView
非空的情況了。
點擊事件
滾動只是視覺效果,子項還可以點擊。修改活動文件 ListViewTest.java
:
protected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.listview_layout);initFruits();FruitAdapter adapter = new FruitAdapter(this, R.layout.fruit_item, fruitList);// FruitAdapter adapter = new FruitAdapter(this, fruitList);// 借助適配器傳遞數據ListView listView = (ListView)findViewById(R.id.list_view);listView.setAdapter(adapter);listView.setOnItemClickListener(new AdapterView.OnItemClickListener(){@Overridepublic void onItemClick(AdapterView<?> parent, View view, int position, long id) {Fruit fruit = fruitList.get(position);Toast.makeText(ListViewTest.this, fruit.getName(), Toast.LENGTH_SHORT).show();}});/* lambda表達式代替內聯函數listView.setOnItemClickListener((AdapterView<?> parent, View view, int position, long id)->{Fruit fruit = fruitList.get(position);Toast.makeText(ListViewTest.this, fruit.getName(), Toast.LENGTH_SHORT).show();});*/}
- 使用
setOnItemClickListener
方法為 ListView 注冊了一個監聽器 - 點擊任何一個子項時回調
onItemClick
方法,通過 position 參數獲得點擊的是哪一個子項 - 獲取相應 Fruit 名稱并通過 Toast 輸出
RecyclerView
布局管理器
ListView 有 性能容易變差、數據只能縱向滾動 的缺點。而 RecyclerView 就支持橫向滾動。
為了讓所有 Android 版本都能使用,RecyclerView 被定義在 support 庫中。因此,需要使用前要在項目的 build.gradle
文件中添加相應依賴庫:
修改布局文件 listview_layout.xml
中控件 ListView
為 RecyclerView
,并修改布局文件名稱為 recyclerview_layout.xml
:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recycler_view"android:layout_width="match_parent"android:layout_height="match_parent"/></androidx.constraintlayout.widget.ConstraintLayout>
子項布局文件 fruit_item.xml
和自定義的 Fruit
類無需修改,但適配器 FruitAdapter
類需要重新實現,原本 getView
方法的職責被三個方法替代:
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {private List<Fruit> FruitList;// 構造函數public FruitAdapter(List<Fruit> fruitList){FruitList = fruitList;}// 三個繼承自父類的函數// 創建內部類實例@NonNull@Overridepublic ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);return new ViewHolder(view);}// 將獲得的Fruit實例作為RecyclerView子項控件的值@Overridepublic void onBindViewHolder(@NonNull ViewHolder holder, int position) {Fruit fruit = FruitList.get(position); // 獲得子項holder.fruitImage.setImageResource(fruit.getImageId());holder.fruitName.setText(fruit.getName());}// 統計子項數目@Overridepublic int getItemCount() {return FruitList == null ? 0 : FruitList.size();}// 緩存Fruit屬性對應控件的內部類static class ViewHolder extends RecyclerView.ViewHolder{ImageView fruitImage;TextView fruitName;public ViewHolder(@NonNull View itemView) {super(itemView);fruitImage = itemView.findViewById(R.id.fruit_image);fruitName = itemView.findViewById(R.id.fruit_name);}}
}
活動文件 ListViewTest.java
:
public class ListViewTest extends AppCompatActivity {private List<Fruit> fruitList = new ArrayList<>();@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.recyclerview_layout);initFruits();RecyclerView recyclerView = findViewById(R.id.recycler_view);// 指定RecyclerView布局方式為線性布局LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);recyclerView.setLayoutManager(linearLayoutManager);// 設置適配器recyclerView.setAdapter(new FruitAdapter(fruitList));}private void initFruits(){for(int i = 0; i < 10; i++){Fruit fruit = new Fruit("apple", R.drawable.cmy1);fruitList.add(fruit);}}
}
與 ListView 不同的是,需要傳入一個布局管理器 LinearLayoutManager 來規定 RecyclerView 是何種布局,一般有三種:
- GridLayoutManager:網格布局
- LinearLayoutManager:線性布局
- StaggeredGridLayoutManager:瀑布流布局
這里以瀑布流布局為例:
StaggeredGridLayoutManager layoutManager = newStaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);// 表示會把布局分為三列,并縱向排列
recyclerView.setLayoutManager(layoutManager);
運行結果:
這里看起來像網格布局是因為每個子項的長寬是一樣的,當長寬不一樣時就會呈現這樣的效果:
此外,還可實現橫向滾動:
protected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.recyclerview_layout);initFruits();RecyclerView recyclerView = findViewById(R.id.recycler_view);// 指定RecyclerView布局方式為線性布局LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);// 布局橫向排列以便橫向滾動linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);recyclerView.setLayoutManager(linearLayoutManager);// 設置適配器recyclerView.setAdapter(new FruitAdapter(fruitList));}
點擊事件
不同于 ListView,RecyclerView 沒有類似于 setOnItemClickListener
這樣的注冊監聽器方法,這需要給子項具體的 View 注冊點擊事件。
修改適配器類 FruitAdapter.java
的 onCreateViewHolder
函數,實現點擊 Fruit 子項的文字部分會彈出 Toast 文本;點擊 Fruit 子項的圖片部分會顯示大圖:
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);final ViewHolder holder = new ViewHolder(view);holder.itemView.setOnClickListener((View v)->{Toast.makeText(v.getContext(), "clicked view", Toast.LENGTH_SHORT).show();});holder.fruitImage.setOnClickListener((View v)->{// 獲取當前子項實例int position = holder.getAdapterPosition();Fruit fruit = FruitList.get(position);// 使用Dialog顯示大圖final Dialog dialog = new Dialog(v.getContext());// 設置緩存圖片的ImageView控件ImageView img = new ImageView(v.getContext());// 用當前Fruit實例的成員(圖片id)為img控件賦值img.setImageResource(fruit.getImageId());/* 也可以不使用position一連串操作,而是通過setImageDrawable將holder.fruitImage的圖片顯示到img中img.setImageDrawable(holder.fruitImage.getDrawable());*/// 設置dialog彈出內容dialog.setContentView(img);// 對話框背景為透明dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);// 顯示dialogdialog.show();// 點擊圖片取消img.setOnClickListener((View vi)->{dialog.cancel();});});return holder;}
點擊圖片的運行結果: