一、概述
使用到ViewDragHelper是自己在做仿微信的侧滑删除操作时用到的,需要对Recycleview内的Item的滑动事件做处理,并将隐藏在屏幕右侧的view滑出来做出删除操作。自己的demo删除了,这里就借用一下ITluochen大神的demo,主要是能为自己以后用到ViewDragHelper提供方便与指引,ITluochen大神的demo讲解的其实非常详细了,大家也可以去看看。
这里先简要介绍ViewDragHelper,后面再结合实例讲解。
ViewDragHelper解决了Android中手势处理过于复杂的问题,在DrawerLayout出现之前,侧滑菜单都是由第三方开源代码实现的,其中著名的当属MenuDrawer ,MenuDrawer重写onTouchEvent方法来实现侧滑效果,代码量很大,实现逻辑也需要很大的耐心才能看懂。如果每个开发人员都从这么原始的步奏开始做起,那对于安卓生态是相当不利的。所以说ViewDragHelper等的出现反映了安卓开发框架已经开始向成熟的方向迈进。
首先我们通过一个简单的例子来看看其快捷的用法,分为以下几个步骤:
(1)创建实例
(2)触摸相关的方法的调用
(3)ViewDragHelper.Callback实例的编写
第一步,
利用ViewDragHelper.create(ViewGroup forParent, Callback cb)会创建一个ViewDragHelper的实例。
第二步,弄一个类继承自ViewDragHelper.Callback,作为第一步中create方法的参数,复写一下这么几个方法并且一些逻辑操作
tryCaptureView()
clampViewPositionHorizontal()
clampViewPositionVertical()
onViewPositionChanged()
onViewReleased()
第三步实现动画效果,
第二步中的复写的那几个方法很重要。这几个方法到底分别的有什么用呢?可以大概这么理解:
我们知道TouchEvent大概可以分为三个状态,Down(按下)、Move(移动)和Up(抬起)。
那么在这三个不同的状态里面,与之关联的就是上面的几个方法:
Touch的down事件:
回调tryCaptureView()
Touch的move事件
回调
clampViewPositionHorizontal()
clampViewPositionVertical()
onViewPositionChanged()
Touch的up事件
回调:onViewReleased()
二、demo实例
这个demo主要实现仿微信侧滑删除item,用一般的手势处理机制也能实现,但是考虑到事件分发、手势位移处理等比较复杂,代码量大,所以还是使用了V4包下的ViewDragHelper比较方便。且viewgroup一般是用户在viewgroup中控制其子view滑动事件比较方便。
布局文件:这里的布局分为两个部分,然后将这两个布局include到主布局中,在主布局中就可以方便的使用ViewDragHelper对两个子布局进行滑动控制。
(1)布局文件
内容部分布局如下所示:
<?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="60dp"
android:background="#97d8da"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/mTvContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="文本"
android:gravity="center"
android:textSize="28dp"
/>
</LinearLayout>
删除部分布局如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="130dp"
android:layout_height="60dp"
android:background="#ff0000"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除"
android:textSize="22dp"
android:textColor="#ffffff"
/>
</LinearLayout>
adapter里面的item(将该Item做为List的item)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.amqr.slidedelete.MainActivity">
<com.amqr.slidedelete.view.SlideDelete
android:layout_width="match_parent"
android:layout_height="60dp">
<include layout="@layout/slide_content"/>
<include layout="@layout/slide_delete"/>
</com.amqr.slidedelete.view.SlideDelete>
</RelativeLayout>
主布局如下:
activity_main
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.amqr.slidedelete.MainActivity">
<ListView
android:id="@+id/mLv"
android:layout_width="match_parent"
android:layout_height="match_parent"></ListView>
</RelativeLayout>
(2)自定义控件SlideDelete
public class SlideDelete extends ViewGroup{
private View mContent;
private View mDelete;
public SlideDelete(Context context) {
super(context);
}
public SlideDelete(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlideDelete(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContent = getChildAt(0);
mDelete = getChildAt(1);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mContent.measure(widthMeasureSpec,heightMeasureSpec);
LayoutParams layoutParams = mDelete.getLayoutParams();
int deleteWidth = MeasureSpec.makeMeasureSpec(layoutParams.width,MeasureSpec.EXACTLY);
int deleteHeight = MeasureSpec.makeMeasureSpec(layoutParams.height,MeasureSpec.EXACTLY);
mDelete.measure(deleteWidth,deleteHeight);
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int mContentWidth = mContent.getMeasuredWidth();
int mContentHeight = mContent.getMeasuredHeight();
mContent.layout(0,0,mContentWidth,mContentHeight);
int mDeleteWidth = mDelete.getMeasuredWidth();
mDelete.layout(mContentWidth,0,
mContentWidth + mDeleteWidth, mContentHeight);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
此时效果图如下:
这里需要解释一下: 删除部分其实已经绘制显示在手机的右侧了,内容部分已经match_parent了,所以屏幕上现在暂时还看不见(需要后期通过ViewDragHelper将他滑动出来)。
(3)MyDrawHelper类
class MyDrawHelper extends ViewDragHelper.Callback {
/**
* Touch的down事件会回调这个方法 tryCaptureView
* @Child:指定要动的孩子 (哪个孩子需要动起来)
* @pointerId: 点的标记
* @return : ViewDragHelper是否继续分析处理 child的相关touch事件
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
System.out.println("调用tryCaptureView");
return mContent == child || mDelete == child;
}
/**
* 捕获了水平方向移动的位移数据
* 可以在该方法中对child移动的边界进行控制
* @param child 移动的孩子View
* @param left 父容器的左上角到孩子View的距离
* @param dx 增量值,其实就是移动的孩子View的左上角距离控件(父亲)的距离,包含正负
* @return 如何动
* 调用完此方法,在android2.3以上就会动起来了,2.3以及以下是海动不了的
* 2.3不兼容怎么办?没事,我们复写onViewPositionChanged就是为了解决这个问题的
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
Log.d("Slide","增量值: "+left);
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return super.clampViewPositionVertical(child, top, dy);
}
/**
* 当View的位置改变时的回调
* @param changedView 哪个View的位置改变了
* @param left changedView的left
* @param top changedView的top
* @param dx x方向的上的增量值
* @param dy y方向上的增量值
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
invalidate();
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
/**
* 相当于Touch的up的事件会回调onViewReleased这个方法
*
* @param releasedChild
* @param xvel x方向的速率
* @param yvel y方向的速率
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
这里对上面的clampViewPositionHorizontal(View child, int left, int dx)稍作解释一下,这个函数比较关键。

在上图中,我标出了left的值,下面以一个限定在灰色区域内滑动的实例代码加深理解。
public int clampViewPositionHorizontal(View child, int left, int dx) {
int leftBound = getPaddingLeft();
int rightBound = getWidth() - child.getWidth() - leftBound;
int newLeft = Math.min(Math.max(left, leftBound), rightBound);
return newLeft;
}
(4)、当一个孩子动起来另外一个孩子也可以跟随着动起来
/**
* 当View的位置改变时的回调 这个方法的价值是结合clampViewPositionHorizontal或者clampViewPositionVertical
* @param changedView 哪个View的位置改变了
* @param left changedView的left
* @param top changedView的top
* @param dx x方向的上的增量值
* @param dy y方向上的增量值
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
invalidate();
if(changedView == mContent){
int tempDeleteLeft = mContentWidth+left;
int tempDeleteRight = mContentWidth+left + mDeleteWidth;
mDelete.layout(tempDeleteLeft,0,tempDeleteRight,mDeleteHeight);
}else{
int tempContentLeft = left - mContentWidth;
int tempContentRight = left;
mContent.layout(tempContentLeft,0,tempContentRight,mContentHeight);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
当前效果如下:

(5)解决越界问题
这个越界的问题为什么会产生,是因为现在在clampViewPositionHorizontal方法里面我们简单粗暴地返回了left。这样肯定是不行的。所以我们需要在这个方法上做一些处理
/**
*
* 捕获了水平方向移动的位移数据
* @param child 移动的孩子View
* @param left 父容器的左上角到孩子View的距离
* @param dx 增量值,其实就是移动的孩子View的左上角距离控件(父亲)的距离,包含正负
* @return 如何动
*
* 调用完此方法,在android2.3以上就会动起来了,2.3以及以下是海动不了的
* 2.3不兼容怎么办?没事,我们复写onViewPositionChanged就是为了解决这个问题的
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
Log.d("Slide","增量值: "+left);
if(child == mContent){
if(left>0){
return 0;
}else if(-left>mDeleteWidth){
return -mDeleteWidth;
}
}
if(child == mDelete){
if(left<mContentWidth - mDeleteWidth){
return mContentWidth - mDeleteWidth;
}else if(left > mContentWidth){
return mContentWidth;
}
}
return left;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
(6)释放时位置的归正
/**
* 相当于Touch的up的事件会回调onViewReleased这个方法
*
* @param releasedChild
* @param xvel x方向的速率
* @param yvel y方向的速率
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
int mConLeft = mContent.getLeft();
if(-mConLeft>mDeleteWidth/2){
mContent.layout(-mDeleteWidth,0,mContentWidth-mDeleteWidth,mContentHeight);
mDelete.layout(mContentWidth-mDeleteWidth,0,mContentWidth,mDeleteHeight);
}else{
mContent.layout(0,0,mContentWidth,mContentHeight);
mDelete.layout(mContentWidth,0,mContentWidth+mDeleteWidth,mDeleteHeight);
}
super.onViewReleased(releasedChild, xvel, yvel);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
(7)位置归正的过渡动画
ViewDragHelper里面给我们提供了一个方法,smoothSlideViewTo(View child, int finalLeft, int finalTo), smooth是平滑的意思,这个方法就是帮助我们做平滑滑动的。
public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
mCapturedView = child;
mActivePointerId = INVALID_POINTER;
boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) {
mCapturedView = null;
}
return continueSliding;
}
三个参数
child,被滑动的那个child
finalLeft 、finalTop: 这两个点构成了孩子运动到最后的左上角的坐标点
通过孩子最后左上角的点就可以确定最后的应该到达的位置
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
int mConLeft = mContent.getLeft();
if(-mConLeft>mDeleteWidth/2){
viewDragHelper.smoothSlideViewTo(mContent,-mDeleteWidth,0);
viewDragHelper.smoothSlideViewTo(mDelete,mContentWidth-mDeleteWidth,0);
}else{
viewDragHelper.smoothSlideViewTo(mContent, 0, 0);
viewDragHelper.smoothSlideViewTo(mDelete,mContentWidth, 0);
}
invalidate();
super.onViewReleased(releasedChild, xvel, yvel);
}
}
@Override
public void computeScroll() {
if(viewDragHelper.continueSettling(true)){
invalidate();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
(8)完整的自定义ViewGroup操作子控件类
三、在ListView里面嵌入我们的自定义控件
新建一个Activity,假设名为MyActivity,并且把这个Activity设置为启动页。
MyActivity
public class MyActivity extends Activity{
private ListView mLv;
private ArrayList<String> mData;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
mLv = (ListView) findViewById(R.id.mLv);
mData=new ArrayList<>();
for(int i=0;i<200;i++){
mData.add("文本"+i);
}
mLv.setAdapter(new MyAdapter());
}
class MyAdapter extends BaseAdapter{
@Override
public int getCount() {
return mData.size();
}
@Override
public Object getItem(int position) {
return mData.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if(convertView == null){
viewHolder = new ViewHolder();
convertView = View.inflate(MyActivity.this,R.layout.item,null);
viewHolder.mSlideDelete = (SlideDelete) convertView.findViewById(R.id.mSlideDelete);
viewHolder.mLlContent = (LinearLayout) convertView.findViewById(R.id.mLlContent);
viewHolder.mLlDelete = (LinearLayout) convertView.findViewById(R.id.mLlDelete);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.mSlideDelete.setOnSlideDeleteListener(new SlideDelete.OnSlideDeleteListener() {
@Override
public void onOpen(SlideDelete slideDelete) {
}
@Override
public void onClose(SlideDelete slideDelete) {
}
});
return convertView;
}
}
class ViewHolder{
SlideDelete mSlideDelete;
LinearLayout mLlContent;
LinearLayout mLlDelete;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
但是仍然有缺陷,实际使用中,我们通常是只能一个item处于打开状态,即A打开,B必须处于关闭,不能A和B都处于打开的状态,所以接下来我们还需要使用回调使得只有一个Item被打开。
(9)只有一个Item被打开
1、我们给SlideDelete添加接口和回调,接口里面有onOpen(SlideDelete slideDelete)和onClose(SlideDelete slideDelete)两个方法。
public interface OnSlideDeleteListener {
void onOpen(SlideDelete slideDelete);
void onClose(SlideDelete slideDelete);
}
2、暴露一个setOnSlideDeleteListener方法给外部调用,把SlideDelete的onViewReleased里面的打开和关闭抽取暴露出来,通过参数boolean决定是否显示delete部分。
private OnSlideDeleteListener onSlideDeleteListener;
public void setOnSlideDeleteListener(OnSlideDeleteListener onSlideDeleteListener){
this.onSlideDeleteListener = onSlideDeleteListener;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
int mConLeft = mContent.getLeft();
if(-mConLeft>mDeleteWidth/2){
isShowDelete(true);
if(onSlideDeleteListener != null){
onSlideDeleteListener.onOpen(SlideDelete.this);
}
}else{
isShowDelete(false);
if(onSlideDeleteListener != null){
onSlideDeleteListener.onClose(SlideDelete.this);
}
}
super.onViewReleased(releasedChild, xvel, yvel);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
3、在MyActivity的Adapter里面调用SlideDelete暴露出来的实现接口的方法。
弄一个集合记录起来已经打开的item,每次getView的执行都先关闭已经打开的item
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder
if(convertView == null){
viewHolder = new ViewHolder()
convertView = View.inflate(MyActivity.this,R.layout.item,null)
viewHolder.mSlideDelete = (SlideDelete) convertView.findViewById(R.id.mSlideDelete)
viewHolder.mTvContent = (TextView) convertView.findViewById(R.id.mTvContent)
viewHolder.mLlDelete = (LinearLayout) convertView.findViewById(R.id.mLlDelete)
convertView.setTag(viewHolder)
}else{
viewHolder = (ViewHolder) convertView.getTag()
}
viewHolder.mTvContent.setText(mData.get(position))
viewHolder.mSlideDelete.setOnSlideDeleteListener(new SlideDelete.OnSlideDeleteListener() {
@Override
public void onOpen(SlideDelete slideDelete) {
closeOtherItem()
slideDeleteArrayList.add(slideDelete)
Log.d("Slide", "slideDeleteArrayList当前数量:" + slideDeleteArrayList.size())
}
@Override
public void onClose(SlideDelete slideDelete) {
slideDeleteArrayList.remove(slideDelete)
Log.d("Slide", "slideDeleteArrayList当前数量:" + slideDeleteArrayList.size())
}
})
return convertView
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
关闭所有已经打开的item的方法
private void closeOtherItem(){
ListIterator<SlideDelete> slideDeleteListIterator = slideDeleteArrayList.listIterator();
while(slideDeleteListIterator.hasNext()){
SlideDelete slideDelete = slideDeleteListIterator.next();
slideDelete.isShowDelete(false);
}
slideDeleteArrayList.clear();
}
至此完成。
附上当前完整的SlideDelete的代码
public class SlideDelete extends ViewGroup{
private View mContent;
private View mDelete;
private ViewDragHelper viewDragHelper;
private int mContentWidth;
private int mContentHeight;
private int mDeleteWidth;
private int mDeleteHeight;
public SlideDelete(Context context) {
super(context);
}
public SlideDelete(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlideDelete(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private OnSlideDeleteListener onSlideDeleteListener;
public void setOnSlideDeleteListener(OnSlideDeleteListener onSlideDeleteListener){
this.onSlideDeleteListener = onSlideDeleteListener;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContent = getChildAt(0);
mDelete = getChildAt(1);
viewDragHelper = ViewDragHelper.create(this,new MyDrawHelper());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mContent.measure(widthMeasureSpec,heightMeasureSpec);
LayoutParams layoutParams = mDelete.getLayoutParams();
int deleteWidth = MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY);
int deleteHeight = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
mDelete.measure(deleteWidth, deleteHeight);
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mContentWidth = mContent.getMeasuredWidth();
mContentHeight = mContent.getMeasuredHeight();
mContent.layout(0, 0, mContentWidth, mContentHeight);
mDeleteWidth = mDelete.getMeasuredWidth();
mDeleteHeight = mDelete.getMeasuredHeight();
mDelete.layout(mContentWidth, 0,
mContentWidth + mDeleteWidth, mContentHeight);
}
class MyDrawHelper extends ViewDragHelper.Callback {
/**
* Touch的down事件会回调这个方法 tryCaptureView
*
* @Child:指定要动的孩子 (哪个孩子需要动起来)
* @pointerId: 点的标记
* @return : ViewDragHelper是否继续分析处理 child的相关touch事件
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
System.out.println("调用tryCaptureView");
System.out.println("contentView : " + (mContent == child));
return mContent == child || mDelete == child;
}
/**
*
* 捕获了水平方向移动的位移数据
* @param child 移动的孩子View
* @param left 父容器的左上角到孩子View的距离
* @param dx 增量值,其实就是移动的孩子View的左上角距离控件(父亲)的距离,包含正负
* @return 如何动
*
* 调用完此方法,在android2.3以上就会动起来了,2.3以及以下是海动不了的
* 2.3不兼容怎么办?没事,我们复写onViewPositionChanged就是为了解决这个问题的
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
if(child == mContent){
if(left>0){
return 0;
}else if(-left>mDeleteWidth){
return -mDeleteWidth;
}
}
if(child == mDelete){
if(left<mContentWidth - mDeleteWidth){
return mContentWidth - mDeleteWidth;
}else if(left > mContentWidth){
return mContentWidth;
}
}
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return super.clampViewPositionVertical(child, top, dy);
}
/**
* 当View的位置改变时的回调 这个方法的价值是结合clampViewPositionHorizontal或者clampViewPositionVertical
* @param changedView 哪个View的位置改变了
* @param left changedView的left
* @param top changedView的top
* @param dx x方向的上的增量值
* @param dy y方向上的增量值
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
invalidate();
if(changedView == mContent){
int tempDeleteLeft = mContentWidth+left;
int tempDeleteRight = mContentWidth+left + mDeleteWidth;
mDelete.layout(tempDeleteLeft,0,tempDeleteRight,mDeleteHeight);
}else{
int tempContentLeft = left - mContentWidth;
int tempContentRight = left;
mContent.layout(tempContentLeft,0,tempContentRight,mContentHeight);
}
}
/**
* 相当于Touch的up的事件会回调onViewReleased这个方法
*
* @param releasedChild
* @param xvel x方向的速率
* @param yvel y方向的速率
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
int mConLeft = mContent.getLeft();
if(-mConLeft>mDeleteWidth/2){
isShowDelete(true);
if(onSlideDeleteListener != null){
onSlideDeleteListener.onOpen(SlideDelete.this);
}
}else{
isShowDelete(false);
if(onSlideDeleteListener != null){
onSlideDeleteListener.onClose(SlideDelete.this);
}
}
super.onViewReleased(releasedChild, xvel, yvel);
}
}
/**
* 是否展示delete部分
* @param isShowDelete
*/
public void isShowDelete(boolean isShowDelete){
if(isShowDelete){
viewDragHelper.smoothSlideViewTo(mContent,-mDeleteWidth,0);
viewDragHelper.smoothSlideViewTo(mDelete,mContentWidth-mDeleteWidth,0);
}else{
viewDragHelper.smoothSlideViewTo(mContent, 0, 0);
viewDragHelper.smoothSlideViewTo(mDelete, mContentWidth, 0);
}
invalidate();
}
@Override
public void computeScroll() {
if(viewDragHelper.continueSettling(true)){
invalidate();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
/**Process a touch event received by the parent view. This method will dispatch callback events
as needed before returning. The parent view's onTouchEvent implementation should call this. */
viewDragHelper.processTouchEvent(event);
return true;
}
public interface OnSlideDeleteListener {
void onOpen(SlideDelete slideDelete);
void onClose(SlideDelete slideDelete);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
附上当前完整的MyActivity代码
public class MyActivity extends Activity{
private ListView mLv;
private ArrayList<String> mData;
private List<SlideDelete> slideDeleteArrayList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
mLv = (ListView) findViewById(R.id.mLv);
mData=new ArrayList<>();
for(int i=0;i<200;i++){
mData.add("文本"+i);
}
mLv.setAdapter(new MyAdapter());
}
class MyAdapter extends BaseAdapter{
@Override
public int getCount() {
if(mData!=null){
return mData.size();
}
return 0;
}
@Override
public Object getItem(int position) {
if(mData!=null){
return mData.get(position);
}
return null;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if(convertView == null){
viewHolder = new ViewHolder();
convertView = View.inflate(MyActivity.this,R.layout.item,null);
viewHolder.mSlideDelete = (SlideDelete) convertView.findViewById(R.id.mSlideDelete);
viewHolder.mTvContent = (TextView) convertView.findViewById(R.id.mTvContent);
viewHolder.mLlDelete = (LinearLayout) convertView.findViewById(R.id.mLlDelete);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.mTvContent.setText(mData.get(position));
viewHolder.mSlideDelete.setOnSlideDeleteListener(new SlideDelete.OnSlideDeleteListener() {
@Override
public void onOpen(SlideDelete slideDelete) {
closeOtherItem();
slideDeleteArrayList.add(slideDelete);
Log.d("Slide", "slideDeleteArrayList当前数量:" + slideDeleteArrayList.size());
}
@Override
public void onClose(SlideDelete slideDelete) {
slideDeleteArrayList.remove(slideDelete);
Log.d("Slide", "slideDeleteArrayList当前数量:" + slideDeleteArrayList.size());
}
});
return convertView;
}
}
class ViewHolder{
SlideDelete mSlideDelete;
TextView mTvContent;
LinearLayout mLlDelete;
}
private void closeOtherItem(){
ListIterator<SlideDelete> slideDeleteListIterator = slideDeleteArrayList.listIterator();
while(slideDeleteListIterator.hasNext()){
SlideDelete slideDelete = slideDeleteListIterator.next();
slideDelete.isShowDelete(false);
}
slideDeleteArrayList.clear();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
4、滑动屏幕就关闭打开的条目
完善一下,如果当前有item的删除部分是展开的,当这个情况下我们去滑动竖直方向滑动屏幕,那么删除部分就会被隐藏回去。其实就是做一下ListView的滑动监听而已
mLv.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if(scrollState == SCROLL_STATE_FLING || scrollState == SCROLL_STATE_TOUCH_SCROLL){
closeOtherItem();
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
});
5、删除按钮按下删除item
viewHolder.mLlDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mData.remove(position);
notifyDataSetChanged();
}
});
最终效果

ViewDragHelper对象的构建通常在viewGroup的内部,用于实现一个自定义的布局的时候,对布局内部的View进行拖动,可用的构造方法有两种。
/**
* 两个工厂方法,通常使用第一个
* forParent 表示所在的ViewGroup
* sensitivity 表示拖动的灵敏度
* cb 表示我们需要实现的拖动的各种监听
* */
public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
return new ViewDragHelper(forParent.getContext(), forParent, cb);
}
public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
final ViewDragHelper helper = create(forParent, cb);
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
return helper;
}
//这个是在父View的构造方法中进行实例化。
public MyLinearLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
private void initView() {
mHelper=ViewDragHelper.create(this, new myCallBack());
}
其中mTouchSlop为能够被系统识别为滑动的移动距离。两个构建方法并不难,我们继续,看下在ViewGroup中还需要做些什么。
/**
* Check if this event as provided to the parent view's onInterceptTouchEvent should
* cause the parent to intercept the touch event stream.
*检测这个作为被提供给父view的onInterceptTouchEvent的事件是否令父view拦截到当前的触摸事件流.
* @param ev MotionEvent provided to onInterceptTouchEvent
* 如果父View在onInterceptTouchEvent方法中应该返回true的话, 则返回true
* 意思就是如果父控件决定拦截,就返回true。
* @return true if the parent view should return true from onInterceptTouchEvent
*/
public boolean shouldInterceptTouchEvent(MotionEvent ev) {}
后面的代码实在是太多就没有全贴,我们只需要知道这个方法是使用就一个了,当然还是推荐大家去看一下里面都做了什么,便于更好的理解和使用这个类。shouldInterceptTouchEvent()方法在ViewGroup的onInterceptTouchEvent()方法中调用,直接把事件传过去,跟GestureDetector用法差不多。接下来还有。
/**
* Process a touch event received by the parent view. This method will dispatch callback events
* as needed before returning. The parent view's onTouchEvent implementation should call this.
* 处理从父view中获取的触摸事件.这个方法将分发callback回调事件.父view的onTouchEvent方法中应该调用该方法.
* @param ev The touch event received by the parent view
*/
public void processTouchEvent(MotionEvent ev) {}
processTouchEvent则在父View的onTouchEvent方法中被调用,用于分发CallBack的回调事件。
onInterceptTouchEvent中通过使用mHelper.shouldInterceptTouchEvent(event)来决定我们是否应该拦截当前的事件。onTouchEvent中通过mHelper.processTouchEvent(event)处理事件。
/**
* Move the captured settling view by the appropriate amount for the current time.
* If <code>continueSettling</code> returns true, the caller should call it again
* on the next frame to continue.
*
* @param deferCallbacks true if state callbacks should be deferred via posted message.
* Set this to true if you are calling this method from
* {@link android.view.View#computeScroll()} or similar methods
* invoked as part of layout or drawing.
* @return true if settle is still in progress
*/
public boolean continueSettling(boolean deferCallbacks) {}
//在父View中的使用
@Override
public void computeScroll() {
if(mHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(this);
}
}
这里就是如果要延迟刷新的话就传入true,返回值如果为True则会重新调用这个方法,直到结束。通常的使用方式就是如上文在父View中使用。至此在父View中我们需要做的就基本结束了。
接下来就是常用方法
* <p>If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)}
* will follow if the capture is successful.</p>
* 决定是否捕获传进来的View
* @param child Child the user is attempting to capture
* @param pointerId ID of the pointer attempting the capture
* @return true if capture should be allowed, false otherwise
*/
public abstract boolean tryCaptureView(View child, int pointerId);
/**
* 决定横向能够拖动的距离
* @param child Child view being dragged
* @param left Attempted motion along the X axis
* @param dx Proposed change in position for left
* @return The new clamped position for left
*/
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;
}
/**
* 决定纵向能够拖动的距离
* @param child Child view being dragged
* @param top Attempted motion along the Y axis
* @param dy Proposed change in position for top
* @return The new clamped position for top
*/
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
三个最常用的方法,在实现拖动的时候基本上都要重写的(第一个是实现),注释已经很明确了,通常在第二,三个方法中直接返回 left,top就可以。