注意:在复用情况下冲突问题无法解决
网上所有的可能性与实现方式我都实验了一边,亲测当动态添加带有EditText的item时,如果使用了复用的方式,无论怎么处理ListView与EditText始终存在冲突。最后我的解决方式只有采用不复用的方式来处理,但滑动存在卡顿需要寻找其他实现方式,这里只是我比较蠢的一种解决方案。
问题
出现的问题主要是,焦点与数据状态保存问题。
分析
大家都知道,ListView的convertView其实始终只有第一屏的那几个,如果说你采用了复用的方式,滑动ListView则其他屏幕显示的item仍旧是第一屏的convertView,只是通过不停的在getView中反复赋值来让item显示相同或不同的效果。
这里需要做个解释,软键盘的伸缩,针对EditText的文字输入等都会造成ListView的重绘,getView方法不断执行,并且,ListView重绘会造成EditText的焦点变换,所以控制起来十分的棘手。
网上的操作是,针对EditText进行文字输入监听,通过保存输入完毕后的内容来重新赋值。有的使用的则是触摸监听来获取position等。这些方式,只要是不重绘的页面上都是正常显示的,也可正常保存数据。一旦复用,数据保存就会错乱,因为重绘操作会不断的走到这些监听里面,造成position的错乱导致存储的数据与position对不上。并且,我的需求是一个动态需求,如图所示:
点击新增一条的时候,新增一条输入框,新增的输入框获取到焦点并弹出键盘。点击删除的时候删除当前item并且保持别的EditText的数据状态不变。
实现起来真是巨麻烦,大概花了一天半的时间解决这个问题,但是还是没有达到最优化。但是我觉得如果你要复用的话那么这个问题应该不能解决。因为牵扯到EditText的焦点变化、文字输入以及键盘的弹出收起等都会导致重绘,你很难去把握这个焦点到底跳到哪里去了。
好那么我们只要看一个文件就好:适配器文件MainAdapter
public class MainAdapter extends BaseAdapter {
private ArrayList<String> dataList;
private EditTextWater watcher;
private int add_tag = -1;//用于控制滑动状态输入法会弹起的bug
public MainAdapter(ArrayList<String> dataList) {
this.dataList = dataList;
}
/**
* 手动新增一条数据
*
* @param sn
* @param listView
*/
public void addData(String sn, ListView listView) {
add_tag = 1;
dataList.add(sn);
notifyDataSetChanged();
listView.smoothScrollToPosition(getCount() - 1);//让ListView滚动到底部使新增的item可以看到
}
/**
* 手动删除一条数据
*
* @param position
*/
public void removeData(int position) {
dataList.remove(position);
notifyDataSetChanged();
}
@Override
public int getCount() {
return dataList.size();
}
@Override
public Object getItem(int position) {
return dataList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View convertView, final ViewGroup parent) {
final ViewHolder holder = new ViewHolder();
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_edittext, parent, false);
holder.tvNum = (TextView) convertView.findViewById(R.id.tv_num);
holder.etNewAdd = (EditText) convertView.findViewById(R.id.et_new_add);
holder.tvChangeTitle = (TextView) convertView.findViewById(R.id.tv_change_title);
convertView.setTag(holder);
watcher = new EditTextWater(position);
holder.etNewAdd.addTextChangedListener(watcher);
holder.etNewAdd.setText(dataList.get(position));
holder.tvNum.setText(position + 1 + "");
holder.etNewAdd.setSelection(dataList.get(position).length());//设置光标问题
holder.tvChangeTitle.setText(R.string.delete);
holder.tvChangeTitle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
holder.etNewAdd.removeTextChangedListener(watcher);
removeData(position);
}
});
/**
* 当前这里手动获取焦点
* 但是实际输入法没有在这里弹出
* 因此如果需要弹出输入法的时候就需要手动写一个
*/
if (position == getCount() - 1 && add_tag == 1) {
add_tag = -1;
holder.etNewAdd.requestFocus();
holder.etNewAdd.post(new Runnable() {
@Override
public void run() {
InputMethodManager imm = (InputMethodManager) parent.getContext().getSystemService(INPUT_METHOD_SERVICE);
imm.showSoftInput(holder.etNewAdd, InputMethodManager.SHOW_IMPLICIT);
}
});
} else {
holder.etNewAdd.clearFocus();
}
return convertView;
}
/**
* 重新文字监听器
* 保存当前输入的信息以保证滑动的时候EditText显示的文字同步
*/
class EditTextWater implements TextWatcher {
private int position;
public EditTextWater(int position) {
this.position = position;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
Log.i("usher", "afterTextChanged: " + position + "-" + s);
dataList.set(position, s.toString());
}
}
class ViewHolder {
TextView tvNum;
EditText etNewAdd;
TextView tvChangeTitle;
}
}
有几个地方比较难:
- 重写文字改变的监听,用于保存你最后一次输入的数据;
- 去除文字改变的监听,用于避免反复绘制item造成的position与数据不匹配;
- 针对新增的item的EditText中弹起键盘的监听与滑动时针对键盘不弹出的控制;
大家可以尝试使用复用convertView的方式来写ListView,你会发现,当绘制item超过一屏的时候,超出的部分中重绘的item中EditText所拿到的数据并不是上一次的数据了,因为反复绘制的时候导致了文字改变的监听始终执行,虽然有规律可循,但是我没有找到控制这一现象的方法。
最后贴上首页的代码:
public class MainActivity extends AppCompatActivity {
private ListView lvMain;
private MainAdapter adapter;
private Button btn_add_new;
private ArrayList<String> list;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
initView();
initData();
}
private void initData() {
list = getData();
adapter = new MainAdapter(list);
lvMain.setAdapter(adapter);
btn_add_new.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
adapter.addData("", lvMain);
}
});
}
private void initView() {
lvMain = (ListView) findViewById(R.id.lv_main);
btn_add_new = (Button) findViewById(R.id.btn_add_new);
}
private ArrayList<String> getData() {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 2; i++) {
list.add("input data from" + i);
}
return list;
}
}
结语
以上方式的处理方法比较笨,同时还有另外一种处理方式,就是再ListView的外层套上一层ScrollView,然后重写ListView用于解决与ScrollView的冲突。但实际上这种处理方式与我所写的根本道理是一致的,都是再不断的新建item,并且友情提示,这种处理方式并不适用于大量的item,第一个较为明显的问题就是卡顿,因为你在滑动的时候,系统是再不停的新建与销毁,这需要较多的内存来处理,打开AS的内存可以看到这个页面的内存消耗还是不小的。当然如果有更优化的处理方式,欢迎留言。