ScrollView 嵌套 ScrollView 的滑动冲突
当我们在xml布局文件中使用 ScrollView 嵌套 ScrollView 时,会出现这样的问题:
外层的 ScrollView 能够正常滑动,但是内部的 ScrollView 却滑动不了。
查看 ScrollView 的源码发现
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onMotionEvent will be called and we do the actual
* scrolling there.
*/
/*
* Shortcut the most recurring case: the user is in the dragging
* state and they is moving their finger. We want to intercept this
* motion.
*/
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
if (super.onInterceptTouchEvent(ev)) {
return true;
}
/*
* Don't try to intercept touch if we can't scroll anyway.
*/
if (getScrollY() == 0 && !canScrollVertically(1)) {
return false;
}
...
...
...
在 onInterceptTouchEvent 方法中,首先就判断了 ev.getAction() 是不是 ACTION_MOVE,
当我们滑动的时候传进来的 MotionEvent.getAction 就是 ACTION_MOVE,此时返回 true 之后,直接拦截。那么接下来他的子 View 就接收不了 event,event 被传入他自己的onTouchEvent 中去进行滚动操作了。
所以,我们内部的 ScrollView 滑动没有响应的原因就是,那时候手指是在滑动的,一直不断传入 ACTION_MOVE,所以 event 一直被外部的 ScrollView 在上述的操作中拦截了。
就是说只要你手指在 ScrollView 上滑动,ScrollView 内部的子 View 就永远接收不到任何事件,就是永远无响应。
解决方案
在自定义控件中重写 onInterceptTouchEvent 告诉所有父 View:不要拦截事件
public class MyScrollView extends ScrollView {
public MyScrollView(Context context) {
super(context);
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 请求父类不拦截 TouchEvent
getParent().requestDisallowInterceptTouchEvent(true);
return super.onInterceptTouchEvent(ev);
}
}
getParent().requestDisallowInterceptTouchEvent(true);
意思就是取得父类,然后请求父类不拦截 TouchEvent 的意思。
重写完成之后 buildProject,最后在 xml 布局文件中将内部的 ScrollView 修改成自定义的MyScrollView,内部的 ScrollView 就能够正常滑动了。
ListView 嵌套 CheckBox 选中状态错乱问题
我们知道,ListView 使用的是复用机制
ListView 复用的原理:ListView中的每一个 Item 显示都需要 Adapter 调用一次 getView 的方法,这个方法会传入一个 convertView 的参数,返回的 View 就是这个 Item 显示的 View。如果当 Item 的数量足够大,再为每一个 Item 都创建一个 View 对象,必将占用很多内存,创建 View(mInflater.inflate(R.layout.lv_item, null); 从 xml 中生成 View,这是属于IO操作)也是耗时操作,所以必将影响性能。
Android提供了一个叫做 Recycler(反复循环器)的构件,就是当 ListView 的 Item 从上方滚出屏幕视角之外,对应 Item 的 View 会被缓存到 Recycler 中,相应的会从下方生成一个 Item,而此时调用的 getView 中的 convertView 参数就是滚出屏幕的 Item 的 View,所以说如果能重用这个 convertView,就会大大改善性能。
如果一个屏幕最多显示7个 Item,当 Item1 滑出屏幕,此时 Item1 的 View 被添加进 Recycler 中,相应的在下部要产生一个 Item8,这时调用 getView 方法,convertView 参数就是 Item1 的View。
ListView 的复用虽然大大提升了性能,但是却也带来很多问题。比如在加载图片时,由于下边的 item 复用了上边的 item,造成下边 item 刚加载出来时显示的还是上边被复用的 item 的图片,等到这个新的 item 加载图片完毕时才会正常显示,这就是 convert view 复用造成 listview图片加载错乱的问题。
与上边问题相似,在 listview 的 item 中存在 CheckBox 时也会由于复用 convert view 导致 CheckBox 的选中状态错乱,本片内容将解决由于复用导致 CheckBox 选中状态错乱的问题。
解决方案
在 Adapter 中加入一个Map集合,其中 Map 的 key 用来存储被选中的 checkbox 的 position,value 用来存储 checkbox 是否被选中。代码中还添加了 checkbox 的监听事件,在监听事件中判断点击的 checkbox 是否被选中,如果被选中了则将 position 添加到集合,并设置状态未 true,否则就将该 checkbox 从集合中移除。然后通过 if 语句判断集合中是否存在该 checkbox,如果存在则证明是被选中的,就将该 checkbox 设置为选中状态 setChecked(true),否则证明 checkbox 没有选中则设置 setChecked(false)。这样就解决了checkbox选中状态错乱的问题。
/**
* @author xinl5457
* @date 2021/10/19
*/
public class MyAdapter extends BaseAdapter {
private List<String> mStringList;
private Context mContext;
/**
* 存放已被选中的 CheckBox
*/
private Map<Integer,Boolean> map = new HashMap<>();
public MyAdapter(List<String> mStringList, Context mContext) {
this.mStringList = mStringList;
this.mContext = mContext;
}
@Override
public int getCount() {
return mStringList.size();
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
MyViewHolder holder;
if (convertView == null) {
convertView = View.inflate(mContext, R.layout.list_item,null);
holder = new MyViewHolder();
holder.tvCity = convertView.findViewById(R.id.tv_city);
holder.cb = convertView.findViewById(R.id.cb);
convertView.setTag(holder);
} else {
holder = (MyViewHolder) convertView.getTag();
}
holder.tvCity.setText(mStringList.get(position));
// 监听 CheckBox 的选中状态
holder.cb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
map.put(position,true);
} else {
map.remove(position);
}
}
});
if (map != null && map.containsKey(position)) {
holder.cb.setChecked(true);
} else {
holder.cb.setChecked(false);
}
return convertView;
}
public static class MyViewHolder{
TextView tvCity;
CheckBox cb;
}
}