项目用到了ListView的侧滑删除的功能,由于当时项目比较赶,就随便在网上找了一个,但是效果不是太好,最近闲了下来,就想自己实现一个,于是就按照QQ的联系人列表的侧滑菜单做了一个,效果基本上是一模一样的。在这个过程中,自己也学习到了不少的东西,下面就把这个过程跟大家分享出来。
废话不多说,首先上效果图。
看完了图如果感觉效果不好,请不要拍砖,好的话请继续往下看~
下面结合代码说说实现的原理:首先自定义一个ViewGroup来实现item的滑动效果。
package com.binbin.slidedelmenu.item;
import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.Scroller;
/**
* Created by -- on 2016/11/3.
* 带侧滑菜单的自定义item
*/
public class MenuItem extends ViewGroup {
private int contentWidth;
private Scroller mScroller;
private int maxWidth,maxHeight;//viewGroup的宽高
private static final int MIN_FLING_VELOCITY = 600; // dips per second
/**最小滑动距离,超过了,才认为开始滑动 */
private int mTouchSlop = 0 ;
/**上次触摸的X坐标*/
private float mLastX = -1;
/**第一次触摸的X坐标*/
private float mFirstX = -1;
private int ratio;
//防止多只手指一起滑动的flag 在每次down里判断, touch事件结束清空
private static boolean isTouching;
private int mRightMenuWidths;//右侧菜单总宽度
private VelocityTracker mVelocityTracker;
private float mMaxVelocity;
private int mPointerId;//多点触摸只算第一根手指的速度
private boolean isQQ=true;//是否是qq效果
private boolean qqInterceptFlag;//qq效果判断标志
private static MenuItem mViewCache;//存储的是当前正在展开的View
private boolean isUserSwiped;// 判断手指起始落点,如果距离属于滑动了,就屏蔽一切点击事件
//仿QQ,侧滑菜单展开时,点击除侧滑菜单之外的区域,关闭侧滑菜单。
//增加一个布尔值变量,dispatch函数里,每次down时,为true,move时判断,如果是滑动动作,设为false。
//在Intercept函数的up时,判断这个变量,如果仍为true 说明是点击事件,则关闭菜单。
private boolean isUnMoved = true;
public MenuItem(Context context) {
this(context,null);
}
public MenuItem(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public MenuItem(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public MenuItem(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init(){
contentWidth=getContext().getResources().getDisplayMetrics().widthPixels;
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
mScroller=new Scroller(getContext());
mMaxVelocity = ViewConfiguration.get(getContext()).getScaledMaximumFlingVelocity();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setClickable(true);//令自己可点击,从而获取触摸事件(很重要,必须写在onMesaure中)
mRightMenuWidths=0;//由于ViewHolder的复用机制,每次这里要手动恢复初始值
/**
* 根据childView计算的出的宽和高,计算容器的宽和高,主要用于容器是warp_content时
*/
for (int i = 0,count = getChildCount(); i < count; i++) {
View childView = getChildAt(i);
//令每一个子View可点击,从而获取触摸事件
childView.setClickable(true);
if(childView.getVisibility()!=View.GONE){
//获取每个子view的自己高度宽度,取最大的就是viewGroup的大小
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
maxWidth = Math.max(maxWidth,childView.getMeasuredWidth());
maxHeight = Math.max(maxHeight,childView.getMeasuredHeight());
if(i>0){//第一个是content,后面的都是菜单
if(childView.getLayoutParams().width== LayoutParams.MATCH_PARENT){
//菜单的宽不能MATCH_PARENT
throw new IllegalArgumentException("======menu'width can't be MATCH_PARENT=====");
}
mRightMenuWidths+=childView.getMeasuredWidth();
}else{
if(childView.getLayoutParams().width!= LayoutParams.MATCH_PARENT){
//content的宽必须MATCH_PARENT
throw new IllegalArgumentException("======content'width must be MATCH_PARENT=====");
}
}
}
}
//为ViewGroup设置宽高
setMeasuredDimension(maxWidth,maxHeight);
ratio=mRightMenuWidths/3;//可能每个item的菜单不同,所以ratio要每次计算
/**
* 根据最大宽高重新设置子view,保证高度充满
*/
//首先判断params.width的值是多少,有三种情况。
//如果是大于零的话,及传递的就是一个具体的值,那么,构造MeasupreSpec的时候可以直接用EXACTLY。
//如果为-1的话,就是MatchParent的情况,那么,获得父View的宽度,再用EXACTLY来构造MeasureSpec。
//如果为-2的话,就是wrapContent的情况,那么,构造MeasureSpec的话直接用一个负数就可以了。
for (int i = 0,count = getChildCount(); i < count; i++) {
View childView = getChildAt(i);
if(childView.getVisibility()!=View.GONE){
//宽度采用测量好的
int widthSpec = MeasureSpec.makeMeasureSpec(childView.getMeasuredWidth(), MeasureSpec.EXACTLY);
//高度采用最大的
int heightSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY);
childView.measure(widthSpec, heightSpec);
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// Log.e("tianbin",content.getMeasuredWidth()+"#"+content.getMeasuredHeight()+"$$$"+menu.getMeasuredWidth()+"#"+menu.getMeasuredHeight());
int left=0;
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
if (childView.getVisibility() != GONE) {
if (i == 0) {//第一个子View是内容 宽度设置为全屏
childView.layout(0, 0, maxWidth, maxHeight);
left += maxWidth;
} else {
childView.layout(left, 0, left + childView.getMeasuredWidth(), getPaddingTop() + maxHeight);
left += childView.getMeasuredWidth();
}
}
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
acquireVelocityTracker(ev);
final VelocityTracker verTracker = mVelocityTracker;
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
// Log.e("tianbin","======MenuItem dispatchTouchEvent======ACTION_DOWN==="+isTouching);
if (isTouching) {//如果有别的指头摸过了,那么就return false。这样后续的move...等事件也不会再来找这个View了。
return false;
} else {
isTouching = true;//第一个摸的指头,赶紧改变标志,宣誓主权。
}
mLastX=ev.getRawX();
mFirstX=ev.getRawX();
//求第一个触点的id, 此时可能有多个触点,但至少一个,计算滑动速率用
mPointerId = ev.getPointerId(0);
isUserSwiped = false;
qqInterceptFlag=false;
isUnMoved=true;
//如果down,view和cacheview不一样,则立马让它还原。且把它置为null
if (mViewCache != null) {
if (mViewCache != this) {
mViewCache.smoothClose();
qqInterceptFlag = isQQ;//当前有侧滑菜单的View,且不是自己的,就该拦截事件咯。
}
//只要有一个侧滑菜单处于打开状态, 就不给外层布局上下滑动了
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_MOVE:
if(qqInterceptFlag){//当前有侧滑菜单的View,且不是自己的,就该拦截事件咯。滑动也不该出现
break;
}
// Log.e("tianbin","======MenuItem dispatchTouchEvent======ACTION_MOVE===11111111111111");
float deltaX= ev.getRawX()-mLastX;
mLastX=ev.getRawX();
//为了在水平滑动中禁止父类ListView等再竖直滑动
if (Math.abs(deltaX) > 10 || Math.abs(getScrollX()) > 10) {//使屏蔽父布局滑动更加灵敏,
getParent().requestDisallowInterceptTouchEvent(true);
// Log.e("tianbin","======MenuItem dispatchTouchEvent======ACTION_MOVE===222222222222222");
}
if (Math.abs(deltaX) > mTouchSlop) {
isUnMoved = false;
}
scrollBy(-(int)deltaX,0);
//越界修正
if (getScrollX() < 0) {
scrollTo(0, 0);
}
if (getScrollX() > mRightMenuWidths) {
scrollTo(mRightMenuWidths, 0);
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
default:
// Log.e("tianbin","======MenuItem dispatchTouchEvent======ACTION_UP===");
if (Math.abs(ev.getRawX() - mFirstX) > mTouchSlop) {
isUserSwiped = true;
}
if(!qqInterceptFlag){
//求伪瞬时速度
verTracker.computeCurrentVelocity(1000, mMaxVelocity);
final float velocityX = verTracker.getXVelocity(mPointerId);
// Log.e("tianbin",qqInterceptFlag+"=============velocityX:"+velocityX);
if (Math.abs(velocityX) > 1000) {//滑动速度超过阈值
if (velocityX < -1000) {
//平滑展开Menu
smoothExpand();
} else {
// 平滑关闭Menu
smoothClose();
}
} else {
if (Math.abs(getScrollX()) > ratio) {//否则就判断滑动距离
//平滑展开Menu
smoothExpand();
} else {
// 平滑关闭Menu
smoothClose();
}
}
}
//释放
releaseVelocityTracker();
isTouching = false;//没有手指在摸我了
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_MOVE:
//屏蔽滑动时的事件(长按事件和侧滑的冲突)
// Log.e("tianbin","======MenuItem onInterceptTouchEvent======ACTION_MOVE===111111111111111");
if (Math.abs(ev.getRawX() - mFirstX) > mTouchSlop) {
// Log.e("tianbin","======MenuItem onInterceptTouchEvent======ACTION_MOVE===22222222222222222");
return true;
}
break;
case MotionEvent.ACTION_UP:
if (getScrollX() > mTouchSlop) {
//这里判断落点在内容区域屏蔽点击,内容区域外,允许传递事件继续向下的。。。
if (ev.getX() < getWidth() - getScrollX()) {
//仿QQ,侧滑菜单展开时,点击内容区域,关闭侧滑菜单。
if (isUnMoved) {
smoothClose();
}
return true;//true表示拦截
}
}
if (isUserSwiped) {
return true;
}
break;
}
if(qqInterceptFlag){
return true;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
private void smoothExpand(){
mViewCache=MenuItem.this;
mScroller.startScroll(getScrollX(),0,mRightMenuWidths-getScrollX(),0);
invalidate();
}
private void smoothClose(){
mViewCache=null;
mScroller.startScroll(getScrollX(),0,-getScrollX(),0);
invalidate();
}
/**
* 快速关闭。
* 用于 点击侧滑菜单上的选项,同时想让它快速关闭(删除 置顶)。
* 这个方法在ListView里是必须调用的,
* 在RecyclerView里,视情况而定,如果是mAdapter.notifyItemRemoved(pos)方法不用调用。
*/
public void quickClose() {
if (this == mViewCache) {
// mViewCache.scrollTo(0, 0);//关闭
mScroller.startScroll(0,0,0,0,0);
mViewCache = null;
}
}
//每次ViewDetach的时候,判断一下 ViewCache是不是自己,如果是自己,关闭侧滑菜单,且ViewCache设置为null,
// 理由:1 防止内存泄漏(ViewCache是一个静态变量)
// 2 侧滑删除后自己后,这个View被Recycler回收,复用,下一个进入屏幕的View的状态应该是普通状态,而不是展开状态。
@Override
protected void onDetachedFromWindow() {
if (this == mViewCache) {
mViewCache.smoothClose();
mViewCache = null;
}
super.onDetachedFromWindow();
}
//展开时,禁止长按
// @Override
// public boolean performLongClick() {
// if (Math.abs(getScrollX()) > mTouchSlop) {
// return false;
// }
// return super.performLongClick();
// }
/**
* @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 VelocityTracker#clear()
* @see VelocityTracker#recycle()
*/
private void releaseVelocityTracker() {
if (null != mVelocityTracker) {
mVelocityTracker.clear();
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
}
一个最简单的屏幕触摸动作触发了一系列Touch事件:ACTION_DOWN->ACTION_MOVE->ACTION_MOVE->ACTION_MOVE...->ACTION_MOVE->ACTION_UP,Android系统中的每个View的子类都具有下面三个和TouchEvent处理密切相关的方法:
public boolean dispatchTouchEvent(MotionEvent ev) 这个方法用来分发TouchEvent
public boolean onInterceptTouchEvent(MotionEvent ev) 这个方法用来拦截TouchEvent(ViewGroup才有)
public boolean onTouchEvent(MotionEvent ev) 这个方法用来处理TouchEvent
当TouchEvent发生时,首先Activity将TouchEvent传递给最顶层的View,TouchEvent最先到达最顶层view的 dispatchTouchEvent,然后由dispatchTouchEvent方法进行分发,如果dispatchTouchEvent返回true或者false,事件均不会继续向下传递,如果down后返回false,则move和up都不会被接受,事件向上传递给Activity处理。这里为什么特别指定的down事件呢,因为如果down返回true,说明后续事件会被传递于此,被自己消费,但是move返回false呢?哈哈,这个就不会影响了,因此说down才是关键。此方法一般用于初步处理事件,因为动作是由此分发,所以通常会调用super.dispatchTouchEvent,这样就会继续调用onInterceptTouchEvent,再由onInterceptTouchEvent决定事件流向。如果interceptTouchEvent 返回 true ,也就是拦截掉了,则交给它的 onTouchEvent 来处理,如果 interceptTouchEvent 返回 false ,那么就传递给子 view ,由子 view 的 dispatchTouchEvent 再来开始这个事件的分发。如果事件传递到某一层的子 view 的 onTouchEvent 上了,这个方法返回了 false ,那么这个事件会从这个 view 往上传递,都是 onTouchEvent 来接收。而如果传递到最上面的 onTouchEvent
也返回 false 的话,这个事件就会“消失”,而且接收不到下一次事件。
下面一张图可以说明一切
里面有个重要方法在此要特别说明一下:requestDisallowInterceptTouchEvent。当检测到水平有移动距离的时候,则调用此方法,将滑动事件交给子View来处理,而ListView自身不再进行垂直滑动,否则会出现水平跟垂直滑动有冲突。它在源码中的定义如下:
/**
* Called when a child does not want this parent and its ancestors to
* intercept touch events with
* {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
*
* This parent should pass this call onto its parents. This parent must obey
* this request for the duration of the touch (that is, only clear the flag
* after this parent has received an up or a cancel.
*
* @param disallowIntercept True if the child does not want the parent to
* intercept touch events.
*/
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);
这是viewparent接口,viewgroup中的实现如下:(viewgroup实现了viewparent接口)
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
自定义item讲完了,下面就说说怎么用(终于派上用场了)
package com.binbin.slidedelmenu.item;
import android.content.Context;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.binbin.slidedelmenu.DividerItemDecoration;
import com.binbin.slidedelmenu.R;
import java.util.ArrayList;
import java.util.List;
/**
* Created by -- on 2016/12/19.
*/
public class RecyclerViewActivity extends AppCompatActivity {
private RecyclerView mRecyclerView;
private List str = new ArrayList<>();
private MyAdapter adapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mRecyclerView = new RecyclerView(this);
setContentView(mRecyclerView);
for (int i = 0; i < 20; i++) {
str.add(i + "个");
}
//设置adapter
adapter=new MyAdapter(str,this);
mRecyclerView.setAdapter(adapter);
//设置布局管理器
LinearLayoutManager linearLayoutManager=new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(linearLayoutManager);
//设置Item增加、移除动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
//添加分割线
mRecyclerView.addItemDecoration(new DividerItemDecoration(this, linearLayoutManager.getOrientation(),R.drawable.divider2));
}
class MyAdapter extends RecyclerView.Adapter {
private List datas;
private Context mContext;
public MyAdapter(List datas,Context mContext){
this.datas=datas;
this.mContext=mContext;
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new MyViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item3, parent,
false));
}
@Override
public void onBindViewHolder(final MyViewHolder holder, final int position) {
holder.tv.setText(datas.get(position));
holder.bt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(RecyclerViewActivity.this,"bt========onClick",Toast.LENGTH_SHORT).show();
}
});
holder.tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(RecyclerViewActivity.this,"tv========onClick",Toast.LENGTH_SHORT).show();
}
});
holder.tvHello.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(RecyclerViewActivity.this,"hello=====onClick",Toast.LENGTH_SHORT).show();
}
});
holder.tvDel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(RecyclerViewActivity.this,holder.getAdapterPosition()+"del========onClick"+position,Toast.LENGTH_SHORT).show();
// ((MenuItem)holder.itemView).quickClose();
str.remove(holder.getAdapterPosition());
notifyItemRemoved(holder.getAdapterPosition());
// notifyDataSetChanged();
}
});
holder.tv.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Toast.makeText(RecyclerViewActivity.this,"tv==========onLongClick",Toast.LENGTH_SHORT).show();
return true;
}
});
holder.tvHello.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Toast.makeText(RecyclerViewActivity.this,"hello============onLongClick",Toast.LENGTH_SHORT).show();
return true;
}
});
}
@Override
public int getItemCount() {
return datas.size();
}
class MyViewHolder extends RecyclerView.ViewHolder {
TextView tv;
TextView tvHello;
TextView tvDel;
Button bt;
public MyViewHolder(View view) {
super(view);
tv = (TextView) view.findViewById(R.id.tv);
tvDel=(TextView) view.findViewById(R.id.tv_del);
tvHello=(TextView) view.findViewById(R.id.tv_hello);
bt= (Button) view.findViewById(R.id.bt);
}
}
}
}
以上就是整个项目的代码,不知道大家理解了没有,写的不好的地方,请大家多多包容与指正~~~
好了,今天的讲解到此结束,有疑问的朋友请在下面留言。
源码下载
最后要感谢张旭童同学的思路,参考文章:http://blog.csdn.net/zxt0601/article/details/53157090