当系统自带的控件不能满足我们的需求时,我们可以利用已有的控件进行组合。
当我们想使用自定义的标题栏时,可以很轻松的定义一个,但如果在多处都要使用该标题栏时,就会变得繁琐,每一次都要重写一遍,有没有别的方法?
以标题栏为例,在layout目录下新建一个title.xml,内容如下:
接下来我们可以在其他布局中,引用我们定义好的标题栏,此处我们在activity_main.xml中引入:
只需要一行include即可实现,同时我们还要隐藏掉系统自带的标题栏,此处以MainActivity为例:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//先调用getSupportActionBar方法获取ActionBar的实例
//然后调用hide方法,隐藏系统自带的标题栏
supportActionBar?.hide()
}
}
这样不管有多少布局需要添加我们的标题栏,只需要一句include即可。
引入布局的技巧解决了重复编写布局的问题,但如果遇到响应事件的情况时,仍然需要在每个Activity中重复编写这些响应事件。而这种情况我们最好使用自定义控件来解决。
因为是使用系统现成的控件进行编写,在最开始是我们编写的自定义控件,需要继承系统的控件。
新建TitleLayout继承自LinearLayout:
//TitleLayout的主构造函数,在布局中引入该控件时就会调用这个方法
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
//init的初始化代码块,在创建类实例对象时执行
init {
LayoutInflater.from(context).inflate(R.layout.title, this)
}
}
现在自定义控件已经创建好了,让我们在布局文件中引用它,修改activity_main.xml:
此时的效果与我们的include并无二异,下面我们尝试添加点击事件:
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
init {
LayoutInflater.from(context).inflate(R.layout.title, this)
//返回按钮的点击事件
titleBack.setOnClickListener {
//在TitleLayout中接收到的context实际上是一个Activity的实例
//此处我们使用as强转context为Activity
val activity = context as Activity
//销毁Activity
activity.finish()
}
titleEdit.setOnClickListener {
Toast.makeText(context, "edit clicked", Toast.LENGTH_SHORT).show()
}
}
}
这样的话,当我们在布局中引用自定义控件时,它的点击事件就可以直接用了。
ListView允许用户上下滑动,将屏幕外的数据滚动到屏幕内。
新建项目ListViewTest,修改activity_main.xml中的代码:
修改MainActivity:
class MainActivity : AppCompatActivity() {
//使用listOf方法创建的data集合,里面包含了我们需要填入ListView的内容
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)
//通过适配器将集合data中的数据,传入listView
//由于data中的数据类型都是String,将ArrayAdapter中的泛型指定为String
//最后需要传入Activity的实例、ListView子项布局的id,以及数据源data
//此处的simple_list_item_1是Android内置的布局文件,里面只有一个TextView,此处我们用它简单显示一段文本
val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, data)
//指定listView的适配器为我们刚定义好的adapter
listView.adapter = adapter
}
}
集合中的数据是无法直接传给ListView的,需要借助适配器Adapter来完成。
Android中提供了很多适配器的实现类,在这里我们使用ArrayAdapter,它可以使用泛型来指定适配的数据类型,我们应该使用最合适的数据类型。此时可运行。
1. 定义一个实体类Fruit,作为ListView适配器的传入的数据类型:
class Fruit(val name: String, val imageId: Int)
2. ListView滚动的时候,我们浏览的每一个项目,被称为ListView的子项。每一个子项拥有自己的布局,在layout下新建子项布局fruit_item.xml:
此时已经有了我们最开始想要的,对系统已有控件的组合。此处便是将子项定义为一个图片和文字的组合。
3. 接下来我们需要创建自定义的适配器,继承自ArrayAdapter,并将它的泛型定义为我们第一步定义的Fruit类。新建FruitAdapter:
//主构造函数,传入Activity的实例context、ListView子项布局的id,以及数据源data
class FruitAdapter(context: Context, val resourceId: Int, data: List) :
ArrayAdapter(context, resourceId, data) {
//重写getView,这个方法在每个子项被滚动到主页中的时候,会被调用
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
//初始化view,加载传入FruitAdapter的布局
val view = LayoutInflater.from(context).inflate(resourceId, parent, false)
//从初始化的view中,通过findViewById获取实例
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName: TextView = view.findViewById(R.id.fruitName)
//position代表当前子项的位置
//通过Fruit类的getItem方法获取当前的fruit实例
val fruit = getItem(position)
//fruit不为空时,提取它的name和imageId,设置到view中
if (fruit != null) {
fruitImage.setImageResource(fruit.imageId)
fruitName.text = fruit.name
}
return view
}
}
4. 修改MainActivity:
class MainActivity : AppCompatActivity() {
//fruitList集合,里面包含了我们需要填入ListView的内容
private val fruitList = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//定义集合数据
initFruits()
listView.adapter = FruitAdapter(this, R.layout.fruit_item, fruitList)
}
private fun initFruits() {
repeat(2) {
//添加元素,需要注意的是泛型为Fruit,传入的自然也是
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))
}
}
}
此时可运行。
在FruitAdapter的getView方法中,每次都将布局重新加载了一遍,当快速滚动时,这将会成为性能的瓶颈。
通过观察可发现,在getView方法中,有一个convertView参数,里面缓存了之前加载好的布局,以便复用。修改如下:
//主构造函数,传入Activity的实例context、ListView子项布局的id,以及数据源data
class FruitAdapter(context: Context, val resourceId: Int, data: List) :
ArrayAdapter(context, resourceId, data) {
//重写getView,这个方法在每个子项被滚动到主页中的时候,会被调用
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
//初始化view,加载传入FruitAdapter的布局
val view: View
if (convertView == null) {
view = LayoutInflater.from(context).inflate(resourceId, parent, false)
} else {
view = convertView
}
//从初始化的view中,通过findViewById获取实例
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName: TextView = view.findViewById(R.id.fruitName)
//position代表当前子项的位置
//通过Fruit类的getItem方法获取当前的fruit实例
val fruit = getItem(position)
//fruit不为空时,提取它的name和imageId,设置到view中
if (fruit != null) {
fruitImage.setImageResource(fruit.imageId)
fruitName.text = fruit.name
}
return view
}
}
现在虽已不会重复加载布局,但在getView方法中,每次都会调用findViewById方法,我们可以借助ViewHolder来优化:
//主构造函数,传入Activity的实例context、ListView子项布局的id,以及数据源data
class FruitAdapter(context: Context, val resourceId: Int, data: List) :
ArrayAdapter(context, resourceId, data) {
//新建内部类,存储image和name
inner class ViewHolder(val fruitImage: ImageView, val fruitName: TextView)
//重写getView,这个方法在每个子项被滚动到主页中的时候,会被调用
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
//初始化view,加载传入FruitAdapter的布局
val view: View
val viewHolder: ViewHolder
if (convertView == null) {
view = LayoutInflater.from(context).inflate(resourceId, parent, false)
//从初始化的view中,通过findViewById获取实例
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName: TextView = view.findViewById(R.id.fruitName)
//将刚获取到的实例,存入viewHolder
viewHolder = ViewHolder(fruitImage, fruitName)
//用View的setTag方法,存入viewHolder
//Tag本质上来说是相关联的View的额外信息,此方法经常用于存储view的某些数据
//此处存储获取到的ViewHolder实例,供下次直接使用
view.tag = viewHolder
} else {
view = convertView
//提取view.tag中存储的ViewHolder实例
viewHolder = view.tag as ViewHolder
}
//position代表当前子项的位置
//通过Fruit类的getItem方法获取当前的fruit实例
val fruit = getItem(position)
//fruit不为空时,提取它的name和imageId,设置到view中
if (fruit != null) {
//从viewHolder中提取事先获取到的控件,开始定义
viewHolder.fruitImage.setImageResource(fruit.imageId)
viewHolder.fruitName.text = fruit.name
}
return view
}
}
修改MainActivity:
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
//listView的子项点击事件监听器
listView.setOnItemClickListener { parent, view, position, id ->
val fruit = fruitList[position]
Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
}
}
...
}
当用户点击到了listView中的子项时,就会回调到这个lambda中,此时通过position确定用户点击的是哪一个子项,依此获得当前的fruit,接下来就可以进行很多操作了。此时可运行。
但此时我们也许会发现,在lambda表达式中,有几个变量被标上了下划线,这是因为在我们的处理中没有用到这几个变量,kotlin允许我们使用下划线代替未被使用的变量:
//listView的子项点击事件监听器
listView.setOnItemClickListener { _, _, position, _ ->
val fruit = fruitList[position]
Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
}
需要注意的是,即使使用了下划线代替,变量之间的位置是不可以变换的。
如果想使用横向滚动或者更多,此时要请出我们的RecyclerView。新建RecyclerViewTest项目。
修改activity_main.xml:
这里我们想要使用RecyclerView来实现和ListView相同的效果,因此就需要准备一份同样的水果图片。简单起见,我们就直接从ListViewTest项目中把图片复制过来,另外顺便将Fruit类和fruit_item.xml也复制过来,省得将同样的代码再写一遍。
RecyclerView也需要适配器,新建一个FruitAdapter类,继承RecyclerView.Adapter,将泛型指定为FruitAdapter.ViewHolder,它是我们在FruitAdapter中定义的一个内部类:
class FruitAdapter(val fruitList: List) : RecyclerView.Adapter() {
//ViewHolder的主构造函数,继承自RecyclerView的ViewHolder
//RecyclerView.ViewHolder需要传入view,这通常是RecyclerView子项的最外层布局,即parent
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName: TextView = view.findViewById(R.id.fruitName)
}
//创建ViewHolder实例,向ViewHolder传入子项布局
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
return ViewHolder(view)
}
override fun getItemCount() = fruitList.size
//用于对子项的数据赋值,会在每个子项滚动到屏幕内时调用
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitImage.setImageResource(fruit.imageId)
holder.fruitName.text = fruit.name
}
}
修改MainActivity:
class MainActivity : AppCompatActivity() {
private val fruitList = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits()
// 指定recyclerView的布局方式
// 此处指定的是线性布局,可以实现类似ListView的效果
val layoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
recyclerView.adapter = FruitAdapter(fruitList)
}
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))
}
}
}
此时可运行,我们在此处实现的效果与ListView类似。
ListView的扩展性并不好,它只能实现纵向滚动的效果,但RecyclerView可以做到。
修改fruit_item.xml:
此处我们指定子项布局中的图片和文字垂直排列、水平居中。
修改MainActivity:
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
// 指定recyclerView的布局方式
// 此处指定的是线性布局,可以实现类似ListView的效果
val layoutManager = LinearLayoutManager(this)
//设置布局的排列方向,默认是纵向排列,此处我们修改为横向
layoutManager.orientation = LinearLayoutManager.HORIZONTAL
recyclerView.layoutManager = layoutManager
recyclerView.adapter = FruitAdapter(fruitList)
}
...
}
依此方式修改的RecyclerView就可以横向移滚动了,此时可运行。
不同于ListView,RecyclerView没有提供类似于setOnItemClickListener的方法,而是要我们给子项具体的View去注册点击事件。
对于ListView而言,他注册的是一整个子项的点击事件,可是如果我想注册子项中某一个view的点击事件时,虽然也可以做到,但会十分复杂,所以RecyclerView干脆舍弃了这一内容,让所有的点击事件都由具体的view去注册。
修改FruitAdapter:
class FruitAdapter(val fruitList: List) : RecyclerView.Adapter() {
...
//创建ViewHolder实例,向ViewHolder传入子项布局
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
val viewHolder = ViewHolder(view)
//设置viewHolder中的itemView的监听事件,itemView代表一整个子项
viewHolder.itemView.setOnClickListener {
//获取当前位置
val position = viewHolder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context, "view ${fruit.name} clicked", Toast.LENGTH_SHORT).show()
}
//设置子项的图片的监听事件
viewHolder.fruitImage.setOnClickListener {
val position = viewHolder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context, "image ${fruit.name} clicked", Toast.LENGTH_SHORT).show()
}
return viewHolder
}
...
}
上述代码,在子项的最外层布局itemView和ImageView都设置了点击事件监听器。因为此时没有设置TextView的点击事件,所以在点击文字的时候,会执行itemView的点击事件。