Android ViewDragHelper仿淘宝拖动加载效果
作者:Maximilian_M
拖动加载是我在淘宝的商品详情界面发现的,感觉很实用。于是就分析它的实现方式,感觉用ViewDragHelper可以很方便的实现这种效果。下面大致把我的思路分步骤写一下。先上图吧。
首先建工程什么的我就不多说了。咱从ViewDragHelper的实现开始说吧,ViewDragHelper一般用在一个自定义ViewGroup的内部,可以对其子View进行移动操作。
创建自定义ViewGroup:
package com.maxi.viewdraghelpertest.widget; import android.content.Context; import android.support.v4.widget.ViewDragHelper; import android.util.AttributeSet; import android.view.View; import android.widget.LinearLayout; public class DragHelperLayout extends LinearLayout{ private ViewDragHelper mDragHelper; @SuppressWarnings("static-access") public DragHelperLayout(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub /* * 创建带回调接口的ViewDragHelper */ mDragHelper = ViewDragHelper.create(this, 10.0f,new DragHelperCallback());// 参数一:该类生成的对象(当前的ViewGroup) // 参数二:敏感度(越大越敏感) } class DragHelperCallback extends ViewDragHelper.Callback { @Override public boolean tryCaptureView(View arg0, int arg1) { // TODO Auto-generated method stub return false; } } }
然后将触摸事件传递给ViewDragHelper:
@Override public boolean onInterceptTouchEvent(MotionEvent event) { return mDragHelper.shouldInterceptTouchEvent(event);//是否应该打断MotionEvent的传递 } @Override public boolean onTouchEvent(MotionEvent event) { mDragHelper.processTouchEvent(event); return true; }
接着我们开始实现DragHelperCallback,这个ViewDragHelper.Callback回调中可以对ViewGroup中的一些View进行操作,在此我们只对本项目涉及到的相关用法做解析,详细点请自行查阅资料。
class DragHelperCallback extends ViewDragHelper.Callback { @Override public boolean tryCaptureView(View arg0, int arg1) { // TODO Auto-generated method stub return true; //返回true表示可以捕捉ViewGroup中的View } /* * (non-Javadoc) * @see android.support.v4.widget.ViewDragHelper.Callback#clampViewPositionVertical(android.view.View, int, int) * 限定View竖直方向上的活动区域,防止滑出ViewGroup */ @Override public int clampViewPositionVertical(View child, int top, int dy) { int topBound = getPaddingTop(); int bottomBound = getHeight() - child.getHeight() - topBound; int newHeight = Math.min(Math.max(top, topBound), bottomBound); return newHeight; } }
在上面的代码段中我已经做了注释,在clampViewPositionVertical中我们对View的竖直方向活动区域做了限制,防止滑出ViewGroup,当然你可以直接return top;不过为了效果我先这么限定一下。还有一个clampViewPositionHorizontal方法,同样是对其水平边界进行控制的,先不多说啦。这个时候咱们自定义的ViewGroup初期已经完成,先去试试水。
在activity_main.xml中加入
<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.maxi.viewdraghelpertest.MainActivity" > <com.maxi.viewdraghelpertest.widget.DragHelperLayout android:id="@+id/dhl" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:background="@android:color/darker_gray" > <TextView android:layout_width="match_parent" android:layout_height="100dp" android:background="@android:color/holo_blue_bright" /> <TextView android:layout_width="match_parent" android:layout_height="100dp" android:background="@android:color/holo_orange_dark" /> </com.maxi.viewdraghelpertest.widget.DragHelperLayout> </RelativeLayout>
运行后的效果:
大家是不是都急了,做个拖动加载怎么搞起这东西了,不要急,这才刚刚开始,大家想想拖动加载是不是就是两个View在同一个ViewGroup里通过ViewDragHelper的滑动操作然后实现的?是不是有思路的?没有思路也没关系,咱慢慢来,想要两个View相关联,就是拖动一个View然后另一个View跟着它走该怎么实现呢?首先我们需要ViewDragHelper回调里的另一个方法onViewPositionChanged,该方法是在View位置发生改变时回调的。为的就是在上面的View上拉的时候让下面的View跟着往上走。来看看我们的实现方法:
首先先将两个View初始化:
private View t1, t2; /* * (non-Javadoc) * @see android.view.View#onFinishInflate() * 初始化两个View */ @Override protected void onFinishInflate() { t1 = getChildAt(0); t2 = getChildAt(1); }
得到两个View后我们在回调中判断哪个位置发生了改变,
@Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { // TODO Auto-generated method stub int childIndex = 1; if (changedView == t2) { childIndex = 2; } viewFollowChanged(childIndex, top); }
上面的代码段中有个方法viewFollowChanged,主要实现的就是View跟着动。
private void viewFollowChanged(int viewIndex, int posTop) { viewH = t1.getMeasuredHeight(); if (viewIndex == 1) { int offsetTopBottom = viewH + t1.getTop() - t2.getTop(); t2.offsetTopAndBottom(offsetTopBottom); } else if (viewIndex == 2) { int offsetTopBottom = t2.getTop() - viewH - t1.getTop(); t1.offsetTopAndBottom(offsetTopBottom); } invalidate(); }
在运行是不是发现没有被点击拖动的View会跟着View一起移动,像一个整体双宿双飞。图我就不加了,大家运行看吧。因为我们要获取View的实际大小所以需要以下代码段的支持:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { measureChildren(widthMeasureSpec, heightMeasureSpec); int maxWidth = MeasureSpec.getSize(widthMeasureSpec); int maxHeight = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension( resolveSizeAndState(maxWidth, widthMeasureSpec, 0), resolveSizeAndState(maxHeight, heightMeasureSpec, 0)); } public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: if (specSize < size) { result = specSize | MEASURED_STATE_TOO_SMALL; } else { result = size; } break; case MeasureSpec.EXACTLY: result = specSize; break; } return result | (childMeasuredState & MEASURED_STATE_MASK); }
然后我们可以尝试将两个View满屏,android:layout_height="match_parent",把clampViewPositionVertical方法里限制的边界去掉吧,暂时先return top;这样试一下是不是有点像拖动加载了,呵呵哒,可是第一个View下拉的时候由于上面没有View怎么办?我们可以在clampViewPositionVertical中将它限定边界啊!
@Override public int clampViewPositionVertical(View child, int top, int dy) { int slideTop = top; if (child == t1) { if (top > 0) { slideTop = 0; } } else if (child == t2) { if (top < 0) { slideTop = 0; } } return child.getTop() + (slideTop - child.getTop()); }
已经大致成型了,然后就是拖动的时候将View自动置顶或置底,因为自动置顶或置底是在滑动松开之后,所以就需要用到ViewDragHelper回调里的onViewReleased方法,该方法就是在滑动松开之后调用,接下来实现它:
@Override public void onViewReleased(View releasedChild, float xvel, float yvel) { putStickOrDown(releasedChild);// 滑动松开后,需要置顶或置底 } private void putStickOrDown(View releasedChild, float yvel) { int finalTop = 0; // 默认是粘到最顶端 if (releasedChild == t1) { // 滑动第一个view松开 if (yvel < 0)//灵敏度自己调吧 finalTop = -viewH; } else { // 滑动第二个view松开 if (yvel > 0)//同上 finalTop = viewH; } if (mDragHelper.smoothSlideViewTo(releasedChild, 0, finalTop)) { ViewCompat.postInvalidateOnAnimation(this);// 会在下一个Frame开始的时候,发起一些invalidate操作 } } @Override public void computeScroll() { if (mDragHelper.continueSettling(true)) { ViewCompat.postInvalidateOnAnimation(this); } }
ok,是可以自动置顶或置底了。对了,那种拖动粘滞效果可以设置clampViewPositionVertical里的返回值,return child.getTop() + (finalTop - child.getTop()) / num;num值越大越粘滞。
然后淘宝第一个View是可以滑动的滑动到最底部然后才把手势事件交给ViewDragHelper处理的。这块试想如果用ScrollView的话,手势事件肯定会优先被它消费,这样肯定达不到我们想要的效果,所以在此我们需要对ScrollView进行自定义,大致的实现思路是当用户用户从触发屏幕开始判断是不是ScrollView在最底端,如果在最底端然后判断手势是否是向上滑动的如果也是则满足条件将touch事件交给父View就可以了,即requestDisallowInterceptTouchEvent该方法。然后自定义的ViewGroup中的onInterceptTouchEvent方法也要做相应修改,这里用GestureDetectorCompat处理事件,其回调用来判断是否是上下滑动。先声明private GestureDetectorCompat gestureDC;然后再gestureDC = new GestureDetectorCompat(context,new YSlideDetector());
class YSlideDetector extends SimpleOnGestureListener { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // TODO Auto-generated method stub return Math.abs(distanceY) > Math.abs(distanceX);//Y方向绝对值大于X方向,上下滑动 } }<pre name="code" class="java"> @Override public boolean onInterceptTouchEvent(MotionEvent event) { boolean is_y_slide = gestureDC.onTouchEvent(event); boolean shouldIntercept = mDragHelper.shouldInterceptTouchEvent(event); int action = event.getActionMasked(); if (action == MotionEvent.ACTION_DOWN) { mDragHelper.processTouchEvent(event);// action_down时就让mDragHelper开始工作,否则有时候导致异常 } return shouldIntercept && is_y_slide; }
OK。就这样。是不是达到了想要的效果了?
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。