博主声明:
转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。
本文首发于此 博主:威威喵 | 博客主页:https://blog.csdn.net/smile_running
系列文章:
自定义 View(一)仿 QQ 列表 Item 侧拉删除功能
自定义 View(二)自己动手实现下拉刷新、上拉加载功能
自定义 View(三)仿 DrawerLayout 实现侧拉功能
分享我学习过的一个ListView侧滑的Item菜单效果,所谓举一反三,一同百通。在我学会了这个之后,很容易的就实现ListView的上拉加载和下拉刷新的效果,还有我们最常见的侧滑抽屉效果。百变不离其中,只要我们搞懂其中的一个实现方式,那么其他的便信手拈来。
目的是为了,解决ListView与侧滑删除按钮的滑动冲突
我要实现的是这样一个效果,QQ的联系人侧滑菜单,而在没学过自定义View之前毫无头绪。既然这样,我先来看一下实现后的效果吧。
完成的效果图现在呢,我们得有一个这样的思路。所谓ListView中Item布局,我们一般是这个样子的:一个父容器,里面一个TextView,ImageView等等内容。比如:
以上的ListView的Item布局,也就是本次我们使用的。
既然,你要学习侧滑的实现效果,别告诉我,你还不会用ListView。当然也是有可能的,那么在这里给一篇ListView的使用姿势文章,不会的请点击这里:ListView使用技巧、优化和用法拓展,掌握ListView
首先,我们存放Item的父容器,这个父容器是有文章的。怎么说呢,大家可以看到上面布局代码中的父容器,其实这就是我自定义的一个继承FrameLayout的类,当然也可以是RelativeLayout。但是别用LinerLayout,因为我们要对子视图进行排列位置,用LinerLayout反正适得其反。
有了这个前提,我们的思路就是:通过onLayout()、onMesure()方法对子视图的测量宽高以及布置它的位置。我们的继承自FrameLayout的类SlideLayout,其中存放两个父容器。一个是ContentView,一个是MenuView。因为MenuView是从右侧滑进来的,我们应该将它布置到屏幕以外,通过滑动显示和隐藏。
我特地画了一张草图,以便更好的理解吧,也许只有我才能看懂~哈哈哈哈
menuView的逻辑理解图,从隐藏到显示整个过程好好理解一下吧,好记性不过烂笔头,在本子上多涂涂画画,或者开启画板自己画一画,才能更好的理解。
那么,我做这些有什么用呢?当然了,请看效果图:
我们来看看实现的代码类:
/**
* @Created by xww.
* @Creation time 2018/8/21.
*/
public class SlideLayout extends FrameLayout {
private View mContentView;
private View mMenuView;
private int mMenuWidth;
private int mMenuHeight;
private int mContentWidth;
private int mContentHeight;
private Scroller mScroller;
private float startX;
private float startY;
public SlideLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContentView = getChildAt(0);
mMenuView = getChildAt(1);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mContentWidth = getMeasuredWidth();
mContentHeight = getMeasuredHeight();
mMenuWidth = mMenuView.getMeasuredWidth();
mMenuHeight = mMenuView.getMeasuredHeight();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
//将menu布局到右侧不可见(屏幕外)
mMenuView.layout(mContentWidth, 0, mContentWidth + mMenuWidth, mMenuHeight);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = x;
startY = y;
break;
case MotionEvent.ACTION_MOVE:
final float dx = (int) (x - startX);
final float dy = (int) (startY - y);
int disX = (int) (getScrollX() - dx);
if (disX <= 0) {
disX = 0;
} else if (disX >= mMenuWidth) {
disX = mMenuWidth;
}
scrollTo(disX, getScrollY());
startX = x;
startY = y;
break;
case MotionEvent.ACTION_UP:
if (getScrollX() < mMenuWidth / 2) {
closeMenu();
} else {
openMenu();
}
break;
}
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
public final void openMenu() {
mScroller.startScroll(getScrollX(), getScrollY(), mMenuWidth - getScrollX(), 0);
invalidate();
}
public final void closeMenu() {
mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
invalidate();
}
}
以上只是简单的实现了而且,但是明显有很多bug,比如每个都可以滑出来、滑到一半卡主、与ListView滑动冲突了等等,下面我们一点一点来解决它,为了更好的体验。
·一、解决滑动冲突
大家可能有疑惑,这个滑动冲突是怎么产生的呢?下面,我们来看一下滑动冲突的结果,你就会明白它产生的大致原因了
滑动冲突了分析一下,我们SlideLayout有左右滑动的动作,ListView有上下滑动的动作。当我在左右滑动的时候不松开而进行上下滑动,那么滑动事件便由SlideLayout传递到了ListView的滑动。可想而知,SlideLayout的滑动事件被父视图给夺走了,所以SlideLayout就犯毛病了,停止了它的滑动行为。
既然是ListView夺走了touch事件,从ListView角度上来说,我(ListView)拦截了SlideLayout的touch事件;从SlideLayout的角度上来说,父(ListView)把我的touch事件给夺走了。
那么有两种相对的角度,就有两种解决的方法。第一种是继承ListView,设置不拦截;第二种是剥夺ListView对touch的处理权。我就用第二种来实现,既然你可以夺走我的,那么我就可以拿回来。所谓相生相克,一物降一物。
我们思路应该这样,比如手指在左右滑动的距离大于上下滑动的距离,那么判定是SlideLayout的滑动事件,在这种情况下才去剥夺ListView的事件处理器。
看一下我们实现的代码:在onTouchEvent();的ACTION_MOVE事件中修改代码,添加如下部分
case MotionEvent.ACTION_MOVE:
final float dx = (int) (x - startX);
final float dy = (int) (startY - y);
int disX = (int) (getScrollX() - dx);
if (disX <= 0) {
disX = 0;
} else if (disX >= mMenuWidth) {
disX = mMenuWidth;
}
scrollTo(disX, getScrollY());
final float moveX = Math.abs(x - downX);
final float moveY = Math.abs(y - downY);
if (moveX > moveY && moveX > 10f) {
//剥夺ListView对touch事件的处理权
getParent().requestDisallowInterceptTouchEvent(true);
}
startX = x;
startY = y;
break;
通过以上的修改,我们达到了目的,看看效果吧。
解决滑动冲突bug二、解决Item点击事件的冲突
我们的Item内可能是有点击事件的,所以呢,又掉入了一个坑。为什么这样说呢?看下面的图你就明白一切了。我给名字设置了点击事件,发生了什么?
在姓名区域无法滑动,其他区域可以滑动分析一下,产生这种异常的原因。为什么在姓名那里却无法移动呢?因为什么呢?我们就知道了onClickListener其实把touch事件给消费了,这就导致了SlideLayout始终处理不了touch事件,因为被Item中的子元素TextView给消费了。
如果不太懂,你也许可以先看这样的一个简单例子:理解View的事件分发、拦截和消费,处理事件冲突的必备技能
既然,TextView是它(SlideLayout)的儿子,作为父亲,那肯定要管管啊,不能让儿子乱来吧。那么,下面我们就管一管,怎么管呢?当然是拦截了,父亲把touch事件给拦截下来。但是,总不能所有情况都拦截吧,那儿子不就废了吗?所以呢,我们给定这样一个条件,就是当手指确实在左右滑动,而非单纯的点击时,这时父亲才拦截儿子的touch事件。也许有点难以想象,不急,我们看一下代码就简单多了。
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercept = false;
final float x = event.getX();
final float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = x;
downY = y;
break;
case MotionEvent.ACTION_MOVE:
final float moveX = Math.abs(x - downX);
if (moveX > 10f) {
//对儿子touch事件进行拦截
intercept = true;
}
break;
case MotionEvent.ACTION_UP:
break;
}
return intercept;
}
通过这样,我们的儿子就听话多了,那么来看看是这样的一种效果。
解决点击事件与滑动的bug这一路,我的bug层出不穷,解决了这个又出了新的bug,真的是一步一个坑。当然,这是幸运的事情,当你爬完了这些坑之后,给你带来的确实另一番天地。不禁扯了一下,言归正传,我们看看出现的Bug情况吧。
多个Item可以被拉出我们来分析一下出现这种情况的根本原因,那就是每个Item都是ListView中独立的一个个体,我们的Count数多少也就是Item数的多少,所以我们要对每个Item的侧滑Menu进行监听,我们自定义一个接口用于保存每一个Item的状态,有两种:Open和Close。但我们还需要一个点击的监听,用于判断是否与当前Item一致。
这部分就比较简单了,也好理解,简单的看一下代码吧。首先,在SlideLayout中定义一个监听接口
private onSlideChangeListenr onSlideChangeListenr;
public interface onSlideChangeListenr {
void onMenuOpen(SlideLayout slideLayout);
void onMenuClose(SlideLayout slideLayout);
void onClick(SlideLayout slideLayout);
}
public void setOnSlideChangeListenr(SlideLayout.onSlideChangeListenr onSlideChangeListenr) {
this.onSlideChangeListenr = onSlideChangeListenr;
}
其次,在三个地方分别设置监听
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercept = false;
final float x = event.getX();
final float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = x;
downY = y;
if (onSlideChangeListenr != null) {
onSlideChangeListenr.onClick(this);
}
break;
case MotionEvent.ACTION_MOVE:
final float moveX = Math.abs(x - downX);
if (moveX > 10f) {
//对儿子touch事件进行拦截
intercept = true;
}
break;
case MotionEvent.ACTION_UP:
break;
}
return intercept;
}
public final void openMenu() {
mScroller.startScroll(getScrollX(), getScrollY(), mMenuWidth - getScrollX(), 0);
invalidate();
if (onSlideChangeListenr != null) {
onSlideChangeListenr.onMenuOpen(this);
}
}
public final void closeMenu() {
mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
invalidate();
if (onSlideChangeListenr != null) {
onSlideChangeListenr.onMenuClose(this);
}
}
最后,在我们适配器中设置监听状态的改变做出相应的处理。
mSlideLayout = (SlideLayout) convertView;
mSlideLayout.setOnSlideChangeListenr(new SlideLayout.onSlideChangeListenr() {
@Override
public void onMenuOpen(SlideLayout slideLayout) {
mSlideLayout = slideLayout;
}
@Override
public void onMenuClose(SlideLayout slideLayout) {
if (mSlideLayout != null) {
mSlideLayout = null;
}
}
@Override
public void onClick(SlideLayout slideLayout) {
if (mSlideLayout != null ) {
mSlideLayout.closeMenu();
}
}
});
接下来,我们运行一下项目,成功的解决了问题。
到此,一个仿QQ侧滑菜单的效果完全的实现了,是不是很赞呢?哈哈哈哈。你以为就结束了吗?开玩笑。。。哈哈哈哈哈哈哈!