热门标签 | 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

推荐阅读
  • Android开发实现的计时器功能示例
    本文分享了Android开发实现的计时器功能示例,包括效果图、布局和按钮的使用。通过使用Chronometer控件,可以实现计时器功能。该示例适用于Android平台,供开发者参考。 ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • 第一步:PyQt4Designer设计程序界面该部分设计类同VisvalStudio内的设计,改下各部件的objectName!设计 ... [详细]
  • [翻译]PyCairo指南裁剪和masking
    裁剪和masking在PyCairo指南的这个部分,我么将讨论裁剪和masking操作。裁剪裁剪就是将图形的绘制限定在一定的区域内。这样做有一些效率的因素࿰ ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • 在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板
    本文介绍了在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板的方法和步骤,包括将ResourceDictionary添加到页面中以及在ResourceDictionary中实现模板的构建。通过本文的阅读,读者可以了解到在Xamarin XAML语言中构建控件模板的具体操作步骤和语法形式。 ... [详细]
  • Spring学习(4):Spring管理对象之间的关联关系
    本文是关于Spring学习的第四篇文章,讲述了Spring框架中管理对象之间的关联关系。文章介绍了MessageService类和MessagePrinter类的实现,并解释了它们之间的关联关系。通过学习本文,读者可以了解Spring框架中对象之间的关联关系的概念和实现方式。 ... [详细]
  • 基于dlib的人脸68特征点提取(眨眼张嘴检测)python版本
    文章目录引言开发环境和库流程设计张嘴和闭眼的检测引言(1)利用Dlib官方训练好的模型“shape_predictor_68_face_landmarks.dat”进行68个点标定 ... [详细]
  • 本文介绍了pack布局管理器在Perl/Tk中的使用方法及注意事项。通过调用pack()方法,可以控制部件在显示窗口中的位置和大小。同时,本文还提到了在使用pack布局管理器时,应注意将部件分组以便在水平和垂直方向上进行堆放。此外,还介绍了使用Frame部件或Toplevel部件来组织部件在窗口内的方法。最后,本文强调了在使用pack布局管理器时,应避免在中间切换到grid布局管理器,以免造成混乱。 ... [详细]
  • 本文介绍了一个Magento模块,其主要功能是实现前台用户利用表单给管理员发送邮件。通过阅读该模块的代码,可以了解到一些有关Magento的细节,例如如何获取系统标签id、如何使用Magento默认的提示信息以及如何使用smtp服务等。文章还提到了安装SMTP Pro插件的方法,并给出了前台页面的代码示例。 ... [详细]
  • 如何优化Webpack打包后的代码分割
    本文介绍了如何通过优化Webpack的代码分割来减小打包后的文件大小。主要包括拆分业务逻辑代码和引入第三方包的代码、配置Webpack插件、异步代码的处理、代码分割重命名、配置vendors和cacheGroups等方面的内容。通过合理配置和优化,可以有效减小打包后的文件大小,提高应用的加载速度。 ... [详细]
  • 使用Flutternewintegration_test进行示例集成测试?回答首先在dev下的p ... [详细]
  • fileuploadJS@sectionscripts{<scriptsrc~Contentjsfileuploadvendorjquery.ui.widget.js ... [详细]
  • 当我在doWork方法中运行代码时,通过单击button1,进度条按预期工作.但是,当我从其他方法(即btn2,btn3)将列表传递给doWork方法时,进度条在启动后会跳转到10 ... [详细]
  • java unhandled,Eclipse编辑java文件报Unhandled event loop exception错误的解
    本人Eclipse版本是”eclipse-jee-kepler-SR2-win32-x86_64“昨天因为换电脑,所以重装了一下软件,装好eclipse ... [详细]
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社区 版权所有