热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

listview侧滑菜单的实现——高仿QQ联系人列表

转载请注明出处:http:blog.csdn.netbinbinqq86articledetails46010951项目用到了ListView的侧滑删除的功能,由于当时项目比较赶,就随

转载请注明出处:http://blog.csdn.net/binbinqq86/article/details/46010951


项目用到了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(){
cOntent======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 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 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() //仿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 的话,这个事件就会“消失”,而且接收不到下一次事件。


下面一张图可以说明一切




里面有个重要方法在此要特别说明一下:r

推荐阅读
  • 本文介绍了使用kotlin实现动画效果的方法,包括上下移动、放大缩小、旋转等功能。通过代码示例演示了如何使用ObjectAnimator和AnimatorSet来实现动画效果,并提供了实现抖动效果的代码。同时还介绍了如何使用translationY和translationX来实现上下和左右移动的效果。最后还提供了一个anim_small.xml文件的代码示例,可以用来实现放大缩小的效果。 ... [详细]
  • 本文介绍了在rhel5.5操作系统下搭建网关+LAMP+postfix+dhcp的步骤和配置方法。通过配置dhcp自动分配ip、实现外网访问公司网站、内网收发邮件、内网上网以及SNAT转换等功能。详细介绍了安装dhcp和配置相关文件的步骤,并提供了相关的命令和配置示例。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • HDFS2.x新特性
    一、集群间数据拷贝scp实现两个远程主机之间的文件复制scp-rhello.txtroothadoop103:useratguiguhello.txt推pushscp-rr ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • CentOS 6.5安装VMware Tools及共享文件夹显示问题解决方法
    本文介绍了在CentOS 6.5上安装VMware Tools及解决共享文件夹显示问题的方法。包括清空CD/DVD使用的ISO镜像文件、创建挂载目录、改变光驱设备的读写权限等步骤。最后给出了拷贝解压VMware Tools的操作。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • Android系统移植与调试之如何修改Android设备状态条上音量加减键在横竖屏切换的时候的显示于隐藏
    本文介绍了如何修改Android设备状态条上音量加减键在横竖屏切换时的显示与隐藏。通过修改系统文件system_bar.xml实现了该功能,并分享了解决思路和经验。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • 在开发app时,使用了butterknife后,在androidStudio打包apk时可能会遇到报错。为了解决这个问题,可以通过打开proguard-rules.pro文件进行代码混淆来解决。本文介绍了具体的混淆代码和方法。 ... [详细]
  • C# WPF自定义按钮的方法
    本文介绍了在C# WPF中实现自定义按钮的方法,包括使用图片作为按钮背景、自定义鼠标进入效果、自定义按压效果和自定义禁用效果。通过创建CustomButton.cs类和ButtonStyles.xaml资源文件,设计按钮的Style并添加所需的依赖属性,可以实现自定义按钮的效果。示例代码在ButtonStyles.xaml中给出。 ... [详细]
  • Android开发实现的计时器功能示例
    本文分享了Android开发实现的计时器功能示例,包括效果图、布局和按钮的使用。通过使用Chronometer控件,可以实现计时器功能。该示例适用于Android平台,供开发者参考。 ... [详细]
author-avatar
香港买iphone
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有