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中的回调方法
首先我们先来介绍一下控制子控件产生拖动效果的回调方法tryCaptureViewclampViewPositionHorizontalclampViewPositionVertical

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的处理

另外我们还需要在我们自定义布局的onInterceptTouchEventonTouchEvent中进行相应的处理(即需要把事件传给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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容