目前这类问题网上已经有很多答案了,例如:
1、有的是加android:descendantFocusability属性;
2、有的是给EditText添加Touch事件,记录下是点击了哪个EditText,然后再adapter里的getView方法里手动给这个EditText获取焦点
我接到的需求是:
1、点击添加按钮,然后再列表里添加一个Item;
2、这个Item里就是一个表单,里面很多个EditText让用户填写信息;
3、同时还要实时监听EditText的文本输入,以及监听它的焦点情况校验用户输入的数据;
4、最后点击保存时,批量保存列表里各个Item里的数据
做好了之后发现输入框焦点会有各种问题,比如EditText无法获取焦点弹出输入框,ListView频繁刷新,EditText频繁获取/失去焦点,使用ViewHolder后还会导致这个Item的数据更新会直接导致其他Item数据也跟着跟新(断点后发现不同Item返回的是同一个ViewHolder),反正各种莫名其妙的问题都来了
在试了各种方法无效之后,既然ListView里嵌入EditText焦点问题这么难以处理,干脆试着用最简单的方式自己模拟做一个ListView:
思路很简单,就是定义一个纵向布局的LinearLayout,然后动态的往里面addView
为了有滑动效果,外层还要嵌套一个ScrollView,于是有了下面最简单的ListView
/**
* ListView里有EditText,解决焦点问题
*
* @author zhujie
* @date 2019/8/10
* @time 22:56
*/
class EditListView(context: Context?, attrs: AttributeSet?) : ScrollView(context, attrs) {
private val mContentView = LinearLayout(context)
private lateinit var mAdapter: BaseAdapter
init {
mContentView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
mContentView.orientation = LinearLayout.VERTICAL
addView(mContentView)
}
/**
* 设置Adapter
*/
fun setAdapter(adapter: BaseAdapter) {
this.mAdapter = adapter
handlerDataSetChanged()
}
/**
* 更新列表
*/
private fun handlerDataSetChanged() {
mContentView.removeAllViews()
for (position: Int in 0 until mAdapter.count) {//通过adapter循环添加子View
val itemView = mAdapter.getView(position, null, null)
itemView.layoutParams = LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
mContentView.addView(itemView)
}
}
}
接着还需要支持显示分割线,为了像ListView那样简单配置,使用了自定义属性,在res/values/attrs.xml中添加:
<declare-styleable name="EditListView">
<!--分隔符-->
<attr name="divider" format="reference" />
<!--分隔符高度-->
<attr name="dividerHeight" format="dimension" />
</declare-styleable>
于是有了下面这个版本的ListView:
/**
* ListView里有EditText,解决焦点问题
*
* @author zhujie
* @date 2019/8/10
* @time 22:56
*/
class EditListView(context: Context?, attrs: AttributeSet?) : ScrollView(context, attrs) {
private val mContentView = LinearLayout(context)
private lateinit var mAdapter: BaseAdapter
private var divider = -1//分隔符资源id
private var dividerHeight = 0//分隔符高度
init {
mContentView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
mContentView.orientation = LinearLayout.VERTICAL
addView(mContentView)
if (attrs != null) {//读取配置的属性值
val typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.EditListView)
divider = typedArray.getResourceId(R.styleable.EditListView_divider, -1)
dividerHeight = typedArray.getDimensionPixelOffset(R.styleable.EditListView_dividerHeight, 0)
typedArray.recycle()
}
}
/**
* 设置Adapter
*/
fun setAdapter(adapter: BaseAdapter) {
this.mAdapter = adapter
handlerDataSetChanged()
}
/**
* 更新列表
*/
private fun handlerDataSetChanged() {
mContentView.removeAllViews()
for (position: Int in 0 until mAdapter.count) {//通过adapter循环添加子View
val itemView = mAdapter.getView(position, null, null)
itemView.layoutParams = LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
mContentView.addView(itemView)
//添加分隔符
if (divider > 0 && dividerHeight > 0) {
val dividerView = View(context)
dividerView.setBackgroundResource(divider)
dividerView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dividerHeight)
mContentView.addView(dividerView)
}
}
}
}
使用时像ListView那样在布局文件里配置就可以了:
<com.xxxx.EditListView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:divider="@color/grey"
app:dividerHeight="1dp">
</com.xxxx.EditListView>
根据项目需要,ListView底部还需要加入一个FooterView:
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.ListAdapter
import android.widget.ScrollView
import com.Guansheng.DaMiYinApp.R
/**
* ListView里有EditText,解决焦点问题
*
* @author zhujie
* @date 2019/8/10
* @time 22:56
*/
class EditListView(context: Context?, attrs: AttributeSet?) : ScrollView(context, attrs) {
private val mContentView = LinearLayout(context)
private lateinit var mAdapter: BaseAdapter
private var divider = -1//分隔符资源id
private var dividerHeight = 0//分隔符高度
private var mFooterView: View? = null//底部的FooterView
init {
mContentView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
mContentView.orientation = LinearLayout.VERTICAL
addView(mContentView)
if (attrs != null) {//读取配置的属性值
val typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.EditListView)
divider = typedArray.getResourceId(R.styleable.EditListView_divider, -1)
dividerHeight = typedArray.getDimensionPixelOffset(R.styleable.EditListView_dividerHeight, 0)
typedArray.recycle()
}
}
/**
* 设置Adapter
*/
fun setAdapter(adapter: BaseAdapter) {
this.mAdapter = adapter
handlerDataSetChanged()
}
/**
* 更新列表
*/
private fun handlerDataSetChanged() {
mContentView.removeAllViews()
for (position: Int in 0 until mAdapter.count) {//通过adapter循环添加子View
val itemView = mAdapter.getView(position, null, null)
itemView.layoutParams = LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
mContentView.addView(itemView)
//添加分隔符
if (divider > 0 && dividerHeight > 0) {
val dividerView = View(context)
dividerView.setBackgroundResource(divider)
dividerView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dividerHeight)
mContentView.addView(dividerView)
}
}
}
/**
* 底部增加一个FooterView
*/
fun addFooterView(view: View) {
mFooterView = view
mContentView.addView(view)
}
}
由于以往我们修改数据后需要调用adapter的notifyDataSetChanged方法通知刷新列表,但是我们这个是自定义的View,要怎么才能跟adapter的notifyDataSetChanged关联起来,接收到通知更新视图呢?
于是查看了下BaseAdapter.java的notifyDataSetChanged源码:
/**
* Common base class of common implementation for an {@link Adapter} that can be
* used in both {@link ListView} (by implementing the specialized
* {@link ListAdapter} interface) and {@link Spinner} (by implementing the
* specialized {@link SpinnerAdapter} interface).
*/
public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {
private final DataSetObservable mDataSetObservable = new DataSetObservable();
public void registerDataSetObserver(DataSetObserver observer) {
mDataSetObservable.registerObserver(observer);
}
public void unregisterDataSetObserver(DataSetObserver observer) {
mDataSetObservable.unregisterObserver(observer);
}
/**
* Notifies the attached observers that the underlying data has been changed
* and any View reflecting the data set should refresh itself.
*/
public void notifyDataSetChanged() {
mDataSetObservable.notifyChanged();
}
}
看得出来这是个典型的观察者模式,系统的ListView一定是调用了adapter的registerDataSetObserver方法注册了监听,所以才能接收到通知,于是修改后的代码如下:
/**
* ListView里有EditText,解决焦点问题
*
* @author zhujie
* @date 2019/8/10
* @time 22:56
*/
class EditListView(context: Context?, attrs: AttributeSet?) : ScrollView(context, attrs) {
private val mContentView = LinearLayout(context)
private lateinit var mAdapter: ListAdapter
private var divider = -1//分隔符资源id
private var dividerHeight = 0//分隔符高度
private var mFooterView: View? = null//底部的FooterView
private val mDataSetObserver = AdapterDataSetObserver()//监听刷新事件
init {
mContentView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
mContentView.orientation = LinearLayout.VERTICAL
addView(mContentView)
if (attrs != null) {//读取配置的属性值
val typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.EditListView)
divider = typedArray.getResourceId(R.styleable.EditListView_divider, -1)
dividerHeight = typedArray.getDimensionPixelOffset(R.styleable.EditListView_dividerHeight, 0)
typedArray.recycle()
}
}
/**
* 设置Adapter
*/
fun setAdapter(adapter: ListAdapter) {
this.mAdapter = adapter
//注册数据更新监听
this.mAdapter.registerDataSetObserver(mDataSetObserver)
handlerDataSetChanged()
}
/**
* 更新列表
*/
private fun handlerDataSetChanged() {
mContentView.removeAllViews()
for (position: Int in 0 until mAdapter.count) {//通过adapter循环添加子View
val itemView = mAdapter.getView(position, null, null)
itemView.layoutParams = LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
mContentView.addView(itemView)
//添加分隔符
if (divider > 0 && dividerHeight > 0) {
val dividerView = View(context)
dividerView.setBackgroundResource(divider)
dividerView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dividerHeight)
mContentView.addView(dividerView)
}
}
}
/**
* 底部增加一个FooterView
*/
fun addFooterView(view: View) {
mFooterView = view
mContentView.addView(view)
}
/**
* 监听数据更新实践
*/
private inner class AdapterDataSetObserver : DataSetObserver() {
override fun onChanged() {
super.onChanged()
handlerDataSetChanged()
}
override fun onInvalidated() {
super.onInvalidated()
handlerDataSetChanged()
}
}
于是一个简单的ListView就完成了,使用了这个自定义的ListView后不管列表中有多少个EditText都不会存在各种焦点问题,其他乱七八糟的问题也就一起消失了
虽然简单的ListView做好了,但是性能还不高,没有利用ViewHolder循环利用布局文件,每次都会创建新的View,后续可以根据滑动的位置,将不在屏幕显示范围内的View缓存起来,以提高性能
后续还可以setOnScrollChangeListener、setOnItemClickListener等常用方法,满足各种需求