Android ListView 中EditText无法获取焦点 最终解决方案

目前这类问题网上已经有很多答案了,例如:
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等常用方法,满足各种需求

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值