Android自定义View实现淘宝物流详情效果
目录
效果展示
逻辑解析
其实整个效果逻辑非常的简单,首先当整个控件是覆盖全屏的情况时,我们拖动向下滑动超过一定的范围的时候它就自动的滑动到下面否则就回弹
而当控件的状态是展开状态的时候,手指向上滑动超过一定的距离的时候就自动恢复到原始状态
代码实现
1.ViewDragHelper的创建方法
这里我们使用Android本身提供的一个非常好用的工具ViewDragHelper它可以非常方便的实现拖动的效果,它的创建函数如下所示(摘自源码):
/**
* Factory method to create a new ViewDragHelper.
*
* @param forParent Parent view to monitor
* @param sensitivity Multiplier for how sensitive the helper should be about detecting
* the start of a drag. Larger values are more sensitive. 1.0f is normal.
* @param cb Callback to provide information and receive events
* @return a new ViewDragHelper instance
*/
public static ViewDragHelper create(@NonNull ViewGroup forParent, float sensitivity,
@NonNull Callback cb) {
final ViewDragHelper helper = create(forParent, cb);
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
return helper;
}
我们可以看到,这里需要三个参数:
forParent:需要子控件实现拖动效果的ViewGroup,这里我们自定义的就是ViewGroup因此传当前控件的对象即可
sensitivity:它是滑动的敏感度,一般传个1就行
cb:这个比较重要,是拖动过程中的回调,因此我们大部分的操作都在这里面提供的回调方法
它的所有回调方法如下所示:
public abstract static class Callback {
/**
* Called when the drag state changes. See the <code>STATE_*</code> constants
* for more information.
*
* @param state The new drag state
*
* @see #STATE_IDLE
* @see #STATE_DRAGGING
* @see #STATE_SETTLING
*/
public void onViewDragStateChanged(int state) {}
/**
* Called when the captured view's position changes as the result of a drag or settle.
*
* @param changedView View whose position changed
* @param left New X coordinate of the left edge of the view
* @param top New Y coordinate of the top edge of the view
* @param dx Change in X position from the last call
* @param dy Change in Y position from the last call
*/
public void onViewPositionChanged(@NonNull View changedView, int left, int top, @Px int dx,
@Px int dy) {
}
/**
* Called when a child view is captured for dragging or settling. The ID of the pointer
* currently dragging the captured view is supplied. If activePointerId is
* identified as {@link #INVALID_POINTER} the capture is programmatic instead of
* pointer-initiated.
*
* @param capturedChild Child view that was captured
* @param activePointerId Pointer id tracking the child capture
*/
public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {}
/**
* Called when the child view is no longer being actively dragged.
* The fling velocity is also supplied, if relevant. The velocity values may
* be clamped to system minimums or maximums.
*
* <p>Calling code may decide to fling or otherwise release the view to let it
* settle into place. It should do so using {@link #settleCapturedViewAt(int, int)}
* or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes
* one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING}
* and the view capture will not fully end until it comes to a complete stop.
* If neither of these methods is invoked before <code>onViewReleased</code> returns,
* the view will stop in place and the ViewDragHelper will return to
* {@link #STATE_IDLE}.</p>
*
* @param releasedChild The captured child view now being released
* @param xvel X velocity of the pointer as it left the screen in pixels per second.
* @param yvel Y velocity of the pointer as it left the screen in pixels per second.
*/
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {}
/**
* Called when one of the subscribed edges in the parent view has been touched
* by the user while no child view is currently captured.
*
* @param edgeFlags A combination of edge flags describing the edge(s) currently touched
* @param pointerId ID of the pointer touching the described edge(s)
* @see #EDGE_LEFT
* @see #EDGE_TOP
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/
public void onEdgeTouched(int edgeFlags, int pointerId) {}
/**
* Called when the given edge may become locked. This can happen if an edge drag
* was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)}
* was called. This method should return true to lock this edge or false to leave it
* unlocked. The default behavior is to leave edges unlocked.
*
* @param edgeFlags A combination of edge flags describing the edge(s) locked
* @return true to lock the edge, false to leave it unlocked
*/
public boolean onEdgeLock(int edgeFlags) {
return false;
}
/**
* Called when the user has started a deliberate drag away from one
* of the subscribed edges in the parent view while no child view is currently captured.
*
* @param edgeFlags A combination of edge flags describing the edge(s) dragged
* @param pointerId ID of the pointer touching the described edge(s)
* @see #EDGE_LEFT
* @see #EDGE_TOP
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/
public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
/**
* Called to determine the Z-order of child views.
*
* @param index the ordered position to query for
* @return index of the view that should be ordered at position <code>index</code>
*/
public int getOrderedChildIndex(int index) {
return index;
}
/**
* Return the magnitude of a draggable child view's horizontal range of motion in pixels.
* This method should return 0 for views that cannot move horizontally.
*
* @param child Child view to check
* @return range of horizontal motion in pixels
*/
public int getViewHorizontalDragRange(@NonNull View child) {
return 0;
}
/**
* Return the magnitude of a draggable child view's vertical range of motion in pixels.
* This method should return 0 for views that cannot move vertically.
*
* @param child Child view to check
* @return range of vertical motion in pixels
*/
public int getViewVerticalDragRange(@NonNull View child) {
return 0;
}
/**
* Called when the user's input indicates that they want to capture the given child view
* with the pointer indicated by pointerId. The callback should return true if the user
* is permitted to drag the given view with the indicated pointer.
*
* <p>ViewDragHelper may call this method multiple times for the same view even if
* the view is already captured; this indicates that a new pointer is trying to take
* control of the view.</p>
*
* <p>If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)}
* will follow if the capture is successful.</p>
*
* @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(@NonNull View child, int pointerId);
/**
* Restrict the motion of the dragged child view along the horizontal axis.
* The default implementation does not allow horizontal motion; the extending
* class must override this method and provide the desired clamping.
*
*
* @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(@NonNull View child, int left, int dx) {
return 0;
}
/**
* Restrict the motion of the dragged child view along the vertical axis.
* The default implementation does not allow vertical motion; the extending
* class must override this method and provide the desired clamping.
*
*
* @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(@NonNull View child, int top, int dy) {
return 0;
}
}
2.ViewDragHelper.Callback的回调方法
接下来我们就重点来了解下我们需要用到的ViewDragHelper.Callback中的回调方法
首先我们先来介绍一下控制子控件产生拖动效果的回调方法tryCaptureView、clampViewPositionHorizontal和clampViewPositionVertical
tryCaptureView:是控制当前触摸的子控件是否可以被拖动
clampViewPositionHorizontal:控制子控件横向拖动的位置(通过改变子控件的left值),这个方法返回的即是子控件left最终的值
clampViewPositionVertical:控制子控件纵向拖动的位置(通过改变子控件top值),这个方法返回的即是子控件top最终的值
我们看下我们的案例代码中怎么使用的:
private ViewDragHelper mViewDragHelper;
private int mMaxExpandOffset = 1400;//最大展开距离
private void init() {
mViewDragHelper = ViewDragHelper.create(this, 1f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
//需要滑动的子控件就返回true(这里我们通过id来规定的)
return child.getId() == R.id.scroll_container;
}
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
return 0;
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
if(top > mMaxExpandOffset){
//当前滑动的控件是需要滑动的控件,如果向下滑动的距离超过了最大的展开距离那就返回设置的最大距离
return mMaxExpandOffset;
}else if(top > 0){
//当前滑动的控件是需要滑动的控件,如果向下滑动的距离没超过最大的展开距离那就按手指的拖动进行移动
return top;
}else {
//滑动距离小于0的时候就返回0(即不动)
return 0;
}
}
});
}
对照着以上代码我们可以知道,我们是通过限定子控件的id来让特定的子控件(id为R.id.scroll_container的子控件)可以拖动,由于我们只需要纵向拖动因此我们将横向拖动的值始终返回0(即横向永远不动),然后我们在纵向拖动的回调方法中限定了滑动的范围(这里我们暂时设置mMaxExpandOffset为1400)
3.onInterceptTouchEvent和onTouchEvent的处理
另外我们还需要在我们自定义布局的onInterceptTouchEvent和onTouchEvent中进行相应的处理(即需要把事件传给ViewDragHelper处理)这是固定的,代码如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
return true;
}
进行到这里实现的效果如下:
我们发现现在并不能做到滑动超过某个范围后自动展开或恢复,所以我们就需要用到ViewDragHelper.Callback中的另外一个方法onViewReleased来进行处理了
4.实现手指抬起自动展开或收回
我们在ViewDragHelper.Callback的手指抬起的回调方法(onViewReleased)中做如下处理,即设置一个标志(mIsExpand)用来记录展开还是收起状态,然后我们根据手指抬起时的top值来判断当前控件的滑动距离如果超出了我们设置的标准(这里我们设置为mExpandOffset=300)那么就通过ViewDragHelper的smoothSlideViewTo方法让控件自动展开或收起,而由于其内部是使用Scroller实现的,因此我们还需要在我们的自定义控件中重写computeScroll方法,至于为什么需要重写?感兴趣的同学可以看下我的这篇文章:Android Scroller使用(附列表滑动删除案例)
private ViewDragHelper mViewDragHelper;
private boolean mIsExpand = false;//是否展开
private int mMaxExpandOffset = 1400;//最大展开距离
private int mExpandOffset = 300;//可以触发展开或收起所滑动的最小距离
private void init() {
mViewDragHelper = ViewDragHelper.create(this, 1f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
//需要滑动的子控件就返回true(这里我们通过id来规定的)
return child.getId() == R.id.scroll_container;
}
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
return 0;
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
if(top > mMaxExpandOffset){
//当前滑动的控件是需要滑动的控件,如果向下滑动的距离超过了最大的展开距离那就返回设置的最大距离
return mMaxExpandOffset;
}else if(top > 0){
//当前滑动的控件是需要滑动的控件,如果向下滑动的距离没超过最大的展开距离那就按手指的拖动进行移动
return top;
}else {
//滑动距离小于0的时候就返回0(即不动)
return 0;
}
}
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
if(mIsExpand){
if(mMaxExpandOffset - releasedChild.getTop() >= mExpandOffset){
//已经展开设置关闭
mIsExpand = false;
mViewDragHelper.smoothSlideViewTo(releasedChild,0,0);
}else {
mViewDragHelper.smoothSlideViewTo(releasedChild,0,mMaxExpandOffset);
}
}else {
if(releasedChild.getTop() >= mExpandOffset){
//没有展开,设置展开
mIsExpand = true;
mViewDragHelper.smoothSlideViewTo(releasedChild,0,mMaxExpandOffset);
}else {
mViewDragHelper.smoothSlideViewTo(releasedChild,0,0);
}
}
invalidate();
}
});
}
@Override
public void computeScroll() {
if(mViewDragHelper != null && mViewDragHelper.continueSettling(true)){
invalidate();
}
}
这样的话就实现了基本的效果
效果优化
我们发现虽然基本效果实现了,但是还是存在某些问题的,比如给我们的这个自定义控件的子View加一个点击事件,那么在这个子View上进行上下滑动的时候是划不动的,这是因为子View在顶层消费了触摸事件所以ViewDragHelper不起作用了,因此我们还需要处理ViewDragHelper.Callback中的getViewVerticalDragRange方法来开启ViewDragHelper.shouldInterceptTouchEvent(event)纵向的状态捕捉功能,如下:
@Override
public int getViewVerticalDragRange(@NonNull View child) {
//默认为0,我们这里需要将它设置为1
return 1;
}
另外我们还发现,假如需要滑动的子控件为ScrollView的话ScrollView就滑不动了,这是因为父控件将ScrollView的滑动事件给拦截了,我们需要做如下处理,即当ScrollView没有触顶的时候屏取消父控件的拦截:
private View mScrollView;//滑动的View
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
//存储滑动的子控件
if(mScrollView == null){
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
if(childAt.getId() == R.id.scroll_container){
mScrollView = childAt;
break;
}
}
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//子控件如果是ScrollView的话就判断是否触顶,如果不是在顶部就按默认的方式处理,不让ViewDragHelper处理
if(mScrollView != null && mScrollView instanceof ScrollView && mScrollView.getScrollY() != 0){
return super.onInterceptTouchEvent(ev);
}
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
我们还可以根据自己需要加一些其他控件的适配,或者加一个滑动值的回调,可以实现标题栏透明度变化的效果,如下:
案例源码
https://gitee.com/itfitness/scroll-layout
额外补充
所谓条条大路通罗马,这里是我闲暇时用Scroller实现的一样的效果的自定义View,在这也分享下可供大家参考
案例源码:https://gitee.com/itfitness/scroller-scroll-layout