Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 開發藝術探索讀書筆記 第三章

Android 開發藝術探索讀書筆記 第三章

編輯:關於Android編程

View的事件體系

一 View的基礎知識

什麼是View
??View是android所有控件的基類,View是一種界面層控件的一種抽象,代表了一個控件。所有控件都是View或者ViewGroup的子類,ViewGroup也繼承了View,即所有控件都是View的子類。

View的位置參數
??View的位置主要由4個頂點來決定的,位置坐標都是相對坐標,相對於父容器。看下圖:
這裡寫圖片描述
上圖我們可以得出View的寬高:

width = right - left;
height = bottom - top;

獲取View的4個參數:

Left = getLeft(); Right = getRight(); Top = getTop(); Bottom = getBottom();

View新增了幾個參數:x、y、translationX和translationY,其中x和y是View左上角的坐標,而translationX和translationY是View左上角相對於父控件容器的偏移量,默認值為0,參數關系如下所示:

x = left + translationX;
y = right + translationY;

需要注意的地方:View在平移過程中,top和left表示的是原始左上角的位置信息,值不會發生改變,發生改變的是x、y、translationX和translationY。

MotionEvent和TouchSlop

MotionEvent
手指接觸屏幕後產生的一些列事件,有如下幾種:

ACTION_DOWN——手指剛接觸屏幕 ACTION_MOVE——手指在屏幕上移動 ACTION_UP——手指從屏幕上松開的一瞬間

通過MotionEvent對象我們可以得到點擊事件的x和y坐標:
getX/getY:返回當前View左上角的x和y坐標
getRawX/getRawY:返回的是相對於手機屏幕左上角的x和y坐標

TouchSlop
TouchSlop是系統所能識別的被認為是滑動的最小距離。我們可以通過ViewConfiguration.get(getContext()).getScaledTouchSlop() 獲取這個常量,這個常量在不同設備上值可能是不同的。我們可以在源碼中找到這個常量的定義,在frameworks/base/core/res/res/values/comfig.xml文件中。

VelocityTracker、GestureDetector和Scroller

VelocityTracker
速度追蹤,用於追蹤手指在滑動過程中的速度。包括水平和豎直方向的速度,使用過程很簡單,步驟如下:

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

我們想知道當前滑動速度時,可以采用如下方式獲得當前速度:

velocityTracker.computeCurrentVelocity(1000);//單位是ms
            int xVelocity = (int) velocityTracker.getXVelocity();
            int yVelocity = (int) velocityTracker.getYVelocity();

獲取速度之前必須先計算速度,即getXVelocity 和`getYVelocity 這兩個方法前面必須先調用computeCurrentVelocity 方法;速度是指一段時間內手指滑過的像素數,速度可以為負數,當手指從右向左即為負值。速度計算公式如下:

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

另外,computeCurrentVelocity 方法的參數表示的是一個時間單元或者說是時間間隔,計算速度得到的就是時間間隔內手指在水平或者豎直方向上所滑動的像素數。
如果不需要使用的時候,我們需要調用clear方法重置並回收內存。

 velocityTracker.clear();
 velocityTracker.recycle();

GestureDetector
手勢檢測,用戶輔助檢測用戶的單擊、滑動、長按、雙擊等行為。
手勢檢測一般需要創建一個GestureDetector對象並實現OnGestureListener接口,還可以實現OnDoubleTapListener 從而能夠監聽雙擊行為:

GestureDetector mGestureDetector = new GestureDetector(this);
            //解決長按屏幕後無法拖動的現象
            mGestureDetector.setIsLongpressEnabled(false);

我們然後接管目標view的onTouchEvent方法,在待監聽View的onTouchEvent方法中添加如下實現:

boolean consume = mGestureDetector.onTouchEvent(event);
return consume; 

我們可以選擇實現OnGestureListener和OnDoubleTapListener 接口,方法如下所示:

public class GestureListenerImpl implements GestureDetector.OnGestureListener,GestureDetector.OnDoubleTapListener {
//手指輕觸屏幕的一瞬間,有1個ACTION_DOWN觸發
@Override
public boolean onDown(MotionEvent e) {
    return false;
}

//手指輕觸屏幕的一瞬間,尚未松開或拖動,有1個ACTION_DOWN觸發
@Override
public void onShowPress(MotionEvent e) {

}

//手指(輕輕觸摸屏幕後)松開,伴隨著一個MotionEvent ACTION_UP而觸發,這是單擊行為
@Override
public boolean onSingleTapUp(MotionEvent e) {
    return false;
}

//手指按下屏幕並拖動,由一個ACTION_DOWN,多個ACTION_MOVE觸發,這是拖動行為
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    return false;
}

//用戶長久按著屏幕不放,長按
@Override
public void onLongPress(MotionEvent e) {

}

//用戶按下觸摸屏,快速滑動後松開,由1個ACTION_DOWN、多個ACTION_MOVE和1個ACTION_UP觸發,這是快速滑動行為
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    return false;
}

//嚴格的單擊行為,觸發了onSingleTapConfirmed,那麼後面不可能再緊跟著另一個單擊行為
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
    return false;
}

//雙擊,由2次連續的單擊組成,不能和onSingleTapConfirmed共存
@Override
public boolean onDoubleTap(MotionEvent e) {
    return false;
}

//表示發生了雙擊行為,在雙擊的期間,ACTION_DOWN、ACTION_MOVE、ACTION_UP都會觸發此回調
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
    return false;
}
}

方法很多,並不是都經常用到,我們如果只是監聽滑動相關的,建議直接在onTouchevent 中實現,如果監聽雙擊這種行為,那麼就使用GestureDetector 。

Scroller
彈性滑動對象,用於實現View的彈性滑動。Scroller本身無法讓View彈性滑動,它需要和View的computeScroll方法配合使用才能共同完成這個功能。具體用法在View的滑動裡面細講。

二 View的滑動

使用scrollTo/scrollBy

/**
 * 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方法,scrollBy()方法是讓View相對於當前的位置滾動某段距離,而scrollTo()方法則是讓View相對於初始的位置滾動某段距離,而初始位置是不變的,所以不改變參數的情況下,多次調用scrollTo()方法都將是滾動到同一個位置。在滑動過程中,scrollTo和scrollBy只能改變View內容的位置而不能改變View在布局中的位置,mScrollX 的值等於view左邊緣和view內容左邊緣在水平方向上的距離,mScrollY 同理,view邊緣指view的位置,內容邊緣指view在布局中的位置。mScrollX 和mScrollY 單位為像素,View左邊緣在View內容邊緣的右邊時,mScrollX 為正值,反之為負值,即如果從左向右滑動,那麼mScrollX 為負值,反之為正值,mScrollY 同理。

使用動畫
可以使用view動畫和屬性動畫來實現平移,平移就是一種滑動,相對簡單。

改變布局參數
改變布局參數即改變LayoutParams,我們想把一個Button向右平移100px,我們只需要將這個Button的marginLeft參數的值增加100px即可;還可以在Button的左邊放置一個空的view,view的默認寬度為0,當我們需要移動時,重新設置空view的寬度即可。

各種滑動方式對比
scrollTo/scrollBy滑動:操作簡單,適合對View內容的滑動 動畫:操作簡單,主要適用於沒有交互的view和實現復雜的動畫效果 改變布局參數: 操作稍微復雜,適用於交互的view

三 彈性滑動

使用Scroller
Scroller的典型用法比較固定,如下所示:

Scroller scroller = new Scroller(context);

public void smoothScrollTo(int destX, int destY) {
    int scrollX = getScrollX();
    int deltaX = destX - scrollX;
    //1000ms內滑向destX,效果就是慢慢滑動
    scroller.startScroll(scrollX, 0, deltaX, 0, 1000);
    invalidate();
}

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

下面我們看一下為什麼能夠實現滑動,看一下startScroll的原型:

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
    mMode = SCROLL_MODE;
    mFinished = false;
    mDuration = duration;
    mStartTime = AnimationUtils.currentAnimationTimeMillis();
    mStartX = startX;
    mStartY = startY;
    mFinalX = startX + dx;
    mFinalY = startY + dy;
    mDeltaX = dx;
    mDeltaY = dy;
    mDurationReciprocal = 1.0f / (float) mDuration;
}

其實Scroller內部什麼也沒有做,只是保存了我們傳遞的幾個參數。方法的參數含義如下:

startX和startY表示的是滑動的起點 dx和dy表示的是滑動的距離。 duration表示的是滑動過程完成所需要的時間

Scroller實現滑動的方法是startScroll方法下面的invalidate方法,invalidate方法會導致view重繪,在View的draw方法中又會調用computeScroll方法,computeScroll方法在view是一個空實現,因此需要我們自己去實現,上面示例代碼實現了computeScroll方法,view才能實現彈性滑動。當View重繪後會在draw方法中調用computeScroll,而computeScroll又會去向Scroller獲取當前的scrollX和scrollY,然後通過scrollTo方法實現滑動;接著調用postInvalidate方法進行第二次重繪,如此反復,直到滑動過程結束。

我們再看一下computeScrollOffset方法的實現,如下所示

public boolean computeScrollOffset() {
    ...
    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

    if (timePassed < mDuration) {
        switch (mMode) {
            case SCROLL_MODE:
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            ...
        }
    }
    return true;
}

我們不去關心這個具體過程,這個方法主要根據時間的流逝來計算出當前的scrollX和scrollY,通過時間流逝的百分比計算出當前的值。返回true表示滑動還未結束,false則表示滑動已經結束。

通過動畫
動畫本身就是一種漸進的過程,通過它本身實現的滑動本身就具有彈性 效果,但是這裡我們使用動畫的特性來實現,采用如下方式實現:

final int startX = 0;
            final int deltaX = 100;
            ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float fraction = animator.getAnimatedFraction();
                    mButton.scrollTo(startX + (int)(deltaX * fraction), 0);
                }
            });
            animator.start();

上面我們的動畫本質沒有作用於任何對象上,只是在1000ms內完成了整個動畫過程,這裡滑動的是View的內容而非本身,我們完全可以在onAnimationUpdate 方法中加入我們想要的其它操作。

使用延時策略
通過發送一系列延時消息從而達到一種漸進式的效果,可以用Handler或View的postDelayed方法,也可以使用線程的sleep方法,對於postDelayed方法來說,通過延時發送消息,然後在消息中進行View滑動,接連不斷的發送這種延時消息,就可以實現彈性滑動的效果。

彈性滑動還有其它方式實現,更多的是實現思想,實際中我們可以對其靈活擴展從而實現更多復雜的效果。

四 View的事件分發機制

點擊事件的傳遞規則
點擊事件的分發實際上就是對MotionEvent 事件的分發過程,系統需要把這個事件傳遞給一個具體的View,而這個傳遞的過程就是分發過程,點擊事件的分發過程有三個重要的方法:

public boolean dispatchTouchEvent(MotionEvent ev)
用來進行事件的分發。如果事件能夠傳遞給當前View,那麼此方法一定會被調用,返回結果受當前的View的onTouchevent和下級的dispatchTouchEvent方法的影響,表示是否消耗當前事件。

public boolean onInterceptTouchEvent(MotionEvent ev)
用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那麼在同一個事件的序列中,此方法不會被再次調用,返回值表示是否攔截當前事件。

public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前View無法再次接受到事件。

上述方法之間的關系可以用下面的偽代碼來描述:

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev);
    } else {
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

通過上面的偽代碼,我們可以大致的了解點擊事件的傳遞規則:
對於一個根ViewGroup來說,點擊事件產生後,首先會傳遞給它,首先調用dispatchTouchEvent ,如果它的onInterceptTouchEvent 返回true就是攔截當前事件,事件交給ViewGroup處理;如果返回false,表示不攔截當前事件,繼續傳遞給它的子元素,接著子元素的dispatchTouchEvent 就會被調用,如此反復直到事件結束。

當一個View需要處理事件時,如果設置了OnTouchListener ,那麼OnTouchListener 中的onTouch方法會被回調。事件如何處理還要看onTouch 的返回值,如果返回false,則當前View的onTouchEvent 會被調用;如果返回true,那麼onTouchEvent 將不會被調用。

點擊事件產生後,它的傳遞過程遵循如下順序:

Activity -> Window-> View

事件總是先傳遞給Activity,Activity在傳遞給Window,最後Window再傳遞給頂級View,頂級View接收事件後,就按照事件分發機制去分發事件。如果一個View的onTouchevent返回false,那麼它的父容器onTouchevent會被調用,一次類推,如果所有元素都不處理這個事件,最終將會傳遞到Activity去處理,即Activity的onTouchevent會被調用。

關於事件傳遞機制的常見一些結論:

正常情況下,一個事件序列只能被一個View攔截並且消耗。一個事件序列中的事件不能分別由兩個View同時處理,但是一個View可以將本該自己處理的是將通過onTouchEvent強行傳遞給其它View處理。 一個View一旦決定攔截,那麼一個事件序列都只能由它來處理,並且它的onInterceptTouchEvent不會再次調用。 某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件(onTouchevent返回了false),那麼同一事件序列中的其它事件都不會再交給它來處理,並且事件將交由它的父元素去處理。 如果View不消耗除ACTION_DOWN意外的其他事件,那麼這個點擊事件會消失,此時父元素的onTouchevent並不會被調用,並且當前View可以持續收到後續的事件,最終這些消失的點擊事件會傳遞給Activity處理。 ViewGroup默認不攔截任何事件,View沒有onInterceptTouchEvent方法。 View的onTouchevent默認都會消耗事件,除非它是不可點擊的(clickable和longClickable同時為false)。View的longClickable都為false,而clickable則分情況,一般Button默認為true,TextView默認為false。 View的enable屬性不影響onTouchEvent的默認返回值。 onClick()會發生的前提是View是可以點擊的,並且收到了down和up的事件。 事件傳遞過程由外向內的,即事件總是先傳遞給父元素,然後再由父元素分發給子View,通過requestDisallowInterceptTouchEvent 方法可以再子元素中干預父元素的事件分發過程,但是ACTION_DOWN除外。

以上結論我們都可以在源碼分析部分得到解釋。

事件分發的源碼分析
源碼解析我會另寫一篇,結合書中還有一些博客分析源碼,到時候整理發出來。

五 View的滑動沖突

常見的滑動沖突場景

外部滑動方向和內部滑動方向不一致,例如Viewpager中的頁面包含listview 外部滑動方向和內部滑動方向一致,例如Viewpager的某一個頁面含有banner圖。 上面兩種情況的嵌套,例如外部有一個SlideMenu,內部有一個ViewPager,Viewpager的每一個頁面又是一個listView。

滑動沖突的處理規則
滑動沖突的一般處理規則根據滑動路徑和水平方向所形成的夾角、距離差、速度差等來判斷,還有就是結合具體的業務需求來得出相應的處理規則。

滑動沖突的解決方式

外部攔截法
點擊事件都要經過父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要此事件就不攔截,需要重寫父容器的onInterceptTouchEvent 方法,偽代碼如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    int x = (int) ev.getX();
    int y = (int) ev.getY();
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            intercepted = false;
            break;

        case MotionEvent.ACTION_MOVE:
            if(父容器需要當前點擊事件){
                intercepted = true;
            }else{
                intercepted = false;
            }
            break;

        case MotionEvent.ACTION_UP:
            intercepted = false;
            break;
    }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
}

內部攔截法
內部攔截法是指父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要子事件就直接消耗掉,否則直接交由父容器進行處理。需要配合requestDisallowInterceptTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: 
        getParent().requestDisallowInterceptTouchEvent(true);
        break;

    case MotionEvent.ACTION_MOVE: 
        int deltaX = x - mLastX;
        int deltaY = y - mLastY;
        if (當前view需要攔截當前點擊事件的條件) {
            getParent().requestDisallowInterceptTouchEvent(false);
        }
        break;

    case MotionEvent.ACTION_UP: 
        break;
}

mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}

OK,第三章除了源碼分析沒寫,基本上結束。

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