Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 中 View移動總結:ViewDragHelper學習及用法詳解

Android 中 View移動總結:ViewDragHelper學習及用法詳解

編輯:關於Android編程

如上圖簡單呈現出兩個方塊後,提出一個需求
1.拖動方塊時,方塊(即子View)可以跟隨手指移動。
2.一個方塊移動時,另一個方塊可以跟隨移動。
3.將方塊移動到左邊區域(右邊區域)後放開(即手指離開屏幕),它會自動移動到左邊界(右邊界)。
4.移動的時候給方塊加點動畫(duang~duang~duang~) 。




View移動的相關方法總結:

1. layout

在自定義控件中,View繪制的一個重寫方法layout(),用來設置顯示的位置。所以,可以通過修改View的坐標值來改變view在父View的位置,以此可以達到移動的效果!但是缺點是只能移動指定的View:

    //通過layout方法來改變位置
    view.layout(l,t,r,b);

2.offsetLeftAndRight() 和 offsetTopAndBottom()

非常方便的封裝方法,只需提供水平、垂直方向上的偏移量,展示效果與layout()方法相同。

    view.offsetLeftAndRight(offset);//同時改變left和right
    view.offsetTopAndBottom(offset);//同時改變top和bottom

3. LayoutParams

此類保存了一個View的布局參數,可通過LayoutParams動態改變一個布局的位置參數,以此動態地修改布局,達到View位置移動的效果!但是在獲取getLayoutParams()時,要根據該子View對應的父View布局來決定自身的LayoutParams 。所以一切的前提是:必須要有一個父View,否則無法獲取LayoutParams

//必須獲取父View的LayoutParams 
        LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)getLayoutParams();
        layoutParams.leftMargin = getLeft() + dx;
        layoutParams.topMargin = getTop() + dy;
        setLayoutParams(layoutParams);

4. scrollTo 和 scrollBy

通過改變scrollXscrollY來移動,但是可以移動所有的子ViewscrollTo(x,y)表示移動到一個具體的坐標點(x,y),而scrollBy(x,y)表示移動的增量為dx,dy。

    scrollTo(x,y);
    scrollBy(xOffset,yOffset);

注意:這裡使用scrollBy(xOffset,yOffset);,你會發現並沒有效果,因為以上兩個方法移動的是View的content。若在ViewGroup中使用,移動的是所有子View;若在View中使用,移動的是View的內容(比如TextView)。

所以,不可在view中使用以上方法!應該在View所在的ViewGroup中使用:

((View)getParent()).scrollBy(offsetX, offsetY);

【視圖坐標系】:
這裡寫圖片描述

可是即使這樣,你會發現view移動的效果與設想方向相反!這是Android試圖移動原因,若參數為正值,content將向坐標軸負方向移動;參數為負值,content將向坐標軸正方向移動。所以要實現隨手指移動而滑動的效果,應將偏移量設置為負值即可:

((View)getParent()).scrollBy(-offsetX, -offsetY);

5. canvas

通過改變Canvas繪制的位置來移動View的內容,用的少:

 canvas.drawBitmap(bitmap, left, top, paint)

總結

但是要完成最開始的提的需求,不管使用哪一種方法,都需要通過onTouchEvent方法來捕捉手勢,自己手動計算移動距離,再改變子View的布局,不免有些麻煩,所以在這裡引出正文,介紹一個強大的類來處理移動:ViewDragHelper






ViewDragHelper介紹:

1. 產生: ViewDragHelper在高版本的v4包(android4.4以上的v4)中,於Google在2013年開發者大會提出的

2. 作用:它主要用於處理ViewGroup中對子View的拖拽處理。

3. 使用:它主要封裝了對View的觸摸位置觸摸速度移動距離等的檢測和Scroller,通過接口回調的方式通知我們。所以我們需要做的只是用接收來的數據指定這些子View是否需要移動,移動多少等。

4. 本質:是一個對觸摸事件的解析類






ViewDragHelper實現

1. 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
     */
viewDragHelper = ViewDragHelper.create(forParent, sensitivity, cb);

create()就是創建ViewDragHelper實例的方法,代碼中的注釋是create()中參數的解釋,來查看:
(1)forParent :“用來監視的父View”。傳入參數父View,即可監視該父View中的所有子View。
(2)sensitivity:“檢測時的敏感度;值越大越敏感,1是正常范圍”。比如說手指在滑動屏幕時速度特別快,敏感度越大時,此時速度快也可以檢測到,反之亦然。
(3)Callback :“提供信息和接受的事件”。最重要的參數!可以從這個回調提供的信息獲取到View滑動的距離、速度等。




2. 自定義View繼承FrameLayout

這裡來個小提示:之前實現的布局中自定義DragLayout是繼承於ViewGroup,並且實現重寫了onMeasure()方法,如下:

DragLayout.java

public class DragLayout extends ViewGroup{

    ...
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //方法一:對子View的測量需求
        /*獲取子View的寬度100dp  的兩種方法:
        int size = (int) getResources().getDimension(R.dimen.width);
        int size = readView.getLayoutParams().width;*/
        int measureSpec = MeasureSpec.makeMeasureSpec(redView.getLayoutParams().width, MeasureSpec.EXACTLY);//具體指定寬高,為精確模式
        redView.measure(measureSpec,measureSpec);//當父控件測量完子控件,才可以填(0,0)
        blueView.measure(measureSpec,measureSpec);

       /* //方法二:如果說沒有特殊的對子View的測量需求,可用如下方法
        measureChild(redView,widthMeasureSpec,heightMeasureSpec);
        measureChild(blueView,widthMeasureSpec,heightMeasureSpec);*/
    }
}

但是現在使DragLayout 類繼承於FrameLayout即可!

public class DragLayout extends FrameLayout{
        ...
}

因為在自定義ViewGroup的時候,如果對子View的測量沒有特殊的需求,那麼可以繼承系統已有的布局(比如FrameLayout、RelativeLayout),目的是為了讓已有的布局幫我們實現onMeasure()

所以在繼承之後,我們無需實現onMeasure()方法,以上代碼全部不需要(這裡選擇繼承FrameLayout幀布局,原因是在Android源碼中其實現最簡單),所以重寫繼承的onLayout()方法其實是重寫幀布局中的onLayout(),如果也注釋的話,你會發現藍色小方塊覆蓋紅色,一起擺放在左上角(其實就是幀布局的擺放規則)




3. callback回調創建

    private ViewDragHelper.Callback callback = new Callback() {
        //必須要實現的方法
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return false;
        }
    };



4. 觸摸、攔截事件

以上部分ViewDragHelper的創建部分已完成,可是還沒結束。比如大家熟悉的一個類:GestureDetector手勢識別器,想要它生效,必須傳一個觸摸事件,這樣GestureDetector類才可以解析當前手勢。道理相同,之前在介紹ViewDragHelper已提到,它只是一個對觸摸事件的解析類,需要傳一個觸摸事件,才會生效。

   //處理是否攔截
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //由viewDragHelper 來判斷是否應該攔截此事件
        boolean result = viewDragHelper.shouldInterceptTouchEvent(ev);
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //將觸摸事件傳給viewDragHelper來解析處理
        viewDragHelper.processTouchEvent(event);
        //消費掉此事件,自己來處理
        return true;
    }

以上則viewDragHelper可以監視並解析我們的手勢了,而且會把信息通過回調傳遞給callback。




5. 處理computeScroll()

該方法是Scroller類的核心,系統在繪制View的時候在draw()中調用此方法,實際與scrollTo()相同。

    @Override
    public void computeScroll() {
        super.computeScroll();
        if(scroller.computeScrollOffset()){
            scrollTo(scroller.getCurrX(),scroller.getCurrY());
            invalidate();
        }

如上,Scroller類提供computeScrollOffset()方法來判斷是否完成了整個滑動,同時getCurrX()getCurrY()來獲得當前滑動坐標。

重點是invalidate()方法,因為只能在computeScroll()方法中獲取模擬過程中的scrollXscrollY,但computeScroll()方法是不會自動調用的,只能通過invalidate() —> draw() —>computeScroll()來間接調用computeScroll()方法!模擬過程結束,if判斷中computeScrollOffset()方法返回false,中斷循環,完成整個平滑移動過程!

但是!!!我們並不采取以上方法,之前介紹過ViewDragHelper已經封裝好了Scroller,用另外一種:

    @Override
    public void computeScroll() {
        super.computeScroll();
        if(viewDragHelper.continueSettling(true)){
            ViewCompat.postInvalidateOnAnimation(DragLayout.this);
        }
    }
}

continueSettling()方法判斷是否結束,同Scroller的方法相似,主要是postInvalidateOnAnimation(),此方法不像Scroller的scrollTo,還需要傳值,其實此方法體內已經封裝好移動的方法,它會自動去測量當前位置進行移動,所以我們只需調用即可!(在手指抬起時回調的方法中也會用到它,後面介紹)




6. 實現callback回調中的方法

之前在創建callback時,默認只實現了tryCaptureView()方法 ,完成需求僅僅不夠,還需要其它方法,依次介紹:

(1) tryCaptureView()

此方法用於判斷是否捕獲當前child的觸摸事件,可以指定ViewDragHelper移動哪一個子View。此例中,需要移動兩個方塊,則判斷當前View是否是自己想移動的,返回boolean值。

        /**用於判斷是否捕獲當前child的觸摸事件
         * @param child         當前觸摸的子View
         * @return              true:捕獲並解析      false:不處理
         */
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return child == blueView || child == redView;
        }

(2) onViewCaptured()

此方法在View被開始捕獲和解析時回調,即當tryCaptureView()中的返回值為true的時候,此方法才會被調用。

例如tryCaptureView()方法中只捕獲紅色方塊,當移動紅方快時,該方法會回調,移動藍色方塊時則不會!

        /** 當View被開始捕獲和解析的回調(用處不大)
         * @param capturedChild     當前被捕獲的子View
         */
        @Override
        public void onViewCaptured(View capturedChild, int activePointerId) {
            super.onViewCaptured(capturedChild, activePointerId);
            Log.e("tag","onViewCaptures");
        }

(3) clampViewPositionHorizontal() 和 clampViewPositionVertical()

這兩個為具體滑動方法,分別對應水平和垂直方向上的移動。要想子View移動,此方法必須重寫實現!

而方法的返回值則是指定View在水平(left)垂直(top)方向上變成的值,參數中的dxdy則是代表相較於上一次位置的增量

        /**     控制child在水平方向的移動
         * @param child
         * @param left  ViewDragHelper會將當前child的left值改變成返回的值
         * @param dx    相較於上一次child在水平方向上移動的
         * @return
         */
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return left;
        }
        /**控制child在垂直方向的移動
         * @param child         
         * @param top           ViewDragHelper會將當前child的top值改變成返回的值
         * @param dy            相較於上一次child在水平方向上移動的
         * @return
         */
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return top;
        }
    };

顯示效果:
這裡寫圖片描述

這裡寫圖片描述

通過以上GIF動圖和日志打印可以看出,僅將返回值設置成方法中的參數,方塊就可以任意移動了。也證實了方法中提供的參數而dx或dy是每一次移動的距離,left或top 是指定View移動到的位置,這是計算好了的,相當於left = child.getLeft() + dx。
若想要它不移動,則:
return left - dx;
將它計算好後的距離減去相較於上次移動的距離即可,此時的View就不會移動。所以根據你的需求,可以任意改變此方法的返回值來移動View。


(4) getViewHorizontalDragRange() 和 getViewVerticalDragRange()

看到以上GIF動圖,你會發現我在移動方塊時,它可以超過邊界,沒有任何限制!有些不合理,想限制它的移動范圍,這兩個方法就可以獲取View的拖拽范圍,將它的返回值設為:父控件的寬/高 - 子控件的寬/高,即控件可以移動的范圍。

        //獲取View水平方向的拖拽范圍
        @Override
        public int getViewHorizontalDragRange(View child) {
            return getMeasuredWidth() - child.getMeasuredWidth();
        }
        //     獲取View垂直方向的拖拽范圍
        @Override
        public int getViewVerticalDragRange(View child) {
            return getMeasuredHeight() - child.getMeasuredHeight();
        }

可是以上實現後,你會發現拖拽方塊還是可以超出邊界,此方法並沒有起作用!是否代表此方法完全無用?這返回的值有何用?

不是,它目前確實並不可以限制邊界,但此方法返回的值會用在:比如說手指抬起時,View緩慢移動的動畫時間的計算會用到此值,最好不要返回0(返回也不會錯)!

但是我們還想要達到限制View拖拽邊界的效果,這時在第三點介紹的clampViewPositionHorizontal() 和 clampViewPositionVertical()發揮效果了,該方法是通過返回值來改變View移動的位置,這時可以在方法中加判斷是否有越界:

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            if(left <0){
                //限制左邊界
                left = 0;
            }else if (left > (getMeasuredWidth() - child.getMeasuredWidth())){
                //限制右邊界
                left = getMeasuredWidth() - child.getMeasuredWidth();
            }
            return left;
        }

顯示效果:
這裡寫圖片描述


(5)onViewPositionChanged()

目前為止,需求已經完成可以任意拖拽View了,接下來完成拖拽View時,另一塊跟隨移動。這時介紹一個新的方法:onViewPositionChanged(),該方法在child(需要捕捉的View)位置改變時執行,參數left(top)跟之前介紹方法中含義相同,為child最新的left(top)位置,而dx(dy)是child相較於上一次移動時水平(垂直)方向上改變的距離。

了解之後,就知道這個方法很強大了,在方法體中判斷具體View,再根據方法提供的參數設置另一View的位置,如下:

    /**當child位置改變時執行
         * @param changedView   位置改變的子View
         * @param left           child最新的left位置
         * @param top            child最新的top位置
         * @param dx            相較於上一次水平移動的距離
         * @param dy            相較於上一次垂直移動的距離
         */
    @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            if(changedView == blueView){
                //拖動藍色方塊時,紅色也跟隨移動
                redView.layout(redView.getLeft()+dx , redView.getTop()+dy ,
                        redView.getRight()+dx , redView.getBottom()+dy);
            }else if(changedView == redView){
                //拖動紅色方塊時,藍色也跟隨移動
                blueView.layout(blueView.getLeft()+dx , blueView.getTop()+dy ,
                        blueView.getRight()+dx , blueView.getBottom()+dy);
            }
        }

顯示效果:
這裡寫圖片描述


(6)onViewReleased()

完成目前需求第三個:手指在左邊(右邊)區域離開屏幕後,方塊自動移動到左邊界(右邊界)。接下來介紹最後一個方法onViewReleased(),手指抬起的時候執行該方法。

這裡有兩個新參數:
xvel: x方向移動的速度,若是正值,則代表向右移動,若是負值則向左移動;
yvel: y方向移動的速度,若是正值則向下移動,若是負值則向上移動。

這裡寫圖片描述

       /**手指抬起的時候執行該方法
         * @param releasedChild   當前抬起的View
         * @param xvel             x方向移動的速度:正值:向右移動  負值:向左移動
         * @param yvel             y方向移動的速度:正值:向下移動  負值:向上移動
         */
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            int centerLeft = getMeasuredWidth()/2 - releasedChild.getMeasuredWidth()/2;
            if(releasedChild.getLeft() < centerLeft){
                //在左半邊,應該向左緩慢移動,不用scroller,ViewDragHelper已封裝好
                viewDragHelper.smoothSlideViewTo(releasedChild,0,releasedChild.getTop());
                //仍需要刷新!
                ViewCompat.postInvalidateOnAnimation(DragLayout.this);
//                scroller.startScroll();
//                invalidate();

            }else {
                //在右半邊,向右緩慢移動
                viewDragHelper.smoothSlideViewTo(releasedChild,getMeasuredWidth() - releasedChild.getMeasuredWidth(),releasedChild.getTop());
                ViewCompat.postInvalidateOnAnimation(DragLayout.this);
            }

        }
    };

顯示效果:
這裡寫圖片描述




7. 執行伴隨動畫

還剩下最後一個需求,在方塊移動時加些動畫,說到動畫引入一個概念:百分比(即子View左側占子View可移動寬度的比例)。在移動子View的時候,比如從左到右,那麼百分比則是0~1。做個實驗,在回調onViewPositionChanged()加入兩行:

            //1.計算view移動的百分比
            float fraction = changedView.getLeft() * 1f / (getMeasuredWidth() - changedView.getMeasuredWidth());
            Log.e("tag","fraction:"+fraction);
            //2.執行一系列的伴隨動畫
            executeAnim(fraction);

結果:
這裡寫圖片描述

證實了我們以上的推論,所以現在可以通過傳參數百分比來完成我們想要的動畫效果:

    /**
     * 執行伴隨動畫
     * @param fraction
     */
    private void executeAnim(float fraction){
        //fraction: 0 - 1
        //縮放
//      ViewHelper.setScaleX(redView, 1+0.5f*fraction);
//      ViewHelper.setScaleY(redView, 1+0.5f*fraction);
        //旋轉
//      ViewHelper.setRotation(redView,360*fraction);//圍繞z軸轉
        ViewHelper.setRotationX(redView,360*fraction);//圍繞x軸轉
//      ViewHelper.setRotationY(redView,360*fraction);//圍繞y軸轉
        ViewHelper.setRotationX(blueView,360*fraction);//圍繞z軸轉
        //平移
//      ViewHelper.setTranslationX(redView,80*fraction);
        //透明
//      ViewHelper.setAlpha(redView, 1-fraction);

    }

最終成品:

這裡寫圖片描述






以上了解後,ViewDragHelper的學習到此為止,接下來利用它做一個側滑什麼的更是不在話下,包括現在網上的彷QQ側滑面板都是利用ViewDragHelper完成的,所以工欲善其事,必先利其器呀~

關於View移動總結的,參照了徐宜生老師的《Android群英傳》,講解了許多View相關知識,重新加深理解了,還是很有幫助的。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved