ViewDragHelper解析以及側(cè)滑控件實(shí)現(xiàn)

在前一篇文章從PhotoView看Android手勢監(jiān)聽實(shí)踐中膏秫,介紹了PhotoView這一控件的手勢控制的分析右遭,其中有三個主要行為的觸發(fā)做盅,Drag,F(xiàn)ling窘哈,Scale吹榴,而在PhotoView的實(shí)現(xiàn)中除了Scale采取的是一個ScaleGestureDetector這樣的一個高級類,前面兩種行為都是依賴原生的手勢來判斷滚婉,十分的麻煩图筹,代碼量也很大, 那么這兩個有沒有比較簡單實(shí)用的類呢让腹?
結(jié)論自然是肯定的远剩,這篇文章要介紹的就是這么一個閃亮的存在,ViewDragHelper骇窍。先看一下官方對這個類的一個定義瓜晤。

ViewDragHelper是一個在自定義ViewGroup中十分實(shí)用的類,它提供了一系列有用的操作和狀態(tài)追蹤來幫助用戶實(shí)現(xiàn)在一個ViewGroup內(nèi)拖動View或者復(fù)位 腹纳。

總體設(shè)計(jì)

ViewDragHelper 只有一個類痢掠,但是內(nèi)部還有一個抽象類CallBack。
CallBack中有一系列方法嘲恍,用來設(shè)置許多屬性足画,可拖動的范圍,邊緣檢測佃牛,哪個View觸發(fā)拖動等等淹辞。這個CallBack是在初始化一個ViewDragHelper 時的必要參數(shù)。

除了CallBack之外俘侠,ViewDragHelper 依然是通過 shouldInterceptTouchEvent和 processTouchEvent 以及設(shè)置的屬性來設(shè)置狀態(tài)判斷拖動象缀,不過這些被封裝后就不需要我們自己寫了,省時省力爷速,ViewDragHelper 內(nèi)部實(shí)際上是一個小型狀態(tài)機(jī)攻冷,在IDLE,DRAGGING遍希,SETTLING三種狀態(tài)之間切換。

流程圖

外部設(shè)計(jì)圖

這個圖是我們在使用一個ViewDragHelper 所需要做的事情里烦,ViewDragHelper使用一個靜態(tài)的方式來創(chuàng)建一個對象

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

第一個參數(shù)就是ParentView的引用凿蒜,第二個參數(shù)是一個觸發(fā)的靈敏程度,默認(rèn)為1.0胁黑,第三個就是圖中的自定義的CallBack废封。
在CallBack中,我們需要根據(jù)自己的需要實(shí)現(xiàn)對應(yīng)的方法丧蘸,總體來說主要是上圖中的幾個方法:

tryCaptureView: 在這個方法中漂洋,我們會去聲明我們想要產(chǎn)生Drag的View,這個方法是有返回值的,只有在返回true的情況下刽漂,才有權(quán)限去真正的產(chǎn)生Drag的行為演训,我們直接看這個方法在源碼中的調(diào)用

    boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
        if (toCapture == mCapturedView && mActivePointerId == pointerId) {
            // Already done!
            return true;
        }
        if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
            mActivePointerId = pointerId;
            captureChildView(toCapture, pointerId);
            return true;
        }
        return false;
    }

toCapture 也就是我們現(xiàn)在手指所在的View,mCapturedView 就是ViewDragHelper 中當(dāng)前已經(jīng)有Drag狀態(tài)的View贝咙,實(shí)際上即使已經(jīng)產(chǎn)生了拖動样悟,這個方法依然會不斷的觸發(fā),在手指Id和View都相同的情況下庭猩,就直接return true窟她,如果是第一次,這里的mCallback.tryCaptureView(toCapture, pointerId) 的返回值決定了是否會走到條件語句之內(nèi)蔼水,因此需要在實(shí)現(xiàn)的時候如果想要觸發(fā)Drag震糖,這個方法一定要返回true。

onEdgeDragStarted:如果我們設(shè)置了可以在邊緣觸摸滑動趴腋,那么可以在這個方法中實(shí)現(xiàn)一個側(cè)滑的效果吊说,通過手動調(diào)用ViewDragHelper的 captureChildView 方法

    public void captureChildView(View childView, int activePointerId) {
        if (childView.getParent() != mParentView) {
            throw new IllegalArgumentException("captureChildView: parameter must be a descendant "
                    + "of the ViewDragHelper's tracked parent view (" + mParentView + ")");
        }

        mCapturedView = childView;
        mActivePointerId = activePointerId;
        mCallback.onViewCaptured(childView, activePointerId);
        setDragState(STATE_DRAGGING);
    }

這個方法可以擺脫前面 tryCaptureView 需要返回true的一個限制,即使返回false于样,在這里依然能夠?qū)鬟M(jìn)來的childView的狀態(tài)置為STATE_DRAGGING疏叨。

clampViewPositionVertical: 這個方法還有一個對應(yīng)方法,這兩個方法主要是用來指定DragView的活動范圍

clampViewPositionVertical(View child, int top, int dy)

      case MotionEvent.ACTION_MOVE: {
            if (mDragState == STATE_DRAGGING) {
                // If pointer is invalid then skip the ACTION_MOVE.
                if (!isValidPointerForActionMove(mActivePointerId)) break;

                final int index = ev.findPointerIndex(mActivePointerId);
                final float x = ev.getX(index);
                final float y = ev.getY(index);
                final int idx = (int) (x - mLastMotionX[mActivePointerId]);
                final int idy = (int) (y - mLastMotionY[mActivePointerId]);

                dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
                saveLastMotion(ev);
...
    private void dragTo(int left, int top, int dx, int dy) {
        int clampedX = left;
        int clampedY = top;
        final int oldLeft = mCapturedView.getLeft();
        final int oldTop = mCapturedView.getTop();
        if (dx != 0) {
            clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
            ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
        }
        if (dy != 0) {
            clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
            ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
        }

在ACTION_MOVE的時候穿剖,根據(jù)移動的距離delta蚤蔓,調(diào)用了dragTo的方法,在這里由我們實(shí)現(xiàn)的clampViewPositionVertical方法根據(jù)一系列參數(shù)糊余,返回了一個最后的X秀又,Y坐標(biāo),通過ViewCompat的兩個方法來實(shí)現(xiàn)View的位置變換贬芥,從上面的變換可以看出我們需要返回的是View最終能到達(dá)的地方吐辙。

onViewReleased: 這個就是在手指抬起的時候或者超出邊界了會觸發(fā),如果想實(shí)現(xiàn)一個側(cè)滑菜單蘸劈,那么在這里可以根據(jù)給予的速度的參數(shù)來決定是否去打開或者關(guān)閉菜單昏苏。

除了CallBack之外,還有一個重要的點(diǎn)威沫,那就是ViewDragHelper 怎么與MotionEvent連接起來贤惯,我們在創(chuàng)建ViewDragHelper 實(shí)例的時候需要傳入一個ParentView,這是一個ViewGroup棒掠,我們需要drag的view就是這個父控件的子View孵构,所以我們需要在onInterceptTouchEvent的時候采取ViewDragHelper 的shouldInterceptTouchEvent 方法

   return mDragState == STATE_DRAGGING;

這個方法的返回是一個判斷語句,判斷是否是Drag狀態(tài)烟很,那么肯定有一個設(shè)置狀態(tài)的地方

                if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
                    tryCaptureViewForDrag(toCapture, pointerId);
                }

在down和Pointer_down的時候去判斷能不能設(shè)置這個狀態(tài),不過前面就說了颈墅,對于邊緣檢測型蜡镶,攔不攔無所謂,直接可以繞過tryCaptureView那一關(guān)恤筛,對于直接Drag的還是需要的官还,不過事件可能被子View截獲了。
除了這個之外叹俏,我們還需要實(shí)現(xiàn)一個onTouchEvent妻枕,ViewDragHelper 也提供了一個對應(yīng)的方法 processTouchEvent ,這個主要就是用來drag view用的粘驰,這里最關(guān)鍵的就是onTouchEvent這個方法的返回值屡谐,具體情況具體分析,如果返回true蝌数,后續(xù)的所有事件就都由這個父控件接送了愕掏,那么自然drag行為也就可以觸發(fā)了。如果不返回true顶伞,那么除了down事件外饵撑,沒有別的事件可以接收了,除非邊緣是一個有點(diǎn)擊事件的子view唆貌。

側(cè)滑實(shí)現(xiàn)

分析了那么多滑潘,還是模仿一個側(cè)滑的實(shí)現(xiàn),效果十分的簡單


b746df2e-d33d-4a11-95c3-eb2e1b2dac76.gif

如果不使用ViewDragHelper锨咙,那么這個需要多長的代碼不清楚语卤,但是使用ViewDragHelper,這個效果不需要100行酪刀。先放代碼

public class NavigationView extends LinearLayout {

    private static final String TAG = "NavigationView";
    private static final int RIGHT = 100;
    private static final int MIN_VELOCITY = 300;
    private static float density;
    private ViewDragHelper mDragHelper;
    private View mContent;
    private View mMenu;

    public NavigationView(Context context) {
        this(context, null);
    }

    public NavigationView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setOrientation(HORIZONTAL);
        mDragHelper = ViewDragHelper.create(this, new CustomCallBack());
        mDragHelper.setEdgeTrackingEnabled(EDGE_LEFT);
        density  = getResources().getDisplayMetrics().density;
    }

    private class CustomCallBack extends ViewDragHelper.Callback {

        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return child == mMenu;
        }

        @Override
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {
            mDragHelper.captureChildView(mMenu,pointerId);
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            int newLeft = Math.max(-child.getWidth(),Math.min(left,0));
            return newLeft;
        }

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            invalidate();
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            if (xvel > MIN_VELOCITY || releasedChild.getLeft()  >-releasedChild.getWidth() * 0.5) {
                mDragHelper.settleCapturedViewAt(0, releasedChild.getTop());
            }else {
                mDragHelper.settleCapturedViewAt(-releasedChild.getWidth(), releasedChild.getTop());
            }
            invalidate();
        }
    }

    @Override
    public void computeScroll() {
        if (mDragHelper.continueSettling(true)){
            invalidate();
        }
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        int count = getChildCount();
        if(count >= 2){
            //簡單寫了  直接寫死
            mMenu = getChildAt(1);
            mContent = getChildAt(0);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //如果menu的寬度是match_parent或者超過限制 那么就需要重新設(shè)置
        int width = (int) (density * RIGHT);
        if (mMenu.getMeasuredWidth() + width > getWidth()){
            int menuWidthSpec = MeasureSpec.makeMeasureSpec(getWidth() -width,MeasureSpec.EXACTLY);
            mMenu.measure(menuWidthSpec,heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mMenu != null){
            mMenu.layout(-mMenu.getMeasuredWidth(),t,0,mMenu.getMeasuredHeight());
        }
        if (mContent != null){
            mContent.layout(0,0,mContent.getMeasuredWidth(),mContent.getMeasuredHeight());
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean event = mDragHelper.shouldInterceptTouchEvent(ev);
        return event;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG,"onTouchEvent" + event.toString());
        mDragHelper.processTouchEvent(event);
        return true;
    }
}

這里盡量寫的簡單粹舵,但是核心的東西不會少,兩個View骂倘,一個是側(cè)滑里面的menu眼滤,一個是外面的主content。這里直接繼承了LinearLayout 历涝,measure時如果寬度過大诅需,也會做一個限制,然后layout到屏幕外面去荧库。

根據(jù)前面的方法的分析诱担,這里的邏輯就一目了然了,設(shè)置一個左邊邊緣檢測电爹,在 onEdgeDragStarted 上面去drag我們的menu菜單,除此之外料睛,在 onViewReleased 的時候根據(jù)速度和當(dāng)前menu的位置判斷后去設(shè)置最終滑動的位置丐箩,這里是一個Scroller摇邦,所有務(wù)必實(shí)現(xiàn)一個 computeScroll

寫的比較的簡潔屎勘,其中還有很多可以完善的地方施籍,比如添加開閉按鈕,判斷更準(zhǔn)確一點(diǎn)概漱,不過這些都是后續(xù)的小細(xì)節(jié)丑慎,這里為的是簡單但不失主體。

整個源碼在github上: https://github.com/sheepm/ViewDragHelper_Sample

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瓤摧,一起剝皮案震驚了整個濱河市竿裂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌照弥,老刑警劉巖腻异,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異这揣,居然都是意外死亡悔常,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門给赞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來机打,“玉大人,你說我怎么就攤上這事片迅〔醒” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵障涯,是天一觀的道長罐旗。 經(jīng)常有香客問我,道長唯蝶,這世上最難降的妖魔是什么九秀? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮粘我,結(jié)果婚禮上鼓蜒,老公的妹妹穿的比我還像新娘。我一直安慰自己征字,他們只是感情好都弹,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著匙姜,像睡著了一般畅厢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上氮昧,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天框杜,我揣著相機(jī)與錄音浦楣,去河邊找鬼。 笑死咪辱,一個胖子當(dāng)著我的面吹牛振劳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播油狂,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼历恐,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了专筷?” 一聲冷哼從身側(cè)響起弱贼,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎仁堪,沒想到半個月后哮洽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡弦聂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年鸟辅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片莺葫。...
    茶點(diǎn)故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡匪凉,死狀恐怖务唐,靈堂內(nèi)的尸體忽然破棺而出父腕,到底是詐尸還是另有隱情,我是刑警寧澤秽五,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布堡纬,位于F島的核電站聂受,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏烤镐。R本人自食惡果不足惜蛋济,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望炮叶。 院中可真熱鬧碗旅,春花似錦、人聲如沸镜悉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽侣肄。三九已至旧困,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背吼具。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工被芳, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人馍悟。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像剩晴,于是被迫代替她去往敵國和親锣咒。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評論 2 348

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

  • ViewDragHelper實(shí)例的創(chuàng)建 ViewDragHelper重載了兩個create()靜態(tài)方法public...
    傀儡世界閱讀 655評論 0 3
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,749評論 25 707
  • 本篇博客講解的是自定義View之側(cè)滑面板赞弥,應(yīng)用場景:QQ毅整,知乎,效果圖如下 一绽左、內(nèi)容摘要 了解ViewDragHe...
    JackChen1024閱讀 508評論 0 1
  • 坐在班車上悼嫉,窗外細(xì)雨濛濛,灰色籠罩的世界已經(jīng)看不清它本來的模樣拼窥,如此刻不知所往的內(nèi)心戏蔑。以為越長大,事情就越簡單鲁纠,自...
    小爬吖閱讀 233評論 0 0
  • 我叫曹梟总棵,是一個十足的二貨。我喜歡騎車改含,是那種遠(yuǎn)距離的情龄。我這個人偶爾也會為了裝逼而看看書,看的也不是很多——半...
    Cao鳥木閱讀 173評論 0 0