第三章 View事件體系(1)

本文為Android開發藝術探索的筆記,僅供學習


首先View雖然不是四大組件,但是它的作用和重要性甚至比Receiver和Provider要重要的多。View是Android提供的控件的基類,然而這些控件遠遠不能滿足我們日常開發的需求,所以我們需要工具需求去自定義新的空間。一個典型的場景就是滑動屏幕,很多情況下我們的應多都支持滑動操作,當處于不同級別的View都去相應用戶的滑動操作的時候,就會帶來一個問題。滑動沖突!想要處理這個問題我們就要去理解View的分發機制,如何去分發?如何去攔截?我們會在后續中去講解。

1 View的基本知識

現在我們先來了解一下View的一些基本知識,以為后面更好的介紹做鋪墊。View的位置參數,MotionEvent和TouchSlop對象,VelocityTracker和Scroll對象,以便大家更好的去了解去理解一些復雜的內容。

1.1 什么是View

View,在前面也說了是所有Android控件的基類,不管是Button,TextView還是復雜的RelativeLayout它們的基類都是View。除了View,還有ViewGroup,從名字上看ViewGroup里面會有很多個View,對ViewGroup就是一個控件組,它里面可以有很多個View,But ViewGroup也繼承了View。關于ViewGroup我們可以這么理解,它里面可以有很多個View,這種關系就是一種View的樹的結構。LinearLayout它既是一個View,也可以是一個ViewGroup。
我們圖來表示一下,因為圖是最直觀的

View的樹形結構圖

再附上一張控件的結構層次表
TsetButton的層次結構圖

](http://upload-images.jianshu.io/upload_images/3986342-c67678af1a40f908.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

1.2 View的位置參數

View的位置有四屬性來決定,top left right bottom,top對應的是左上角的縱坐標,ldft對應的是左上角的橫坐標,right對應的是右下角的橫坐標,bottom對應的是右下角的縱坐標。(注意這些坐標都是相對與父控件的。)還是如此我們來花個圖來解釋一下吧

View的位置坐標與父控件的關系

從上圖,我們可以得到View的高是bottom-top View的寬是right-left,那么你們會問如何獲取到這四個屬性的值呢,其實只要通過getTop getLeft 就可以獲取相應的值。現在我們再提四個參數,X,Y,translationX,translationiY。X,Y是View左上角的左上角的坐標,而translationX和translationY,則表示可偏移量,這幾個參數相當于父控件的坐標,而且translationX和translationY的初始值都是0,同樣View也為他們提供了Get和Set的方法。

這幾個參數的關系式 X = left + translationX Y = top + translationY

需要注意的是View在平移的時候,top和left表示的是原始左上角的位置,其值并不會該表,此時發送改變的是X,Y,translationX,translationY.

1.3 MotionEvent 和 TouchSlop

1.MotionEvent

在手指接觸屏幕后發生的一系列事件,典型的有以下幾種

  • ACTION_DOWN------手指剛接觸屏幕的時候
  • ACTION_MOVE-------手指在屏幕上移動的時候
  • ACTION_UP-------手指離開屏幕的時候

正常情況下,一次手指接觸屏幕會出發兩種情況

  1. 第一種,DOWN-->UP 當點擊屏幕馬上離開
  2. 第二種,DOWN-->MOVE-->...-->MOVE-->UP 當點擊屏幕并且在屏幕上移動在離開
  3. 第三種,DOWN-->MOVE-->...-->MOVE 就是點擊屏幕,并且移動,移動到屏幕外面

上述三種是典型的事件順序,同時我們可以通過MotionEvent去獲取點擊時間發送的X,Y的坐標。系統提供了兩組方法,getX\getY, getRawX\getRawY,其中第一組是用來返回當前View的左上角的x y坐標,第二組方法是用來返回手機屏幕左上角的x y坐標。那么我們還是看圖說話吧


getX\getY, getRawX\getRawY的圖解

2 TouchSlop

TouchSlop就是系統所能識別的最小滑動距離,也就是說當用于滑動的距離小于該值則視為無效滑動。這是一個常量,不同的設備是不一樣的。我們可以通過ViewConfiguration.get(getApplicationContext()).getScaledTouchSlop(); 來獲取最小滑動距離。為了讓大家更好的理解附上源碼
可以看到最小識別的距離為8dp


1.4 VelocityTracker和Scroll

1 VelocityTracker

VelocityTracker通過跟蹤一連串事件實時計算出當前的速度,通過它我們可以得數水平滑動和豎直滑動的速率,一般作用在onTouchEvent方法中,主要用到下面幾個方法
addMovement(MotionEvent)函數將Motion event加入到VelocityTracker類實例中
getXVelocity() 或getXVelocity()獲得橫向和豎向的速率到速率時,computeCurrentVelocity(int)來初始化速率的單位 。
VelocityTracker.obtain();獲得VelocityTracker類實例
話不多說直接上代碼。

onTouchEvent(MotionEvent ev){
    if (mVelocityTracker == null) { 
            mVelocityTracker = VelocityTracker.obtain();//獲得VelocityTracker類實例 
    } 
    mVelocityTracker.addMovement(ev);//將事件加入到VelocityTracker類實例中 
    //判斷當ev事件是MotionEvent.ACTION_UP時:計算速率 
    // 1000 provides pixels per second 
    velocityTracker.computeCurrentVelocity(1, (float)0.01); //設置maxVelocity值為0.1時,速率大于0.01時,顯示的速率都是0.01,速率小于0.01時,顯示正常 
    Log.i("test","velocityTraker"+velocityTracker.getXVelocity());                     
    velocityTracker.computeCurrentVelocity(1000); //設置units的值為1000,意思為一秒時間內運動了多少個像素 
    Log.i("test","velocityTraker"+velocityTracker.getXVelocity()); 
}

2 Scroll

彈性滑動對象,用于實現View的彈性滑動。我們知道,當使用View的scrollTo/scrollBy方法來進行滑動時,其過程是瞬間完成,沒有過渡效果的滑動用戶體驗不好。這個時候就需要使用Scroller來實現有過渡效果的滑動,大致實現過程后面會詳細介紹,下面就附上實現代碼。

        Scroller scroller;
        scroller = new Scroller(context);

         //調用此方法滾動到目標位置
    public void smoothScrollTo(int fx, int fy, boolean back) {
        int dx = fx;
        int dy = fy;
        smoothScrollBy(dx, dy);
    }

    //調用此方法設置滾動的相對偏移
    public void smoothScrollBy(int dx, int dy) {
        //設置scroller的滾動偏移量
            scroller.startScroll(scroller.getFinalX(), scroller.getFinalY(), dx, dy);
            invalidate();//這里必須調用invalidate()才能保證computeScroll()會被調用,否則不一定會刷新界面,看不到滾動效果}
    }

    @Override
    public void computeScroll() {
        //先判斷scroller滾動是否完成
        if (scroller.computeScrollOffset()) {
            //這里調用View的scrollTo()完成實際的滾動
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            //必須調用該方法,否則不一定能看到滾動效果
            postInvalidate();
        }
        super.computeScroll();
    }

2 View的滑動

在View的事件體系(1)中,我們已經了解到View的基本知識,這一節要來講解很重要的東西就是View的滑動。在Android的設備上,滑動可以說以一種標配,不管是下拉刷新還是什么,他們的基礎都是滑動。不管是任何酷炫的滑動效果,歸根結底他們都是由不同的滑動和一些動畫組成。所以我們還有必要去了解滑動的基礎,接下來我們來了解三種實現滑動的方法。
1.View通過自生的scrollTo/scrollBy來實現滑動
2.通過動畫來給View添加平移的效果來實現滑動
3.改變View的LayoutParams使得View重新布局來實現滑動。

2.1使用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實際上也是調用了scrollTo方法,它實現了基于當前位置的相對滑動,而scrollTo是實現了基于所傳參數的絕對滑動。利用這兩個方法我們就可以實現View的滑動,但是我們要明白滑動過程中View的兩個內部屬性的作用mScrollX mScrollY,這兩個參數我們可以通過getScrollX getScrollY去獲取。mScrollX的總值等于View內容原始左邊緣到View內容現在左邊緣的水平方向的距離mScrollY的總值等于View內容原始上邊緣到View內容現在上邊緣的水平方向的距離。記住一句話上正下負右正左負,意思就是內容的上邊緣在View的上邊緣的上面,mScrollY為正,其他同理,給大家一個圖便于理解


切記,再怎么滑動不能將View的位置進行改變,只能改變View內容的位置,比如TextView改變里面的文字

在 使用 getScrollY() 方法的時候,就是 getScrollY()的值 一直是 1.0
解決:通過查看 getScrollY() 方法 ,發現它有兩個 返回值 一個 int , 一個 float , 后 將值 賦值給 int 類型后,就可以使用了;而直接 相加的為 float 類型;

2.2 使用動畫

通過動畫我們可以讓View進行平移,而平移也是一種滑動。我們可以使用View動畫也可以使用屬性動畫。

//View動畫
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:startOffset="1000"
    android:fillAfter="true">
    <scale
        android:fromXScale="0.0"
        android:toXScale="1.4"
        android:fromYScale="0.0"
        android:toYScale="1.4"
        android:pivotX="50"
        android:pivotY="50"
        android:duration="700" />
    <alpha
        android:fromAlpha="0.0"
        android:toAlpha="1.0" />
</set>
       Animation animation = AnimationUtils.loadAnimation(this, R.anim.demo);
       tv1.startAnimation(animation);
//屬性動畫
        ObjectAnimator.ofFloat(tv1,"translationY",150).start();
        ValueAnimator animator = ObjectAnimator.ofInt(tv1, "backgroundColor", 0xFFFF8080, 0xFF8080FF);
        animator.setDuration(3000);
        animator.setEvaluator(new ArgbEvaluator());
        animator.setRepeatCount(5);
        animator.setRepeatMode(ValueAnimator.REVERSE);
        animator.start();

切記,我們對通過動畫對View的移動其實是對View的影像的移動,若我們不把fillAfter設為true的話,移動完后又會回到起點,若為true則會保留不動。但我們也View設置一個點擊事件的時候,就要區分動畫的類型,若是View動畫則點擊移動后的View卻觸發不了點擊事件,若是屬性動畫則點擊移動后的View卻觸發點擊事件。針對View動畫的解決方案,我們需要在移動后的位置再建立一個通向的View。

2.3 改變布局參數

我們可以改變布局參數LayoutParams ,讓我們想讓一個Button向右移動100dp,那么我們只要設施其marginleft就可以了,還可以這這個Button設置一個寬度為0的View,改變其的寬度為,那個Button就會自動被擠到右邊。


2.4各種滑動方式的對比

  • scrollTo/scrollBy,這種方法其實是View提供的原生的滑動方式,他可以實現View的滑動也不影響其內部的點擊事件,缺點就是只能滑動View的內容

  • 動畫滑動,如果是android3.0以上的話可以采用屬性動畫,這種方法并沒有什么缺點,如果是3.0一下的話就絕不能改變View本生的屬性。實際上如果動畫不需要響應用戶的交互,那么這種滑動方式是比較合適的。但是動畫有一個明顯的有點,就是一些復雜的效果必須通過動畫來實現。

  • 改變布局的方式,除了使用起來麻煩以外就沒有什么明顯的缺點了,非常適合對象具有交互的View,因為這些View是要與用戶交互,直接通過動畫會有問題。

總結一下就是
scrollTo/scrollBy:操作簡單,適合對View內容的滑動
動畫:操作簡單,主要適用于沒有交互的View和實現復雜的動畫效果
改變布局參數:操作稍微復雜,適用于有交互的View

下面附上一個拖動的Demo
public class Move_textview extends TextView {
    String TAG = "move";

    public Move_textview(Context context) {
        this(context, null);
    }

    private int mScaledTouchSlop;//可識別的最小滑動距離
    // 分別記錄上次滑動的坐標
    private int mLastX = 0;
    private int mLastY = 0;

    public Move_textview(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public Move_textview(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        mScaledTouchSlop = ViewConfiguration.get(getContext())
                .getScaledTouchSlop();
        Log.d(TAG, "sts:" + mScaledTouchSlop);
    }

    @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 move_x = x - mLastX;
                int move_y = y - mLastY;
                int translationX = (int) ViewHelper.getTranslationX(this) + move_x;
                int translationY = (int)ViewHelper.getTranslationY(this) + move_y;
                ViewHelper.setTranslationX(this, translationX);
                ViewHelper.setTranslationY(this, translationY);
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }
}

3.彈性滑動

知道了View的滑動,但是這樣的滑動有時候是比較生硬的,用戶體驗太差了,所以我們要去了解彈性滑動,就是將一次滑動分成若干個小滑動。主要是通過Scroller,handler #postDelaked,Thread#sleep

3.1Scroller的使用

我們先來看看Scroller的最基本的使用
Scroller scroller = new Scroller(context);
private void smoothScrollTo(int destX, int destY) {//自己寫的方法
    int scrollX = getScrollX();//View的內容的左邊緣到View左邊緣的距離
    int deltaX = destX + scrollX;//加上要移動的距離后的位置
    scroller.startScroll(scrollX, 0, deltaX, 0);
    invalidate();注解1
}
@Override
public void computeScroll() {
    if (scroller.computeScrollOffset()) {
        scrollTo(scroller.getCurrX(), scroller.getCurrY());
        postInvalidate();
    }
}

我們可以看到顯示構造了一個Scroller對象,在調用它的startScroll方法,其實Scroller內部什么都沒做就是用來保存幾個參數

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

startX,startY是起始位置,dx dy是要滑動的距離,duration就是在規定的時間內滑動
那么Scroller到底是怎么進行彈性滑動的呢?注解1invalidate()

大致的流程是這樣子的,invalidate方法會導致View去重繪,ondraw是繪制的方法,改方法又會去調用ComputeScroll方法,此時我們需要對ComputeScroll進行重寫,在里面去判斷彈性滑動是否結束,沒結束就再獲取Scroller當前的位置,在去進行第二次繪制,直至彈性滑動結束。

那我們來看看Scroller的computeScrolloffset方法

public boolean computeScrollOffset() {
    if (mFinished) {
        return false;
    }
    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;
        case FLING_MODE:
            final float t = (float) timePassed / mDuration;
            final int index = (int) (NB_SAMPLES * t);
            float distanceCoef = 1.f;
            float velocityCoef = 0.f;
            if (index < NB_SAMPLES) {
                final float t_inf = (float) index / NB_SAMPLES;
                final float t_sup = (float) (index + 1) / NB_SAMPLES;
                final float d_inf = SPLINE_POSITION[index];
                final float d_sup = SPLINE_POSITION[index + 1];
                velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                distanceCoef = d_inf + (t - t_inf) * velocityCoef;
            }
            mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;      
            mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
            // Pin to mMinX <= mCurrX <= mMaxX
            mCurrX = Math.min(mCurrX, mMaxX);
            mCurrX = Math.max(mCurrX, mMinX);
            
            mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
            // Pin to mMinY <= mCurrY <= mMaxY
            mCurrY = Math.min(mCurrY, mMaxY);
            mCurrY = Math.max(mCurrY, mMinY);
            if (mCurrX == mFinalX && mCurrY == mFinalY) {
                mFinished = true;
            }
            break;
        }
    }
    else {
        mCurrX = mFinalX;
        mCurrY = mFinalY;
        mFinished = true;
    }
    return true;
}

該方法就是根據流逝時間的百分比會算去scrollX和scrollY,當返回true的時候表示彈性滑動還沒結束,false就表示彈性滑動結束
那么現在來總結一下,scroller本生是不能滑動的,它需要配合computeScroll來實現彈性滑動,它會不斷的去重繪View,每次重繪是有時間間隔的,通過這個時間間隔和Scroller的computeScrolloffset方法來返回相應的位置,通過View自身的scrollTo和返回來的位置去移動View。這個思想很巧妙,竟然連計時器都沒有用到。
注意:滑動的還是View的內容而不是View

3.2 通過動畫

動畫本來就是一種漸進的過程,因此通過它來實現的滑動天然就具有彈性效果,比如以下代碼可以讓一個Button實現一個寬度的變化動畫。

 private static class View_button {
        View view;
        public View_button(View view) {
            this.view = view;
        }
        public int getWidth() {
            return view.getLayoutParams().width;
        }
        public void setWidth(int w) {
            view.getLayoutParams().width = w;
            view.requestLayout();
        }
    }

       ObjectAnimator.ofInt(new View_button(tv1),"width",500).setDuration(3000).start();

至于屬性動畫的詳情,在后續中會詳細介紹

3.3 使用延時策略

通過Handler里去改變控件的位置。

  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);
                    mButton1.scrollTo(scrollX, 0);
     mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
                }
                break;
            }
            default:
                break;
            }
        };
    };
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容