Android View的事件體系(上)

本章將介紹Android中十分重要的一個概念View

文章目錄:

View.png

3.1 View的基礎(chǔ)知識

1. 什么是View :

內(nèi)容 含義
View 一個單一的控件,如Button、TextView
ViewGroup 一個控件組,如RelativeLayout、LinearLayout

View的視圖結(jié)構(gòu):


View的視圖結(jié)構(gòu)
  • View的位置參數(shù):
    View的寬高和坐標的關(guān)系

View的寬高和坐標的關(guān)系:

width = right - left
height = bottom - top

位置獲取方式

View的位置是通過view.getxxx()函數(shù)進行獲取:(以Top為例)
// 獲取Top位置

public final int getTop() {  
    return mTop;  
}  

// 其余如下:
  getLeft();      //獲取子View左上角距父View左側(cè)的距離
  getBottom();    //獲取子View右下角距父View頂部的距離
  getRight();     //獲取子View右下角距父View左側(cè)的距離

2. MotionEvent和TouchSlop

  • MotionEvent
    在手指接觸屏幕后所產(chǎn)生的事件:
常量 含義
ACTION_DOWN 手指接觸屏幕(按下)
ACTION_MOVE 手指在屏幕上移動(滑動)
ACTION_UP 手指從屏幕上松開的一瞬間(離開)

上述三種情況是典型的時間序列,同時通過 MontionEvent 對象我們可以得到點擊事件發(fā)生的 x 和 y 坐標。系統(tǒng)提供兩組方法:getX / getY 和 getRawX / getRawY。

get() 和 getRaw() 的區(qū)別

具體代碼:

@Override
    public boolean onTouchEvent(MotionEvent event) {

        //get() :觸摸點相對于其所在組件坐標系的坐標
        event.getX();
        event.getY();

        //getRaw() :觸摸點相對于屏幕默認坐標系的坐標
        event.getRawX();
        event.getRawY();
        return super.onTouchEvent(event);
    }
  • TouchSlop
    TouchSlop 是系統(tǒng)所能識別出的被認為是滑動的最小距離。換句話說,當手指在屏幕上滑動時,如果兩次滑動之間的距離小于這個常量,那么系統(tǒng)就不認為你是在進行滑動操作。原因很簡單滑動的距離太短,系統(tǒng)不認為是滑動。這是一個常量和設(shè)備有關(guān),不同的設(shè)備上這個值可能會不同。獲取方式:
     ViewConfiguration.get(this).getScaledTouchSlop(); 

4. VelocityTracher、GestureDetector和Scroller

  • VelocityTracher
    速度追蹤,用于追蹤手指在滑動過程中的速度,包括水平方向和豎直方向的速度。
 @Override
    public boolean onTouchEvent(MotionEvent event) {

        VelocityTracker velocityTracker = VelocityTracker.obtain();
        velocityTracker.addMovement(event);
        velocityTracker.computeCurrentVelocity(1000);//設(shè)置時間間隔為1000
        int xVelocity = (int) velocityTracker.getXVelocity();
        int yVelocity = (int) velocityTracker.getYVelocity();
        Log.e("event", "xVelocity ;" + xVelocity + "  yVelocity ;" + yVelocity);
        //當我們結(jié)束的時候,需要調(diào)用 clear 方法來重置并且回收內(nèi)存。
        velocityTracker.clear();
        velocityTracker.recycle();
        return super.onTouchEvent(event);
    }

這里需要注意,第一點,獲取速度之前必須先計算速度,即 getXVelocity() 和 getYVelocity() 必須在 computeCurrentVelocity 的后面,第二點,這里的速度是指一段時間內(nèi)手指所劃過的像素數(shù),比如將時間間隔設(shè)置有 1000ms 時,在1s 內(nèi)手指在水平方向劃過100像素,那么水平速度就是 100 ,當手指從右向左滑動時,速度為負數(shù),公式:
<div align = center>速度 = (終點位置 - 起始位置)/ 時間段</div>
不要管時間間隔是傳統(tǒng)含義,這里只要根據(jù)公式來計算即可。

  • GestureDetector
    手勢檢測,用于輔助檢測用戶的單擊、滑動、長按、雙擊等行為。首先創(chuàng)建一個MianActivity 實現(xiàn)GestureDetector.OnGestureListener ,OnDoubleTapListener接口 ,
public class MainActivity extends AppCompatActivity implements GestureDetector.OnGestureListener,
       GestureDetector.OnDoubleTapListener{
private GestureDetector mGestureDetector;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mGestureDetector = new GestureDetector(this,this);
        //解決長按屏幕后無法拖動的現(xiàn)象
        mGestureDetector.setIsLongpressEnabled(false);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {return mGestureDetector.onTouchEvent(event);}
    //手指輕輕觸摸屏幕的一瞬間,由一個 ACTION_DOWN 觸發(fā)。
    @Override
    public boolean onDown(MotionEvent e) { return false;}
    //手指輕輕觸摸屏幕,尚未松開或拖動,由一個 ACTION_DOWN 觸發(fā)。*注意和 onDown 的區(qū)別,它強調(diào)的是沒有松開或者拖動的狀態(tài)*
    @Override
    public void onShowPress(MotionEvent e) {}
    //手指(輕輕觸摸屏幕后)松開,伴隨著一個 MontionEvent ACTION_UP 而觸發(fā),這是單擊行為。
    @Override
    public boolean onSingleTapUp(MotionEvent e) {return false; }
    //手指按下屏幕并拖動,由 1 個 ACTION_DOWN,多個 ACTION_MOVE 觸發(fā),這是拖動行為。
    @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 和 ACTION_UP 觸發(fā),這是快速滑動行為
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; }
    //雙擊,由 2 次聯(lián)系的單擊組成,它不可能和 onSingleTapConfirmed 共存。
    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {return false; }
    //嚴格的單擊行為
    //*注意它和 onSingleTapUp的區(qū)別,如果觸發(fā)了onSingleTapConfirmed,那么后面不可能再緊跟著另一個單擊行為,即這只可能是單擊,而不可能是雙擊中的一次單擊*
    @Override
    public boolean onDoubleTap(MotionEvent e) {return false;}
    //表示發(fā)生了雙擊行為,在雙擊的期間,ACTION_DOWN 、ACTION_MOVE 、ACTION_UP 都會觸發(fā)此回調(diào)。 
    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {return false;}
}

這點可能會奇怪 setIsLongpressEnabled(false)參數(shù)要為false,經(jīng)過我測試,setIsLongpressEnabled(true)的時候 ,長按屏幕觸發(fā) onLongPress 會直接攔截掉其他的觸摸
down、move、up 事件,為 false 的時候,onLongPress 則不會觸發(fā),其他正常。

方法很多,但是并不是所有的方法都會被時常用到,在日常開發(fā)中,比較常用的onSingleTapUp(單擊),onFling(快速滑動),onScroll(推動),onLongPress(長按)和onDoubleTap(雙擊),另外要說明的是,在實際開發(fā)中可以不使用 GestureDetector,完全可以自己在view中的onTouchEvent中去實現(xiàn)。

  • Scroller
    彈性的滑動對象,用于實現(xiàn)View的彈性滑動。

3.2 View 的滑動

常見的的三種 View 滑動實現(xiàn)方式:

  • 通過 View 本身提供的 scrollTo / scrollBy 方法來實現(xiàn)滑動
  • 通過動畫給 View 施加平移想過
  • 通過改變 View 的LayoutParams 使得 View 重新布局從而實現(xiàn)滑動。
3.2.1 使用 scrollTo/scrollBy

  為了實現(xiàn) View 的滑動, View 提供了 scrollTo 和 scrollBy,如下所示。

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

    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

從上面的源碼可以看出,scrollBy 實際上也是調(diào)用了scrollTo 方法,我們需要知道 scrollTo 智能改變 View 內(nèi)容的位置而不能改變 View 在布局中的位置。mScrollX 和 mScrollY 單位為像素。

變換規(guī)律示意圖(單位:像素)

完整代碼地址
列出部分代碼:

public class ScollerView extends View implements View.OnClickListener {
...
 public ScollerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
        this.setOnClickListener(this);
}
private void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int delta = destX - scrollX;
        int scrollY = getScrollY();
        int deltY = scrollY + destY;
        //100ms 內(nèi)滑向 destX ,效果就是慢慢滑動
        mScroller.startScroll(scrollX, 0, delta, deltY, 1000);
        invalidate();
    }

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

3.2.2 使用動畫

使用動畫來移動 View ,主要操作 View 的 translatuonX 和 translationY 屬性,既可以采用傳統(tǒng)的 View 動畫,也可以采用屬性動畫。書中的3.0兼容就不介紹了,現(xiàn)在也基本用不到。
  采用 View 動畫的代碼,如下所示,此動畫可以在100ms 內(nèi)將一個 View 從原來位置像右下角移動 100 個像素。

GIF.gif

在 res 目錄中創(chuàng)建一個 anim 目錄,在新建一個 set :

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true" android:zAdjustment="normal"
    >
    <translate
        android:duration="100"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXDelta="100"
        android:toYDelta="100"/>
</set>

在 Activity 中使用改方法,就可以移動mButton

  ObjectAnimator.ofFloat(mButton,"translationX",0,100).setDuration(2000).start();

View 動畫是對 View 的影像做操作,它并不能真正改變 View 的位置參數(shù),包括寬高。并且如果希望動畫后的狀態(tài)得以保存還必須將 fillAfter 設(shè)為 true,為 false 時動畫結(jié)束后 View 會恢復(fù)原狀。

3.2.3 改變布局參數(shù)

第三種實現(xiàn) View 滑動的方法,那就是改變布局參數(shù),即改變 LayoutParams。改變 View 的位置,將LayoutParams 中的位置關(guān)系設(shè)置一下即可。實現(xiàn)方法很簡單:

        ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) 
        mButton.getLayoutParams();
        params.width += 100;
        params.leftMargin += 100;
        mButton.requestLayout();
       //或者mButton.setLayoutParams(params);

3.2.4 各種滑動方式的對比

  • scrollTo/scrollBy:操作簡單,適合對 View 內(nèi)容的滑動;
  • 動畫:操作簡單,主要適用于沒有交互的 View 和實現(xiàn)復(fù)雜的動畫效果;
  • 改變布局參數(shù):操作稍微復(fù)雜,適用于有交互的 View;
GIF.gif

GIF.gif

自定義一個 View 繼承 Button:

...
@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) (getTranslationX() + deltaX);
                int translationY = (int) (getTranslationY() + deltaY);
                setTranslationX(translationX);
                setTranslationY(translationY);
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
![GIF.gif](http://upload-images.jianshu.io/upload_images/5531940-42d068be5205a447.gif?imageMogr2/auto-orient/strip)

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

通過上述代碼可以看出,這一全屏滑動的效果實現(xiàn)起來相當簡單。首先我們通過 getRawX 和 getRawY 方法來獲取手指當前的坐標,注意這里不能使用 getX 和 getY 方法,getRawX 是獲取全屏坐標,getX 是獲取 View 的相對坐標(前面有講到)。

3.3 彈性滑動

知道了 View 的滑動,還要知道如何實現(xiàn) View 的彈性滑動。

3.3.1 使用 Scroller

之前使用過Scroller,現(xiàn)在來分析一下它的源碼,探究一下為什么它能實現(xiàn) View 的彈性滑動。

private void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int delta = destX - scrollX;
        int scrollY = getScrollY();
        int deltY = scrollY + destY;
        //100ms 內(nèi)滑向 destX ,效果就是慢慢滑動
        mScroller.startScroll(scrollX, 0, delta, deltY, 1000);
        invalidate();
    }

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

上面是 Scroller 的典型的使用方法,這里先描述它的工作原理:當我們構(gòu)造一個Scroller 對象并且調(diào)用它的 startScroll 方法時,Scroller 內(nèi)部其實上面也沒做,它只是保存了,我們傳遞的幾個參數(shù)。這幾個參數(shù)從 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;
    }

這個方法的參數(shù)含義很清楚, startX 和 startY 表示的是滑動的七點,dx 和 dy 表示的是要滑動的距離,而 duration 表示的是滑動時間,即整個滑動過程完成所需要的時間,注意這里的滑動是指 View 滑動內(nèi)容的滑動而非 View 本身的位置的改變,可以看到僅僅是調(diào)用 startScroll 方法是無法讓 View 滑動的,因為它內(nèi)部并沒有做滑動相關(guān)的事,那么 Scroller 到底是如何讓 View 彈性滑動的呢 ?答案就是 startScroll 方法下面的 invalidate 方法,雖然有點不可思議,但是的確是這樣的。invalidate 方法會導(dǎo)致 View 重繪, 在 View 的 draw 方法中又會去調(diào)用 computeScroll 方法, computeScroll 方法在View 中是一個空實現(xiàn),因此需要我們自己去實現(xiàn),方面的代碼已經(jīng)實現(xiàn)了 computeScroll 方法。正是因為這個 computeScroll 方法,View 才能實現(xiàn)彈性滑動。這看起來還是很抽象,其實是這樣的:當 View 重繪后在 draw 方法中調(diào)用 computeScroll ,而 computeScroll 又回去向 Scroller 獲取當前的 scrollX 和 scrollY,然后通過 scrollTo 方法實現(xiàn)滑動;接著又調(diào)用 postInvalidate 方法來進行第二次重繪,這一次重繪的過程和第一次重繪一樣,還是會導(dǎo)致 computeScroll 方法被調(diào)用了;然后繼續(xù)向 Scroller 獲取當前的 scrollX 和 scrollY。并通過 scrollTo 方法滑動到新的位置,如此反復(fù),知道整個滑動的過程結(jié)束。
  我們再看一下 Scroller 的 computeScrollOffset 方法的實現(xiàn),如下所示:

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

是不是突然就明白了?這個方法會根據(jù)時間的流逝來計算當前的scrollX和Y的值,計算方法也很簡單,大意就是根據(jù)時間流逝的百分比來計算scrollX和Y改變的百分比并計算出當前的值,這個過程相當于動畫的插值器的概念,這里我們先不去深究這個具體的過程,這個方法的返回值也很重要,他返回true表示滑動還未結(jié)束,false表示結(jié)束,因此這個方法返回true的時候,我們繼續(xù)讓View滑動

通過上面的分析,我相信大家應(yīng)該都已經(jīng)明白了Scroller的滑動原理了,這里做一個概括,他本身并不會滑動,需要配合computeScroll方法才能完成彈性滑動的效果,不斷的讓View重繪,而每次都有一些時間間隔,通過這個事件間隔就能得到他的滑動位置,這樣就可以用ScrollTo方法來完成View的滑動了,就這樣,View的每一次重繪都會導(dǎo)致View進行小幅度的滑動,而多次的小幅度滑動形成了彈性滑動,整個過程他對于View沒有絲毫的引用,甚至在他內(nèi)部連計時器都沒有。

3.3.2 通過動畫

一位大神的 View 系列 傳送門:http://blog.csdn.net/harvic880925/article/details/50995268
  動畫本身就是一種漸進的過程,因此通過它來實現(xiàn)的滑動天然就具有彈性效果。

GIF.gif

代碼:

ValueAnimator animator = ValueAnimator.ofInt(0, 10, 60, 200).setDuration(2000);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float fraction = animation.getAnimatedFraction();
                int animatedValue = (int) animation.getAnimatedValue();
                mButton.scrollTo(animatedValue, 0);//根據(jù)動畫本身移動200px
                mButton2.scrollTo((int) (fraction * 100), 0);//自定義移動100px
            }
        });
        animator.start();

在上述代碼中,mButton 動畫移動距離 200,我們的動畫本質(zhì)上沒有作用于任何對象上,只是在 2000ms 內(nèi)完成了整個動畫過程。利用這一特性,我們就可以在動畫的每一幀到來時獲取動畫完成的比例 fraction ,然后再根據(jù)這個比例計算出當前 View 所要滑動的距離。mButton2 通過改變百分比 fraction 來完成 View 的滑動,采用這種方法除了能夠完成彈性滑動以外,還可以實現(xiàn)其他動畫效果,我們完全可以在 onAnimationUpdate 方法中加上我們的其他操作。

3.3.3 使用延時策略

另一種實現(xiàn)彈性滑動的方法,延時策略。核心思想是使用 Handler 或 View 的 postDelayed 方法,也可以使用線程的 sleep 方法。

mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
private static final int MESSAGE_SCROLL_TO = 1;
    private static final int FRAME_COUNT = 30;
    private static final int DELAYED_TIME = 30;
    private int mCount = 0;
    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_SCROLL_TO: {
                    mCount++;
                    if (mCount <= FRAME_COUNT) {
                        float fraction = mCount / (float) FRAME_COUNT;
                        int scrollX = (int) (fraction * 100);
                        mButton.scrollTo(scrollX, 0);
                        mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
                    }
                    break;
                }
                default:
                    break;
            }
        };
    };
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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