Android自定義View實現(xiàn)淘寶物流詳情效果

目錄

效果展示

邏輯解析

其實整個效果邏輯非常的簡單萍倡,首先當(dāng)整個控件是覆蓋全屏的情況時靴寂,我們拖動向下滑動超過一定的范圍的時候它就自動的滑動到下面否則就回彈




而當(dāng)控件的狀態(tài)是展開狀態(tài)的時候,手指向上滑動超過一定的距離的時候就自動恢復(fù)到原始狀態(tài)



代碼實現(xiàn)

1.ViewDragHelper的創(chuàng)建方法

這里我們使用Android本身提供的一個非常好用的工具ViewDragHelper它可以非常方便的實現(xiàn)拖動的效果,它的創(chuàng)建函數(shù)如下所示(摘自源碼):

/**
     * 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;
    }

我們可以看到,這里需要三個參數(shù):
forParent:需要子控件實現(xiàn)拖動效果的ViewGroup,這里我們自定義的就是ViewGroup因此傳當(dāng)前控件的對象即可
sensitivity:它是滑動的敏感度类咧,一般傳個1就行
cb:這個比較重要,是拖動過程中的回調(diào)蟹腾,因此我們大部分的操作都在這里面提供的回調(diào)方法
它的所有回調(diào)方法如下所示:

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的回調(diào)方法

接下來我們就重點來了解下我們需要用到的ViewDragHelper.Callback中的回調(diào)方法
首先我們先來介紹一下控制子控件產(chǎn)生拖動效果的回調(diào)方法tryCaptureView痕惋、clampViewPositionHorizontalclampViewPositionVertical

tryCaptureView:是控制當(dāng)前觸摸的子控件是否可以被拖動
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來規(guī)定的)
                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){
                    //當(dāng)前滑動的控件是需要滑動的控件娃殖,如果向下滑動的距離超過了最大的展開距離那就返回設(shè)置的最大距離
                    return mMaxExpandOffset;
                }else if(top > 0){
                    //當(dāng)前滑動的控件是需要滑動的控件值戳,如果向下滑動的距離沒超過最大的展開距離那就按手指的拖動進(jìn)行移動
                    return top;
                }else {
                    //滑動距離小于0的時候就返回0(即不動)
                    return 0;
                }
            }
        });
    }

對照著以上代碼我們可以知道,我們是通過限定子控件的id來讓特定的子控件(id為R.id.scroll_container的子控件)可以拖動炉爆,由于我們只需要縱向拖動因此我們將橫向拖動的值始終返回0(即橫向永遠(yuǎn)不動)堕虹,然后我們在縱向拖動的回調(diào)方法中限定了滑動的范圍(這里我們暫時設(shè)置mMaxExpandOffset為1400)

3.onInterceptTouchEvent和onTouchEvent的處理

另外我們還需要在我們自定義布局的onInterceptTouchEventonTouchEvent中進(jìn)行相應(yīng)的處理(即需要把事件傳給ViewDragHelper處理)這是固定的,代碼如下:

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mViewDragHelper.processTouchEvent(event);
        return true;
    }

進(jìn)行到這里實現(xiàn)的效果如下:


我們發(fā)現(xiàn)現(xiàn)在并不能做到滑動超過某個范圍后自動展開或恢復(fù)芬首,所以我們就需要用到ViewDragHelper.Callback中的另外一個方法onViewReleased來進(jìn)行處理了

4.實現(xiàn)手指抬起自動展開或收回

我們在ViewDragHelper.Callback的手指抬起的回調(diào)方法(onViewReleased)中做如下處理赴捞,即設(shè)置一個標(biāo)志(mIsExpand)用來記錄展開還是收起狀態(tài),然后我們根據(jù)手指抬起時的top值來判斷當(dāng)前控件的滑動距離如果超出了我們設(shè)置的標(biāo)準(zhǔn)(這里我們設(shè)置為mExpandOffset=300)那么就通過ViewDragHelper的smoothSlideViewTo方法讓控件自動展開或收起郁稍,而由于其內(nèi)部是使用Scroller實現(xiàn)的赦政,因此我們還需要在我們的自定義控件中重寫computeScroll方法,至于為什么需要重寫?感興趣的同學(xué)可以看下我的這篇文章:Android Scroller使用(附列表滑動刪除案例)

private ViewDragHelper mViewDragHelper;
private boolean mIsExpand = false;//是否展開
private int mMaxExpandOffset = 1400;//最大展開距離
private int mExpandOffset = 300;//可以觸發(fā)展開或收起所滑動的最小距離
private void init() {
        mViewDragHelper = ViewDragHelper.create(this, 1f, new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(@NonNull View child, int pointerId) {
                //需要滑動的子控件就返回true(這里我們通過id來規(guī)定的)
                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){
                    //當(dāng)前滑動的控件是需要滑動的控件恢着,如果向下滑動的距離超過了最大的展開距離那就返回設(shè)置的最大距離
                    return mMaxExpandOffset;
                }else if(top > 0){
                    //當(dāng)前滑動的控件是需要滑動的控件桐愉,如果向下滑動的距離沒超過最大的展開距離那就按手指的拖動進(jìn)行移動
                    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){
                        //已經(jīng)展開設(shè)置關(guān)閉
                        mIsExpand = false;
                        mViewDragHelper.smoothSlideViewTo(releasedChild,0,0);
                    }else {
                        mViewDragHelper.smoothSlideViewTo(releasedChild,0,mMaxExpandOffset);
                    }
                }else {
                    if(releasedChild.getTop() >= mExpandOffset){
                        //沒有展開,設(shè)置展開
                        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();
        }
    }

這樣的話就實現(xiàn)了基本的效果

效果優(yōu)化

我們發(fā)現(xiàn)雖然基本效果實現(xiàn)了掰派,但是還是存在某些問題的从诲,比如給我們的這個自定義控件的子View加一個點擊事件,那么在這個子View上進(jìn)行上下滑動的時候是劃不動的靡羡,這是因為子View在頂層消費(fèi)了觸摸事件所以ViewDragHelper不起作用了系洛,因此我們還需要處理ViewDragHelper.Callback中的getViewVerticalDragRange方法來開啟ViewDragHelper.shouldInterceptTouchEvent(event)縱向的狀態(tài)捕捉功能,如下:

@Override
            public int getViewVerticalDragRange(@NonNull View child) {
                //默認(rèn)為0略步,我們這里需要將它設(shè)置為1
                return 1;
            }

另外我們還發(fā)現(xiàn)碎罚,假如需要滑動的子控件為ScrollView的話ScrollView就滑不動了,這是因為父控件將ScrollView的滑動事件給攔截了纳像,我們需要做如下處理,即當(dāng)ScrollView沒有觸頂?shù)臅r候屏取消父控件的攔截:

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的話就判斷是否觸頂拯勉,如果不是在頂部就按默認(rèn)的方式處理竟趾,不讓ViewDragHelper處理
        if(mScrollView != null && mScrollView instanceof ScrollView && mScrollView.getScrollY() != 0){
            return super.onInterceptTouchEvent(ev);
        }
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

我們還可以根據(jù)自己需要加一些其他控件的適配,或者加一個滑動值的回調(diào)宫峦,可以實現(xiàn)標(biāo)題欄透明度變化的效果岔帽,如下:


案例源碼

https://gitee.com/itfitness/scroll-layout

額外補(bǔ)充

所謂條條大路通羅馬,這里是我閑暇時用Scroller實現(xiàn)的一樣的效果的自定義View导绷,在這也分享下可供大家參考


案例源碼:https://gitee.com/itfitness/scroller-scroll-layout

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末犀勒,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子妥曲,更是在濱河造成了極大的恐慌贾费,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件檐盟,死亡現(xiàn)場離奇詭異褂萧,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)葵萎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門导犹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人羡忘,你說我怎么就攤上這事谎痢。” “怎么了卷雕?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵节猿,是天一觀的道長。 經(jīng)常有香客問我爽蝴,道長沐批,這世上最難降的妖魔是什么纫骑? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮九孩,結(jié)果婚禮上先馆,老公的妹妹穿的比我還像新娘。我一直安慰自己躺彬,他們只是感情好煤墙,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宪拥,像睡著了一般仿野。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上她君,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天脚作,我揣著相機(jī)與錄音,去河邊找鬼缔刹。 笑死球涛,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的校镐。 我是一名探鬼主播亿扁,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鸟廓!你這毒婦竟也來了从祝?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤引谜,失蹤者是張志新(化名)和其女友劉穎牍陌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體煌张,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡呐赡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了骏融。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片链嘀。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖档玻,靈堂內(nèi)的尸體忽然破棺而出怀泊,到底是詐尸還是另有隱情,我是刑警寧澤误趴,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布霹琼,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏枣申。R本人自食惡果不足惜售葡,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望忠藤。 院中可真熱鬧挟伙,春花似錦、人聲如沸模孩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽榨咐。三九已至介却,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間块茁,已是汗流浹背齿坷。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留数焊,地道東北人胃夏。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像昌跌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子照雁,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容