ListView子View包含EditText后的各种问题分析及解决方案

来源:互联网 发布:中文可以编程吗 编辑:程序博客网 时间:2024/05/21 11:11

ListView子View包含EditText后的各种问题分析及解决方案

前不久,群里面一个妹子在群里面咨询了这样了一个需求:ListView的子View里面包含一个EditTextListView的item点击事件没有效果了。在尝试了查询资料,独立思考很久后,她还是没有解决问题。当时看到这个问题后,也觉得应该不是什么大问题,以前做过ListViewView内包含CheckBox等容易优先获取焦点的控件相关的需求,所以给她的答复是百度看看 descendantFocusability 这个属性的使用场景。但是过了一会儿,她还是没能解决问题,于是私聊她跟她说帮她写个demo,尝试一下解决这个问题。

在编写布局文件,编写代码后,运行了自己demo后,我惊讶的发现自己也没能解决这个问题。稍微思考了一下,又换了一种方式,没有使用OnItemClick,直接让Adapter.getView方法返回的convertView 设置onClick 事件,最后还是成功的了解决了问题。

虽然解决了问题,但是作为一名的合格的程序员应当具有一定的探索精神,在发现descendantFocusability 这个属性没有效果后,便又尝试了一番,这不尝试没什么,一尝试吓一跳,代码一运行后发现,滑动各种数据错乱,这一下子问题就更大了。起初我的demo只是用来验证点击事件没有效果的问题,之后联想到ListView的数据复用性问题,便对demo代码做了修改,这里贴出修改后的代码:

Activity布局代码

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical">    <ListView        android:id="@+id/list_view"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:dividerHeight="10dp" /></LinearLayout>

ListView子View布局代码

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:id="@+id/item_root"    android:layout_width="match_parent"    android:layout_height="50dp"    android:background="#ffcccc"    android:gravity="center"    android:orientation="vertical">    <EditText        android:id="@+id/et_test"        android:layout_width="200dp"        android:layout_height="50dp"        android:layout_gravity="right"        android:gravity="center"        android:hint="EditText提示信息"        android:singleLine="true"        android:textSize="20dp" /></LinearLayout>

数据填充

public class TestListViewWithEditTextActivity extends AppCompatActivity {    public Activity getActivity() {        return this;    }    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.layout_test_list_view_with_edit_text);        ListView listView = (ListView) findViewById(R.id.list_view);        List<ItemTest> list = new ArrayList<>();        for (int i = 0; i < 20; i++) {            ItemTest itemTest = new ItemTest();            itemTest.setText(i + "");            list.add(itemTest);        }        MyAdapter adapter = new MyAdapter(this, list);        listView.setAdapter(adapter);    }}

Adapter代码

public class MyAdapter extends ArrayAdapter<ItemTest> implements View.OnClickListener {    private int selectedEditTextPosition = -1;    private TextWatcher mTextWatcher = new SimpleTextWatcher() {        @Override        public void onTextChanged(CharSequence s, int start, int before, int count) {            if (selectedEditTextPosition != -1) {                Log.d("MyAdapter", "onTextChanged position: " + selectedEditTextPosition + ", s: " + s);                ItemTest itemTest = getItem(selectedEditTextPosition);                itemTest.setText(s.toString());            }        }    };    public MyAdapter(Context context, List<ItemTest> objects) {        super(context, 0, objects);    }    @NonNull    @Override    public View getView(int position, View convertView, ViewGroup parent) {        VH vh;        if (convertView == null) {            convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_test, null);            vh = new VH(convertView);            convertView.setTag(vh);            convertView.setTag(R.id.item_root, position);        } else {            vh = (VH) convertView.getTag();        }        vh.editText.addTextChangedListener(mTextWatcher);        vh.editText.setOnClickListener(this);        vh.editText.setTag(position);        String text = getItem(position).getText();        vh.editText.setText(text);        vh.editText.setSelection(vh.editText.length());        convertView.setOnClickListener(this);        return convertView;    }    @Override    public void onClick(View v) {        if (v.getId() == R.id.et_test) {            EditText editText = (EditText) v;            selectedEditTextPosition = (int) editText.getTag();            Log.d("MyAdapter", "clicked position: " + selectedEditTextPosition);        } else if (v.getId() == R.id.item_root) {            int position = (int) v.getTag(R.id.item_root);            Toast.makeText(getContext(), "点击第 " + position + " 个item", Toast.LENGTH_SHORT).show();        }    }}

数据填充以上的代码就不详细解释了,无非就是创建了20个ItemTest数据填充。重点看Adapter的代码(没有贴ViewHolder代码),在getView方法里面, 先常规性的创建并且利用ViewHolder复用子View,然后获取item数据填充到EditText,注意到Adapter内的一个mTextWatcher,它用来处理EditText编辑数据时的回调。同事还有个selectedEditTextPosition变量,因为TextWatcher是全局变量,所有EditText共享这个回调接口,为了记录当前是编辑哪一个EditText,我用了这个变量来保存当前触发了哪一个EditText的编辑事件,从代码中能看到

vh.editText.setTag(position)

if (v.getId() == R.id.et_test) {    selectedEditTextPosition = (int) v.getTag();}

第一次运行后的效果

从运行结果的gif来看,当刚加载进入界面后,数据都是完好的,手动滑动到第15个item后,点击了第10个EditText,然后弹出软键盘,然后关闭软件盘,第二次再点击第十个EditText,再关闭软键盘,期间并没有做任何修改数据的操作,但是,当第二次关闭软键盘时,第10个EditText数据变了,变成了9,之后继续滑动,把第10个item向下滑动出去,再滑动出来,发现,第10个EditText数据又变成0了。。。如果你继续点击任意的EditText,你会发现,数据变化的越来越离谱。而且,明明设置item点击事件,点击item也不会执行,这问题很无语啊.

运行结果不会欺骗你的眼睛的,实际上数据确实变化了,因为当弹出输入法时,界面上移,所以ListView复用了子View,子View在重新绘制了,由于复用了View,数据会发生错乱,但是,从代码可以看出,数据都是根据position来保存和获取的,如果position没有发生变动,数据理论上不会发生改变的,那么到底是哪里出了问题呢?这里不打算卖关子,实际上我能想到的可能性原因是输入法弹出时,不止一个EditText获取了焦点!由于一次点击EditText时,界面重绘,导致很多个EditText同时获取焦点,当第二次弹出软键盘时,由于界面重绘,同时View复用,导致多个EditText数据在设置数据时同时回调TextWatcheronTextChanged接口,同时细心的读者会发现,EditText的光标会在界面滑动时,出现多个在闪烁,因为同时获取了焦点。当然为了验证猜想,肯定得祭出log:

Log.d("MyAdapter", "onTextChanged position: " + selectedEditTextPosition + ", s: " + s);

log截图:


从log来看,操作流程和log打印里面的红框的描述是一致的,但是却出现了一个问题,第一次点击EditText,并没有打印出 clickposition: 10 这样的log,但是第二次点击时却打印了,同时,数据回调接口回调了position为10的次数非常多,但是,数据s确是不同的,这就验证起初猜想。此时暂且假装找到了问题的原因是由于多个EditText同时获得了焦点导致界面重绘时存储了错乱的数据。

既然问题找到了,就自然得有解决的办法。淡定思考三分钟,怎么确保同一时刻EditText当中只有一个能获取到焦点呢?我觉得这个这问题的答案其实已经暴露在原有的代码当中了,答案就在那个selectedEditTextPositionEditText焦点设置的处理上了。那么我的想法是这样的:既然界面重绘会引发多个EditText同时获取焦点,那么保证只有点击到的EditText才获取焦点,也就是点击的EditText所在的 position 和 selectedEditTextPosition 的值相同,同时,这两个值不相等时,对应的EditText应该取消焦点。但是有个问题,这个条件要建立在selectedEditTextPosition 变量的值一定在界面重绘之前就应当被修改为点击的EditText对应的position才行,所以,更改了代码如下:

 vh.editText.addTextChangedListener(mTextWatcher); vh.editText.setOnClickListener(this); vh.editText.setTag(position); // 保证每个时刻只有一个EditText能获取到焦点 if (selectedEditTextPosition != -1 && position == selectedEditTextPosition) {       vh.editText.requestFocus(); } else {      vh.editText.clearFocus(); }       

增加了底下四行代码,判断positionselectedEditTextPosition的值是否一样来获取或者清除焦点。当运行此时的代码,按照前面的操作流程过一遍后,运行结果很令我惊讶,因为并没有什么卵用…,和原先的问题一模一样,貌似没有啥变化。仔细查看代码发现,傻逼了,上面发现了一问题,第一此点击EditText时并没有触发click事件那么selectedEditTextPosition的值就不会记录当前点击了哪个EditText,那么那几行根据position来判断是否获取焦点的代码肯定是不起作用了 ,唉。。。

静下心来,吃块薯片压压惊,于是我又想到一定要触发某个事件让selectedEditTextPosition这个值发生改变,嗯,必须要改变!了解事件分发原理的读者应该知道onClick事件是在onTouchEvent(还有个OnTouchListener.onTouch)的up事件后触发的,onClick此时既然不起作用了,那么就只好用onTouchEvent,不过再一想,onTouchEvent要复写控件,所以只能用onTouch事件了,于是代码继续改动如下:

vh.editText.setOnClickListener(this);

更改为:

vh.editText.setOnTouchListener(this);@Overridepublic boolean onTouch(View v, MotionEvent event) {     if (event.getAction() == MotionEvent.ACTION_UP) {          EditText editText = (EditText) v;          selectedEditTextPosition = (int) editText.getTag();          Log.d("MyAdapter", "clicked position: " + selectedEditTextPosition);     }     return false;}  

于是继续跑代码测试,结果如下:

这里写图片描述

你妈嗨,框中框的log奇迹般的复活出现了。but!这还是有问题啊啊!第一次点击后,数据还是错乱的啊,焦点问题理论上已经解决了,相关代码涉及到selectedEditTextPosition变量的值也能正确获取了,还是不行,fuck again!这尼玛坑真多啊。。。这心是静不下来了啊。。。数据错乱反正是存储导致的,反正肯定是焦点导致的,现在这个代码先设置了TextWatcher,在对应获取焦点,会不会弄反了啊,或者说界面重绘时,是不是应该只让当前点击的EditText获取焦点的时候,才给它加上TextWatcher,失去焦点的EditText不应该添加TextWatcher, 这样即使界面重绘没有获取到焦点的EditText即使重新绘制也不会触发数据存储回调。想法好像没问题, 那就这么干,既然设置TextWatcher是在获得焦点和失去焦点时执行的,很容易想到再弄个OnFocusChangedListener,于是接着改:

vh.editText.addTextChangedListener(mTextWatcher);vh.editText.setOnTouchListener(this); // 修改前vh.editText.setTag(position);
vh.editText.setOnTouchListener(this);vh.editText.setOnFocusChangeListener(this); // 修改后vh.editText.setTag(position);@Overridepublic void onFocusChange(View v, boolean hasFocus) {    EditText editText = (EditText) v;    if (hasFocus) {        editText.addTextChangedListener(mTextWatcher);    } else {        editText.removeTextChangedListener(mTextWatcher);    }}    

继续执行代码,看结果:

这里写图片描述

我草,尽然成功了,第一次点击EditText 只回调了一次onTextChanged,同时,修改数据后,关闭输入法,界面重绘,数据也没有错乱。这是个奇迹!皇天不负有心人,在尝试了各种方案,各种猜想后,终于大功告成了。

哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈嗝。

结尾总结:问题的出现一定是有迹可循,代码是不会骗人的。笔者最后解决了问题,但是过程中难在猜想,因为对系统源码的不熟悉,猜想的方向其实也很难把握,所以,在遇到类似问题的时候,尽自己最大的努力去尝试吧。

PS:最后把结果丢给妹子的时候。。。

源码传送门

阅读全文
3 1