一直以来都认为不必分散太多精力在UI控件上,但是逆向的过程中看到有许多控件都没有实践过,所以趁着这个机会学习一下几个UI。
1.Adapter
在学习UI之前首先学习下适配器Adapter,它的作用类似于MVC模式下的C,用来控制数据以怎样的方式显示到V上:
网上找到的一张Adapter的继承结构图:
常用的有BaseAdapter(经常重写来使用)、ArrayAdapter(最简单的适配器,只显示一行文字)、SimpleAdapter(比ArrayAdapter效果好一点,可以自定义多种效果)。
2.ListView
多说无益,首先来学习ListView,虽然ListView逐渐被RecyclerView所取代,但是还是有学习的必要,之后学习RecyclerView的时候会看到两者的区别。适配器首先选择简单的ArrayAdapter和SimpleAdapter,当然重写BaseAdapter也可以。
首先定义activity_main布局:布局里放了一个listview作为MVC里面的V
然后要准备M然后定义C:
这里通过数组的方式准备数据源,适配器选择最简单的ArrayAdapter,静态或动态添加数据都可以:
这样看起来太单调,考虑在此基础上添加水果的图片:
item布局如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="horizontal">
<ImageView
android:id="@+id/fruit_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/fruit_name"
android:textSize="20sp"
android:textColor="#000000"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/fruit_says"
android:textSize="15sp"
android:textColor="#000000"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
首先新建水果类:定义了三个变量,并设置相应的setter与getter
使用自定义适配器,新建FruitAdapter继承自BaseAdapter,并重写其getView方法 (viewholder的作用是getview方法中存在重复加载布局以及控件(dfs)的情况 viewholder可以优化):
然后在MainActivity中动态添加:
效果如下:
Kotlin Fruit类:
class Fruit(var icon:Int,var name:String,var says:String)
FruitAdapter继承ArrayAdapter,与Java逻辑相同:
class FruitAdapter(activity: Activity,resourceid:Int,fruits:List<Fruit>):ArrayAdapter<Fruit>(activity,resourceid,fruits) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val viewHolder:ViewHolder
val view:View
if(convertView == null){
view = LayoutInflater.from(context).inflate(R.layout.fruit_item,parent,false)
viewHolder = ViewHolder(view.findViewById(R.id.fruit_icon),view.findViewById(R.id.fruit_name),view.findViewById(R.id.fruit_says))
view.tag = viewHolder
}else{
view = convertView
viewHolder = view.tag as ViewHolder
}
val fruit = getItem(position)
if(fruit!=null){
viewHolder.icon.setImageResource(fruit.icon)
viewHolder.name.text = fruit.name
viewHolder.says.text = fruit.says
}
return view;
}
inner class ViewHolder(val icon:ImageView,val name:TextView,val says:TextView)
}
MainActivity中添加了点击事件:
class MainActivity : AppCompatActivity() {
private val fruits = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
fruits.add(Fruit(R.drawable.apple_pic,"苹果","我是苹果"))
fruits.add(Fruit(R.drawable.cherry_pic,"樱桃","我是樱桃"))
fruits.add(Fruit(R.drawable.mango_pic,"芒果","我是芒果"))
val adapter = FruitAdapter(this,R.layout.fruit_item,fruits)
listview.adapter = adapter
listview.setOnItemClickListener { parent, view, position, id ->
val fruit = fruits[position]
Toast.makeText(this,"点击了${fruit.name}",Toast.LENGTH_SHORT).show()
}
}
}
效果如下:
之后要完成对listview的数据更新操作,这里添加功能是靠按钮来实现的,依次添加苹果,当然也可以选择位置插入添加,删除操作使用长按item子项弹出菜单实现的:
(1)添加功能
自定义适配器中添加了add功能:
Mainactivity中的调用:
效果如下:
Kotlin版本:
FruitAdapter:
class FruitAdapter(activity: Activity,resourceid:Int,val fruits:ArrayList<Fruit>):ArrayAdapter<Fruit>(activity,resourceid,fruits) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val viewHolder:ViewHolder
val view:View
if(convertView == null){
view = LayoutInflater.from(context).inflate(R.layout.fruit_item,parent,false)
viewHolder = ViewHolder(view.findViewById(R.id.fruit_icon),view.findViewById(R.id.fruit_name),view.findViewById(R.id.fruit_says))
view.tag = viewHolder
}else{
view = convertView
viewHolder = view.tag as ViewHolder
}
val fruit = getItem(position)
if(fruit!=null){
viewHolder.icon.setImageResource(fruit.icon)
viewHolder.name.text = fruit.name
viewHolder.says.text = fruit.says
}
return view;
}
**fun add(fruit:Fruit){
//添加功能
fruits?.add(fruit)
notifyDataSetChanged()
}**
inner class ViewHolder(val icon:ImageView,val name:TextView,val says:TextView)
}
MainActivity,增加按钮点击监听:
class MainActivity : AppCompatActivity() {
private var fruits = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
fruits.add(Fruit(R.drawable.apple_pic,"苹果","我是苹果"))
fruits.add(Fruit(R.drawable.cherry_pic,"樱桃","我是樱桃"))
fruits.add(Fruit(R.drawable.mango_pic,"芒果","我是芒果"))
val adapter = FruitAdapter(this,R.layout.fruit_item,fruits)
listview.adapter = adapter
add.setOnClickListener{
adapter.add(Fruit(R.drawable.apple_pic,"苹果","我是苹果"))
}
listview.setOnItemClickListener { parent, view, position, id ->
val fruit = fruits[position]
Toast.makeText(this,"点击了${fruit.name}",Toast.LENGTH_SHORT).show()
}
}
}
点击增加苹果,效果如下:
(2)删除功能
首先为listview添加注册:
之后重写onCreateContextMenu方法和点击事件onContextItemSelected方法:
效果如下:
长按芒果:
点击删除:
Kotlin版本,修改MainActivity即可,逻辑和Java 相同:
class MainActivity : AppCompatActivity() {
private var fruits = ArrayList<Fruit>()
private var item_position:Int = 0
private var adapter : FruitAdapter ? = null
override fun onContextItemSelected(item: MenuItem): Boolean {
when(item.itemId){
R.id.remove -> {
fruits.removeAt(item_position)
adapter?.notifyDataSetChanged()
listview.invalidate()
}
}
return true
}
override fun onCreateContextMenu(
menu: ContextMenu?,
v: View?,
menuInfo: ContextMenu.ContextMenuInfo?
) {
val info = menuInfo as AdapterView.AdapterContextMenuInfo
item_position = info.position
var inflator = MenuInflater(this)
inflator.inflate(R.menu.menu,menu)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
fruits.add(Fruit(R.drawable.apple_pic,"苹果","我是苹果"))
fruits.add(Fruit(R.drawable.cherry_pic,"樱桃","我是樱桃"))
fruits.add(Fruit(R.drawable.mango_pic,"芒果","我是芒果"))
adapter = FruitAdapter(this,R.layout.fruit_item,fruits)
listview.adapter = adapter
add.setOnClickListener{
adapter?.add(Fruit(R.drawable.apple_pic,"苹果","我是苹果"))
}
listview.setOnItemClickListener { parent, view, position, id ->
val fruit = fruits[position]
Toast.makeText(this,"点击了${fruit.name}",Toast.LENGTH_SHORT).show()
}
registerForContextMenu(listview)
}
}
长按子项,效果如下:
点击删除:
2.1 ListView缓存策略
ListView是两级缓存,它里面缓存的是ItemView。
如上图所示,ListView里面有一个内部类 RecycleBin,RecycleBin有两个对象Active View和Scrap View来管理缓存,Active View是第一级,Scrap View是第二级。
- Active View:缓存在屏幕内的ItemView,当列表数据发生变化时,屏幕内的数据可以直接拿来复用(返回ItemView),无须进行数据绑定。
- scrap View:缓存在屏幕外的ItemView,这里所有的缓存的数据都是"脏的",也就是数据需要重新绑定,也就是说屏幕外的所有数据在进入屏幕的时候都要走一遍getView()方法。
当Active View和Scrap View中都没有缓存的时候就会直接create view。此外,ListView的这种缓存机制在使用时可能会出现一些问题例如在快速滑动或网络状况不好时ListView异步加载图片错位、重复问题:比如ListView上有100个Item,一屏只显示10个Item,第11个Item的View复用了第1个Item View对象,ImageView也同时被复用,所以当图片没下载出来,这个ImageView(第11个Item)显示的数据就是复用(第1个Item)的数据,造成了加载错误。这种情况下如果每次getView能给对象一个标识,在异步加载完成时比较标识与当前行Item的标识是否一致,一致则显示,否则不做处理即可。
2.2 ListView优化
ListView优化的几个方面有:
- 复用convertView缓存布局。
- 复用ViewHolder缓存控件实例。
- 避免在getView中执行耗时操作。
- 开启硬件加速。
3.RecyclerView
需要引入依赖包:
RecyclerView相对ListView来说在性能方面进行了优化,例如子项中控件的点击事件比ListView简洁;也可以实现ListView不能实现的横向滚动。首先先利用RecyclerView实现上面ListView中实现的布局,activity_main.xml添加RecyclerView控件作为V
与ListView一样也要有C来控制数据的显示方式,注意RecyclerView强制要实现ViewHolder模式,具体原因之后再说:
ViewHolder类通过onCreateViewHolder方法中LayoutInflater.from(parent.getContext()).inflate方法将item布局加载并传给ViewHolder类的构造函数,并在构造函数里通过findViewbyId绑定实例。onBindViewHolder方法中利用ViewHolder实例进行赋值。
MainActivity与ListView类似,完成数据的动态加载:
与ListView不同的是增加了设置LinearLayoutManager实例,以用来指定RecyclerView的布局方式,LinearLayoutManager是线性布局的意思,因此效果与ListView类似:指定不同的布局方式显示的效果也不尽相同
Kotlin:逻辑与Java相同
子项布局文件:
FruitAdapter:首先定义了一个内部类ViewHolder,ViewHolder的主构造函数中需要传入一个View参数,这个参数就是Recycler子项的最外层布局,有了这个最外层布局,就可以通过findViewById获取子项布局中的各个控件实例
class FruitAdapter(val fruits:List<Fruit>): RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val fruit_icon:ImageView = itemView.findViewById(R.id.fruit_icon)
val fruit_name:TextView = itemView.findViewById(R.id.fruit_name)
val fruit_says:TextView = itemView.findViewById(R.id.fruit_says)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
//继承RecyclerView.Adapter必须要重写的方法
val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item,parent,false)
return ViewHolder(view)
}
//继承RecyclerView.Adapter必须要重写的方法
override fun getItemCount(): Int = fruits.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
//继承RecyclerView.Adapter必须要重写的方法 对子项控件实例进行赋值
val fruit = fruits[position]
holder.fruit_icon.setImageResource(fruit.icon)
holder.fruit_name.text = fruit.name
holder.fruit_says.text = fruit.says
}
}
MainActivity:layoutManager用于指定Recyclerview的布局方式,这里使用LinearLayoutManager就是现形布局的意思,可以实现和listview相同的效果
class MainActivity : AppCompatActivity() {
private val fruits = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
fruits.add(Fruit(R.drawable.apple_pic,"苹果","我是苹果"))
fruits.add(Fruit(R.drawable.cherry_pic,"樱桃","我是樱桃"))
fruits.add(Fruit(R.drawable.mango_pic,"芒果","我是芒果"))
val adapter = FruitAdapter(fruits)
val layoutManager = LinearLayoutManager(this)
recyclerview.layoutManager = layoutManager
recyclerview.adapter = adapter
}
}
效果如下:
接着看下上面说的与listview不同的横向滚动和子项控件点击事件优化。
横向滚动:
改动的地方有2:
1是item布局的宽度,当然这里没有设置图片与文字的对齐方式
2是通过 linearLayoutManager设置布局的排列方式
Kotlin:
再看点击事件,这里设置了子项view的点击事件以及子项中三个字控件的点击事件,在onCreateViewHolder中实现:
效果如下:
Kotlin:
同样在FruitAdapter的onCreateViewHolder方法中实现,逻辑与Java相同
效果如下:
3.1 RecyclerView缓存策略
RecyclerView是四级缓存,和ListView不同,它里面缓存的就是ViewHolder,这也是为什么RecyclerView要强制我们使用ViewHolder的原因,如果缓存的就是ViewHolder但是你又不用这不就自相矛盾了。ViewHolder当中会持有对应的ItemView的所有信息,比如说:position、view、width等等,拿到了ViewHolder基本就拿到了ItemView的所有信息,因此ViewHolder使用起来相比itemView更加方便。
RecyclerView的缓存分为四级:
- Scrap:对应ListView缓存中的Active View,就是屏幕内的缓存数据,可以直接拿来复用。
- Cache:刚刚移出屏幕的缓存数据,默认大小是2个。当其容量被充满同时又有新的数据添加的时候,会根据FIFO原则,把优先进入的缓存数据移出并放到下一级缓存中,然后再把新的数据添加进来。Cache里面的数据是干净的,也就是携带了原来的ViewHolder的所有数据信息,数据可以直接来拿来复用。需要注意的是,cache是根据position来寻找数据的,这个postion是根据第一个或者最后一个可见的item的position以及用户操作行为(上拉还是下拉)。例如当前屏幕内第一个可见的item的position是1,用户进行了一个下拉操作,那么当前预测的position就相当于(1-1=0),也就是position=0的那个item要被拉回到屏幕,此时RecyclerView就从Cache里面找position=0的数据,如果找到了就直接拿来复用。
- RecycledViewPool:Cache默认的缓存数量是2个,当Cache缓存满了以后会根据FIFO(先进先出)的规则把Cache先缓存进去的ViewHolder移出并缓存到RecycledViewPool中,RecycledViewPool默认的缓存数量是5个。RecycledViewPool与Cache相比不同的是,从Cache里面移出的ViewHolder再存入RecycledViewPool之前ViewHolder的数据会被全部重置,相当于一个新的ViewHolder,所以取出来的时候需要走onBindViewHolder()方法。而且Cache是根据position来获取ViewHolder,而RecycledViewPool是根据itemType获取的,如果没有重写getItemType()方法,itemType就是默认的。
- ViewCacheExtension:需要自定义,默认不实现。
3.2 RecyclerView优化
RecyclerView优化可以从以下方面:
- 布局优化:减少过渡绘制,减少布局层级,可以考虑使用自定义 View 来减少层级,或者更合理地设置布局来减少层级,不推荐在 RecyclerView 中使用 ConstraintLayout。
- 通过 RecycleView.setItemViewCacheSize(size); 来加大 RecyclerView 的缓存,用空间换时间来提高滚动的流畅性。
- 尽量将复杂的数据处理操作放到异步中完成。RecyclerView需要展示的数据经常是从远端服务器上请求获取,但是在网络请求拿到数据之后,需要将数据做扁平化操作,尽量将最优质的数据格式返回给UI线程。
因此对布局进行优化以及避免过多耗时操作、避免图片数据过多或者过大都可以避免RecyclerView卡顿。
4 ListView与RecyclerView的区别
从上面的介绍中我们也可以知道一些简单的不同之处:
- 从布局上看:ListView自己管理布局,而RecyclerView交给了LayoutManager;RecyclerView还可以实现网格布局、瀑布流布局等。
- 点击事件监听上看:ListView具体到子项里面的某个控件的点击事件的处理会比较麻烦,RecyclerView需要我们自己通过ViewHolder去给子项具体的View去注册点击事件,相比ListView来说简单很多。
- 缓存机制:ListView二级缓存,并且缓存的是ItemView,RecyclerView四级缓存,缓存的是ViewHolder,因此RecyclerView强制我们使用ViewHolder。
除此之外,在数据刷新方面,二者也是不相同的:
- ListView 更新数据源后需要通过 Adapter 的 notifyDataSetChanged 方法来通知视图更新变化,它会重绘每个 Item,但是实际上却并不是每个 Item 都需要重绘。而RecyclerView.Adapter 提供了 notifyItemChanged 用于更新单个 Item View 的刷新,我们可以省去自己写局部更新的工作。
参考: