今天我们准备做侧滑删除的自定义视图,我采用了v4包里面ViewDragHelper。2013年谷歌i/o大会上介绍了两个新的layout: SlidingPaneLayout和DrawerLayout也是用的ViewDragHelper来处理拖动。
其实ViewDragHelper并不是第一个用于分析手势处理的类,gesturedetector也是,但是在和拖动相关的手势分析方面gesturedetector只能说是勉为其难。
关于ViewDragHelper有如下几点:
ViewDragHelper.Callback是连接ViewDragHelper与view之间的桥梁(这个view一般是指拥子view的容器即parentView);
ViewDragHelper的实例是通过静态工厂方法创建的;
你能够指定拖动的方向;
ViewDragHelper可以检测到是否触及到边缘;
ViewDragHelper并不是直接作用于要被拖动的View,而是使其控制的视图容器中的子View可以被拖动,如果要指定某个子view的行为,需要在Callback中想办法;
ViewDragHelper的本质其实是分析onInterceptTouchEvent和onTouchEvent的MotionEvent参数,然后根据分析的结果去改变一个容器中被拖动子View的位置( 通过offsetTopAndBottom(int offset)和offsetLeftAndRight(int offset)方法 ),他能在触摸的时候判断当前拖动的是哪个子View;
虽然ViewDragHelper的实例方法 ViewDragHelper create(ViewGroup forParent, Callback cb) 可以指定一个被ViewDragHelper处理拖动事件的对象 ,但ViewDragHelper类的设计决定了其适用于被包含在一个自定义ViewGroup之中,而不是对任意一个布局上的视图容器使用ViewDragHelper。
好,咱们先来看下我们要做的效果:
这里开始贴MainActivity代码(代码里有详细注释,这里就不再描述):
package com.wyw.slide;
import java.util.ArrayList;
import android.app.Activity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.BaseAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends Activity {
/**列表控件*/
private ListView listview;
/**列表数据集*/
private ArrayList<String> list = new ArrayList<String>();
/**当前滑动的下标*/
private int index=-1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listview = (ListView) findViewById(R.id.listview);
for (int i = 0; i < 30; i++) {
list.add(i+"");
}
listview.setAdapter(new listAdapter());
//listivew滑动监听
listview.setOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
//滚动的时候,把侧滑还原
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
if (index != -1) {
if (listview.getChildAt(index - listview.getFirstVisiblePosition()) != null) {
SwipeLayout swipeLayout = (SwipeLayout) listview.getChildAt(index - listview.getFirstVisiblePosition()).findViewById(R.id.swipelayout);
//还原滑动
swipeLayout.revert();
index = -1;
}
}
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
// TODO Auto-generated method stub
}
});
}
//listview适配器
private class listAdapter extends BaseAdapter {
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
final ViewHolder viewHolder;
if (convertView == null) {
viewHolder = new ViewHolder();
convertView = LayoutInflater.from(MainActivity.this).inflate(R.layout.item_main, null);
viewHolder.swipeLayout = (SwipeLayout) convertView.findViewById(R.id.swipelayout);
viewHolder.txt_content = (TextView) convertView.findViewById(R.id.text);
viewHolder.txt_delete = (TextView) convertView.findViewById(R.id.text_delete);
viewHolder.txts = (TextView) convertView.findViewById(R.id.texts);
viewHolder.linear = (LinearLayout) convertView.findViewById(R.id.linear);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.linear.setVisibility(View.GONE);
//设置内容
viewHolder.txt_content.setText(list.get(position));
//删除按钮
viewHolder.txt_delete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
list.remove(position);
notifyDataSetChanged();
}
});
//已读按钮
viewHolder.txts.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "已读", Toast.LENGTH_SHORT).show();
}
});
//设置自定义监听
viewHolder.swipeLayout.setOnSlide(new SwipeLayout.onSlideListener() {
//侧滑完了之后调用 true已经侧滑,false还未侧滑
@Override
public void onSlided(boolean isSlide) {
if (isSlide) {//是否滑动成功(包括侧滑之后的返回滑动)
if (index != -1) {
//当第一个已经侧滑了,在侧滑第二个的时候,就把第一个还原
if (listview.getChildAt(index - listview.getFirstVisiblePosition()) != null) {
SwipeLayout swipeLayout = (SwipeLayout) listview.getChildAt(index - listview.getFirstVisiblePosition()).findViewById(R.id.swipelayout);
swipeLayout.revert();
}
}
index = position;
}
}
//未侧滑状态下的默认显示整体的点击事件
@Override
public void onClick() {
Toast.makeText(MainActivity.this, list.get(position), Toast.LENGTH_SHORT).show();
}
});
return convertView;
}
private class ViewHolder {
/** 滑动父控件 */
private SwipeLayout swipeLayout;
/** 内容按钮 */
private TextView txt_content;
/** 删除按钮 */
private TextView txt_delete;
/** 已读按钮 */
private TextView txts;
/** 右边试图*/
private LinearLayout linear;
}
}
}
自定义侧滑view代码(代码里有详细注释):
package com.wyw.slide;
import android.annotation.SuppressLint;
import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
/**
* 这个类本身是个layout,所以处理在他下面的包含的子控件
*
* Created by wyw
*/
@SuppressLint("NewApi")
public class SwipeLayout extends LinearLayout {
// 分析手势处理的类
private ViewDragHelper viewDragHelper;
//第一个view
private View contentView;
//第二个view
private View actionView;
private int dragDistance;
private final double AUTO_OPEN_SPEED_LIMIT = 400.0;
private int draggedX;
/**
* 滑动监听
*/
private onSlideListener onSlide;
//按下的x
private float downX;
private float downY;
public SwipeLayout(Context context) {
this(context, null);
}
public SwipeLayout(Context context, AttributeSet attrs) {
this(context, attrs, -1);
}
public SwipeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 创建一个带有回调接口的ViewDragHelper
viewDragHelper = ViewDragHelper.create(this, new DragHelperCallback());
}
// 当View中所有的子控件 均被映射成xml后触发
@Override
protected void onFinishInflate() {
// 拿到第一个内容显示视图
contentView = getChildAt(0);
// 拿到第二个内容显示视图(即删除视图)
actionView = getChildAt(1);
// 默认不显示
actionView.setVisibility(GONE);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
dragDistance = actionView.getMeasuredWidth();
}
/**
* 还原
*/
public void revert() {
if (viewDragHelper != null) {
viewDragHelper.smoothSlideViewTo(contentView, 0, 0);
invalidate();
}
}
/**
* 手势处理的监听实现
*/
private class DragHelperCallback extends ViewDragHelper.Callback {
// tryCaptureView如何返回ture则表示可以捕获该view,你可以根据传入的第一个view参数决定哪些可以捕获
@Override
public boolean tryCaptureView(View view, int i) {
return view == contentView || view == actionView;
}
// 当captureview的位置发生改变时回调
@Override
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
//左边移动了多少
draggedX = left;
// 拦截父视图事件,不让父试图事件影响
getParent().requestDisallowInterceptTouchEvent(true);
if (changedView == contentView) {
actionView.offsetLeftAndRight(dx);
} else {
contentView.offsetLeftAndRight(dx);
}
if (actionView.getVisibility() == View.GONE) {
actionView.setVisibility(View.VISIBLE);
}
//刷新视图
invalidate();
}
/**
* clampViewPositionHorizontal,
* clampViewPositionVertical可以在该方法中对child移动的边界进行控制, left , top
* 分别为即将移动到的位置,比如横向的情况下,我希望只在ViewGroup的内部移动,即:最小>=paddingleft,
* 最大<=ViewGroup.getWidth()-paddingright-child.getWidth。就可以按照如下代码编写:
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
if (child == contentView) {
final int leftBound = getPaddingLeft();
final int minLeftBound = -leftBound - dragDistance;
final int newLeft = Math.min(Math.max(minLeftBound, left), 0);
return newLeft;
} else {
final int minLeftBound = getPaddingLeft()
+ contentView.getMeasuredWidth() - dragDistance;
final int maxLeftBound = getPaddingLeft()
+ contentView.getMeasuredWidth() + getPaddingRight();
final int newLeft = Math.min(Math.max(left, minLeftBound),
maxLeftBound);
return newLeft;
}
}
/**
* 原因是什么呢?主要是因为,如果子View不消耗事件,那么整个手势(DOWN-MOVE*-UP)
* 都是直接进入onTouchEvent,在onTouchEvent的DOWN的时候就确定了captureView。
* 如果消耗事件,那么就会先走onInterceptTouchEvent方法,判断是否可以捕获, 而在判断的过程中会去判断另外两个回调的方法:
* getViewHorizontalDragRange和getViewVerticalDragRange,
* 只有这两个方法返回大于0的值才能正常的捕获。所以, 如果你用Button测试,或者给TextView添加了clickable = true
* ,都要记得重写下面这两个方法:
*/
@Override
public int getViewHorizontalDragRange(View child) {
return dragDistance;
}
// 手指释放的时候回调
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
boolean settleToOpen = false;
if (xvel > AUTO_OPEN_SPEED_LIMIT) {
settleToOpen = false;
} else if (xvel < -AUTO_OPEN_SPEED_LIMIT) {
settleToOpen = true;
} else if (draggedX <= -dragDistance / 2) {
settleToOpen = true;
} else if (draggedX > -dragDistance / 2) {
settleToOpen = false;
}
final int settleDestX = settleToOpen ? -dragDistance : 0;
if (onSlide != null) {
if (settleDestX == 0) {
onSlide.onSlided(false);
} else {
onSlide.onSlided(true);
}
}
viewDragHelper.smoothSlideViewTo(contentView, settleDestX, 0);
ViewCompat.postInvalidateOnAnimation(SwipeLayout.this);
}
}
public void setOnSlide(onSlideListener onSlide) {
this.onSlide = onSlide;
}
/**
* 由于整个视图都用了ViewDragHelper手势处理,
* 所以导致不滑动的视图点击事件不可用,所以需要自己处理点击事件
*/
public interface onSlideListener {
/**
* 侧滑完了之后调用 true已经侧滑,false还未侧滑
*/
void onSlided(boolean isSlide);
/**
* 未侧滑状态下的默认显示整体的点击事件
*/
void onClick();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// 刚开始开启父视图事件.让onTouchEvent监听是移动还是点击
getParent().requestDisallowInterceptTouchEvent(false);
if (viewDragHelper.shouldInterceptTouchEvent(event)) {
return true;
}
return super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//记录按下的坐标
if (event.getAction() == MotionEvent.ACTION_DOWN) {
downX = event.getRawX();
downY = event.getRawY();
}
if (event.getAction() == MotionEvent.ACTION_UP) {
//x,y移动的距离小于10就出发点击事件
if (Math.abs(downX - event.getRawX()) < 10
&& Math.abs(downY - event.getRawY()) < 10) {
if (onSlide != null) {
onSlide.onClick();
}
}
}
// 处理拦截到的事件,这个方法会在返回前分发事件
viewDragHelper.processTouchEvent(event);
// 表示消费了事件,不会再往下传递
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
if (viewDragHelper.continueSettling(true)) {
/**
* 导致失效发生在接下来的动画时间步,通常下显示帧。 这个方法可以从外部的调用UI线程只有当这种观点是附加到一个窗口。
*/
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
最后贴下布局文件:
<?xml version="1.0" encoding="utf-8"?>
<com.wyw.slide.SwipeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipelayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal" >
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@android:color/white"
android:gravity="center"
android:textColor="@android:color/black" />
<LinearLayout
android:id="@+id/linear"
android:layout_width="160dp"
android:layout_height="50dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/text_delete"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:background="@android:color/black"
android:gravity="center"
android:text="删除"
android:textColor="@android:color/white" />
<TextView
android:id="@+id/texts"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:background="@android:color/black"
android:gravity="center"
android:text="已读"
android:textColor="@android:color/white" />
</LinearLayout>
</com.wyw.slide.SwipeLayout>
本篇博客就到这里,如果对ViewDragHelper有兴趣的朋友可以去下下demo看看,研究下,会发现这个东西确实不错。
希望大家多多关注我的博客,多多支持我。
如有好意见或更好的方式欢迎留言谈论。
尊重原创转载请注明:(http://blog.csdn.net/u013895206) !
下面是地址传送门:
http://download.csdn.net/detail/u013895206/9302511