先来看看项目中的需求,直接上图吧:

这一块的功能主要包含两个:
功能一:子阶段实现侧滑删除功能;
功能二:点击子阶段,展示子阶段下包含的任务。
功能实现:整个页面外层有一个scroll view,子阶段是一个自定义LinearLayout布局,每个自阶段下包含一个recycler view,用于放置子阶段下的任务,点击子阶段展开时显示recycler view,再次点击隐藏recycler view。
功能实现大概就是上面那样,下面主要说一下开发过程中遇到的两个bug:
bug 一:点击子阶段时,下面的recycler view不显示,点击事件未响应;
bug 二:上下滚动屏幕时,初次按下的焦点落在子阶段布局内明显感觉滚动不流畅。
上述两个bug都与自定义LinearLayout布局有关,下面先贴上自定义布局的代码:
public class SwipeLayout extends LinearLayout {
private static final String TAG = "SwipeLayout";
private Context mContext;
private LinearLayout mContentView;
private RelativeLayout mRightView;
private Scroller mScroller;
private OnSlideListener mOnSlideListener;
private int mHolderWidth = 80;
private int mLastX = 0;
private int mLastY = 0;
private static final int TAN = 2;
private int mSlideState = 0;
private int firstClickX;
private int firstClickY;
public interface OnRightMenuClickListener {
void rightClick(View view);
}
private OnRightMenuClickListener onRightMenuClickListener;
public void setOnRightMenuClickListener(OnRightMenuClickListener onRightMenuClickListener) {
this.onRightMenuClickListener = onRightMenuClickListener;
}
public interface OnContentClickListener {
void contentClick(View view);
}
private OnContentClickListener onContentClickListener;
public void setOnContentClickListener(OnContentClickListener onContentClickListener) {
this.onContentClickListener = onContentClickListener;
}
public interface OnSlideListener {
int SLIDE_STATUS_OFF = 0;
int SLIDE_STATUS_START_SCROLL = 1;
int SLIDE_STATUS_ON = 2;
void onSlide(View view, int status);
}
public SwipeLayout(Context context) {
super(context);
initView();
}
public SwipeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
private void initView() {
mContext = getContext();
mScroller = new Scroller(mContext);
setOrientation(LinearLayout.HORIZONTAL);
View.inflate(mContext, R.layout.swipelayout_right, this);
mContentView = findViewById(R.id.view_content);
mRightView = findViewById(R.id.view_right);
mHolderWidth = Math.round(TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, mHolderWidth, getResources()
.getDisplayMetrics()));
//删除按钮的监听
mRightView.setOnClickListener(v -> {
if (onRightMenuClickListener != null)
onRightMenuClickListener.rightClick(v);
});
//内容view的监听
mContentView.setOnClickListener(v -> {
if (null != onContentClickListener) {
onContentClickListener.contentClick(v);
}
});
}
//将View加入到mContentView中
public void setContentView(View view) {
mContentView.addView(view);
}
public View getContentView() {
return mContentView;
}
public void setOnSlideListener(OnSlideListener onSlideListener) {
mOnSlideListener = onSlideListener;
}
//将当前状态设置为关闭
public void shrink() {
if (getScrollX() != 0) {
this.smoothScrollTo(0, 0);
}
}
//如果其子View存在消耗点击事件的View,那么SwipeLayout的onTouchEvent不会被执行,
// 因为在ACTION_MOVE的时候返回true,执行其onTouchEvent方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
firstClickX = (int) ev.getX();
firstClickY = (int) ev.getY();
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
if (mOnSlideListener != null) {
mOnSlideListener.onSlide(this,
OnSlideListener.SLIDE_STATUS_START_SCROLL);
mSlideState = OnSlideListener.SLIDE_STATUS_START_SCROLL;
}
//这里需要记录mLastX,mLastY的值,不然当SwipeLayout已经处于开启状态时,
// 用于再次滑动SwipeLayout时,会先立即复原到关闭状态,用户体验不太好
mLastX = (int) ev.getX();
mLastY = (int) ev.getY();
return false;
case MotionEvent.ACTION_MOVE:
//返回值为true表示本次触摸事件由自己执行,即执行SwipeLayout的onTouchEvent方法
int deltaX = x - mLastX;
int deltaY = y - mLastY;
//这里作用是来比较X、Y轴的滑动距离,如果X轴的滑动距离小于两倍的Y轴滑动距离,则不执行SwipeLayout的滑动事件
if (Math.abs(deltaX) < Math.abs(deltaY) * TAN) {
return false;
}
return true;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int scrollX = getScrollX();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
//如果SwipeLayout是在譬如ScrollView、ListView这种可以上下滑动的View中
//那么当用户的手指滑出SwipeLayout的边界,那么将会触发器ACTION_CANCEL事件
//如果此情形发生,那么SwipeLayout将会处于停止状态,无法复原。
//当布局内竖直滑动距离小于水平滑动距离时 自己处理事件
if (Math.abs(firstClickX - x) > Math.abs(firstClickY - y)) {
//增加下面这句代码,就是告诉父控件,不要cancel我的事件,我的事件我继续处理。
getParent().requestDisallowInterceptTouchEvent(true);
int newScrollX = scrollX - deltaX;
if (deltaX != 0) {
if (newScrollX < 0) {
//最小的滑动距离为0
newScrollX = 0;
} else if (newScrollX > mHolderWidth) {
//最大的滑动距离就是mRightView的宽度
newScrollX = mHolderWidth;
}
this.scrollTo(newScrollX, 0);
}
}else {//当布局内竖直滑动距离大于水平滑动距离时 交给父控件处理事件
this.scrollTo(0, 0);//触发上下滚动的时候 让swiperlayout恢复到初态
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
break;
case MotionEvent.ACTION_UP: {
int cuX = (int) event.getX();
if (cuX == firstClickX) {
if (mContentView != null && null != onContentClickListener) {
onContentClickListener.contentClick(mContentView);
}
} else {
int newScrollX = 0;
//如果已滑动的距离满足下面条件,则SwipeLayout直接滑动到最大距离,不然滑动到最小距离0
if (scrollX - mHolderWidth * 0.8 > 0) {
newScrollX = mHolderWidth;
}
this.smoothScrollTo(newScrollX, 0);
if (newScrollX == 0) {
mSlideState = OnSlideListener.SLIDE_STATUS_OFF;
} else
mSlideState = OnSlideListener.SLIDE_STATUS_ON;
}
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
//获取当前SwipeLayout的滑动状态
public int getSideState() {
return mSlideState;
}
public void setmSlideState(int state) {
mSlideState = state;
}
private void smoothScrollTo(int destX, int destY) {
// 缓慢滚动到指定位置
int scrollX = getScrollX();
int delta = destX - scrollX;
mScroller.startScroll(scrollX, 0, delta, 0, Math.abs(delta) * 3);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
}
bug解决过程:
bug 一解决思路:最开始的时候,因为子阶段自定义布局内还包裹了一层布局,所以我把点击事件放到了最里面的那层布局上去处理,导致焦点问题不能响应点击事件,后来我在自定义布局中设置一个接口,然后在onTouchEvent()方法中去拦截屏幕的响应事件,在MotionEvent.ACTION_UP(手指抬起时触发)动作中去判断此次事件是否是点击事件,是的话再去调用接口中的方法,具体代码如下:
设置内容view部分的点击监听:
public interface OnContentClickListener {
void contentClick(View view);
}
private OnContentClickListener onContentClickListener;
case MotionEvent.ACTION_UP:
//手指抬起时x坐标
int cuX = (int) event.getX();
if (cuX == firstClickX) {//通过判断手指抬起时和按下时x的坐标是否相等来判定此次事件为滑动还是点击
//事件为点击的时候 调用监听接口
if (mContentView != null && null != onContentClickListener) {
onContentClickListener.contentClick(mContentView);
}
} else {///事件为滑动时 按滑动处理
int newScrollX = 0;
//如果已滑动的距离满足下面条件,则SwipeLayout直接滑动到最大距离,不然滑动到最小距离0
if (scrollX - mHolderWidth * 0.8 > 0) {
newScrollX = mHolderWidth;
}
this.smoothScrollTo(newScrollX, 0);
if (newScrollX == 0) {
mSlideState = OnSlideListener.SLIDE_STATUS_OFF;
} else
mSlideState = OnSlideListener.SLIDE_STATUS_ON;
}
break;
以上就是第一个bug的解决过程;
bug 二解决思路:滚动时获取焦点在子布局中X和Y方向上滑动的距离,将二者的值做一个比较,当Y方向上的距离大于X方向上时,那么就将事件交给父布局(也是就scrollview)去处理滚动事件,反之就由子布局去处理它自己的侧滑事件。具体代码如下:
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
//如果SwipeLayout是在譬如ScrollView、ListView这种可以上下滑动的View中
//那么当用户的手指滑出SwipeLayout的边界,那么将会触发器ACTION_CANCEL事件
//如果此情形发生,那么SwipeLayout将会处于停止状态,无法复原。
//当布局内竖直滑动距离小于水平滑动距离时 自己处理事件
if (Math.abs(firstClickX - x) > Math.abs(firstClickY - y)) {
//增加下面这句代码,就是告诉父控件,不要cancel我的事件,我的事件我继续处理。
getParent().requestDisallowInterceptTouchEvent(true);
int newScrollX = scrollX - deltaX;
if (deltaX != 0) {
if (newScrollX < 0) {
//最小的滑动距离为0
newScrollX = 0;
} else if (newScrollX > mHolderWidth) {
//最大的滑动距离就是mRightView的宽度
newScrollX = mHolderWidth;
}
this.scrollTo(newScrollX, 0);
}
}else {//当布局内竖直滑动距离大于水平滑动距离时 交给父控件处理事件
this.scrollTo(0, 0);//触发上下滚动的时候 让swiperlayout恢复到初态
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
以上就是解决第二个bug的过程。
总结:写这篇文章来记录一下scrollview与其嵌套的布局在事件冲突问题上的解决过程,希望看到的大佬能指出其中一些理解不对的地方,感谢。
网友评论