尊重原创,转载请注明(http://blog.csdn.net/aoshiwenrou/article/details/42971193)
最近的项目需求有一条是要实现仿QQ的侧滑删除效果,网上搜到了很多,但是与预想的都不太一样,于是自己研究了一下,写了一个Demo,记录下来。
功能:
1.实现了仿QQ的Item侧滑效果
2.可根据item的长度计算侧滑范围
3.实现item条目点击监听与删除按钮监听
4.解决了删除按钮出现时ListView滑动错位问题(偷懒:滑动时item复位)
主要代码:
SlidePauseListView:
package com.jcking.slidepauselistview.view;import android.content.Context;import android.util.AttributeSet;import android.util.DisplayMetrics;import android.util.Log;import android.view.MotionEvent;import android.view.VelocityTracker;import android.view.View;import android.view.ViewConfiguration;import android.view.WindowManager;import android.widget.AdapterView;import android.widget.ListView;import android.widget.Scroller;public class SlidePauseListView extends ListView { /** * 当前滑动的ListView position */ private int slidePosition; /** * 手指按下X的坐标 */ private int downY; /** * 手指按下Y的坐标 */ private int downX; /** * 屏幕宽度 */ private int screenWidth; /** * ListView的item */ private View itemView; /** * 滑动类 */ private Scroller scroller; /** * 滑动速度边界值 */ private static final int SNAP_VELOCITY = 600; /** * 速度追踪对象 */ private VelocityTracker velocityTracker; /** * 是否响应滑动,默认为不响应 */ private boolean isSlide = false; /** * 认为是用户滑动的最小距离 */ private int mTouchSlop; /** * 最多可滑动的范围 */ private int mTouchLimit; /** * 是否已经侧滑 */ private boolean hasSlided = false; public SlidePauseListView(Context context) { this(context, null); } public SlidePauseListView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SlidePauseListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); DisplayMetrics dm = new DisplayMetrics(); ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(dm); screenWidth = dm.widthPixels; scroller = new Scroller(context); mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); mTouchLimit = screenWidth / 4; } /** * 设置可滑动的最大值 * @param limit */ public void setTouchLimit(int limit){ this.mTouchLimit = limit; } /** * 获取可滑动的最大值 * @return */ public int getTouchLimit(){ return this.mTouchLimit; } /** * 退回原位 */ public void slideBack(){ if(hasSlided && itemView != null){ scrollXBy(itemView, -mTouchLimit); hasSlided = false; } } /** * 分发事件,主要做的是判断点击的是那个item, 以及通过postDelayed来设置响应左右滑动事件 */ @Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { addVelocityTracker(event); // 假如scroller滚动还没有结束,我们直接返回 if (!scroller.isFinished()) { return super.dispatchTouchEvent(event); } // 如果已经侧滑了,我们直接返回 if(hasSlided){ return super.dispatchTouchEvent(event); } downX = (int) event.getX(); downY = (int) event.getY(); slidePosition = pointToPosition(downX, downY); // 无效的position, 不做任何处理 if (slidePosition == AdapterView.INVALID_POSITION) { return super.dispatchTouchEvent(event); } // 获取我们点击的item view itemView = getChildAt(slidePosition - getFirstVisiblePosition()); if(itemView instanceof SlideListItem){ SlideListItem item = (SlideListItem) itemView; int count = item.getChildCount(); mTouchLimit = 0; for (int i = 0; i < count; i++) { if(i > 0){ mTouchLimit += item.getChildAt(i).getMeasuredWidth(); } } } break; } case MotionEvent.ACTION_MOVE: {// Log.d("test", "downX : " + downX + ", event.getX() : " + event.getX() + ", mTouchSlop : " + mTouchSlop); if (Math.abs(getScrollVelocity()) > SNAP_VELOCITY || (downX - event.getX() > mTouchSlop && Math .abs(event.getY() - downY) < mTouchSlop)) { isSlide = true; } break; } case MotionEvent.ACTION_UP: recycleVelocityTracker(); break; } return super.dispatchTouchEvent(event); } /** * 处理我们拖动ListView item的逻辑 */ @Override public boolean onTouchEvent(MotionEvent ev) { Log.d("test", "hasSlided : " + hasSlided + ", isSlide : " + isSlide); // 如果已经侧滑了,回归原位 if(hasSlided){ scrollXBy(itemView, -mTouchLimit); hasSlided = false; // 拦截事件,不继续处理 return false; } if (isSlide && slidePosition != AdapterView.INVALID_POSITION) { requestDisallowInterceptTouchEvent(true); addVelocityTracker(ev); final int action = ev.getAction(); int x = (int) ev.getX(); switch (action) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: MotionEvent cancelEvent = MotionEvent.obtain(ev); cancelEvent.setAction(MotionEvent.ACTION_CANCEL | (ev.getActionIndex()<< MotionEvent.ACTION_POINTER_INDEX_SHIFT)); onTouchEvent(cancelEvent); int deltaX = downX - x; downX = x; // 手指拖动itemView滚动, deltaX大于0向左滚动,小于0向右滚 scrollXBy(itemView, deltaX); return true; //拖动的时候ListView不滚动 case MotionEvent.ACTION_UP: int velocityX = getScrollVelocity(); if (velocityX < -SNAP_VELOCITY) { scrollLeft(); } else { scrollByDistanceX(); } recycleVelocityTracker(); // 手指离开的时候就不响应左右滚动 isSlide = false; break; } }// boolean result = super.onTouchEvent(ev);// Log.d("test", "result : " + result); //否则直接交给ListView来处理onTouchEvent事件 return super.onTouchEvent(ev); } /** * 让指定的view滚动x位置,设置左右边界,view最多滚动到边界位置 * @param view * @param x */ private void scrollXBy(View view, int x){ // 如果已经滑动了最大值,并希望继续向左滑,忽略 if(view.getScrollX() >= mTouchLimit && x >= 0) return; // 如果已经回到原位,并希望继续想右划,忽略 if(view.getScrollX() <= 0 && x <= 0) return; if(view.getScrollX() + x > mTouchLimit){ x = mTouchLimit - view.getScrollX(); }else if(view.getScrollX() + x < 0){ x = -view.getScrollX(); } view.scrollBy(x, 0); } @Override public void computeScroll() { // 调用startScroll的时候scroller.computeScrollOffset()返回true, if (scroller.computeScrollOffset()) { // 让ListView item根据当前的滚动偏移量进行滚动 itemView.scrollTo(scroller.getCurrX(), scroller.getCurrY()); postInvalidate(); } } /** * 向左滑动,根据上面我们知道向左滑动为正值 */ private void scrollLeft() { final int delta = (mTouchLimit - itemView.getScrollX()); // 调用startScroll方法来设置一些滚动的参数,我们在computeScroll()方法中调用scrollTo来滚动item scroller.startScroll(itemView.getScrollX(), 0, delta, 0, Math.abs(delta)); postInvalidate(); // 刷新itemView hasSlided = true; } /** * 根据手指滚动itemView的距离来判断是滚动到开始位置还是向左或者向右滚动 */ private void scrollByDistanceX() { // 如果向左滚动的距离大于屏幕的二分之一,就让其删除 if (itemView.getScrollX() >= mTouchLimit / 2) { scrollLeft(); } else { // 滚回到原始位置,为了偷下懒这里是直接调用scrollTo滚动 itemView.scrollTo(0, 0); } } /** * 添加用户的速度跟踪器 * * @param event */ private void addVelocityTracker(MotionEvent event) { if (velocityTracker == null) { velocityTracker = VelocityTracker.obtain(); } velocityTracker.addMovement(event); } /** * 移除用户速度跟踪器 */ private void recycleVelocityTracker() { if (velocityTracker != null) { velocityTracker.recycle(); velocityTracker = null; } } /** * 获取X方向的滑动速度,大于0向右滑动,反之向左 * * @return */ private int getScrollVelocity() { velocityTracker.computeCurrentVelocity(1000); int velocity = (int) velocityTracker.getXVelocity(); return velocity; }}继承ListView,实现主要功能,dispatchTouchEvent进行事件分发,由自身的onTouchEvent方法处理侧滑事件。其中
if(itemView instanceof SlideListItem){ SlideListItem item = (SlideListItem) itemView; int count = item.getChildCount(); mTouchLimit = 0; for (int i = 0; i < count; i++) { if(i > 0){ mTouchLimit += item.getChildAt(i).getMeasuredWidth(); } } }通过计算item的长度,设定侧滑的最大偏移量。ScrollXBy方法,防止侧滑越界。
/** * 让指定的view滚动x位置,设置左右边界,view最多滚动到边界位置 * @param view * @param x */ private void scrollXBy(View view, int x){ // 如果已经滑动了最大值,并希望继续向左滑,忽略 if(view.getScrollX() >= mTouchLimit && x >= 0) return; // 如果已经回到原位,并希望继续想右划,忽略 if(view.getScrollX() <= 0 && x <= 0) return; if(view.getScrollX() + x > mTouchLimit){ x = mTouchLimit - view.getScrollX(); }else if(view.getScrollX() + x < 0){ x = -view.getScrollX(); } view.scrollBy(x, 0); }
SlideListItem:继承LinearLayout,主要通过重写onLayout方法使其中的子View不压缩变形。
package com.jcking.slidepauselistview.view;import android.content.Context;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.widget.LinearLayout;public class SlideListItem extends LinearLayout { public SlideListItem(Context context) { this(context, null); } public SlideListItem(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int count = getChildCount(); for (int i = 0; i < count; i++) { measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec); } } @Override public boolean dispatchTouchEvent(MotionEvent event) { Log.d("test", "event : " + event); return super.dispatchTouchEvent(event); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int margeLeft = 0; int size = getChildCount(); for (int i = 0; i < size; i++) { View view = getChildAt(i); if (view.getVisibility() != View.GONE) { int childWidth = view.getMeasuredWidth(); // 将内部子孩子横排排列 view.layout(margeLeft, 0, margeLeft + childWidth, view.getMeasuredHeight()); margeLeft += childWidth; } } }}
SlideAdapter:在Adapter中绑定Click事件,分别监听。
package com.jcking.slidepauselistview;import java.util.List;import android.content.Context;import android.view.LayoutInflater;import android.view.View;import android.view.View.OnClickListener;import android.view.ViewGroup;import android.widget.BaseAdapter;import android.widget.Button;import android.widget.TextView;public class SlideAdapter extends BaseAdapter { private List<String> mData; private LayoutInflater mInflater; private ViewHolder mHolder; private OnSlideClickListener mListener; class ViewHolder{ TextView tv; View front; Button btnClock; Button btnDelete; } public SlideAdapter(Context context, List<String> data, OnSlideClickListener listener){ this.mData = data; this.mInflater = LayoutInflater.from(context); this.mListener = listener; } @Override public int getCount() { return mData == null ? 0 : mData.size(); } @Override public String getItem(int position) { return mData == null ? null : mData.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if(convertView == null){ convertView = mInflater.inflate(R.layout.item_listview, null); mHolder = new ViewHolder(); mHolder.tv = (TextView) convertView.findViewById(R.id.tv); mHolder.front = convertView.findViewById(R.id.front); mHolder.btnClock = (Button) convertView.findViewById(R.id.btnClock); mHolder.btnDelete = (Button) convertView.findViewById(R.id.btnDelete); convertView.setTag(mHolder); }else{ mHolder = (ViewHolder) convertView.getTag(); } mHolder.tv.setText(getItem(position)); // 使隐藏的按钮数量不一样,测试可滑动范围 mHolder.btnClock.setVisibility(position%2==0 ? View.GONE : View.VISIBLE); if(mListener != null){ final int pos = position; final View item = convertView; mHolder.btnClock.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mListener.onClockClick(pos, item); } }); mHolder.btnDelete.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mListener.onDeleteClick(pos, item); } }); mHolder.front.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mListener.onItemClick(pos, item); } }); } return convertView; } public interface OnSlideClickListener{ public void onItemClick(int position, View item); public void onClockClick(int position, View item); public void onDeleteClick(int position, View item); }}
MainActivity:简单调用
package com.jcking.slidepauselistview;import java.util.ArrayList;import java.util.List;import android.app.Activity;import android.os.Bundle;import android.view.View;import android.widget.Toast;import com.jcking.slidepauselistview.SlideAdapter.OnSlideClickListener;import com.jcking.slidepauselistview.view.SlidePauseListView;public class MainActivity extends Activity implements OnSlideClickListener { private SlidePauseListView slideCutListView; private SlideAdapter adapter; private List<String> dataSourceList = new ArrayList<String>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } private void init() { slideCutListView = (SlidePauseListView) findViewById(R.id.slideCutListView); for (int i = 0; i < 20; i++) { dataSourceList.add("滑动删除" + i); } adapter = new SlideAdapter(this, dataSourceList, this); slideCutListView.setAdapter(adapter); } @Override public void onItemClick(int position, View item) { // TODO Auto-generated method stub Toast.makeText(this, "点击条目 " + position, Toast.LENGTH_SHORT).show(); } @Override public void onClockClick(int position, View item) { // TODO Auto-generated method stub Toast.makeText(this, "点击闹钟 " + position, Toast.LENGTH_SHORT) .show(); } @Override public void onDeleteClick(int position, View item) { // TODO Auto-generated method stub Toast.makeText(this, "点击删除 " + position, Toast.LENGTH_SHORT) .show(); slideCutListView.slideBack(); dataSourceList.remove(position); adapter.notifyDataSetChanged(); }}
activity_main:主视图布局
<?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="vertical" > <com.jcking.slidepauselistview.view.SlidePauseListView android:id="@+id/slideCutListView" android:layout_width="match_parent" android:layout_height="match_parent" android:cacheColorHint="@android:color/transparent" android:divider="#2b2b2b" android:dividerHeight="0.5dp" android:listSelector="@android:color/transparent" > </com.jcking.slidepauselistview.view.SlidePauseListView></LinearLayout>
item_listview:item布局
<?xml version="1.0" encoding="UTF-8"?><com.jcking.slidepauselistview.view.SlideListItem xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#acacac" android:orientation="horizontal" > <!-- 这里一定要设置android:layout_width属性为充满屏幕 --> <LinearLayout android:id="@+id/front" android:layout_width="match_parent" android:layout_height="wrap_content" android:clickable="true" android:gravity="center_vertical" android:orientation="horizontal" android:padding="5dp" > <TextView android:id="@+id/tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="测试数据" android:textSize="18sp" /> </LinearLayout> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:orientation="horizontal" > <Button android:id="@+id/btnClock" android:layout_width="wrap_content" android:layout_height="match_parent" android:background="#ff0000" android:text="闹铃" android:textColor="#ffffff" /> <Button android:id="@+id/btnDelete" android:layout_width="wrap_content" android:layout_height="match_parent" android:background="#00ffff" android:text="删除" android:textColor="#ffffff" /> </LinearLayout></com.jcking.slidepauselistview.view.SlideListItem>
提示:
1.item布局中,第一个一定要充满屏幕
问题:
1.灵敏度不高,不能像QQ一样完全精确识别手势
参考:http://blog.csdn.net/xiaanming/article/details/17539199
http://blog.csdn.net/jwzhangjie/article/details/39006007
点击此处,下载源码