您的位置:首页 > 其它

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

2016-06-06 19:59 477 查看
本文转载自:http://blog.csdn.net/binbinqq86/article/details/46010951

项目用到了ListView的侧滑删除的功能,由于当时项目比较赶,就随便在网上找了一个,但是效果不是太好,最近闲了下来,就想自己实现一个,于是就按照QQ的联系人列表的侧滑菜单做了一个,效果基本上是一模一样的。在这个过程中,自己也学习到了不少的东西,下面就把这个过程跟大家分享出来。

废话不多说,首先上效果图。



看完了图如果感觉效果不好,请不要拍砖,奋斗好的话请继续往下看~得意

下面结合代码说说实现的原理:首先自定义一个ViewGroup来实现item的滑动效果。

package com.binbin.slid;

import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.Scroller;

/**
* 此视图可以单独作为一个ListView的item
* 可以侧滑拉出菜单的自定义View,如ListView侧滑删除效果
* 菜单item一起滑动
* @author tianbin
*
*         Created on 2015-5-5 下午4:32:19
*/
public class TSlidLayout extends ViewGroup{
/** 用于滑动的类*/
private Scroller mScroller;
/** 用来跟踪触摸速度的类*/
private VelocityTracker mVelocityTracker;
/** 最小滑动的速度*/
private static final int SNAP_VELOCITY = 300;
/**最小滑动距离,超过了,才认为开始滑动  */
private int mTouchSlop = 0 ;
/**上次触摸的X坐标*/
private float mLastX = -1;
/**上次触摸的Y坐标*/
private float mLastY = -1;
private Context mContext;
/**菜单与item视图*/
private View menu,mContentView;
/**viewgroup的宽高*/
private int maxWidth,maxHeight;
/**滑出菜单是否可见*/
private boolean isMenuVisible=false;
/**滑出菜单的宽度*/
private int slidMenuWidth=0;
/**是否正在左右滑动*/
private boolean isSliding=false;
/**为每个item记录位置,判断点击的是哪个item*/
public int pos=-1;

public TSlidLayout(Context context) {
super(context);
// TODO Auto-generated constructor stub
init(context);
}

public TSlidLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
init(context);
}

@SuppressLint("NewApi")
public TSlidLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// TODO Auto-generated constructor stub
init(context);
}

private void init(Context context) {
this.mContext=context;
mScroller = new Scroller(context);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
slidMenuWidth=mContext.getResources().getDimensionPixelSize(R.dimen.slidemenu_right_width);
}

/**
* 添加视图和滑出菜单
* @param content
* @param menuLeft
* @param menuRight
*/
public void addItemAndMenu(View content,View menu){
//具体宽高在addView的时候设置,里面的控件充满行高
addView(content,new LayoutParams(-1, -1));
addView(menu,new LayoutParams(mContext.getResources().getDimensionPixelSize(R.dimen.slidemenu_right_width),-1));
this.menu=menu;
this.mContentView=content;
content.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
// 调用ListView的item点击事件
((ListView)getParent()).performItemClick(v, pos, pos);
}
});
}

/**
* 计算所有ChildView的宽度和高度 然后根据ChildView的计算结果,设置ViewGroup自己的宽和高
* Exactly:width代表的是精确的尺寸
AT_MOST:width代表的是最大可获得的空间
MATCH_PARENT(FILL_PARENT)对应于EXACTLY,WRAP_CONTENT对应于AT_MOST
其他情况(有具体值的)也对应于EXACTLY
*/
@SuppressLint("NewApi")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//当我们需要重写onMeasure时,记得要调用setMeasuredDimension来设置自身的mMeasuredWidth和mMeasuredHeight,否则,就会抛出异常
/**
* 获得此ViewGroup上级容器为其推荐的宽和高,以及计算模式
*/
//      final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
//      final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//
//        int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
//        int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);

/**
* 根据childView计算的出的宽和高,计算容器的宽和高,主要用于容器是warp_content时
*/
for (int i = 0,count = getChildCount(); i < count; i++) {
View childView = getChildAt(i);
//获取每个子view的自己高度宽度,取最大的就是viewGroup的大小
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
maxWidth = Math.max(maxWidth,childView.getMeasuredWidth());
maxHeight = Math.max(maxHeight,childView.getMeasuredHeight());
}
//为ViewGroup设置宽高
setMeasuredDimension(maxWidth,maxHeight);

