Android View 事件體系筆記(一):View 基礎(chǔ)屬性與滑動(dòng)

Android View(一).png

聲明:本文內(nèi)容根據(jù)《Android開發(fā)藝術(shù)探索》的思路,基于 API 26 進(jìn)行總結(jié)

一、Android View基礎(chǔ)知識(shí)

背景:

  • 常用的系統(tǒng)控件很多時(shí)候不能滿足需求,因此需要根據(jù)具體需求自定義新的控件。
  • 一般都是通過繼承某View來重寫核心方法,重新設(shè)置屬性來完成控件的自定義。
  • Android手機(jī)屬于移動(dòng)設(shè)備,特點(diǎn)是通過屏幕進(jìn)行一系列操作,比如滑動(dòng)切換。由于不同層級(jí)的Veiw都可以響應(yīng)滑動(dòng),所以就帶來了滑動(dòng)沖突的問題,詳細(xì)了解View的事件分發(fā)機(jī)制就可以根據(jù)其特性來解決這個(gè)問題。

定義:

  • View是 Android 中所有控件的基類,常用的各種控件包括RelativeLayout等都是View的子類。
  • ViewGroup 可翻譯為控件組,也繼承了View。特點(diǎn)是包含一個(gè)或多個(gè)View、ViewGroup的子View同樣可以是ViewGroup。

1.1 View 位置參數(shù):

1.1.1 View 基本參數(shù)
  • View的位置由它的四個(gè)頂點(diǎn)來決定:top、left、right、bottom。
  • top:上邊距離父容器(ViewGroup)距離。
    public final int getTop(){ return mTop; }
    left:左邊距離父容器距離。
    public final int getLeft(){ return mLeft; }
    right:右邊距離父容器距離。
    public final int getRight(){ return mRight; }
    bottom:下邊距離父容器距離。
    public final int getBottom(){ return mBottom; }

View的寬度:width = right - left;
View的高度:height = bottom - top。

  • Android3.0開始增加額外參數(shù):
    x 和 y :View左上角坐標(biāo);
    translationX 和 translationY:View左上角相對(duì)于父容器的偏移量(默認(rèn)為 0)。
    同樣提供 get/set 方法,注意平移過程中 top 和 left 并不會(huì)改變,發(fā)生變化的是 x、y、translationX、translationY。

x = left + translationX
y = right + translationY

View參數(shù).png
1.1.2 MotionEvent 和 TouchSlop
  1. MotionEvent (移動(dòng)事件)
  • ACTION_DOWN: 手指放下,接觸屏幕
  • ACTION_MOVE: 手指在屏幕移動(dòng)
  • ACTION_UP: 手指離開屏幕的瞬間
  • 點(diǎn)擊后離開會(huì)經(jīng)歷:ACTION_DOWN --> ACTION_UP
  • 點(diǎn)擊后滑動(dòng)再離開:ACTION_DOWN --> ACTION_MOVE --> ACTION_MOVE ... --> ACTION_UP

通過 MotionEvent 對(duì)象可以得到點(diǎn)擊事件發(fā)生的坐標(biāo) x 和 y 。
getX/getY: 指相對(duì)于當(dāng)前 View 左上角的 x/y 坐標(biāo)。
getRawX/getRawY: 相對(duì)于屏幕左上角的 x/y 坐標(biāo)。

  1. TouchSlop (最小滑動(dòng))
    TouchSlop 定義系統(tǒng)能夠識(shí)別的最小滑動(dòng)距離。
    是一個(gè)常量,如果手指滑動(dòng)小于這個(gè)距離,系統(tǒng)則不認(rèn)為是在滑動(dòng)。不同設(shè)備上可能值不相同。
    獲取 ViewConfiguration.get(getContext()).getScaledTouchSlop()
    源碼目錄:frameworks/base/core/res/res/values/config.xml
1.1.3 VelocityTracker 、GestureDetector 和 Scroller
  1. VelocityTracker (速度追蹤)
    速度追蹤,用于追蹤手指在滑動(dòng)中的速度,包括水平和垂直的速度。步驟如下:

(1) 在 View 的 onTouchEvent 方法中追蹤當(dāng)前點(diǎn)擊事件的速度:

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

(2) 獲取當(dāng)前事件的速度后,獲取在一定時(shí)間內(nèi),手指劃過的像素

velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();

獲取速度前必須先計(jì)算速度:先調(diào)用 computeCurrentVelocity() 方法。
一定時(shí)間劃過的像素?cái)?shù):上面參數(shù) 1000 ,通過 getXVelocity 獲得的就是1000 ms 內(nèi)手指劃過的 x 像素值。

速度 = (終點(diǎn)位置 - 起點(diǎn)位置)/ 時(shí)間段

(3) 釋放并回收內(nèi)存

velocityTracker.clear();
velocityTracker.recycle();
  1. GestureDetector (手勢(shì)檢測(cè))
    用于檢測(cè)用戶單擊、滑動(dòng)、長(zhǎng)按、雙擊等。使用過程:

(1) 創(chuàng)建 GestureDetector 對(duì)象并實(shí)現(xiàn) OnGestureListener 接口,實(shí)現(xiàn) OnDoubleTapListener 監(jiān)聽雙擊行為:

GestureDetector mGestureDetector = new GestureDetector(this);
// 解決長(zhǎng)按屏幕后無法拖動(dòng)
mGestureDetector.setIsLongpressEnabled(false);

(2) 在需要監(jiān)聽 View 的 onTouchEvent 方法中添加:

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

(3) 根據(jù)需要有選擇地實(shí)現(xiàn) OnGestureListenerOnDoubleTapListener 中的方法。

方法名 描述 所述接口
onDown(MotionEvent e) 手指觸摸屏幕瞬間,由一個(gè)ACTION_DOWN觸發(fā) OnGestureListener
onShowPress(MotionEvent e) 手指輕觸屏幕,尚未松開或拖動(dòng) OnGestureListener
onSingleTapUp(MotionEvent e) 手指輕觸屏幕后松開,伴隨一個(gè)ACTION_UP觸發(fā),單擊行為 OnGestureListener
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) 手指按下屏幕并移動(dòng),一個(gè) ACTION_DOWN,多個(gè)ACTION_MOVE 觸發(fā),是拖動(dòng)行為 OnGestureListener
onLongPress(MotionEvent e) 長(zhǎng)按行為 OnGestureListener
onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) 用戶觸摸屏幕、快速滑動(dòng)后松開,由一個(gè) ACTION_DOWN、多個(gè) ACTION_MOVE 和一個(gè) ACTION_UP 觸發(fā)。 OnGestureListener
onDoubleTap(MotionEvent e) 雙擊,由兩次連續(xù)的單擊組成,不可能和 onSingleTapConfirmed 共存 OnDoubleTapListener
onSingleTapConfirmed(MotionEvent e) 嚴(yán)格的單擊行為,如果在一定時(shí)間內(nèi)再次點(diǎn)擊,則不會(huì)觸發(fā)此方法 OnDoubleTapListener
onDoubleTapEvent(MotionEvent e) 表示發(fā)生了雙擊行為,在此期間, ACTION_DOWN、ACTION_MOVE 和 ACTION_UP 都會(huì)觸發(fā)此回調(diào) OnDoubleTapListener

在實(shí)際開發(fā)中,可以不使用 GestureDetector ,完全可以在 View 的 onTouchEvent 方法中實(shí)現(xiàn)所需監(jiān)聽。如果只是監(jiān)聽滑動(dòng)相關(guān)的,可在 onTouchEvent 實(shí)現(xiàn),如果監(jiān)聽雙擊的話,用 GestureDetector。

  1. Scroller(彈性滑動(dòng)對(duì)象)
    用于實(shí)現(xiàn) View 的彈性滑動(dòng)。使用 View 的 scrollTo/scrollBy 方法來滑動(dòng)時(shí),過程是瞬間完成的,使用 Scroller 和 View 的 computeScroll 方法配合來完成彈性滑動(dòng)。
Scroller scroller = new Scroller(getContext());

private void smoothScrollTo(int destX, int destY){
    int scrollX = getScrollX();
    int delta = destX - scrollX;
    // 1000ms內(nèi)慢慢滑動(dòng)至 destX
    scroller.startScroll(scrollX, 0, delta, 0, 1000);
    invalidate();
}

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

二、View 滑動(dòng)

常見的三種方式實(shí)現(xiàn) View 的滑動(dòng):

  • 通過 View 本身提供的 scrollTo/scrollBy 方法。
  • 通過動(dòng)畫給 View 施加平移效果。
  • 改變 View 的 LayoutParams 使得 View 重新布局。

2.1 使用 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);
    }

注意:

  • scrollTo 和 scrollBy 只能改變 View 內(nèi)容的位置,而不能改變 View 在布局中的位置
  • mScrollX 表示 View 左邊緣和 View 內(nèi)容左邊緣水平的距離,左邊為正,右邊為負(fù),單位為像素
    mScrollY 表示 View 上邊緣和 View 內(nèi)容上邊緣垂直的距離,上邊為正,下邊為負(fù),單位像素
  • 也就是說,假設(shè) View 的內(nèi)容向左滑動(dòng)100px, mScrollX 為 100px。View 的內(nèi)容向下滑動(dòng) 50px ,mScrollY 為 -50px。

2.2 使用動(dòng)畫

通過操作 View 的 translationX 和 translationY 屬性,可以使用 View 動(dòng)畫(包括幀動(dòng)畫(Frame Animation)和補(bǔ)間動(dòng)畫(Tweened Animation))或?qū)傩詣?dòng)畫(3.0以下需要兼容動(dòng)畫庫 nineoldandroids)。

View 動(dòng)畫向 100ms 右下角平移 100 像素。

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true"
    android:zAdjustment="normal">
    <!-- android:fillAfter="true":移動(dòng)完畢后保存狀態(tài) -->
    <!-- duration:時(shí)常 -->
    <!-- interpolator:動(dòng)畫效果:linear_interpolator 表示常量速率變化 -->
    <translate
        android:duration = "100"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXDelta="100"
        android:toYDelta="100"/>
</set>

屬性動(dòng)畫 100ms 向右平移 100 像素。

ObjectAnimator.ofFloat(new MyView(this),"translationX", 0, 100).setDuration(100).start();

設(shè)置 android:fillAfter 屬性為false,View 移動(dòng)后會(huì)瞬間回去,true 會(huì)保存移動(dòng)狀態(tài)
使用 View 動(dòng)畫不會(huì)真正地改變 View 的位置參數(shù),包括寬/高。所以 View 使用 View動(dòng)畫,其內(nèi)的控件如 Button 位置不會(huì)改變,可事先在目標(biāo)位置設(shè)置 Button,待移動(dòng)完成隱藏原來 Button。
Android 3.0以上使用屬性動(dòng)畫則沒有這個(gè)問題。

2.3 改變布局參數(shù)

通過改變某 View 的 LayoutParams。
比如想使一個(gè) Button 向右平移 100px,只需要設(shè)置其 LayoutParams 的 marginLeft 參數(shù)增加 100px即可。
還可在 Button 左邊放置一個(gè)空 View,Button 需要移動(dòng)時(shí)設(shè)置空 View 的寬度,在 LinearLayout 的水平方向布局里 Button 就會(huì)被擠壓到右邊一定的寬度。

ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) mButton.getLayoutParams();
// 寬度增加100px
marginLayoutParams.width += 100;
// 左間距增加100px
marginLayoutParams.leftMargin += 100;
// mButton應(yīng)用修改
mButton.requestLayout();
//或者 mButton.setLayoutParams(marginLayoutParams);

三種移動(dòng)動(dòng)畫特點(diǎn):

  • scrollTo/scrollBy: 操作簡(jiǎn)單,適合 View 內(nèi)容的滑動(dòng);
  • 動(dòng)畫:操作簡(jiǎn)單,用于沒有交互或復(fù)雜動(dòng)畫效果的實(shí)現(xiàn);
  • 改變布局參數(shù):操作稍微復(fù)雜,適用于有交互的 View。

小Demo:自定義 View 實(shí)現(xiàn)跟隨手指在屏幕上移動(dòng)

public ScreenMoveView(Context context) {
    super(context);
}
// 必須實(shí)現(xiàn)這個(gè)構(gòu)造函數(shù)
public ScreenMoveView(Context context, AttributeSet attrs) {
    super(context, attrs);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getRawX();
    int y = (int) event.getRawY();
    switch (event.getAction()){
        // 手指落下
        case MotionEvent.ACTION_DOWN:
            break;
        // 手指移動(dòng)
        case MotionEvent.ACTION_MOVE:
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            // ViewHelper 是 nineoldandroids 提供的動(dòng)畫兼容庫,可在github下載
            int translationX = (int) (ViewHelper.getTranslationX(this) + deltaX);
            int translationY = (int) (ViewHelper.getTranslationY(this) + deltaY);
            ViewHelper.setTranslationX(this,translationX);
            ViewHelper.setTranslationY(this,translationY);
            break;
        // 手指抬起
        case MotionEvent.ACTION_UP:
            break;
        default:
            break;
    }
    mLastX = x;
    mLastY = y;
    // true攔截父類傳遞
    return true;
}

三、彈性滑動(dòng)

3.1 使用 Scroller 實(shí)現(xiàn)彈性滑動(dòng)

// step1:實(shí)例化 Scroller 對(duì)象
Scroller mScroller = new Scroller(mContext);

private void smoothScrollTo(int destX, int destY){
    // getScrollX獲取View在屏幕上從初始點(diǎn)偏移的值
    int scrollX = getScrollX();
    int deltaX = destX - scrollX;
    // step2:開始滑動(dòng) 1000ms 平滑滑向destX
    mScroller.startScroll(scrollX,0,deltaX,0,1000);
    invalidate(); // --> 重繪 View 調(diào)用 draw 方法
}

// step3:draw方法調(diào)用該方法
@Override
public void computeScroll() {
    // step4:判斷是否滑動(dòng)完畢
    if(mScroller.computeScrollOffset()){
        scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        postInvalidate();
    }
}

startScroll() 函數(shù)源碼:

    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ù)僅僅保存了傳遞的幾個(gè)參數(shù),startX 和 startY 表示滑動(dòng)的起點(diǎn),dx 和 dy 表示的是要滑動(dòng)的距離,duration 表示滑動(dòng)的時(shí)間。這里的滑動(dòng)是 View 內(nèi)容的滑動(dòng)而并非 View 本身位置的改變。

僅僅調(diào)用 startScroll 是無法讓 View 進(jìn)行滑動(dòng)的,實(shí)際讓 View 實(shí)現(xiàn)彈性滑動(dòng)的是 invalidate(),該方法會(huì)導(dǎo)致 View 重繪,在 View 的 draw 方法又會(huì)調(diào)用 computeScroll 方法。computeScroll 方法是 View 的一個(gè)空實(shí)現(xiàn),需要自己去實(shí)現(xiàn)。

原理:View 重繪 --> draw 方法調(diào)用 computeScroll --> computeScroll 向Scroller 獲取當(dāng)前的 scrollX 和 scrollY --> 通過 scrollTo 方法實(shí)現(xiàn)滑動(dòng) --> 調(diào)用 postInvalidate 方法二次重繪 --> 依然調(diào)用 computeScroll 方法 --> 繼續(xù)獲取 scrollX 和 scrollY 并通過 scrollTo 方法滑動(dòng)到新位置直到結(jié)束。

step4: mScroller.computeScrollOffset() 源碼:

    public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }
        // 滑動(dòng)動(dòng)畫過去的時(shí)間
        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
        // 滑動(dòng)的時(shí)間小于設(shè)定的總滑動(dòng)時(shí)間
        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                // 根據(jù)時(shí)間的流逝的百分比來算出 scrollX 和 scrollY 改變的百分比
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                // 再根據(jù)百分比來計(jì)算出當(dāng)前的值
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
        ...
        return true;
    }

最后返回 true 表明動(dòng)畫還沒結(jié)束需要繼續(xù)滑動(dòng),false 則表明滑動(dòng)完成。

Scroller 滑動(dòng)原理:Scroller 配合 View 的 computeScroll 方法完成彈性滑動(dòng),該方法不斷讓 View 重繪,每次重繪根據(jù)時(shí)間間隔來計(jì)算出 View 當(dāng)前滑動(dòng)的位置并使用 scrollTo 方法完成 View 的滑動(dòng)。

3.2 通過動(dòng)畫

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 = animation.getAnimatedFraction();
        mButton.scrollTo(startX + (int)(deltaX * fraction),0);
    }
});
animator.start();

上述代碼僅僅是完成一個(gè) 1000ms 的動(dòng)畫,同時(shí)設(shè)定動(dòng)畫刷新監(jiān)聽,再按照比例通過 scrollTo 方法來完成某個(gè) View 的動(dòng)畫。由于 scrollTo 針對(duì)的是 View 的內(nèi)容而非本身,所以這里只能變動(dòng) View 內(nèi)容并非本身。

3.3 使用延時(shí)策略

// msg.what
private static final int MESSAGE_SCROLL_TO = 1;
// 總共更新次數(shù)
private static final int FRAME_COUNT = 30;
// 每一次移動(dòng)間隔
private static final int DELAYED_TIME = 33;
// 記錄移動(dòng)數(shù)量,要小于總數(shù)
private int mCount = 0;

private Handler mHandler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what){
            case MESSAGE_SCROLL_TO:
                mCount++;
                if(mCount <= FRAME_COUNT){
                    // 計(jì)算當(dāng)前移動(dòng)比例
                    float fraction = mCount/(float)FRAME_COUNT;
                    int scrollx = (int) (fraction * 100);
                    mButton.scrollTo(scrollx,0);
                    // 再次發(fā)送消息移動(dòng)
                    mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO,DELAYED_TIME);
                }
            break;
        }
    }
};

使用 Handler 或 View 的 postDelayed 方法來循環(huán)發(fā)送動(dòng)畫消息,來完成 View 的緩慢移動(dòng)效果。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本章將介紹Android中十分重要的一個(gè)概念View 文章目錄: 3.1 View的基礎(chǔ)知識(shí) 1. 什么是View...
    kongjn閱讀 1,027評(píng)論 0 2
  • View的基礎(chǔ) View的位置參數(shù) 表示位置的幾種參數(shù) left/right/top/bottom表示view(v...
    X_Sation閱讀 330評(píng)論 0 0
  • 就在昨天,老媽和我微信視頻,說:聽說你買了一輛車??刹皇菃幔课医K于買了一輛自行車。哈哈哈哈,兩人不約大笑。 我買了...
    木木木俠閱讀 1,046評(píng)論 0 4
  • 金燦燦的玉米囤滿農(nóng)家的庭院 紅通通的小棗晾曬在屋前院后 黑黝黝的豆兒裝好袋子聚在屋檐下 豐收!今年的秋,農(nóng)家院落顯...
    豐盈倉廩閱讀 925評(píng)論 0 0
  • 寒風(fēng)凜冽,寒氣襲人,枯草蕭疏,綠色盡失,今年的冬天如此蒼白凄涼且冷漠. M點(diǎn)燃一支煙,吞云吐霧起來,一圈圈的煙霧,...
    幽谷泉涌閱讀 969評(píng)論 0 1

友情鏈接更多精彩內(nèi)容