听说这是最常用和最难用的控件
本文项目中所用的图片资源均来自于https://www.ituring.com.cn/book/2744
ListView
处处可见,比如手机QQ的消息列表,新闻应用的信息流页面……
不过相对于Button
,TextView
这些简单的控件,ListView
的用法就要复杂很多。
先创建布局文件,这一步和其他控件一样
<ListView
android:id="@+id/my_listView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
宽度和高度都设置为
match_parent
,从而使ListView
占满整个布局空间
接下来就需要将我们的数据列表有序的放在ListView
上面,使数据与ListView建立联系
class MainActivity : AppCompatActivity() {
//创建数据源
private val data = listOf<String>("A","B","C","D","E",
"F","G","H","I","J","K","L","M","N")
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,从而建立ListView和数据之间的联系
my_listView.adapter = adapter
}
}
由于集合中的数据无法直接传递给ListView,我们还需要借助适配器(如ArrayAdapter)来实现
ArrayAdapter通过泛型来指定要适配的数据类型,在这里我们指定了String类型
ArrayAdapter的详细解释:https://www.jianshu.com/p/b996d4af54b0
上述简单的例子只能显示单调的文字,但是我们常用的手机QQ的列表前还配有头像等其他的元素,要实现这样的效果,我们就得自己来定制一个ListView
的界面。
首先,列表中每一个单独的子项都看作一个类,我们先定义这个类
class Fruit(val name:String,val imageId:Int)
之后再为我们的每一个子项整一个布局,在layout目录下创建fruit_item.xml
文件
<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>
ListView与数据连接需要一个适配器,在上面那个示例我们使用了ArrayAdapter
作为其适配器。但是我们现在的数据是一个Fruit
类,而不是简简单单的String
。因此,我们需要创建一个自定义的适配器,继承于ArrayAdapter
,并将泛型指定为Fruit
类型
class FruitAdapter(activity:Activity,val resourceId:Int,data:List<Fruit>):
ArrayAdapter<Fruit>(activity,resourceId,data){
//重写getView方法,每当子项被滚入屏幕时调用此方法来构建子项
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
//为子项加载布局
val view = LayoutInflater.from(context).inflate(resourceId,parent,false)
//分别获取布局中ImageView和TextView的实例
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName:TextView = view.findViewById(R.id.fruitName)
//获取当前项在ArrayList集合中Fruit实例
val fruit = getItem(position)
//设置图片和文字
if(fruit != null){
fruitImage.setImageResource(fruit.imageId)
fruitName.text = fruit.name
}
//最后将布局返回
return view
}
}
最后再在MainActivity
中使用构建好的这几个类,并为ListView
添加监听
class MainActivity : AppCompatActivity() {
//创建数据源
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//初始化数据列表
initfruit()
//创建适配器
val adapter = FruitAdapter(this,R.layout.fruit_item,fruitList)
//将适配器对象传入ListView,从而建立ListView和数据之间的联系
my_listView.adapter = adapter
//使用setOnItemClickListener()方法为每一项创建监听
my_listView.setOnItemClickListener{parent,view,position,id->
val fruit = fruitList[position]
Toast.makeText(this,fruit.name,Toast.LENGTH_SHORT).show()
}
}
private fun initfruit(){
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))
}
}
}
由于每次调用getView()
方法时都需要使用LayoutInflater
重新加载布局。
所以当用户快速滑动的时候,会频繁调用getView()
方法反复加载同一个布局,使效率低下。
//重写getView方法,每当子项被滚入屏幕时调用此方法来构建子项
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
//会反复调用LayoutInflater来加载布局
val view = LayoutInflater.from(context).inflate(resourceId,parent,false)
//······略·······
}
要解决这个问题,就要使用getView()
方法中的一个参数convertView
。
这个参数用于将之前加载好的布局进行缓存,以便以后进行使用。
我们利用convertView
参数来优化上述代码:
//重写getView方法,每当子项被滚入屏幕时调用此方法来构建子项
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
//创建一个
val view:View
//第一次加载,则使用LayoutInflater来加载布局,同时convertView会自动缓存这个布局
if(convertView==null){
view = LayoutInflater.from(context).inflate(resourceId,parent,false)
}else{
//convertView不为空,则说明存有缓存好的布局,直接调用即可
view = 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 fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName:TextView = view.findViewById(R.id.fruitName)
//······略······
}
}
不难发现,重复被调用还有findViewById()
方法,它会反复获取相同控件的实例。
我们也用缓存的方式来解决这个问题
在FruitAdapter
中创建一个用来缓存控件实例的内部类ViewHolder
inner class ViewHolder(val fruitImage:ImageView,val fruitName:TextView)
使用这个类来缓存实例,从而提高运行效率
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的setTag()方法将ViewHolder对象存储在View中
view.tag = viewHolder
}else{//如果已经缓存过了
//复用缓存的布局
view = convertView
//调用getTag()方法将ViewHolder实例取出
viewHolder = view.tag as ViewHolder
}
val fruit = getItem(position)
if(fruit!=null){
viewHolder.fruitImage.setImageResource(fruit.imageId)
viewHolder.fruitName.text = fruit.name
}
return view
}
}