// 计算出所有的childView的宽和高---可用
//        measureChildren(widthMeasureSpec, heightMeasureSpec);

/**
* 设置所有的childView的宽和高,此处如果不设置,会造成多个子view的情况下,有的子view设置成match_parent但是不能充满父控件的问题
*/
//首先判断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);
int widthSpec = 0;
int heightSpec = 0;
LayoutParams params = childView.getLayoutParams();
if(params.width > 0){
widthSpec = MeasureSpec.makeMeasureSpec(params.width, MeasureSpec.EXACTLY);
}else if (params.width == -1) {
widthSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY);
} else if (params.width == -2) {
widthSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST);
}

if(params.height > 0){
heightSpec = MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY);
}else if (params.height == -1) {
heightSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY);
} else if (params.height == -2) {
heightSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST);
}
childView.measure(widthSpec, heightSpec);
}
}
/*
* 首先执行onMeasure,然后就会执行onLayout
* 为子View指定位置:相对父控件的位置!!!!!!
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
//此处left,top相对父视图为0,0
mContentView.layout(0, 0, right, maxHeight);
menu.layout(mContentView.getMeasuredWidth(), 0, mContentView.getMeasuredWidth()+menu.getMeasuredWidth(), maxHeight);

}

/**
* 注:Scroller中:正值代表向左移动,负值代表向右移动
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// TODO Auto-generated method stub
if (mVelocityTracker == null) {
// 使用obtain方法得到VelocityTracker的一个对象
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
mVelocityTracker.computeCurrentVelocity(1000);
// 获得当前的速度
int velocityX = (int) mVelocityTracker.getXVelocity();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX=ev.getRawX();
mLastY=ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
// 计算当前的速度
if(Math.abs(velocityX)>SNAP_VELOCITY||(Math.abs(mLastX-ev.getRawX())>mTouchSlop)){
isSliding=true;

int deltaX = (int) (mLastX - ev.getRawX());
mLastX = ev.getRawX();
if(getScrollX()>=0&&getScrollX()<=slidMenuWidth){
if(isMenuVisible){
if(getScrollX()+deltaX<=0){
deltaX=-getScrollX();
}
if((getScrollX()+deltaX)>=slidMenuWidth){
//此时菜单可见,不能左滑,只能右滑隐藏菜单
deltaX=0;
}
}else{
if((getScrollX()+deltaX)>=slidMenuWidth){
deltaX=slidMenuWidth-getScrollX();
}
if(getScrollX()+deltaX<=0){
//菜单不可见,此时不能右滑,只能左滑显示菜单
deltaX=0;
}
}
scrollBy(deltaX,0);
}
}
break;
default:
isSliding=false;
//速度加滑动距离满足一个即自动显示或隐藏
int delta=0;
if(isMenuVisible){
//右菜单可见时
if(velocityX >= SNAP_VELOCITY||(slidMenuWidth-getScrollX())>=slidMenuWidth/3){
//自动隐藏
delta=-getScrollX();
mScroller.startScroll(getScrollX(), 0,delta, 0);
invalidate();
}
if((velocityX>0&&velocityX < SNAP_VELOCITY)||(slidMenuWidth-getScrollX())<slidMenuWidth/3){
//自动显示
delta=slidMenuWidth-getScrollX();
mScroller.startScroll(getScrollX(), 0,delta, 0);
invalidate();
}
}else{
//右菜单不可见时
if(velocityX <= -SNAP_VELOCITY||getScrollX()>=slidMenuWidth/3){
//滑动速度超过或者滑动距离超过一半时,松手自动显示
delta=slidMenuWidth-getScrollX();
mScroller.startScroll(getScrollX(), 0,delta,0);
invalidate();
}
if((velocityX<0&&velocityX > -SNAP_VELOCITY)||getScrollX()<slidMenuWidth/3){
//自动隐藏
delta=-getScrollX();
mScroller.startScroll(getScrollX(), 0,delta, 0);
//startScroll只是设置滑动的初始化参数,一定要调用下面这句,才能真正开始滑动
invalidate();
}
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// TODO Auto-generated method stub
if(isSliding){
//如果正在滑动(非点击),则拦截此事件,不传递给子View
return true;
}
return super.onInterceptTouchEvent(ev);
}

/**
* ViewGroup在分发绘制自己的孩子的时候,会对其子View调用computeScroll()方法
*/
@Override
public void computeScroll() {
// TODO Auto-generated method stub
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}else{//滑动结束后,改变菜单状态
changeMenuVisibleState();
}
}

/**
* 在手动或者自动滚动完成后,改变菜单可见状态
*/
private void changeMenuVisibleState(){
//必须在滚动完成后,判断菜单是否可见,否则会出现判断错误的情况
if(getScrollX()==slidMenuWidth){
isMenuVisible=true;
}else{
isMenuVisible=false;
}
}

/***
* 当菜单可见时,隐藏它
*/
public void hideMenuWithAnimation(){
mScroller.startScroll(getScrollX(), 0,-getScrollX(), 0);
invalidate();
isMenuVisible=false;
}

/***
* 删除的时候,瞬间隐藏所有菜单
*/
public void hideMenu() {
//此函数参数意义:首先瞬间移动到startX,然后在规定的时间内缓慢平移指定距离(dy:非坐标)
//一般startX用getScrollX()代表从当前位置开始平移
mScroller.startScroll(0, 0, 0, 0, 0);
invalidate();
isMenuVisible=false;
}

public boolean getIsMenuVisible(){
return isMenuVisible;
}

}


自定义的item就这么多,里面包含了添加自身布局及菜单布局,以及滑动菜单的处理,下面是ListView的重写,因为主要逻辑都在item中,所有重写的ListView就简单多了,废话不多说,请看代码

package com.binbin.slid;

import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.ListView;

public class TListView extends ListView {
/**上次触摸的X坐标*/
private float mLastX = -1;
private int mLastPointToPosition=-1;
/**最小滑动距离,超过了,才认为开始滑动  */
private int mTouchSlop = 0 ;

public TListView(Context context) {
super(context);
// TODO Auto-generated constructor stub
init(context);
}

public TListView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
init(context);
}

public TListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// TODO Auto-generated constructor stub
init(context);
}

@SuppressLint("NewApi")
public TListView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
// TODO Auto-generated constructor stub
init(context);
}

private void init(Context context) {
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// TODO Auto-generated method stub
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX=ev.getRawX();
//在此处改变mLastPointToPosition,否则return时未能改变
int posTemp=mLastPointToPosition;
mLastPointToPosition=pointToPosition((int)ev.getX(), (int)ev.getY());
//此处注意:在GridView和ListView中,getChildAt ( int position ) 方法中position指的是当前可见区域的第几个元素,而不是整个listview中的位置
for(int i=0;i<=getLastVisiblePosition()-getFirstVisiblePosition();i++){
if(getChildAt(i)!=null){
TSlidLayout tsl=(TSlidLayout)getChildAt(i);
//当有菜单出现时,只能点击菜单,点击其他任何地方均收起菜单
if(tsl.getIsMenuVisible()){
if(posTemp!=(pointToPosition((int)ev.getX(), (int)ev.getY()))){
//说明点击的不是有菜单的那个item
tsl.hideMenuWithAnimation();
//拦截此事件,不再向下传递
return false;
}else{
//如果点击的不是菜单,则隐藏菜单,否则传递给子View
if(inRangeOfView(tsl.getChildAt(0), ev)){
tsl.hideMenuWithAnimation();
//拦截此事件,不再向下传递,包括自身的事件传递

return false;
}
}
}
}
}
break;
case MotionEvent.ACTION_MOVE:
if((Math.abs(mLastX-ev.getRawX())>mTouchSlop)){
//只要水平方向有滑动,就不进行垂直滑动(请求不允许拦截子View触摸事件,即交给子View处理)
//此时不会调用本身的onTouchEvent
requestDisallowInterceptTouchEvent(true);
}
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}

/**
* 判断是否点击在view的内部
* @param view
* @param ev
* @return
*            true 点击在view的内部
*            false 点击在view的外部
*/
private boolean inRangeOfView(View view, MotionEvent ev) {
int[] location = new int[2];
//此处需要取在屏幕上的坐标,并且只需判断x坐标,因为listview中点击的item已经确定
view.getLocationOnScreen(location);
int x = location[0];
if (ev.getX() < x || ev.getX() > (x + view.getWidth())) {
return false;
}
return true;
}

public void hideAllMenuView(){
for(int i=0;i<=getLastVisiblePosition()-getFirstVisiblePosition();i++){
if(getChildAt(i)!=null){
TSlidLayout tsl=(TSlidLayout)getChildAt(i);
//当有菜单出现时,只能点击菜单,点击其他任何地方均收起菜单
if(tsl.getIsMenuVisible()){
tsl.hideMenu();
}
}
}
}
}


自定义的ListView主要就是做一个触摸事件的拦截及向子View(即item)的事件分发,这里首先讲一下Android触摸事件的分发机制。

一个最简单的屏幕触摸动作触发了一系列Touch事件:ACTION_DOWN->ACTION_MOVE->ACTION_MOVE->ACTION_MOVE…->ACTION_MOVE->ACTION_UP

Android系统中的每个View的子类都具有下面三个和TouchEvent处理密切相关的方法:

1)public boolean dispatchTouchEvent(MotionEvent ev) 这个方法用来分发TouchEvent

2)public boolean onInterceptTouchEvent(MotionEvent ev) 这个方法用来拦截TouchEvent

3)public boolean onTouchEvent(MotionEvent ev) 这个方法用来处理TouchEvent

当TouchEvent发生时,首先Activity将TouchEvent传递给最顶层的View,TouchEvent最先到达最顶层view的 dispatchTouchEvent,然后由dispatchTouchEvent方法进行分发,如果dispatchTouchEvent返回true或者false,事件均不会继续向下传递,如果down后返回false,则move和up都不会被接受,只能接受下个动作。这里为什么特别指定的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自身不再进行垂直滑动,否则会出现水平跟垂直滑动有冲突。

以上两个自定义View都讲完了,下面就说说怎么用(终于派上用场了)

package com.binbin.slid;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends Activity {

private TListView lv;
private List<String> str=new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
for(int i=0;i<20;i++){
str.add(i+"个");
}
lv=(TListView) findViewById(R.id.lv);
lv.setAdapter(new MyAdapter());
lv.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
// TODO Auto-generated method stub
Toast.makeText(MainActivity.this, id+"aaaaaaaaaaaaaaaaa"+position, 0).show();
}
});
}

class MyAdapter extends BaseAdapter{

@Override
public int getCount() {
// TODO Auto-generated method stub
return str.size();
}

@Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return str.get(position);
}

@Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
HolderView holder = null;
if (convertView == null) {
holder = new HolderView();
convertView=View.inflate(MainActivity.this,R.layout.slidmenuitem, null);
holder.tsl=(TSlidLayout) convertView.findViewById(R.id.tsl);
holder.content=View.inflate(MainActivity.this,R.layout.slid_layout_menu3, null);
holder.menu=View.inflate(MainActivity.this,R.layout.slidmenuright, null);
holder.tsl.addItemAndMenu(holder.content,holder.menu);
convertView.setTag(holder);
}else {
holder = (HolderView) convertView.getTag();
}

final int pos=position;
//必须设置此句,否则不知道点击的是哪个item中的menu
holder.tsl.pos=position;
holder.menu.findViewById(R.id.menu_delete).setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
// TODO Auto-generated method stub
str.remove(pos);
notifyDataSetChanged();
lv.hideAllMenuView();

}
});
holder.menu.findViewById(R.id.menu_hello).setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
// TODO Auto-generated method stub
Toast.makeText(MainActivity.this, "hello"+pos, 0).show();
}
});
((TextView)holder.content.findViewById(R.id.tv_top)).setText(str.get(position));
return convertView;
}

}
static class HolderView {
public TSlidLayout tsl;
public View content;
public View menu;
}

}


下面是用到的布局

slidmenuright.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center"
android:orientation="horizontal" >

<TextView
android:id="@+id/menu_hello"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@android:color/darker_gray"
android:gravity="center"
android:padding="20dp"
android:text="HELLO"
android:textColor="#fff"
android:textSize="18sp" />

<TextView
android:id="@+id/menu_delete"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#aa2369"
android:gravity="center"
android:padding="20dp"
android:text="删除"
android:textColor="#fff"
android:textSize="18sp" />

</LinearLayout>


slidmenuitem.xml

<com.binbin.slid.TSlidLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tsl"
android:descendantFocusability="blocksDescendants"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#fa2365" />


slid_layout_menu3.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >

<TextView
android:id="@+id/tv_top"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#235690"
android:gravity="center"
android:text="item"
android:textColor="#fff"
android:textSize="20sp" />

</LinearLayout>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: