Kotlin的自定义控件、Adapter

自定义控件

当系统自带的控件不能满足我们的需求时,我们可以利用已有的控件进行组合。

引入布局

当我们想使用自定义的标题栏时,可以很轻松的定义一个,但如果在多处都要使用该标题栏时,就会变得繁琐,每一次都要重写一遍,有没有别的方法?

以标题栏为例,在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

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,它可以使用泛型来指定适配的数据类型,我们应该使用最合适的数据类型。此时可运行。

定制ListView的界面

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))
        }
    }
}

此时可运行。

提升运行效率:convertView和ViewHolder

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
    }
}

ListView的点击事件

修改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

如果想使用横向滚动或者更多,此时要请出我们的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的点击事件。

你可能感兴趣的:(kotlin,ui)