由于手机屏幕空间有有限,能够一次性在屏幕上显示的内容并不多,而程序中有大量的数据需要展示的时候,就需要借助ListView来实现。
简单用法
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
上面的代码中创建了一个ListView,然后使之占满整个布局空间。
package com.example.listviewtest
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ArrayAdapter
import kotlinx.android.synthetic.main.activity_main.*
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 adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,data)
listView.adapter = adapter
}
}
上面的代码中提供了一个字符串列表,然后通过适配器ArrayAdapter指定了适配的数据类型,然后在构造函数中依此传入Activity的实例,ListView子项布局的id,以及数据源。这里使用android.R.layout.simple_list_item_1作为ListView子项布局的id,这是一个Android内置的布局文件,其中只有一个TextView,可用于简单地显示一段文本。
最后调用ListView的setAdapter方法,传入构建好的适配器对象。程序运行结果为:
定制ListView的界面
只能显示文本的ListView显然不能满足现在的需求,这里对该界面进行定制。
定义一个实体类Fruit,作为ListView适配器的适配类型:
class Fruit(val name:String, val imageId:Int)
该类只有两个字段,name表示水果名,imageId表示对应的图片资源ID。
然后为ListView子项指定自定义布局,在layout目录下新建fruit_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp">
<ImageView
android:id="@+id/fruitImage"
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="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"/>
</LinearLayout>
在该布局中,定义了ImageView用于显示图片,定义了TextView用于显示文本,并使之在垂直方向上居中显示。
然后创建适配器,继承自ArrayAdapter,并将泛型指定为Fruit:
package com.example.listviewtest
import android.app.Activity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
class FruitAdapter(activity: Activity, 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 fruitImage:ImageView = view.findViewById(R.id.fruitImage)
val fruitName:TextView = view.findViewById(R.id.fruitName)
val fruit = getItem(position)
if (fruit != null) {
fruitImage.setImageResource(fruit.imageId)
fruitName.text = fruit.name
}
return view
}
}
FruitAdapter重写了getView方法,该方法在每个子项被滚动到屏幕内的时候会被调用。
在getView方法中,首先使用LayoutInflater来为该子项加载用户传入的布局,其inflate方法接收3个参数,前两个参数之前已经提到了,第三个参数false用表示只让用户在父布局中声明的layout属性生效,但不会为该View添加父布局。因为一旦View有了父布局之后,就不能再添加到ListView中了。
然后调用View的findViewById分别获取ImageView和TextView的实例,然后通过getItem获取当前项的Fruit实例,并分别调用其setImageResource和setText方法设置显示的图片和文字,然后布局,适配器就定义完成了。
修改MainActivity中的代码:
package com.example.listviewtest
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ArrayAdapter
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
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方法,用于初始化所有的数据。在Fruit类的构造函数中将水果的名字和对应的图片Id传入,然后将创建好的对象添加到列表。另外使用repeat函数对Lambda表达式内容执行多次。最后在onCreate方法中创建了FruitAdapter对象,并将之作为适配器传递到ListView。程序运行后的结果为:
提升ListView的运行效率
上面的ListView运行效率是很低的,因为在FruitAdapter的getView方法中,每次都会将布局加载一遍,当ListView快速滚动的时候,就会导致性能低下。
其实,在getView方法中还有一个convertView参数,该参数用于将之前加载好的布局进行缓存,以便之后进行重用,这里可以使用该参数进行性能优化:
class FruitAdapter(activity: Activity, 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 fruitImage:ImageView = view.findViewById(R.id.fruitImage)
val fruitName:TextView = view.findViewById(R.id.fruitName)
val fruit = getItem(position)
if (fruit != null) {
fruitImage.setImageResource(fruit.imageId)
fruitName.text = fruit.name
}
return view
}
}
上面代码中,在getView方法中进行判断,重用了convertView,提高了ListView的运行效率,在快速滚动的时候可以表现出更好的性能。
而虽然现在不会重复加载布局,但是每次还是会使用findViewById方法获取一次控件的实例。这里可以借助ViewHolder来对这部分性能进行优化:
class FruitAdapter(activity: Activity, 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:View
val viewHolder:ViewHolder
if (convertView == null) {
view = LayoutInflater.from(context).inflate(resourceId, parent, false)
val fruitImage:ImageView = view.findViewById(R.id.fruitImage)
val fruitName:TextView = view.findViewById(R.id.fruitName)
viewHolder = ViewHolder(fruitImage, fruitName)
view.tag = viewHolder
} else {
view = convertView
viewHolder = view.tag as ViewHolder
}
val fruit = getItem(position)
if (fruit != null) {
viewHolder.fruitImage.setImageResource(fruit.imageId)
viewHolder.fruitName.text = fruit.name
}
return view
}
}
上面的代码中,新增了内部类ViewHolder,用于缓存ImageView和TextView的实例。Kotlin使用inner class关键字来构建内部类。而当convertView为null时,就创建一个ViewHolder对象,并将控件实例存放在ViewHolder中,然后调用View的setTag方法,将ViewHolder对象存储在View中。当convertView不为null时,就调用View的getTag方法,将ViewHolder取出,以优化运行效率。
ListView的点击事件
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits()
val adapter = FruitAdapter(this, R.layout.fruit_item,fruitList)
listView.adapter = adapter
listView.setOnItemClickListener { parent, view, position, id ->
val fruit = fruitList[position]
Toast.makeText(this, fruit.name,Toast.LENGTH_SHORT).show()
}
}
上面的代码调用ListView的setOnItemClickListener方法注册了监听器,当用户点击了ListView中的某一个子项时,就会回调Lambda表达式。这里通过position参数判断用户点击的是哪一个子项,然后获取到对应的内容,进行打印。程序运行结果为:
同时由于只用到了position这一个参数,可以将代码修改为:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits()
val adapter = FruitAdapter(this, R.layout.fruit_item,fruitList)
listView.adapter = adapter
listView.setOnItemClickListener { _, _, position, _ ->
val fruit = fruitList[position]
Toast.makeText(this, fruit.name,Toast.LENGTH_SHORT).show()
}
}
上边用下划线代替没用的参数,但参数位置不能改变。