创建一个ListViewTest项目。
创建一个布局放入Listview控件,代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
在mainActivity中,代码如下:
class MainActivity : AppCompatActivity() {
private val data = listOf("Apple", "Banana", "Orange", "Watermelon",
"Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango",
"Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape",
"Pineapple", "Strawberry", "Cherry", "Mango")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val listView: ListView = findViewById(R.id.listView)
val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data)
listView.adapter = adapter
}
}
先创建一组数据 data ,这些数据可以从网上下载,也可以从数据库中读取,应该视具体的应用程序场景而定。但是数据本身不能直接传输到 listView 中去,需要适配器,Android提供了很多适配器的实现类。
可以使用 ArrayAdapter ,它可以通过泛型来指定要适配的数据类型,然后再构造函数中把数据传入。
由于data均为String,指定泛型为String,然后在构造函数中分别传入 Activity 的实例,ListView子项布局的id,以及数据源。
我们使用了 android.R.layout.simple_list_item_1 作为ListView子项布局的id,这是一个Android内置的布局文件,里面只有一个TextView,可用于简单地显示一段文本。结果如下:
使用这个ListVire子项布局仅显示一段文本,太过单调,不好看,如果想定制ListView的子项布局,能够优化界面显示。
1、定制ListView界面:
首先定义一个实体类 Fruit
作为ListView的适配器类型,之前使用的是String,由于每次要适配的数据类型不可能仅仅是简单的数据类型,一般可能涉及到很多种数据类型的组合,因此需要构建一个实体类,来进行适配。
class Fruit(val name: String, val imageId: Int)
然后为ListView的子项指定一个我们自定义的布局。
在layout目录中构建一个新的 fruit_item.xml,代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="60dp">
<ImageView
android:id="@+id/imageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp" />
<TextView
android:id="@+id/fruitName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp" />
</LinearLayout>
构建成如下这种样式:
一个Image,旁边一个TextView。
接下来需要建立一个自定义的适配器,这个适配器继承自ArrayAdapter,并将泛型指定为Fruit。
代码如下:
class FruitAdapter(activity: Activity, private val resourceId: Int, data: List<Fruit>) :
ArrayAdapter<Fruit>(activity, resourceId, data) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = LayoutInflater.from(context).inflate(resourceId,parent,false)
val fruitName: TextView = view.findViewById(R.id.fruitName)
val fruitImage: ImageView = view.findViewById(R.id.imageView)
val fruit = getItem(position) //获取当前Fruit实例
if (fruit != null) {
fruitImage.setImageResource(fruit.imageId)
fruitName.text = fruit.name
}
return view
}
}
因为是继承了ArrayAdapter,FruitAdapter有一个主构造函数,用于将Activity的实例,ListView的子项布局id,数据源传递进来。
重写了 getView() 方法,这个方法使得每个子项在被滚动到屏幕的时候都会被调用。
getView()方法,首先使用 LayoutInflater 来为这个子项加载我们传入的布局。LayoutInflater 的inflate()函数接受三个参数:第一个参数是要加载的布局文件的id,第二个参数是给加载好的布局添加一个父布局,第三个参数是false,表示只让我们在父布局中声明的layout属性生效,但不会为了这个View添加父布局。
通过findViewById()获得ImageView和ImageView 的实例。
通过getItem()方法得到当前的Fruit实例,并分别调用setImageResource() 和 setText()来设置图片和文字。最后将布局返回,这就是我们自定义的适配器。
最后修改MainActivity中的代码如下:
class MainActivity : AppCompatActivity() {
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val listView: ListView = findViewById(R.id.listView)
initFruits()
val adapter = FruitAdapter(this, R.layout.fruit_item, fruitList)
listView.adapter = adapter
}
private fun initFruits() {
repeat(2) {
fruitList.add(Fruit("Apple", R.drawable.apple_pic))
fruitList.add(Fruit("Banana", R.drawable.banana_pic))
fruitList.add(Fruit("Orange", R.drawable.orange_pic))
fruitList.add(Fruit("Watermelon", R.drawable.watermelon_pic))
fruitList.add(Fruit("Pear", R.drawable.pear_pic))
fruitList.add(Fruit("Grape", R.drawable.grape_pic))
fruitList.add(Fruit("Pineapple", R.drawable.pineapple_pic))
fruitList.add(Fruit("Strawberry", R.drawable.strawberry_pic))
fruitList.add(Fruit("Cherry", R.drawable.cherry_pic))
fruitList.add(Fruit("Mango", R.drawable.mango_pic))
}
}
}
添加了一个 initFruits() 方法,用于初始化所有水果数据。
repeat()方法是Kotlin的一个标准函数,它允许传入一个数值n,然后把Lambda中的内容执行n遍。
最后在onCreate()函数中创建FruitAdapter对象,并将它作为适配器传递给ListView,并将R.layout.fruit_item作为它的子项布局,fruitList作为数据源。
只要修改layout中的fruit_item就可以修改内容,定制各种复杂的界面!!!!
2、提升ListView的运行效率
目前的listview效率很低,因为FruitAdapter的getView()方法,每次都将布局重新加载一遍,造成性能浪费。
getView()中有一个 convertView 参数,这个参数用于将之前加载好的布局进行缓存,方便重用,可以用此来提升效率。
class FruitAdapter(activity: Activity, private val resourceId: Int, data: List<Fruit>) :
ArrayAdapter<Fruit>(activity, resourceId, data) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(context).inflate(resourceId, parent, false)
val fruitName: TextView = view.findViewById(R.id.fruitName)
val fruitImage: ImageView = view.findViewById(R.id.imageView)
val fruit = getItem(position) //获取当前Fruit实例
if (fruit != null) {
fruitImage.setImageResource(fruit.imageId)
fruitName.text = fruit.name
}
return view
}
}
其中用到了 ?: 判空操作符。当convertView 存在等于convertView ,否则重新创建。
此时,虽然已经调用了已加载的布局,但是还可以继续优化,因为每次调用getView都要重新通过findViewById()获取控件实例,我们可以通过一个 ViewHolder 来进行优化,代码如下:
class FruitAdapter(activity: Activity, private val resourceId: Int, data: List<Fruit>) :
ArrayAdapter<Fruit>(activity, resourceId, data) {
inner class ViewHolder(val fruitImage: ImageView, val fruitName: TextView)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
//val view = convertView ?: LayoutInflater.from(context).inflate(resourceId, parent, false)
val view: View
val viewHolder: ViewHolder
if (convertView == null) {
view = LayoutInflater.from(context).inflate(resourceId, parent, false)
val fruitName: TextView = view.findViewById(R.id.fruitName)
val fruitImage: ImageView = view.findViewById(R.id.imageView)
viewHolder = ViewHolder(fruitImage, fruitName)
view.tag = viewHolder
} else {
view = convertView
viewHolder = view.tag as ViewHolder
}
val fruit = getItem(position) //获取当前Fruit实例
if (fruit != null) {
viewHolder.fruitImage.setImageResource(fruit.imageId)
viewHolder.fruitName.text = fruit.name
}
return view
}
}
新增了一个内部类 ViewHolder ,对ImageView 和 TextView 进行缓存。Kotlin中使用inner class关键字来定义内部类。
当convertView 为空时,创建一个viewHolder对象,并将控件实例放到viewHolder对象中,然后调用view的setTag() 方法,将viewHolder对象存储在View中。
不为空时,调用View的getTag()方法,将viewHolder重新取出,这样就不需要每次都通过findViewById()来获取实例了。
3、ListView的点击事件
需要使用ListView的 setOnItemClickListener 函数,四个参数,点进setOnItemClickListener 源码
public void setOnItemClickListener(@Nullable AdapterView.OnItemClickListener listener) {
throw new RuntimeException("Stub!");
}
setOnItemClickListener 接收一个 OnItemClickListener 参数,是一个接口,这是一个Java单抽象方法接口,他只有一个待实现方法,否则也不可能作为参数。
OnItemClickListener 的源码:
public interface OnItemClickListener {
void onItemClick(AdapterView<?> var1, View var2, int var3, long var4);
}
该唯一方法接收四个参数,但是我们实际上只用到了var3,也就是position参数,针对这种情况,Kotlin允许使用 下划线来代替。
listView.setOnItemClickListener { _, _, position, _ ->
val fruit = fruitList[position]
Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
}
可以用下划线代替,但是他们之间的位置不可改变!!!!