本篇文章主要谈谈QQ左滑删除效果的制作,本ScrollDeleteView跟QQ的实现思路有些差别,QQ的实现是将整块列表作为一个容器操作的,使得效果更加绚丽,更好的控制左滑删除的效果和事件分发冲突,本View不存在滑动冲突。
先讲解一下我的思路,这个列表中承载的是ScrollDeleteView,分为两个部分
1.cotentView;
2.隐藏的View;
那么我们怎么让其滑动起来?,使用scrollTo()和Scroller工具类来配合完成。
难点:
1.如何让隐藏的View获得down和up事件,因为我们需要点击,那么这两个事件是必不可少的。
2.如何是的item整体的点击事件能响应;
3.如何控制隐藏部分的显示和影藏;
一、重写onMeasure()和onLayout(),集成LinearLayour会简单一些,这边也是按照LinearLayout的逻辑进行操作的。
//这里制定如下规则,第一个child作为content,后面的child作为附加视图
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//这里制定如下规则,第一个child作为content,后面的child作为附加视图
int count = getChildCount();
if (count == 0) return;
int w = MeasureSpec.getSize(widthMeasureSpec);
int wModel = MeasureSpec.getMode(widthMeasureSpec);
int h = MeasureSpec.getSize(heightMeasureSpec);
int hModel = MeasureSpec.getMode(heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
//width
if (wModel != MeasureSpec.EXACTLY)
w = measureWidth();
if (hModel != MeasureSpec.EXACTLY)
h = measureHeight(count);
setMeasuredDimension(w, h);
}
private int measureWidth() {
int w = 0;
View child = getChildAt(0);
MarginLayoutParams lp;
if (child.getLayoutParams() instanceof MarginLayoutParams) {
lp = (MarginLayoutParams) child.getLayoutParams();
} else {
lp = new MarginLayoutParams(child.getLayoutParams());
}
//加上child的宽度和左右的Margin
w += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
//还要加上容器的左右的Padding值
return w + getPaddingLeft() + getPaddingRight();
}
private int measureHeight(int count) {
int h = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams lp;
if (child.getLayoutParams() instanceof MarginLayoutParams) {
lp = (MarginLayoutParams) child.getLayoutParams();
} else {
lp = new MarginLayoutParams(child.getLayoutParams());
}
//要加上top和bottom的距离
h = Math.max(h, child.getHeight() + lp.topMargin + lp.bottomMargin);
}
//加上top和bottom的padding的距离
return h + getPaddingTop() + getPaddingBottom();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//这里制定如下规则,第一个child作为content,后面的child作为附加视图
int count = getChildCount();
if (count == 0) return;
int left;
int right;
int top;
int bottom;
int currentW = getMeasuredWidth();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams lp;
if (child.getLayoutParams() instanceof MarginLayoutParams) {
lp = (MarginLayoutParams) child.getLayoutParams();
} else {
lp = new MarginLayoutParams(child.getLayoutParams());
}
if (i == 0)
left = getPaddingLeft() + lp.leftMargin;
else left = currentW + lp.leftMargin;
top = getPaddingTop() + lp.topMargin;
right = left + child.getMeasuredWidth();
bottom = top + child.getMeasuredHeight();
child.layout(left, top, right, bottom);
currentW = right + lp.rightMargin;
}
//获得可滑动的最大边界
MaxScrollX = currentW - getMeasuredWidth();
}
现在我们已经绘制出了界面,接下来,就要考虑什么时候拦截事件了
二、事件的拦截
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//一般不拦截
isIntercept = false;
mTouchX = ev.getRawX();
break;
case MotionEvent.ACTION_MOVE:
//分两种情况去拦截MineDistance 表示的是系统认为的最小滑动距离
float distance = mTouchX - ev.getRawX();
switch (mState) {
case HIDE:
if (distance >= MineDistance && distance <= MaxScrollX) isIntercept = true;
break;
case EXPAEND:
if (distance <= -MineDistance && distance >= -MaxScrollX)
isIntercept = true;
break;
default:
isIntercept = false;
break;
}
break;
case MotionEvent.ACTION_UP:
//必须不能拦截,如果要选择性拦截的话,太过于复杂,原因后面会讲到
isIntercept = false;
break;
}
return isIntercept;
}
为什么UP事件不能拦截呢?因为当我们拦截了UP事件了,后面的隐藏View的点击事件将失效,原因是,点击事件的激发的条件是,UP和DOWN事件必须成对出现,否则将不会调用该方法,所以我们不能拦截DOWN事件。
三、移动View,重写OnTuchEvent方法
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//这里改变背景
if (mState == HIDE)
setBackgroundColor(Color.BLUE);
isclick = true;
break;
case MotionEvent.ACTION_MOVE:
int distance = (int) (mTouchX - event.getRawX());
if (Math.abs(distance) < MineDistance) {
isMove = false;
} else {
isMove = true;
setBackgroundColor(Color.WHITE);
}
if (mState == HIDE && distance <= MaxScrollX && distance >= MineDistance)
scrollTo(distance, 0);
if (mState == EXPAEND && distance >= -MaxScrollX && distance <= -MineDistance)
scrollTo(MaxScrollX + distance, 0);
break;
case MotionEvent.ACTION_UP:
setBackgroundColor(Color.WHITE);
if (isclick && !isMove) {
if (mOnAppendClickListener != null)
mOnAppendClickListener.clickBg();
}
if (getScrollX() >= MaxScrollX) {
mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 500);
invalidate();
mState = HIDE;
break;
}
if (getScrollX() >= MaxScrollX / 2) {
mScroller.startScroll(getScrollX(), 0, MaxScrollX - getScrollX(), 0, 500);
invalidate();
mState = EXPAEND;
} else {
mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 500);
invalidate();
mState = HIDE;
}
break;
case MotionEvent.ACTION_CANCEL:
setBackgroundColor(Color.WHITE);
isclick = false;
//这里是事件丢失
if (getScrollX() >= MaxScrollX / 2) {
mScroller.startScroll(getScrollX(), 0, MaxScrollX - getScrollX(), 0, 500);
invalidate();
mState = EXPAEND;
} else {
mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 500);
invalidate();
mState = HIDE;
}
break;
}
return true;
}
这里说明一点,由于我们的滑动删除空间只是一个Item,事件响应范围比较窄,当事件序列中断的话,我们要处理滑动布局的滑动位置,要么恢复,要么我显示隐藏部分,不作处理的话很可能停在任何一个位置,UP事件是不会调用的,只会调用CANCLE这个事件。
四、处理点击事件,本容器内部的子View的点击事件好解决,让收到UP和DOWN事件就ok,但是本身的点击事件就不好处理了,在OnTuchEvent中消费了事件了,就不会触发点击事件的回调,如果要做条件判断的话,有点复杂,所以我在OnTuchEvent中档HIDE和UP和DOWN没有MOVE的情况下,而我们认为点击了该容器。
public interface OnAppendClickListener {
//点击隐藏部分的监听
void click(View v, int position);
//点击容器的监听
void clickBg();
}
当我们点击了隐藏部分时,我们要将不就还原
public void hide() {
scrollTo(0, 0);
invalidate();
mState = HIDE;
}
这么下来,就基本完成了,欢迎留言讨论。
Github地址:https://github.com/yzzAndroid/ScrollDeleteView