1. 程式人生 > >Android中View的知識體系之基礎知識

Android中View的知識體系之基礎知識

[TOC] 網上有很多寫關於Android中view的文章,關於view 的知識比較多也比較雜,每次從網上找到相關文章後,都有新的收穫。這次準備寫關於view知識體系的文章,也吸取了網上許多優秀的文章的觀點和案例並加上自己的理解和感悟,這系列的文章會從view的基礎到自定義view。在總結中學習,在學習中總結。 這篇文章主要介紹view的基礎知識。

1.View和ViewGroup

Android應用的所有UI元件都是繼承了View類,View元件非常類似於Swing程式設計的JPanel,它表示一個空白的矩形區域。View類還有一個很重要的子類:ViewGroup,但是ViewGroup通常作為其他元件的容器使用。Android的所有UI元件都是建立在View、ViewGroup基礎之上的,Android採用了“組合器”模式來設計View和ViewGroup:ViewGroup是View的子類,因此ViewGroup也可以被當做View使用。對於一個Android應用的圖形使用者介面來說,ViewGroup作為容器來盛裝其他元件,而ViewGroup裡除了可以包含普通View元件之外,還可以再次包含ViewGroup元件。 這裡寫圖片描述

View和ViewGroup中有許多方法都值得我們去研究,不管是處理view的滑動衝突還是明白view的工作原理以至於我們要自定義view,都起到至關重要的作用。這裡只是表明了它們之間的關係,具體到一些重要的方法函式,我會逐個介紹。

2.MotionEvent和TouchSlop

2.1 MotionEvent

當手指觸控式螢幕幕是(View或ViewGroup派生的控制元件),將產生Touch事件Touch事件的相關細節(觸控發生的位置、事件及怎麼的觸控)被封裝成MotionEvent物件。 在手指觸控式螢幕幕後所產生的一系列Touch事件中,典型的事件型別如下幾種:

事件型別 具體動作
MotionEvent.ACTION_DOWN 手指剛接觸螢幕
MotionEvent.ACTION_UP 手指從螢幕上鬆開的一瞬間
MotionEvent.ACTION_MOVE 手指在螢幕上移動
MotionEvent.ACTION_CANCEL 動作取消(非人為)

正常情況下,一次手指觸控式螢幕幕的行為會觸發一系列點選事件,一般會有兩種情況: (1)點選屏幕後離開鬆開,事件序列為 DOWN -> UP; (2)點選螢幕滑動一會再鬆開,事件序列為 DOWN -> MOVE ->….-> MOVE -> UP; 在上述典型的三種Tocuh事件,我們可以通過TotionEvent物件得到點選事件發生的x和y的座標。為此,系統提供了兩組方法:getX/getY和getRawX/getRawY。區別如下: (1)getX/getY:返回的是相對於當前View

左上角的 x 和 y 座標 (2)getRawX/getRawY : 返回的是相對於手機螢幕左上角的 x 和 y 座標

2.2 TouchSlop

TouchSlop是系統所能識別出的被認為是滑動的最小距離,即當手指在螢幕上滑動時,如果兩次滑動之間的距離小於這個常量,那麼系統就不認為你是在進行滑動操作。原因是:滑動的距離太短,系統不認為它不是滑動。這是個常量,和裝置有關,在不同的裝置上這個值是不同的,可通過:ViewConfiguration.get(getContext()).getScaledTouchSlop()來獲取這個常量。 這個常量的意義在於:當我們在處理滑動時,可以利用這個常量來做一些過濾,比如當兩次滑動事件的滑動距離小於這個值,我們就可以認為未達到滑動距離的臨界值,因此就可以認為它們不是滑動,這樣做可以有更好的使用者體驗。

3.手勢(Gesture)

所謂手勢,其實是指使用者手指在觸控式螢幕上的連續碰撞行為,比如在螢幕上從左至右劃出的一個動作,就是手勢;在比如在螢幕上畫出一個圓圈也是手勢。手勢這種連續的碰撞會形成某個方向上的移動趨勢,也會形成一個不規則的幾何圖形。Android 對於這兩種手勢行為都提供了支援。 對於第一種手勢行為,Android 提供了手勢檢測,併為手勢檢測提供了相應的監聽器。 對於第二種手勢行為,Android 允許開發者新增手勢,並提供相應的API識別使用者手勢。 下面,來介紹手勢相關的重要的類。

3.1 VelocityTracker

速度追蹤,用於追蹤手指在滑動過程中的速度,包括水平和豎直方向的速度,過程也很簡單。首先,在 View 的onTouchEvent 方法中追蹤當前點選事件的速度。

        VelocityTracker tracker = VelocityTracker.obtain();
        tracker.addMovement(event);

接著,當我們想知道當前的滑動速度時,這個時候可以採用如下方式來獲取當前的速度:

        tracker.computeCurrentVelocity(1000);//時間間隔的單位是:毫秒(ms)
        float xVelocity = tracker.getXVelocity();
        float yVelocity = tracker.getYVelocity();

需要注意的是: (1)獲取速度之前必須先計算速度,即 getXVelocitygetYVelocity這兩個方法的前面必須要呼叫 computeCurrentVelocity 方法; (2)這裡的速度是指一段時間內手指所滑動過的畫素。 比如講時間間隔設為 1000ms 時,在 1s 內,手指在水平方向從左向右滑動 100 畫素,那麼水平速度就是 100。注意速度可以是負數,當手指從右向左滑動時,水平方向速度即為負值。速度的計算公式可以表示如下:

      速度 = (終點位置 - 起點位置) / 時間段 

3.2 GestureDetector

3.2.1 概述

Android 為手勢提供了一個 GestureDetector 類,GestureDetector 例項代表了一個手勢檢測器,用於輔助檢測使用者的單擊、滑動、長按、雙擊等行為。GestureDetector內部有3個Listener介面,用來回調不同型別的觸控事件,我們來看看類圖: 這裡寫圖片描述 GestureDetector 這個類對外提供了3個介面和一個靜態內部類,通過一個表格簡單介紹這4個監聽器:

監聽器 簡介
OnGestureListener 手勢檢測,主要有事件型別為:按下(Down)、快速滑動(Fling)、長按(LongPress)、滾動(Scroll)、觸控反饋(ShowPress)和單擊擡起(SingleTapUp)
OnDoubleTapListener 雙擊檢測,有三個回撥型別:雙擊(DoubleTap)、單擊確認(SingleTapConfirmed)和雙擊事件回撥(DoubleTapEvent)
OnContextClickListener 這是Android6.0(23)才新增的,用於檢測外部裝置上的按鈕是否按下,一般情況下可忽略
SimpleOnGestureListener 這個是上述三個介面的空實現,一般情況下使用這個比較多,也比較方便

3.2.2 建構函式

GestureDetector中有5中建構函式,其中我們關注其中的2個建構函式即可:

//第一種建構函式
public GestureDetector(Context context, OnGestureListener listener) {
        this(context, listener, null);
    }

//第二種建構函式
public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
        if (handler != null) {
            mHandler = new GestureHandler(handler);
        } else {
            mHandler = new GestureHandler();
        }
        mListener = listener;
        if (listener instanceof OnDoubleTapListener) {
            setOnDoubleTapListener((OnDoubleTapListener) listener);
        }
        if (listener instanceof OnContextClickListener) {
            setContextClickListener((OnContextClickListener) listener);
        }
        init(context);
    }

第一種建構函式裡面需要傳遞兩個引數,上下文(Context)和手勢監聽器(OnGestureListener),這個很好理解,我們經常用的就是這種方式。 第二種建構函式則需要傳遞的引數多一個Handler。這Handler的作用是為了給 GestureDetector 提供了一個Looper。通常情況下是不需要Handler的,因為它會在內部自動建立一個 Handler 用於處理資料,如果你在主執行緒中建立 GestureDetector ,那麼它內部建立的 Handler 會自動獲取主執行緒的 Looper ,如果你在一個沒有建立 Looper 的子執行緒中建立 GestureDetector 則需要傳遞一個帶有 Looper 的 Handler 給它,否則就會因為無法獲取到 Looper 導致建立失敗。具體的 Handler 和 Looper 的關係可見 Android的訊息機制

第二種建構函式使用方式如下(在子執行緒中建立 GestureDetector):

// 方式一、在主執行緒建立 Handler
final Handler handler = new Handler();
new Thread(new Runnable() {
    @Override public void run() {
        final GestureDetector detector = new GestureDetector(MainActivity.this, new
                GestureDetector.SimpleOnGestureListener() , handler);
        // ... 省略其它程式碼 ...
    }
}).start();

// 方式二、在子執行緒建立 Handler,並且指定主執行緒的Looper
new Thread(new Runnable() {
    @Override public void run() {
        final Handler handler = new Handler(Looper.getMainLooper());
        final GestureDetector detector = new GestureDetector(MainActivity.this, new
                GestureDetector.SimpleOnGestureListener() , handler);
        // ... 省略其它程式碼 ...
    }
}).start();

也可以用其他方式來建立 Handler ,重點是傳遞的 Handler 一定要用 Looper。

new Thread(new Runnable() {
    @Override public void run() {
        Looper.prepare(); // <- 重點在這裡
        final GestureDetector detector = new GestureDetector(MainActivity.this, new
                GestureDetector.SimpleOnGestureListener());
        // ... 省略其它程式碼 ...
    }
}).start();

這種方式用到了 GestureDetector 的第一種建構函式,在子執行緒中準備了 Looper 也是可以的。

3.2.3 重要方法詳細介紹

方法名 描述 所屬介面
onDwon 手指觸控式螢幕幕的一瞬間,由1個ACTION_DOWN觸發 OnGestureListener
onShowPress 手指觸控式螢幕幕,尚未鬆開或拖動,由1個ACTION_DOWN觸發。注意:和onDwon的區別,它強調的是沒有鬆開或拖動的狀態 OnGestureListener
onSingleTapUp 手指觸控式螢幕幕後鬆開,伴隨著1個ACTION_UP而觸發,這是單擊行為 OnGestureListener
onScroll 手指按下螢幕並拖動,由一個ACTION_DOWN和多個ACTION_MOVE觸發,這是拖動行為 OnGestureListener
onLongPress 使用者長久按著螢幕不放,即長按 OnGestureListener
onFling 使用者按下觸控式螢幕、快速滑動後鬆開,由1個ACTION_DOWN、多個ACTION_MOVE和1個ACTION_UP觸發,這是快速滑動行為 OnGestureListener
onDoubleTap 雙擊,有兩次連續的單擊組成,它不可能和 onSingleTapConfirmed共存 OnDoubleTapListener
onSingleTapConfirmed 嚴格的單擊行為。注意:它和onSingleTapUp的區別,如果觸發了onSingleTapConfirmed,那麼後面不可能在緊跟著另一個單擊行為,即這隻可能是單擊,而不可能是雙擊中的一次單擊 OnDoubleTapListener
onDoubleTapEvent 表示發生了雙擊行為,在雙擊的期間,ACTION_DWON、ACTION_MOVE和ACTION_UP都會觸發此回撥 OnDoubleTapListener

4.View的座標系

View 的位置主要由它的四個頂點來決定的,分別對應於 View 的四個屬性:top、left、right、bottom。其中: top是左上角縱座標,left是左上角橫座標,right是右下角橫座標,bottom是右下角縱座標。 需要注意的是:這些座標都是相當於 View 的父容器來說的,這是一種相對座標。View 的座標和父容器的關係如下: 這裡寫圖片描述 下面這張圖是 父 View移除屏幕後子 View 的getLef()的示意圖: 這裡寫圖片描述

4.1靜態座標系

View座標 引數含義
left = getLeft() View自身左側到父View左側的距離
top = getTop() View自身頂部到父View頂部的距離
right = getRight() View自身右側到父View左側的距離
botton = getBotton() View自身底部到父View頂部的距離
getTranslationX() View左上角相對父View的X軸偏移量
getTranslationY() View左上角相對父View的Y軸偏移量
getX() 其值為:getLeft()+getTranslationX(),當setTranslationX 時,getLeft()不會變,getX會變
getY() 其值為:getTop()+getTranslationY(),當setTranslationY 時,getTop()不會變,getY會變

MationEvent 觸控事件座標:

MotionEvent座標方法 引數含義
getX() 當前觸控點距離當前 View 自身左邊的距離
getY() 當前觸控點距離當前 View 自身頂部的距離
getRawX() 當前觸控點距離螢幕左邊的距離(Android絕對座標系)
getRawY() 當前觸控點距離螢幕頂部的距離(Android絕對座標系)

4.2 滑動座標系

關於View提供的與座標息息相關的另一組常用的重要方法就是滾動或者滑動相關的方法,為了實現 View 的滑動,View 提供了專門的方法來實現這個功能,那就是 scrollToscrollBy,我們先來看看這兩個方法的原始碼:

 /**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

/**
     * Move the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the amount of pixels to scroll by horizontally
     * @param y the amount of pixels to scroll by vertically
     */
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

從原始碼可以看出,scrollBy 實際也是呼叫了 scrollTo 方法,它實現了基於當前位置的相對滑動,而 scrollTo 則實現了基於所傳遞引數的絕對滑動。這裡我們要注意的是:我們要明白滑動過程中 View 內部的兩個屬性 mScrollXmScrollY 的改變規則,這兩個屬性可以通過 getScrollXgetScrollY 方法分別獲取到。簡單概括一下,在滑動過程中,mScrollX 的值總是等於 View 左邊緣View 內容左邊緣在水平方向的距離,而 mScrollY 的值總是等於View 上邊緣View 內容上邊緣在豎直方向的距離。View 邊緣是指 View 的位置,由四個頂點組成,而 View 內容邊緣是指 View 中的內容的邊緣。 scrollTo 和 scrollBy 只能改變 View 內容的位置而不能改變 View 在佈局中的位置。 mScrollX 和 mScrollY 單位是畫素,並且當 View 左邊緣在 View 內容左邊緣的右側是,mScrollX 為正值,反之為負值;當 View 上邊緣在 View 內容上邊緣的下邊時,mScrollY 為正值,反之為負值。換句話說,如果從左向右滑動,那麼 mScrollX 為負值,反之為正值;如果從上往下滑動,那麼 mScrollY 為負值,反之為正值。

下面我們先來看看關於滑動的座標系: 這裡寫圖片描述

用一張表格具體看看相關引數的含義:

View滑動座標 引數含義
offsetLeftAndRight(int offset) 水平方向挪動View,offset 為正則向x軸正向移動,移動的是整個View,getLeft()會變,自定義view很有用
offsetTopAndBottom(int offset) 垂直方向挪動View,offset為正則向y軸正向移動,移動的是整個View,getTop()會變,自定義View很有用
scrollTo(int x,int y) View內容(不是整個View)滑動到相應的位置,參考座標原點為ParentView左上角,x,y為正則向x,y軸反向移動,反之為負
scrollBy(int x,int y) 在scrollTo()的基礎上繼續滑動x,y
setScrollX(int value) 實質為scrollTo(),只是改變x軸滑動
setScrollY(int value) 實質為scrollTo(),只是改變y軸滑動
getScrollX()/getScrollY() 獲得當前滑動位置偏移量。分別對應著mScrollX,mScrollY。

5.View的滑動

在Android裝置上,滑動幾乎是應用的標配,不管是下拉重新整理還是 SlidingMenu ,它們的基礎都是滑動。從另一方面來說,Android 手機由於螢幕比較小,為了給使用者呈現更多內容,就需要使用滑動來隱藏和顯示一些內容。滑動在 Android 開發中具有很重要的作用,不管一些滑動效果多麼酷炫,歸根到底,它們都是由不同的滑動外加一些特效所組成的。在瞭解了 Android 座標系和觸控事件後,我們開看看如何使用系統提供的 API 來實現動態修改一個 View 的座標,即實現滑動效果。而不管採用哪種方式,是實現的思想基本是一致的,當觸控 View 時,系統記下當前觸控點座標;當手指移動時,系統記下移動後的觸控點座標,從而獲取到相對於前一次座標點的偏移量,並通過偏移量來修改 View 的座標,這樣不斷重複,從而實現滑動過程。

5.1 scrollTo與scrollBy

為了實現 View 的滑動,View 提供了專門的方法來實現這個功能,那就是 scrollTo 和 scrollBy 。實際上在 4.2滑動座標系 中大體介紹的了相關內容。這裡我們詳細說明一下。

  1. scrollTo(int x,int y): 如果偏移位置發生了改變,就會給 mScrollX 和 mScrollY 賦新值,mScrollX表示裡檢視起始位置的x水平放的偏移量(getScrollX()可以獲取到),mScrollY 表示離檢視起始位置的y垂直方向的偏移量(getScrollY()可以獲取到)。改變當前位置。

    注意:x , y 代表的不是座標點,而是偏移量

  2. scrollBy(int x,int y): 從原始碼中看出,它實際上是呼叫 了 scrollTo(mScrollX + x,mScrollY + y);

    注意:mScrollX + x 和 mScrollY + y ,即代表在原先偏移的基礎上再發生偏移,通俗的講就是相對我們當前位置的偏移。

scrollTo、scrollBy 方法移動的都是 View 的 content,即讓 View 的內容移動。如果在 ViewGroup 中使用 scrollTo 、scrollBy 方法,那麼移動的將是所有子 View ;但如果在 View 中使用,那麼移動的將是 View 的內容。例如TextView ,content 就是它的文字,ImageView,content 就是它的 drawable 物件。 如果我們想拖動整個 View,這兩個方法可以用嗎?其實我們可以在該 View 所在的 ViewGroup 中來呼叫 scrollTo和scrollBy 方法,已達到來移動 ViewGroup 子 View 的效果。程式碼如下:

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

讓自定義的 View 跟隨手指移動的核心程式碼為:

    // 分別記錄上次滑動的座標
    private int mLastX = 0;
    private int mLastY = 0;

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        int rawX = (int)event.getRawX();
        int rawY = (int)event.getRawY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.i("MotionEvent","MotionEvent.ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i("MotionEvent","MotionEvent.ACTION_MOVE");
                int offsetX = rawX - mLastX;
                int offsetY = rawY - mLastY;
                //View內容移動
                scrollBy(-offsetX,-offsetY);
                //View整體移動
                //((View) getParent()).scrollBy(offsetX, offsetY);
                break;
            case MotionEvent.ACTION_UP:
                Log.i("MotionEvent","MotionEvent.ACTION_UP");
                break;
        }
        mLastX = rawX;
        mLastY = rawY;
       return super.onTouchEvent(event);
    }

5.2 Scroller

使用scrollTo()scrollBy()來實現 View 的滑動的時候並不多,因為這兩個方法產生的滑動時不連貫的,跳躍的,最終的效果也不夠平滑。所以,Android提供了Scroller這個類來實現平滑的滑動。下面的也是讓自定義的 View 跟隨手指移動的核心程式碼為:

//初始化Scroller
Scroller scroller = new Scroller(context);

// 分別記錄上次滑動的座標
 private int mLastX = 0;
 private int mLastY = 0;

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        int rawX = (int)event.getRawX();
        int rawY = (int)event.getRawY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.i("MotionEvent","MotionEvent.ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i("MotionEvent","MotionEvent.ACTION_MOVE");
                int offsetX = rawX - mLastX;
                int offsetY = rawY - mLastY;

                smoothScrollTo(-offsetX,-offsetY);
                break;
            case MotionEvent.ACTION_UP:
                Log.i("MotionEvent","MotionEvent.ACTION_UP");
                break;
        }
        mLastX = rawX;
        mLastY = rawY;
       return super.onTouchEvent(event);
    }

 private void smoothScrollTo(int destX,int  destY){
        scroller.startScroll(0,0,destX,destY,1000);
        invalidate();
 }

 @Override
 public void computeScroll() {
        // 判斷Scroller是否執行完畢
        if(scroller.computeScrollOffset()){
             //使整個控制元件都滑動
            ((View)getParent()).scrollBy(scroller.getCurrX(),scroller.getCurrY());
            // 通過重繪來不斷呼叫computeScroll
            invalidate();
        }
 }

startScroll()來開啟平滑移動過程,前兩個引數的意思是起始座標,接下來的兩個座標是偏移量,最後一個是顯示的時長。Scroller類提供了 computeScrollOffset()方法來判斷是否完成了滑動,同時也提供了 getCurrX()getCurrY()來獲取當前滑動座標。需要注意的是,invalidate()方法,因為只能在computeScroll()方法中獲取滑動過程中的scrollX和scrollY的座標。但是computeScroll()方法是不會自動呼叫的,只能通過 invalidate() –>draw() –> computeScroll() 來間接呼叫computeScroll()方法,所以,需要在上述程式碼中呼叫 invalidate(),實現迴圈獲取 scrollX和scrollY的目的。當滑動結束後,scroller.computeScrollOffset()方法會返回false,從而實現整個平滑移動的過程。

5.3 layout

在View進行繪製時,會呼叫 onLayout()方法來設定顯示的位置。當熱,可以通過修改View的 left,top,right,bottom 四個屬性來控制View 的座標。在onTouchEvent()方法時呼叫onLayout()方法。下面的也是讓自定義的 View 跟隨手指移動的核心程式碼為:


// 分別記錄上次滑動的座標
 private int mLastX = 0;
 private int mLastY = 0;

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        int rawX = (int)event.getRawX();
        int rawY = (int)event.getRawY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.i("MotionEvent","MotionEvent.ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i("MotionEvent","MotionEvent.ACTION_MOVE");
                int offsetX = rawX - mLastX;
                int offsetY = rawY - mLastY;

                layout(getLeft()+offsetX,
                        getTop()+offsetY,
                        getRight()+offsetX,
                        getBottom()+offsetY);
                break;
            case MotionEvent.ACTION_UP:
                Log.i("MotionEvent","MotionEvent.ACTION_UP");
                break;
        }
        mLastX = rawX;
        mLastY = rawY;
       return super.onTouchEvent(event);
    }

5.4 offsetLeftAndRight 和 offsetTopAndBottom

這個方法相當於系統提供的一個對上下、左右移動的封裝。當計算出偏移量後,只要使用如下的自定義View 也可以跟隨手指移動:


// 分別記錄上次滑動的座標
 private int mLastX = 0;
 private int mLastY = 0;

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        int rawX = (int)event.getRawX();
        int rawY = (int)event.getRawY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.i("MotionEvent","MotionEvent.ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i("MotionEvent","MotionEvent.ACTION_MOVE");
                int offsetX = rawX - mLastX;
                int offsetY = rawY - mLastY;

                // 同時對left和right進行偏移
                offsetLeftAndRight(offsetX);
                // 同時對top和bottom進行偏移
                offsetTopAndBottom(offsetY);
                break;
            case MotionEvent.ACTION_UP:
                Log.i("MotionEvent","MotionEvent.ACTION_UP");
                break;
        }
        mLastX = rawX;
        mLastY = rawY;
       return super.onTouchEvent(event);
    }

5.5 LayoutParams

LayoutParams儲存了一個View的佈局引數。因此可以在程式中,通過改變LayoutParams來動態地修改一個佈局的位置引數,從而達到改變View位置的效果。我們可以很方便地在程式中使用getLayoutParams()來獲取一個View的LayouParams。當然,計算偏移量的方法與在Layout方法中計算offset也是一樣。當獲取到偏移量之後,就可以通過setLayoutParams來改變其LayoutParams,程式碼如下所示。

// 分別記錄上次滑動的座標
 private int mLastX = 0;
 private int mLastY = 0;

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        int rawX = (int)event.getRawX();
        int rawY = (int)event.getRawY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.i("MotionEvent","MotionEvent.ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i("MotionEvent","MotionEvent.ACTION_MOVE");
                int offsetX = rawX - mLastX;
                int offsetY = rawY - mLastY;

//ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) getLayoutParams();①
//ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();②

                LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft() + offsetX;
                layoutParams.topMargin = getTop() + offsetY;
                Log.i("MotionEvent","MotionEvent.ACTION_MOVE leftMargin:"+(getLeft() + offsetX));
                Log.i("MotionEvent","MotionEvent.ACTION_MOVE topMargin:"+(getTop() + offsetY));
                setLayoutParams(layoutParams);
                break;
            case MotionEvent.ACTION_UP:
                Log.i("MotionEvent","MotionEvent.ACTION_UP");
                break;
        }
        mLastX = rawX;
        mLastY = rawY;
       return super.onTouchEvent(event);
    }

通過getLayoutParams()獲取LayoutParams時,需要根據View所在父佈局的型別來設定不同的型別,比如這裡將View放在LinearLayout中,那麼就可以使用LinearLayout.LayoutParams。類似地,如果在RelativeLayout中,就要使用RelativeLayout.LayoutParams。當然,這一切的前提是你必須要有一個父佈局,不然系統不法獲取LayoutParams。不過這裡需要注意的是,①和②兩種方式的佈局不能移動自定義View。正在解決這個問題…..

5.6 其他方式

包括 使用動畫ViewDragHelper ,這兩種方式我會單獨再分開來將總結一下。