Android自定义view系列之99.99%实现QQ侧滑删除效果实例代码详解

首先声明本文是基于GitHub上"baoyongzhang"的SwipeMenuListView修改而来,该项目地址:

https://github.com/baoyongzhang/SwipeMenuListView

可以说这个侧滑删除效果是我见过效果最好且比较灵活的项目,没有之一!!!

但是在使用它之前需要给大家提两点注意事项:

1,该项目支持Gradle dependence,但是目前作者提供的依赖地址对应的项目不是最新的项目,依赖过后的代码与demo中使用的不一致,会提示没有BaseSwipeListAdapter这个类,因为这个类是其他的开发者后来提交上去的,所以如果想使用最新的代码,目前还是得把代码下载下来,然后把library文件拷贝到自己项目中使用.

下图是目前作者提供的依赖地址,不是最新的,所以想用最新代码的朋友还是直接下载代码到本地吧.

2,第二点注意事项应该算是一个bug吧,如果你测试过作者给的demo,你会发现如果某一项item已经被拉出来了,这个时候你再把ListView向上或向下滑动,让这个被拉出来的item移出屏幕,然后再移回来,这个已经被拉出来的item会直接恢复到未拉出的状态.这会让用户感觉很困惑,我明明已经拉出了这个菜单,怎么又不见了,然后可能就会产生这个软件做的真垃圾的想法,进而可能把你的软件卸载掉.如下图:

对于上面两个注意事项,第一个倒是没什么好说的,第二个问题怎么办呢?别急,这正是我们今天要说的内容.

首先我们可以先研究一下QQ的侧滑删除效果,说到这你可以打开你的qq看看它的具体效果.

你会发现,如果一个item被拉出来了,当你的手指放到其他的item上时,它会直接先把被拉出的那个item关闭掉,并且当前动作的后续的事件也都不再响应,除非你再次把手指放到屏幕上,他才会响应相关事件,而如果你的手指放到当前被拉出的item上,他不会隐藏这个item,并且可以正常响应左右滑动事件.

ok,QQ的效果我们分析完毕,我们探讨一下它的实现原理:

1,如果一个item已经被拉了出来,当你的手指放到其他的item上时,它会直接先把被拉出的那个item关闭掉, 怎么实现呢?

首先我们需要判断我们当前所按下的这个item是不是被拉出来的那个item,不是的话,我们才需要关闭,是的话,则不用管.代码如下:

if (view instanceof SwipeMenuLayout) { SwipeMenuLayout touchView = (SwipeMenuLayout) view; if (!touchView.isOpen()) { mTouchView.smoothCloseMenu(); } }

2,并且当前动作的后续的事件也都不再响应, 怎么实现呢?

这就很简单了,根据view的事件分发原理,如果在某一个触摸事件中返回了false,那么该事件后续的事件都不会再交给他处理,也就是说,如果我们在ACTION_DOWN的时候返回了false,那么后续的ACTION_MOVE,ACTION_UP等事件都不会响应,所以要实现这个效果,我们只需要在关闭菜单的后面,返回false就行了,完整的代码如下:

/********新添加的内容,当按下的item不是当前已经打开的item,则关闭已经打开的item,并返回false.不再响应down以后的事件,仿qq效果********/ if (view instanceof SwipeMenuLayout) { SwipeMenuLayout touchView = (SwipeMenuLayout) view; if (!touchView.isOpen()) { mTouchView.smoothCloseMenu(); return false; } }

这样的几行代码就实现了刚才分析的qq效果中的前半部分效果,即如果一个item被拉出来了,当你的手指放到其他的item上时,它会直接先把被拉出的那个item关闭掉,并且当前动作的后续的事件也都不再响应,除非你再次把手指放到屏幕上,他才会响应相关事件.

上面的那几行代码是基于SwipeMenuListView类修改而来,完整的修改之后的SwipeMenuListView代码如下所示:

package com.lanma.swipemenulistviewdemo.swipemenulistview; import android.content.Context; import android.util.AttributeSet; import android.util.TypedValue; import android.view.MotionEvent; import android.view.View; import android.view.animation.Interpolator; import android.widget.ListAdapter; import android.widget.ListView; /** * @author baoyz * @date 2014-8-18 * qiang_xi修改于2016-09-07(新增qq的效果) */ public class SwipeMenuListView extends ListView { private static final int TOUCH_STATE_NONE = 0; private static final int TOUCH_STATE_X = 1; private static final int TOUCH_STATE_Y = 2; public static final int DIRECTION_LEFT = 1; public static final int DIRECTION_RIGHT = -1; private int mDirection = 1;//swipe from right to left by default private int MAX_Y = 5; private int MAX_X = 3; private float mDownX; private float mDownY; private int mTouchState; private int mTouchPosition; private SwipeMenuLayout mTouchView; private OnSwipeListener mOnSwipeListener; private SwipeMenuCreator mMenuCreator; private OnMenuItemClickListener mOnMenuItemClickListener; private OnMenuStateChangeListener mOnMenuStateChangeListener; private Interpolator mCloseInterpolator; private Interpolator mOpenInterpolator; public SwipeMenuListView(Context context) { super(context); init(); } public SwipeMenuListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } public SwipeMenuListView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { MAX_X = dp2px(MAX_X); MAX_Y = dp2px(MAX_Y); mTouchState = TOUCH_STATE_NONE; } @Override public void setAdapter(ListAdapter adapter) { super.setAdapter(new SwipeMenuAdapter(getContext(), adapter) { @Override public void createMenu(SwipeMenu menu) { if (mMenuCreator != null) { mMenuCreator.create(menu); } } @Override public void onItemClick(SwipeMenuView view, SwipeMenu menu, int index) { boolean flag = false; if (mOnMenuItemClickListener != null) { flag = mOnMenuItemClickListener.onMenuItemClick( view.getPosition(), menu, index); } if (mTouchView != null && !flag) { mTouchView.smoothCloseMenu(); } } }); } public void setCloseInterpolator(Interpolator interpolator) { mCloseInterpolator = interpolator; } public void setOpenInterpolator(Interpolator interpolator) { mOpenInterpolator = interpolator; } public Interpolator getOpenInterpolator() { return mOpenInterpolator; } public Interpolator getCloseInterpolator() { return mCloseInterpolator; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { //在拦截处处理,在滑动设置了点击事件的地方也能swip,点击时又不能影响原来的点击事件 int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mDownX = ev.getX(); mDownY = ev.getY(); boolean handled = super.onInterceptTouchEvent(ev); mTouchState = TOUCH_STATE_NONE; mTouchPosition = pointToPosition((int) ev.getX(), (int) ev.getY()); View view = getChildAt(mTouchPosition - getFirstVisiblePosition()); //只在空的时候赋值 以免每次触摸都赋值,会有多个open状态 if (view instanceof SwipeMenuLayout) { //如果有打开了 就拦截. if (mTouchView != null && mTouchView.isOpen() && !inRangeOfView(mTouchView.getMenuView(), ev)) { return true; } mTouchView = (SwipeMenuLayout) view; mTouchView.setSwipeDirection(mDirection); } //如果摸在另外个view if (mTouchView != null && mTouchView.isOpen() && view != mTouchView) { handled = true; } if (mTouchView != null) { mTouchView.onSwipe(ev); } return handled; case MotionEvent.ACTION_MOVE: float dy = Math.abs((ev.getY() - mDownY)); float dx = Math.abs((ev.getX() - mDownX)); if (Math.abs(dy) > MAX_Y || Math.abs(dx) > MAX_X) { //每次拦截的down都把触摸状态设置成了TOUCH_STATE_NONE 只有返回true才会走onTouchEvent 所以写在这里就够了 if (mTouchState == TOUCH_STATE_NONE) { if (Math.abs(dy) > MAX_Y) { mTouchState = TOUCH_STATE_Y; } else if (dx > MAX_X) { mTouchState = TOUCH_STATE_X; if (mOnSwipeListener != null) { mOnSwipeListener.onSwipeStart(mTouchPosition); } } } return true; } } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { if (ev.getAction() != MotionEvent.ACTION_DOWN && mTouchView == null) return super.onTouchEvent(ev); int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: int oldPos = mTouchPosition; mDownX = ev.getX(); mDownY = ev.getY(); mTouchState = TOUCH_STATE_NONE; mTouchPosition = pointToPosition((int) ev.getX(), (int) ev.getY()); /*******把这句代码提前*********/ View view = getChildAt(mTouchPosition - getFirstVisiblePosition()); if (mTouchPosition == oldPos && mTouchView != null && mTouchView.isOpen()) { /********新添加的内容,当按下的item不是当前已经打开的item,则关闭已经打开的item,并返回false.不再响应down以后的事件,仿qq效果********/ if (view instanceof SwipeMenuLayout) { SwipeMenuLayout touchView = (SwipeMenuLayout) view; if (!touchView.isOpen()) { mTouchView.smoothCloseMenu(); return false; } } /***************************/ mTouchState = TOUCH_STATE_X; mTouchView.onSwipe(ev); return true; } if (mTouchView != null && mTouchView.isOpen()) { mTouchView.smoothCloseMenu(); mTouchView = null; // return super.onTouchEvent(ev); // try to cancel the touch event MotionEvent cancelEvent = MotionEvent.obtain(ev); cancelEvent.setAction(MotionEvent.ACTION_CANCEL); onTouchEvent(cancelEvent); if (mOnMenuStateChangeListener != null) { mOnMenuStateChangeListener.onMenuClose(oldPos); } return true; } if (view instanceof SwipeMenuLayout) { mTouchView = (SwipeMenuLayout) view; mTouchView.setSwipeDirection(mDirection); } if (mTouchView != null) { mTouchView.onSwipe(ev); } break; case MotionEvent.ACTION_MOVE: //有些可能有header,要减去header再判断 mTouchPosition = pointToPosition((int) ev.getX(), (int) ev.getY()) - getHeaderViewsCount(); //如果滑动了一下没完全展现,就收回去,这时候mTouchView已经赋值,再滑动另外一个不可以swip的view //会导致mTouchView swip 。 所以要用位置判断是否滑动的是一个view if (!mTouchView.getSwipEnable() || mTouchPosition != mTouchView.getPosition()) { break; } float dy = Math.abs((ev.getY() - mDownY)); float dx = Math.abs((ev.getX() - mDownX)); if (mTouchState == TOUCH_STATE_X) { if (mTouchView != null) { mTouchView.onSwipe(ev); } getSelector().setState(new int[]{0}); ev.setAction(MotionEvent.ACTION_CANCEL); super.onTouchEvent(ev); return true; } else if (mTouchState == TOUCH_STATE_NONE) { if (Math.abs(dy) > MAX_Y) { mTouchState = TOUCH_STATE_Y; } else if (dx > MAX_X) { mTouchState = TOUCH_STATE_X; if (mOnSwipeListener != null) { mOnSwipeListener.onSwipeStart(mTouchPosition); } } } break; case MotionEvent.ACTION_UP: if (mTouchState == TOUCH_STATE_X) { if (mTouchView != null) { boolean isBeforeOpen = mTouchView.isOpen(); mTouchView.onSwipe(ev); boolean isAfterOpen = mTouchView.isOpen(); if (isBeforeOpen != isAfterOpen && mOnMenuStateChangeListener != null) { if (isAfterOpen) { mOnMenuStateChangeListener.onMenuOpen(mTouchPosition); } else { mOnMenuStateChangeListener.onMenuClose(mTouchPosition); } } if (!isAfterOpen) { mTouchPosition = -1; mTouchView = null; } } if (mOnSwipeListener != null) { mOnSwipeListener.onSwipeEnd(mTouchPosition); } ev.setAction(MotionEvent.ACTION_CANCEL); super.onTouchEvent(ev); return true; } break; } return super.onTouchEvent(ev); } public void smoothOpenMenu(int position) { if (position >= getFirstVisiblePosition() && position <= getLastVisiblePosition()) { View view = getChildAt(position - getFirstVisiblePosition()); if (view instanceof SwipeMenuLayout) { mTouchPosition = position; if (mTouchView != null && mTouchView.isOpen()) { mTouchView.smoothCloseMenu(); } mTouchView = (SwipeMenuLayout) view; mTouchView.setSwipeDirection(mDirection); mTouchView.smoothOpenMenu(); } } } public void smoothCloseMenu() { if (mTouchView != null && mTouchView.isOpen()) { mTouchView.smoothCloseMenu(); } } private int dp2px(int dp) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getContext().getResources().getDisplayMetrics()); } public void setMenuCreator(SwipeMenuCreator menuCreator) { this.mMenuCreator = menuCreator; } public void setOnMenuItemClickListener( OnMenuItemClickListener onMenuItemClickListener) { this.mOnMenuItemClickListener = onMenuItemClickListener; } public void setOnSwipeListener(OnSwipeListener onSwipeListener) { this.mOnSwipeListener = onSwipeListener; } public void setOnMenuStateChangeListener(OnMenuStateChangeListener onMenuStateChangeListener) { mOnMenuStateChangeListener = onMenuStateChangeListener; } public static interface OnMenuItemClickListener { boolean onMenuItemClick(int position, SwipeMenu menu, int index); } public static interface OnSwipeListener { void onSwipeStart(int position); void onSwipeEnd(int position); } public static interface OnMenuStateChangeListener { void onMenuOpen(int position); void onMenuClose(int position); } public void setSwipeDirection(int direction) { mDirection = direction; } /** * 判断点击事件是否在某个view内 * * @param view * @param ev * @return */ public static boolean inRangeOfView(View view, MotionEvent ev) { int[] location = new int[2]; view.getLocationOnScreen(location); int x = location[0]; int y = location[1]; if (ev.getRawX() < x || ev.getRawX() > (x + view.getWidth()) || ev.getRawY() < y || ev.getRawY() > (y + view.getHeight())) { return false; } return true; } }

这样看来实现qq的那个效果是不是很简单,不过也多亏了原作者写的好,我修改起来才更容易.

话说回来,当按下的item不是已经被拉出来的那个item时,相应的效果我们已经实现,如果按下的item正好是那个已经被拉出来的item,效果怎么实现呢?

其实对于这个效果原作者其实已经实现了,只不过实现的不够好,存在一些问题,我这里把这个问题修复了.

存在的问题就是:如果在当前被拉出的item上左右滑动时,当你在抬起手指的那一刻并且滑动方向是向着拉开的方向滑动,有很大几率,这个被拉开的item会被关闭,举个栗子,比如你设定的是向左滑动是拉开的方向,当你在已经被拉开的item上左右滑动,并且在抬起手指的前一刻你是向左滑动的,讲道理的话这个item应该是处于被拉开的状态,但是实际上有很大几率当你抬起手指时,这个item却被关闭了,所以这是一个相当影响用户体验问题,一个item的其实就是一个SwipeMenuLayout,在SwipeMenuLayout类中,有一个onSwipe方法,该方法的参数MotionEvent就是从SwipeMenuListView的onTouchEvent方法中传递过来的,之前说过,当手指抬起时才会出问题,那么我们直接就看ACTION_UP当中的实现代码,

以下代码是有问题的:

if ((isFling || Math.abs(mDownX - event.getX()) > (mMenuView.getWidth() / 2)) && Math.signum(mDownX - event.getX()) == mSwipeDirection) { // open smoothOpenMenu(); } else { // close smoothCloseMenu(); return false; }

我们来分析一下,isFliing不用看,问题不是出在这里,我们看后面的判断逻辑:X轴方向上滑动距离的绝对值大于菜单宽度的一半并且滑动方向是向着拉开的方向滑动时,一个item才会被拉开,不然其他所有情况都会直接进else语句关闭item,问题就是出现在这里,而上面说的问题也是由这个导致的,试想一种情况,由于当前的item是已经被拉出来过的,如果我们的滑动距离绝对值没有超过菜单宽度的一半但是我们滑动的方向是向着item被拉出来的方向,讲道理的话我们的item是应该继续以被拉出的状态显示才对,但是根据代码中的判断逻辑,这种情况是直接进else语句的,也就是直接关闭这个item的,所以这里的逻辑判断有大问题,我们要的效果是:既然你已经被拉出来了只要你是继续往拉出的方向滑动,我就不会进else,而是直接再在if中进行判断滑动的距离,这样的逻辑才是正确的,而为了在第一次滑动时,不出现问题,我们还得为第一次滑动做相应操作.

所以修改之后的代码逻辑如下所示:

if ((isFling || Math.signum(mDownX - event.getX()) == mSwipeDirection)) { // open /**************新添加内容****防止在已被拉开的item上向拉开的方向滑动然后抬起手指时,item有很大几率关闭的问题******************/ if (Math.abs(mDownX - event.getX()) > (mMenuView.getWidth() / 2)) { smoothOpenMenu(); } else { //没有item打开时,且滑动距离不满足打开的条件才进行关闭 if (!isOpen()) { smoothCloseMenu(); } } /*******************************************/ } else { // close smoothCloseMenu(); return false; }

这样,我们就完美实现了qq的第二个效果,即如果你的手指放到当前被拉出的item上,他不会隐藏这个item,并且可以正常响应左右滑动事件.

完整的修改之后的SwipeMenuLayout类的代码如下所示:

package com.lanma.swipemenulistviewdemo.swipemenulistview; import android.content.Context; import android.support.v4.view.GestureDetectorCompat; import android.support.v4.widget.ScrollerCompat; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.GestureDetector.OnGestureListener; import android.view.GestureDetector.SimpleOnGestureListener; import android.view.MotionEvent; import android.view.View; import android.view.animation.Interpolator; import android.widget.AbsListView; import android.widget.FrameLayout; /** * @author baoyz * @date 2014-8-23 * qiang_xi修改于2016-09-07 */ public class SwipeMenuLayout extends FrameLayout { private static final int CONTENT_VIEW_ID = 1; private static final int MENU_VIEW_ID = 2; private static final int STATE_CLOSE = 0; private static final int STATE_OPEN = 1; private int mSwipeDirection; private View mContentView; private SwipeMenuView mMenuView; private int mDownX; private int state = STATE_CLOSE; private GestureDetectorCompat mGestureDetector; private OnGestureListener mGestureListener; private boolean isFling; private int MIN_FLING = dp2px(15); private int MAX_VELOCITYX = -dp2px(500); private ScrollerCompat mOpenScroller; private ScrollerCompat mCloseScroller; private int mBaseX; private int position; private Interpolator mCloseInterpolator; private Interpolator mOpenInterpolator; private boolean mSwipEnable = true; public SwipeMenuLayout(View contentView, SwipeMenuView menuView) { this(contentView, menuView, null, null); } public SwipeMenuLayout(View contentView, SwipeMenuView menuView, Interpolator closeInterpolator, Interpolator openInterpolator) { super(contentView.getContext()); mCloseInterpolator = closeInterpolator; mOpenInterpolator = openInterpolator; mContentView = contentView; mMenuView = menuView; mMenuView.setLayout(this); init(); } // private SwipeMenuLayout(Context context, AttributeSet attrs, int // defStyle) { // super(context, attrs, defStyle); // } private SwipeMenuLayout(Context context, AttributeSet attrs) { super(context, attrs); } private SwipeMenuLayout(Context context) { super(context); } public int getPosition() { return position; } public void setPosition(int position) { this.position = position; mMenuView.setPosition(position); } public void setSwipeDirection(int swipeDirection) { mSwipeDirection = swipeDirection; } private void init() { setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); mGestureListener = new SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { isFling = false; return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // TODO if (Math.abs(e1.getX() - e2.getX()) > MIN_FLING && velocityX < MAX_VELOCITYX) { isFling = true; } // Log.i("byz", MAX_VELOCITYX + ", velocityX = " + velocityX); return super.onFling(e1, e2, velocityX, velocityY); } }; mGestureDetector = new GestureDetectorCompat(getContext(), mGestureListener); // mScroller = ScrollerCompat.create(getContext(), new // BounceInterpolator()); if (mCloseInterpolator != null) { mCloseScroller = ScrollerCompat.create(getContext(), mCloseInterpolator); } else { mCloseScroller = ScrollerCompat.create(getContext()); } if (mOpenInterpolator != null) { mOpenScroller = ScrollerCompat.create(getContext(), mOpenInterpolator); } else { mOpenScroller = ScrollerCompat.create(getContext()); } LayoutParams contentParams = new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); mContentView.setLayoutParams(contentParams); if (mContentView.getId() < 1) { mContentView.setId(CONTENT_VIEW_ID); } mMenuView.setId(MENU_VIEW_ID); mMenuView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); addView(mContentView); addView(mMenuView); // if (mContentView.getBackground() == null) { // mContentView.setBackgroundColor(Color.WHITE); // } // in android 2.x, MenuView height is MATCH_PARENT is not work. // getViewTreeObserver().addOnGlobalLayoutListener( // new OnGlobalLayoutListener() { // @Override // public void onGlobalLayout() { // setMenuHeight(mContentView.getHeight()); // // getViewTreeObserver() // // .removeGlobalOnLayoutListener(this); // } // }); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); } public boolean onSwipe(MotionEvent event) { mGestureDetector.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mDownX = (int) event.getX(); isFling = false; break; case MotionEvent.ACTION_MOVE: // Log.i("byz", "downX = " + mDownX + ", moveX = " + event.getX()); int dis = (int) (mDownX - event.getX()); if (state == STATE_OPEN) { dis += mMenuView.getWidth() * mSwipeDirection; } swipe(dis); break; case MotionEvent.ACTION_UP: if ((isFling || Math.signum(mDownX - event.getX()) == mSwipeDirection)) { // open /**************新添加内容****防止在已被拉开的item上向拉开的方向滑动然后抬起手指时,item有很大几率关闭的问题******************/ if (Math.abs(mDownX - event.getX()) > (mMenuView.getWidth() / 2)) { smoothOpenMenu(); } else { //没有item打开时,且滑动距离不满足打开的条件才进行关闭 if (!isOpen()) { smoothCloseMenu(); } } /*******************************************/ } else { // close smoothCloseMenu(); return false; } break; } return true; } public boolean isOpen() { return state == STATE_OPEN; } @Override public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); } private void swipe(int dis) { if (!mSwipEnable) { return; } if (Math.signum(dis) != mSwipeDirection) { dis = 0; } else if (Math.abs(dis) > mMenuView.getWidth()) { dis = mMenuView.getWidth() * mSwipeDirection; } mContentView.layout(-dis, mContentView.getTop(), mContentView.getWidth() - dis, getMeasuredHeight()); if (mSwipeDirection == SwipeMenuListView.DIRECTION_LEFT) { mMenuView.layout(mContentView.getWidth() - dis, mMenuView.getTop(), mContentView.getWidth() + mMenuView.getWidth() - dis, mMenuView.getBottom()); } else { mMenuView.layout(-mMenuView.getWidth() - dis, mMenuView.getTop(), -dis, mMenuView.getBottom()); } } @Override public void computeScroll() { if (state == STATE_OPEN) { if (mOpenScroller.computeScrollOffset()) { swipe(mOpenScroller.getCurrX() * mSwipeDirection); postInvalidate(); } } else { if (mCloseScroller.computeScrollOffset()) { swipe((mBaseX - mCloseScroller.getCurrX()) * mSwipeDirection); postInvalidate(); } } } public void smoothCloseMenu() { state = STATE_CLOSE; if (mSwipeDirection == SwipeMenuListView.DIRECTION_LEFT) { mBaseX = -mContentView.getLeft(); mCloseScroller.startScroll(0, 0, mMenuView.getWidth(), 0, 350); } else { mBaseX = mMenuView.getRight(); mCloseScroller.startScroll(0, 0, mMenuView.getWidth(), 0, 350); } postInvalidate(); } public void smoothOpenMenu() { if (!mSwipEnable) { return; } state = STATE_OPEN; if (mSwipeDirection == SwipeMenuListView.DIRECTION_LEFT) { mOpenScroller.startScroll(-mContentView.getLeft(), 0, mMenuView.getWidth(), 0, 350); } else { mOpenScroller.startScroll(mContentView.getLeft(), 0, mMenuView.getWidth(), 0, 350); } postInvalidate(); } public void closeMenu() { if (mCloseScroller.computeScrollOffset()) { mCloseScroller.abortAnimation(); } if (state == STATE_OPEN) { state = STATE_CLOSE; swipe(0); } } public void openMenu() { if (!mSwipEnable) { return; } if (state == STATE_CLOSE) { state = STATE_OPEN; swipe(mMenuView.getWidth() * mSwipeDirection); } } public View getContentView() { return mContentView; } public SwipeMenuView getMenuView() { return mMenuView; } private int dp2px(int dp) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getContext().getResources().getDisplayMetrics()); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mMenuView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec( getMeasuredHeight(), MeasureSpec.EXACTLY)); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { mContentView.layout(0, 0, getMeasuredWidth(), mContentView.getMeasuredHeight()); if (mSwipeDirection == SwipeMenuListView.DIRECTION_LEFT) { mMenuView.layout(getMeasuredWidth(), 0, getMeasuredWidth() + mMenuView.getMeasuredWidth(), mContentView.getMeasuredHeight()); } else { mMenuView.layout(-mMenuView.getMeasuredWidth(), 0, 0, mContentView.getMeasuredHeight()); } } public void setMenuHeight(int measuredHeight) { Log.i("byz", "pos = " + position + ", height = " + measuredHeight); LayoutParams params = (LayoutParams) mMenuView.getLayoutParams(); if (params.height != measuredHeight) { params.height = measuredHeight; mMenuView.setLayoutParams(mMenuView.getLayoutParams()); } } public void setSwipEnable(boolean swipEnable) { mSwipEnable = swipEnable; } public boolean getSwipEnable() { return mSwipEnable; } }

那么.由于我们的标题名字起的是99.99%实现侧滑删除效果,那么我们就来看下效果图,看看到底是不是99.99%的一样:

QQ效果图:

修改后的效果图:

上面的两张效果图可能看不出什么,特别是我们上面说的那几个效果,主要是视屏转成gif之后又卡播放速度又快,所以想比较qq的效果和我们自己的效果,还是下载demo自己比较吧.我只能说改过之后的效果和QQ的效果基本就是一样的.嘎嘎~~

时间: 2024-11-08 21:33:37

Android自定义view系列之99.99%实现QQ侧滑删除效果实例代码详解的相关文章

Android自定义View圆形和拖动圆、跟随手指拖动效果

单纯的自定义一个圆非常简单 只需要几步就完成 拖动圆添加实现触摸事件即可 我在第一次自定义View圆遇到的几个Bug: 1.拖动圆的话在xml里面设置的自定义圆的宽和高是它能活动的空间的大小 不是圆控件的大小 如果你定义了100dp 拖动它的时候超过100dp这个距离这个圆就会看不见 就像下面这样 如果想活动于整个屏幕直接给宽和高match_parent属性就好了 2.我在定义充满属性match_parent的时候运行会报错,什么方法都用了就是不行,耐心等待过一会就好了-有可能是studio没来

Android自定义View之仿vivo i管家病毒扫描动画效果

技术是永无止境的,如果真的爱技术,那就勇敢的坚持下去.我很喜欢这句话,当我在遇到问题的时候.当我觉得代码枯燥的时候,我就会问自己,到底是不是真的热爱技术,这个时候,我心里总是起着波澜,我的答案是肯定的,我深深的爱着这门技术. 今天我们继续聊聊Android的自定义View系列.先看看效果吧: 这个是我手机杀毒软件的一个动画效果,类似于雷达搜索,所以用途还是很广泛的,特别是先了解一下这里的具体逻辑和写法,对技术的进步一定很有用. 先简单的分析一下这里的元素,主要有四个圆.一个扇形.还有八条虚线.当

Android使用ViewDragHelper实现QQ6.X最新版本侧滑界面效果实例代码_Android

(一).前言: 这两天QQ进行了重大更新(6.X)尤其在UI风格上面由之前的蓝色换成了白色居多了,侧滑效果也发生了一些变化,那我们今天来模仿实现一个QQ6.X版本的侧滑界面效果.今天我们还是采用神器ViewDragHelper来实现. 本次实例具体代码已经上传到下面的项目中,欢迎各位去star和fork一下. https://github.com/jiangqqlmj/DragHelper4QQ FastDev4Android框架项目地址:https://github.com/jiangqqlm

Android自定义View系列之Path绘制仿支付宝支付成功动画_Android

前言 使用支付宝付款时,我们可以看到成功或者失败都会有个动画提示,如果我们需要做这样的效果的话,当然,你可以让设计师给你做个GIF,但是我们知道图像比较耗内存的,我们自己可以用代码实现还是代码实现好点吧. 效果 实现方法 首先我们需要了解PathMeasure这个类,这个类我们可以理解为用来管理Path.我们主要看几个方法. PathMeasure(): 构造方法 ,实例化一个对象 PathMeasure(Path path,boolean isClosed):传入Path对象和是否闭合,pat

Android使用ViewDragHelper实现QQ6.X最新版本侧滑界面效果实例代码

(一).前言: 这两天QQ进行了重大更新(6.X)尤其在UI风格上面由之前的蓝色换成了白色居多了,侧滑效果也发生了一些变化,那我们今天来模仿实现一个QQ6.X版本的侧滑界面效果.今天我们还是采用神器ViewDragHelper来实现. 本次实例具体代码已经上传到下面的项目中,欢迎各位去star和fork一下. https://github.com/jiangqqlmj/DragHelper4QQ FastDev4Android框架项目地址:https://github.com/jiangqqlm

Android 获取手机联系人实例代码详解_Android

我的风格,废话不多说了,直接给大家贴代码了. 具体代码如下所示: package com.org.demo.demo; import com.org.wangfeng.R; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Butt

Android 获取手机联系人实例代码详解

我的风格,废话不多说了,直接给大家贴代码了. 具体代码如下所示: package com.org.demo.demo; import com.org.wangfeng.R; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Butt

自定义View系列教程07--详解ViewGroup分发Touch事件

探索Android软键盘的疑难杂症 深入探讨Android异步精髓Handler 详解Android主流框架不可或缺的基石 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Android多分辨率适配框架(3)- 使用指南 自定义View系列教程00–推翻自己和过往,重学自定义View 自定义View系列教程01–常用工具介绍 自定义View系列教程02–onMeasure源码详尽分析 自定义View

自定义View系列教程08--滑动冲突的产生及其处理

探索Android软键盘的疑难杂症 深入探讨Android异步精髓Handler 详解Android主流框架不可或缺的基石 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Android多分辨率适配框架(3)- 使用指南 自定义View系列教程00–推翻自己和过往,重学自定义View 自定义View系列教程01–常用工具介绍 自定义View系列教程02–onMeasure源码详尽分析 自定义View