1. 程式人生 > >View的事件體系-View的滑動

View的事件體系-View的滑動

上節介紹了View的一些基礎知識和概念,本節開始介紹一個很重要的內容:View的滑動。在Android裝置上,滑動幾乎是應用的標配,不管是下拉重新整理還是SlidingMenu,它們的基礎都是滑動。從另一方面來說,Android手機由於螢幕較小,為了給客戶呈現更多的內容,就需要滑動來隱藏和顯示一些內容。基於上述兩點,可以知道,滑動再Android開發中具有很重要的作用,不管一些滑動的效果多麼絢麗,歸根結底,它們都是由不同的滑動外加一些特效所組成的。因此,掌握滑動的方法是實現絢麗的自定義控制元件的基礎。通過三種方式可以實現View的滑動:第一種是通過View本身提供的scrollTo/scrollBy方法來實現滑動;第二種是通過動畫給View施加平移效果來實現滑動;第三種是改變View的LayoutParams使得View重新佈局從而實現滑動。從目前來看,廠家的滑動方式就這麼三種,下面一一進行分析。

1.使用scrollTo/scrollBy

為了實現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則實現了基於所傳遞引數的絕對滑動,這個不難理解。利用scrollTo和scrollBy來實現View的滑動,這不是一件困難的事,但是我們要明白滑動過程中View內部的兩個屬性mScrollX和mScrollY的改變規則,這兩個屬性可以通過getScrollX和getScrollY方法分別得到。這裡先簡要概況一下:再滑動過程中,mScrollX的指總是等於View的左邊緣和View內容左邊緣在水平方向的鉅鹿,而mScrollY的值總是等於View的上邊緣和View內容上邊緣在豎直方向的距離。View邊緣時指View的位置,由四個頂點組成,而View內容邊緣是指View中的內容的邊緣,scrollTo和scrollBy只能改變View內容的位置而不能改變View在佈局中的位置。mScrollX和mScrollY的單位為畫素,並且當View左邊緣在View內容左邊緣的右邊時,mScroll為正值,反之為負值;當View的上邊緣在View內容上邊緣的下班時,mScrollY為正值,反之為負值;換句話說,如果從左向右滑動,那麼mScrollX為負值,反之為正值;如果從上往下滑動,那麼mScrollY為負值,反之為正值。
為了更好地理解這個問題,下面舉個例子,如圖所示。在途中假設水平和豎直方向的滑動距離都為100畫素,針對途中各種清空,都給出了對應的mScrollX和mScrollY的值。根據上面的分析,可以知道,使用scrollTo和scrollBy來實現View的滑動,只能將View的內容進行移動,並不能將View本身進行移動,也就是說,不管怎麼滑動,也不能將當前View滑動到附件View所在的區域,這個需要仔細體會一下。

2.使用動畫

上面一屆介紹了採用scrollTo/scrollBy來實現View的滑動,本節介紹另外一種滑動方式,即使用動畫,通過動畫,我們能夠讓一個View進行平移,而平移就是一種滑動。使用動畫來移動View,主要時操作View的translationX和translationY屬性,即可以採用傳統的View動畫,也可以採用屬性動畫,如果採用屬性動畫的話
,為了能夠相容3.0一下的版本,需要採用開源動畫庫nineoldandroids(http://nineoldandroids.com/)。
採用View動畫的程式碼,如下所示。此動畫將一個View從原始位置向右下角移動100個畫素。
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true">
    <translate android:fromXDelta="0"
        android:duration="100"
        android:fromYDelta="0"
        android:toXDelta="100"
        android:toYDelta="100"/>
</set>

如果採用屬性動畫的話,就更簡單了,以下程式碼可以將一個View在100ms內從原始位置向右平移100畫素。
ObjectAnimator.ofFloat(targetView,"translationX", 0,100).setDuration(100).start();
上面介紹了通過動畫來移動View的方法,關於動畫會在第5章中進行詳細的說明。使用動畫來做View的滑動需要注意一點,View動畫時對View的影像左操作,它並不能真正改變View的位置引數,包括寬/高,並且如果希望動畫後的狀態得以保留還必須將fillAfter屬性設定為true,否則動畫完成後啟動話結果會消失。比如我們要把View向右移動100畫素,如果fillAfter為false,那麼在動畫完成的一瞬間,View會瞬間恢復到動畫前的狀態;如果fillAfter為true,在動畫完成後,View會停留在據原始位置100畫素的右邊。使用屬性動畫並不存在上述問題,但是在Android3.0以下無法使用屬性動畫,這個時候我們可以使用動畫相容庫nineoldandroids來實現動畫,儘管如此,在Android3.0以下的手機上通過nineoldandroids來實現的屬性動畫本質上仍然時View動畫。上面提到View動畫並不能真正改變View的位置,這會帶來一個很嚴重的問題。試想以下,比如我們通過View動畫將一個Button向右移動100px,並且這個View設定的有單擊事件,然後你會驚奇地發現,單擊新位置無法觸發onClick事件,而單擊原始位置仍然可以觸發onClick事件,儘管Button一屆不再原始位置了。這個問題帶來地影響時致命地,但它又是可以理解地,因為不管Button怎麼左變換,但是它的位置資訊(四個頂點和寬/高)並不會隨著動畫而改變,因此在系統眼裡,這個Button並沒有發生任何改變,它的真身仍然在原始位置。在這種情況下,單擊新位置當然不會觸發onClick事件了,因為Button的真身並沒有發生改變,在新位置上只是View的影像而已。基於這一點,我們不能簡單地給一個View做平移動畫並且還希望它在新位置繼續觸發一些單擊事件。從Android3.0開始,使用屬性動畫可以解決上面的問題,但是大多數應用都需要相容到Android2.2,在Android2.2上無法使用屬性動畫,因此這裡還是會有問題。那麼這種問題難道就無法解決了嗎?也不是的,雖然不能直接的解決這個問題,但是可以間接解決這個問題,這裡給出一個簡單的解決辦法。針對上面View動畫的問題,我們可以在新位置預先建立一個和目標Button一摸一樣的Button,它們不但外觀一樣連onClick事件也一樣。當目標Button完成平移動畫後,就把目標Button隱藏,同時把預先建立的Button顯示出來,通過這種間接的方式我們解決了上面的問題。這僅僅是個參考,面對這種問題時,可以靈活應對。

3.改變佈局引數

本節介紹第三者實現View滑動的方法,那就是改變佈局引數,即改變LayoutParams。這個比較好理解了,比如我們想把一個Button向右平移100px,我們只需要將這個Button的LayoutParams裡的marginLeft引數的值增加100px即可,是不是很簡單呢?還有一種清空,為了達到移動Button的目的,我們可以在Button的左邊放一個空的View,這個View的預設寬度為0,當我們需要向右移動Button時,只需要重新設定空View的寬度介面,當空View的寬度增大時(假設Button的父容器時水平方向的LinearLayout),Button就自動被擠向右邊,及實現了向右平移的效果。如何重新設定一個View的LayoutParams呢?很簡單,如下所示。
        ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) targetView.getLayoutParams();
        params.width += 100;
        params.leftMargin += 100;
        targetView.requestLayout();
        //或者targetView.setLayoutParams(params);

通過改變LayoutParams的方式去實現View的滑動同樣是一種很靈活的方法,需要根據不同清空去做不同的處理。

4.各種滑動方式的對比

上面分別介紹了三種不同的滑動方式,它們都能實現View的滑動,那麼它們之間的差別時什麼呢?先看看scrollTo/scrollBy這種方式,他是View提供的原生方法,其作用時專門用於View的滑動,它可以比較方便地實現滑動效果並且不影響內部元素的單擊事件。但是它的缺點也是很顯然的;它只能滑動View的內容,並不能滑動View本身。再看動畫,通過動畫來實現的View的滑動,這個要分情況。如果時3.0以上並採用屬性動畫,那麼採用這種方式沒有明顯的缺點;如果是使用View動畫或者在3.0以下使用屬性動畫,均不能改變View本身的屬性。在實際中,如果動畫元素不需要響應使用者的互動,那麼使用動畫來做滑動比較合適,否則就不太合適。但是動畫有一個明顯的有點,那就是一些複雜的效果必須要通過動畫才能實現。最後再看一下改變佈局這種方式,它除了使用起來麻煩點外,也沒有明顯的缺點,它主要使用物件是一些互動性的View,因為這些View需要和使用者互動,直接通過動畫去實現會有問題,這在2節中已經有所介紹,所以這個時候我們可以使用直接改變佈局引數的方式去實現。針對上面的分析做一下總結,如下所示。scrollTo/scrollBy:操作簡單,適合對View的內容的滑動。動畫:操作簡單,主要適用於沒有互動的View和實現複雜的動畫效果;改變佈局引數:操作稍微複雜,適用於有互動的View。下面我們實現一個跟手滑的效果,這是一個自定義View,拖動它可以讓他在整個螢幕上隨意滑動。這個View實現起來很簡單,我們只要重寫它的onTouchEvent方法並出路ACTION_MOVE事件,根據兩個滑動之間的鉅鹿就可以實現它的滑動了。為了實現全平滑到,我們採用動畫的方式來實現。原因很簡單,這個效果無法採用scrollTo來實現。另外,它還可以採用改變佈局的方式來實現,這裡僅僅為了演示,所以就選擇了動畫的方式,核心程式碼如下。
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int)event.getRawX();
        int y = (int)event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:{
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                int translationX = (int) ViewHelper.getTranslationX(this) + deltaX;
                int translationY = (int) ViewHelper.getTranslationY(this) + deltaY;
                Log.d(TAG, "MOVE, deltaX:" + deltaX + " deltaY:" + deltaY);
                ViewHelper.setTranslationX(this, translationX);
                ViewHelper.setTranslationY(this, translationY);
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }

        }

        mLastX = x;
        mLastY = y;
        return true;
    }

通過上述程式碼可以看出,這一全屏滑動的效果實現起來相當簡單。首先我們通過getRawX和getRawY方法來獲取手指當前的座標,注意不能使用getX和getY方法,因為這個是要全屏滑動的,所以需要獲取當前點選事件在螢幕中的座標而不是相對於View本身的座標;其次,我們要得到兩次滑動之間的位移,有了這個位於就可以移動當前的View,移動方法採用的是動畫相容庫nineoldandroids中的ViewHelper類所提供的setTranslationX和setTransLationY方法。實際上ViewHelper類提供了一些了get/set方法,因為View的setTranslationX和setTransLationY方法只能在3.0及其以上的版本才能使用,但ViewHelper所提供的方法是沒有版本要求的,於此類似的還有setX、setScaleX、setAlpha等方法,這一系列方法實際上是為屬性動畫服務的,更為詳細的內容會在第5章進一步介紹。這個自定義View可以在2.x及其以上版本工作,但是由於動畫的性值,如果給它加上onClick事件,那麼在3.0以下版本它將無法在新位置響應使用者的點選,這個問題在簽名已經提到過。