文章目录
- 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;}
点击图片的运行结果: