【Android】毫无耦合性,一个Item根布局搞定 item侧滑删除菜单,像IOS那样简单的使用侧滑删除。(1)

自定义ViewGroup实现侧滑删除简单,难得是还要同时 处理多指滑动的屏蔽,防止两个侧滑菜单同时出现,等等,

有办法将这些东西都用一个ViewGroup搞定么?

看本文如何巧用static类变量来解决这些矛盾冲突。

==================================================================================

【2 预览】

那么我们先看一下最终的效果:

非阻塞式Android特色版本(我司自用版本 ) 平滑滚动动画用属性动画实现 ,即使有一个侧滑菜单处于打开状态,在打开其他item侧滑菜单时,依然无阻塞,会自动关闭上次开启的菜单:~

为了满足个别产品的,高仿IOS版本 平滑滚动用Scroller实现 阻塞式交互(自己的说法) 打开了某个侧滑菜单后 点击其他地方会自动关闭这个侧滑菜单 并且不能做其他操作 :

包含且不仅包含以下功能:

1 侧滑拉出菜单。

2 点击除了这个item的其他位置,菜单关闭。

3 侧滑过程中,不许父控件上下滑动。

4 多指同时滑动,屏蔽后触摸的几根手指。

5 不会同时展开两个侧滑菜单。

6 侧滑菜单时 拦截了长按事件。

7 侧滑时,拦截了点击事件(20160905更新)

8 通过开关 isLeftSwipe支持左滑右滑(2016 09 30 更新):

**

**

==================================================================================

【3 使用预览】

看起来还不错吧,忽略颜值,可以再细调,主要的是解决了那几个难题,那么,使用起来麻烦么。

<mcxtzhang.listswipemenudemo.view.CstSwipeDelMenuViewGroup xmlns:android=“http://schemas.android.com/apk/res/android”

android:layout_width=“match_parent”

android:layout_height=“wrap_content”

android:clickable=“true”>

<TextView android:id=“@+id/content”

android:layout_width=“wrap_content”

android:layout_height=“match_parent”

android:gravity=“center”

android:text=“试试看” />

<Button android:id=“@+id/btnDelete”

android:layout_width=“60dp”

android:layout_height=“match_parent”

android:background=“@color/red_ff4a57”

android:text=“删除” />

</mcxtzhang.listswipemenudemo.view.CstSwipeDelMenuViewGroup>

就这么简单,

只需要在 侧滑删除的item的layout的xml里,将根布局父控件换成我们的自定义ViewGroup即可。

第一个子View放置item的内容即可(正式项目里一般是一个ViewGroup),

从2+子View开始,是我们的侧滑菜单区域,如我们的demo图,是三个Button。

//注意事项,设置item点击,不能对整个holder.itemView设置咯,只能对第一个子View,即原来的content设置,这算是局限性吧。

(holder.content).setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

Toast.makeText(mContext, “”+mDatas.get(position).name, Toast.LENGTH_SHORT).show();

}

});

==================================================================================

【4 实现方法】

使用起来这么简单,让我们一步一步实现它吧。

首先说的是,颜值非本文的重点,UI动画仍有调整空间,重要的是在一个自定义ViewGroup里处理那些冲突。

首先,本类继承自ViewGroup,那么onMeasure()和onLayout()方法,就需要我们自己动手写了,而且在上文我们也提到,使用时,第一个子View放置item内容,2+子View为侧滑菜单区域,那么这需要我们在onMeasure()和onLayout()方法里进行一些特殊处理,设置第一个childView宽度为全屏。

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

//Log.d(TAG, "onMeasure() called with: " + “widthMeasureSpec = [” + widthMeasureSpec + “], heightMeasureSpec = [” + heightMeasureSpec + “]”);

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

mRightMenuWidths = 0;//由于ViewHolder的复用机制,每次这里要手动恢复初始值

int childCount = getChildCount();

//add by 2016 08 11 为了子View的高,可以matchParent(参考的FrameLayout 和LinearLayout的Horizontal)

final boolean measureMatchParentChildren = MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;

boolean isNeedMeasureChildHeight = false;

for (int i = 0; i < childCount; i++) {

View childView = getChildAt(i);

if (childView.getVisibility() != GONE) {

//measureChild(childView, widthMeasureSpec, heightMeasureSpec);

measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);

final MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();

mHeight = Math.max(mHeight, childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);

if (measureMatchParentChildren && lp.height == LayoutParams.MATCH_PARENT) {

isNeedMeasureChildHeight = true;

}

if (i > 0) {//第一个布局是Left item,从第二个开始才是RightMenu

mRightMenuWidths += childView.getMeasuredWidth();

}

}

}

setMeasuredDimension(mScreenW, mHeight);//宽度取屏幕宽度

mLimit = mRightMenuWidths * 4 / 10;//滑动判断的临界值

//Log.d(TAG, "onMeasure() called with: " + “mRightMenuWidths = [” + mRightMenuWidths); if (isNeedMeasureChildHeight) {//如果子View的height有MatchParent属性的,设置子View高度

forceUniformHeight(childCount, widthMeasureSpec);

}

}

/**

* 给MatchParent的子View设置高度 * * @param count

* @param widthMeasureSpec

* @see android.widget.LinearLayout# 同名方法

*/

private void forceUniformHeight(int count, int widthMeasureSpec) {

// Pretend that the linear layout has an exact size. This is the measured height of

// ourselves. The measured height should be the max height of the children, changed // to accommodate the heightMeasureSpec from the parent int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(),

MeasureSpec.EXACTLY);//以父布局高度构建一个Exactly的测量参数

for (int i = 0; i < count; ++i) {

final View child = getChildAt(i);

if (child.getVisibility() != GONE) {

MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

if (lp.height == LayoutParams.MATCH_PARENT) {

// Temporarily force children to reuse their old measured width

// FIXME: this may not be right for something like wrapping text?

int oldWidth = lp.width;//measureChildWithMargins 这个函数会用到宽,所以要保存一下

lp.width = child.getMeasuredWidth();

// Remeasure with new dimensions

measureChildWithMargins(child, widthMeasureSpec, 0, uniformMeasureSpec, 0);

lp.width = oldWidth;

}

}

}

}

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

//LogUtils.d(TAG, "onLayout() called with: " + “changed = [” + changed + “], l = [” + l + “], t = [” + t + “], r = [” + r + “], b = [” + b + “]”);

int childCount = getChildCount();

int left = l;

for (int i = 0; i < childCount; i++) {

View childView = getChildAt(i);

if (childView.getVisibility() != GONE) {

if (i == 0) {//第一个子View是内容 宽度设置为全屏

childView.layout(left, getPaddingTop(), left + mScreenW, getPaddingTop() + childView.getMeasuredHeight());

left = left + mScreenW;

} else {

childView.layout(left, getPaddingTop(), left + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight());

left = left + childView.getMeasuredWidth();

}

}

}

//Log.d(TAG, "onLayout() called with: " + “maxScrollGap = [” + maxScrollGap + “], l = [” + l + “], t = [” + t + “], r = [” + r + “], b = [” + b + “]”);

}

onMeasure()的时候,保存右侧菜单区域的宽度(这个值同时也是滑动的最大距离),然后调用setMeasuredDimension(),分别传入屏幕的宽度,和计算出的高度。这里还加了一些额外的代码了是为了让自定义的ViewGroup,在子View的height设置为match_parent的情况下,height正确,否则子View设置match_parent的效果是wrap_content,这些代码参考了源码FrameLayout和LinearLayout的Horizontal模式。

由于我们希望子View的LayoutParams是MarginLayoutParams,需要如下重写generateLayoutParams()这个方法。

@Override

public LayoutParams generateLayoutParams(AttributeSet attrs) {

return new MarginLayoutParams(getContext(), attrs);

}

以上是准备工作,下面是让我们的item可以侧滑起来并且在其中解决多指同时触摸 以及 多个侧滑menu同时显示的问题。

我这里重写的是dispatchTouchEvent()方法,目的就是为了好处理多指滑动等冲突。

@Override

public boolean dispatchTouchEvent(MotionEvent ev) {

//LogUtils.d(TAG, "dispatchTouchEvent() called with: " + “ev = [” + ev + “]”);

if (isSwipeEnable) {

acquireVelocityTracker(ev);

final VelocityTracker verTracker = mVelocityTracker;

switch (ev.getAction()) {

case MotionEvent.ACTION_DOWN:

if (isTouching) {//如果有别的指头摸过了,那么就return false。这样后续的move…等事件也不会再来找这个View了。

return false;

} else {

isTouching = true;//第一个摸的指头,赶紧改变标志,宣誓主权。

}

mLastP.set(ev.getRawX(), ev.getRawY());

//如果down,view和cacheview不一样,则立马让它还原。且把它置为null

if (mViewCache != null) {

if (mViewCache != this) {

mViewCache.smoothClose();

mViewCache = null;

}

//只要有一个侧滑菜单处于打开状态, 就不给外层布局上下滑动了

getParent().requestDisallowInterceptTouchEvent(true);

}

//求第一个触点的id, 此时可能有多个触点,但至少一个,计算滑动速率用

mPointerId = ev.getPointerId(0);

break; case MotionEvent.ACTION_MOVE:

float gap = mLastP.x - ev.getRawX();

//为了在水平滑动中禁止父类ListView等再竖直滑动

if (gap > ViewConfiguration.get(getContext()).getScaledTouchSlop()) {

getParent().requestDisallowInterceptTouchEvent(true);

}

//如果scroller还没有滑动结束 停止滑动动画

/* if (!mScroller.isFinished()) {

mScroller.abortAnimation(); }*/ scrollBy((int) (gap), 0);//滑动使用scrollBy

//修正 if (getScrollX() < 0) {

scrollTo(0, 0);

}

if (getScrollX() > mRightMenuWidths) {

scrollTo(mRightMenuWidths, 0);

}

mLastP.set(ev.getRawX(), ev.getRawY());

break; case MotionEvent.ACTION_UP:

case MotionEvent.ACTION_CANCEL:

//求伪瞬时速度

verTracker.computeCurrentVelocity(1000, mMaxVelocity);

final float velocityX = verTracker.getXVelocity(mPointerId);

if (Math.abs(velocityX) > 1000) {//滑动速度超过阈值

if (velocityX < -1000) {

//平滑展开Menu

smoothExpand();

//展开就加入ViewCache:

mViewCache = this;

} else {

//平滑关闭Menu

smoothClose();

}

} else {

if (getScrollX() > mLimit) {//否则就判断滑动距离

//平滑展开Menu smoothExpand();

//展开就加入ViewCache:

mViewCache = this;

} else {

//平滑关闭Menu

smoothClose();

}

}

//释放

releaseVelocityTracker();

//LogUtils.i(TAG, “onTouch A ACTION_UP ACTION_CANCEL:velocityY:” + velocityX);

isTouching = false;//没有手指在摸我了

break;

default:

break;

}

}

return super.dispatchTouchEvent(ev);

}

VelocityTracker相关代码是为了计算手指滑动的速度,帮助我们在手指抬起时判断,确定当前item是展开还是收缩,令UI更友好。

首先看case MotionEvent.ACTION_DOWN:

在手指按下时,我们用一个 布尔值 static类变量存储当前是否有手指在触摸该ViewGroup,

//防止多只手指一起滑我的flag 在每次down里判断, touch事件结束清空

private static boolean isTouching;

之所以用类变量,是因为一个类里它是唯一的,所以在RecyclerView,ListView里,即使一个屏幕上有多个item,但是他们的isTouching的变量是同一个,这样便能达到控制单指触摸的目的。所以控制单侧滑菜单出现也是同理。后面会提到。

boolean变量,默认值是false,所以当出现两指同时触摸时(同时触摸其实还是有先后的),第一个触摸的指头在down事件里,就将isTouching改变为true,这样后续赶到的那个指头所在的ViewGroup 的down事件里 isTouching就是true了,在dispatchTouchEven()t里就会被return false,这样后续的事件它统统接收不到了。就这么简单就解决了多指触摸的问题。(ACTION_DOWN也没有往子View分发,后续所有事件自己和子View都接收不到了)。

然后正常流程往下,我们先存储一下当前触摸点的x y 坐标,然后判断viewCache变量是否为空。

viewCache变量也是一个类变量,全局就这一份,存储的是全局处于展开状态的那个View对象。

如果它不为空,说明已经有一个侧滑菜单在展开了,那么我们判断一下,是否是this,即自己这个View,那么忽略,如果不是自己,那么就将那个处于展开状态的View的侧滑菜单关闭。并将viewCache置空,同时只要有一个侧滑菜单在展开了,我们都剥夺父控件处理TouchEvent的权利。

//存储的是当前正在展开的View

private static CstSwipeDelMenuViewGroup mViewCache;

然后看case MotionEvent.ACTION_MOVE:

我们先通过当前的x值和我们保存的上次的x左边值,计算这次滑动的距离gap。

如果gap大于系统认定的滑动阈值(通过 ViewConfiguration.get(getContext()).getScaledTouchSlop() 得到),则说明是水平方向的侧滑动作,那么也剥夺父控件处理TouchEvent的权利。

然后调用View的scrollBy()方法,滑动我们自定义的ViewGroup的所有子View

滑动后需要判断是否越界,需要修正一下,这里用到了我们在onMeasure时保存的侧滑菜单的宽度。

最后我们将此时的x,y坐标保存一下。

scrollBy方法,很多人容易懵逼,可以简单的记一下结论,不是本文重点暂不深究,像让子View 向左 向上滑,传入正值,向 右 向下滑,传负值。

最后看 case MotionEvent.ACTION_UP:和case MotionEvent.ACTION_CANCEL:

注意的是,一定要加上ACTION_CANCEL,不要仅仅判断ACTION_UP。

因为在一些情况下,例如当你的手指一直触摸 从屏幕边缘,离开了屏幕,只会出发CANCEL不会出发UP事件,即当用户保持按下操作,并从你的控件转移到外层控件时,会触发ACTION_CANCEL。

这里的操作就是,根据手指滑动速率,判断手指离开、取消时的动作,是关上侧滑菜单,还是展开侧滑菜单。如果是展开侧滑菜单的操作,要将viewCache设置为this,

同时在最后要将isTouching设置为false,因为此时已经没有人在摸你啦。别忘记释放VelocityTracker~

关于平滑展开smoothExpand() 和平滑关闭smoothClose()方法如下:

/**

* 平滑展开 */ public void smoothExpand() {

/*mScroller.startScroll(getScrollX(), 0, mRightMenuWidths - getScrollX(), 0);

invalidate();*/ ValueAnimator valueAnimator = ValueAnimator.ofInt(getScrollX(), mRightMenuWidths);

valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

scrollTo((Integer) animation.getAnimatedValue(), 0);

}

});

valueAnimator.setInterpolator(new OvershootInterpolator());

valueAnimator.setDuration(300).start();

}

/**

* 平滑关闭 */ public void smoothClose() {

/* mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0);

invalidate();*/ ValueAnimator valueAnimator = ValueAnimator.ofInt(getScrollX(), 0);

valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

scrollTo((Integer) animation.getAnimatedValue(), 0);

}

});

valueAnimator.setInterpolator(new AnticipateInterpolator());

valueAnimator.setDuration(300).start();

//LogUtils.d(TAG, "smoothClose() called with:getScrollX() " + getScrollX());

}

一开始我用的是scroller做的,后来发现属性动画的 OvershootInterpolator   AnticipateInterpolator 貌似更酷炫一些,就改了一下,这都不重要~

获取加速度 和释放的函数:

/**

* @param event 向VelocityTracker添加MotionEvent

* @see VelocityTracker#obtain()

* @see VelocityTracker#addMovement(MotionEvent)

*/

private void acquireVelocityTracker(final MotionEvent event) {

if (null == mVelocityTracker) {

mVelocityTracker = VelocityTracker.obtain();

}

mVelocityTracker.addMovement(event);

}

/**

* * 释放VelocityTracker * * @see android.view.VelocityTracker#clear()

* @see android.view.VelocityTracker#recycle()

*/

private void releaseVelocityTracker() {

if (null != mVelocityTracker) {

mVelocityTracker.clear();

mVelocityTracker.recycle();

mVelocityTracker = null;

}

}

==================================================================================

【5 一些不能忘的事】

由于我们使用的是类静态变量存储的处于展开状态的View,所以我们需要在恰当的时机释放它,否则恭喜你就会内存泄漏啦

释放时机呢,我选在onDetachedFromWindow()这里函数里,每次当View从屏幕上移除会回调这个函数,在其中我们判断,ViewCache是不是等于自己,如果是,那么关闭侧滑菜单,同时将viewCache赋值null。注释也蛮详细的~

//每次ViewDetach的时候,判断一下 ViewCache是不是自己,如果是自己,关闭侧滑菜单,且ViewCache设置为null,

// 理由:1 防止内存泄漏(ViewCache是一个静态变量)

// 2 侧滑删除后自己后,这个View被Recycler回收,复用,下一个进入屏幕的View的状态应该是普通状态,而不是展开状态。

@Override

protected void onDetachedFromWindow() {

if (this == mViewCache) {

mViewCache.smoothClose();

mViewCache = null;

}

super.onDetachedFromWindow();

}

前面不是提到一个展开时,还禁止了长按么,在如下方法里判断,如果getScrollX大于0,说明已经侧滑了一点点,那么就return,屏蔽长按就好啦,如果这个方法你没见过,那么我告诉你其实我也是才看见的,因为下午在做侧滑删除的时候,产品还让我加上长按删除,结果出现了一个小冲突,侧滑的时候同时长按,就会触发长按事件,我觉得要处理一下,就看了一下setOnLongClickListener的源码,顺藤摸瓜,几分钟就可以找到,就不细说啦。

//展开时,禁止长按

@Override

public boolean performLongClick() {

if (getScrollX() > 0) {

return false;

}

return super.performLongClick();

}

20160905 新增

另外附上侧滑时,单击事件的屏蔽,之前忘了补充(代码可以在github上下载)

侧滑时,拦截了点击事件

增加一个变量存储scaleTouchSlop,这个值是系统定义的,超过这个值即判断此次动作是在滑动。我们利用这个值判断是否处于侧滑。

private int mScaleTouchSlop;//为了处理单击事件的冲突

mScaleTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

要拦截点击事件,就是在ACTION_UP里判断,当前处于侧滑状态,并且落点是在内容区域,才返回true,事件不再分发给子View:

如果落点在侧滑菜单区域,是可以点击的。

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

switch (ev.getAction()) {

case MotionEvent.ACTION_UP:

//为了在侧滑时,屏蔽子View的点击事件

if (getScrollX() > mScaleTouchSlop) {

//add by 2016 09 10 解决一个智障问题~ 居然不给点击侧滑菜单 我跪着谢罪

//这里判断落点在内容区域屏蔽点击,内容区域外,允许传递事件继续向下的的。。。 if (ev.getX() < getWidth() - getScrollX()) {

return true;//true表示拦截

}

}

break;

}

return super.onInterceptTouchEvent(ev);

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

文末

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

mg-62HpO1B7-1712683300074)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

文末

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

[外链图片转存中…(img-YjO3PsjR-1712683300075)]

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-4myDuBbJ-1712683300075)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值