View的基礎知識介紹

轉載請以鏈接形式標明出處:
本文出自:103style的博客

《Android開發藝術探索》 學習記錄


可以帶着以下問題來看本文:

  • View的座標系和座標,平移等動畫改變的是什麼屬性?
  • View有哪些事件?
  • 如果獲取系統可識別的最短滑動距離?
  • 如果計算滑動的速度?
  • 單擊、雙擊、長按等事件的監聽?
  • 彈性滑動的實現?

目錄

  • View 與 ViewGroup
  • View 的位置參數
  • MotionEvent 和 TouchSlop
  • VelocityTracker
  • GestureDetector
  • Scroller

View與ViewGroup

View

public class View
extends Object implements Drawable.Callback,KeyEvent.Callback,AccessibilityEventSource
java.lang.Object
   ↳ android.view.View
Known direct subclasses
AnalogClock , ImageView,KeyboardViewMediaRouteButtonProgressBar , Space , SurfaceView , TextViewTextureViewViewGroup,ViewStub.
Known indirect subclasses
AbsListViewAbsSeekBarAbsSpinner , AbsoluteLayoutAutoCompleteTextViewButtonCalendarViewCheckBoxCheckedTextViewChronometer, and 57 others..

ViewGroup

public abstract class ViewGroup
extends View implements ViewParent, ViewManager
java.lang.Object
android.view.View
android.view.ViewGroup
Known direct subclasses
AbsoluteLayout, AdapterView<T extends Adapter>, FragmentBreadCrumbs, FrameLayout, GridLayout, LinearLayout, RelativeLayout, SlidingDrawer, Toolbar, TvView.
Known indirect subclasses
AbsListView, AbsSpinner, CalendarView, DatePicker, ExpandableListView, Gallery, GridView, HorizontalScrollView,ImageSwitcher, and 26 others.

通過上面的官方介紹,我們可以看到,View 是我們平常看到的視圖上所有元素的父類,按鈕Button、文本TextView、圖片ImageView 等。
ViewGroup 也是 View 的子類,ViewGroup 相當與 View 的容器,可以包含很多的 View.


View的位置參數

View的座標系如下圖:
View座標系

左上角爲原點O(0,0),X、Y軸分別向右向下遞增。
圖中 View 和 ViewGroup 的位置由其四個頂點決定,以View爲例,分別對應四個屬性:LeftTopRightBottom.
所以 Width = Right - Left, Height = Bottom - Top.

Android 3.0 開始,View又增加了 xytranslationXtranslationY 四個參數。
xy 即爲上圖中的A點,分別對應A點在View座標系中的X、Y軸上的座標。
translationXtranslationY則爲相對於父容器ViewGroup的偏移量,默認爲 0
他們的關係爲: x = left + tranlastionXy = top + tranlastionY.

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

下面我們來測試看看:

<!--  activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:padding="8dp"
        android:text="Hello World!" />
</LinearLayout>
//MainActivity.java
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final TextView tv = findViewById(R.id.tv);
        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(TAG, "tv.getLeft() = " + tv.getLeft());
                Log.e(TAG, "tv.getTop() = " + tv.getTop());
                Log.e(TAG, "tv.getRight() = " + tv.getRight());
                Log.e(TAG, "tv.getBottom() = " + tv.getBottom());
                Log.e(TAG, "tv.getWidth() = " + tv.getWidth());
                Log.e(TAG, "tv.getHeight() = " + tv.getHeight());
                Log.e(TAG, "tv.getX() = " + tv.getX());
                Log.e(TAG, "tv.getY() = " + tv.getY());
                Log.e(TAG, "tv.getTranslationX() = " + tv.getTranslationX());
                Log.e(TAG, "tv.getTranslationY() = " + tv.getTranslationY());
            }
        });
    }
}

點擊按鈕,打印日誌如下:

MainActivity: tv.getLeft() = 21
MainActivity: tv.getTop() = 21
MainActivity: tv.getRight() = 263
MainActivity: tv.getBottom() = 114
MainActivity: tv.getWidth() = 242
MainActivity: tv.getHeight() = 93
MainActivity: tv.getX() = 21.0
MainActivity: tv.getY() = 21.0
MainActivity: tv.getTranslationX() = 0.0
MainActivity: tv.getTranslationY() = 0.0

我們可以看到 left、top、right、bottom 是整形的, 而 x、y、translationX、translationY 是浮點型的


MotionEvent 和 TouchSlop

MotionEvent 即爲我們點擊屏幕所產生的一些列事件,主要有以下幾個:

  • ACTION_DOWN:手指剛接觸屏幕。
  • ACTION_MOVE:手指在屏幕上滑動。
  • ACTION_UP:手指離開屏幕的一瞬間。
  • ACTION_CANCEL:消耗了DOWN事件卻沒有消耗UP事件,再次觸發DOWN時,會先觸發CANCEL事件。

一般依次點擊屏幕操作,會產生一些列事件:DOWN → 0個或多個 MOVE → UP
通過MotionEvent 我們可以知道事件發生的 x , y 座標, 可以通過系統提供的 getX()/getY()getRawX()/getRawY()獲取。
getX()/getY()是對於當前View左上角的座標.
getRawX()/getRawY()則是對於屏幕左上點的座標.

TouchSlop 則是系統所能識別的最短的滑動距離,
這個距離可以通過 ViewConfiguration.get(getContext()).getScaledTouchSlop() 獲得。
在 Genymotion上的 Google pixel 9.0系統 420dpi 的模擬器上得到的值如下:

MainActivity: getScaledTouchSlop = 21

VelocityTracker

VelocityTracker 是用來記錄手指滑動過程中的速度的,包括水平方向和數值方向。
可以通過如下方式來獲取當前事件的滑動速度:

tv.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                VelocityTracker velocityTracker = VelocityTracker.obtain();
                velocityTracker.addMovement(event);
                velocityTracker.computeCurrentVelocity(1000);
                float vX = velocityTracker.getXVelocity();
                float vY = velocityTracker.getYVelocity();
                Log.e(TAG, "vX = " + vX + ", vY = " + vY);
                velocityTracker.clear();
                velocityTracker.recycle();
                break;
        }
        return true;
    }
});
MainActivity: vX = 542.164, vY = 271.18683
MainActivity: vX = 2257.9578, vY = 291.47467
MainActivity: vX = 2237.9333, vY = 379.69537
MainActivity: vX = 1676.5919, vY = 697.79443
MainActivity: vX = 1672.0844, vY = 288.5999
MainActivity: vX = 645.7418, vY = 322.51065
MainActivity: vX = 810.2783, vY = 270.19778

當然最後,在不用的時候記得調用以下代碼重置並回收掉 VelocityTracker:

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

GestureDetector

GestureDetector 即手勢檢測,用於輔助我們捕獲用戶的 單擊、雙擊、滑動、長按等行爲。

使用也很簡單,只需要創建一個下面來看個示例。
在構造函數中創建 通過 gestureDetector = new GestureDetector(context, this) 創建 GestureDetector,
然後實現 GestureDetector.OnGestureListenerGestureDetector.OnDoubleTapListener 接口,
然後在 onTouchEvent 中 返回 gestureDetector.onTouchEvent(event)

public class TestGestureDetector extends View implements GestureDetector.OnGestureListener,
        GestureDetector.OnDoubleTapListener {
    private static final String TAG = "TestGestureDetector";
    GestureDetector gestureDetector;
    public TestGestureDetector(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        gestureDetector = new GestureDetector(context, this);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return gestureDetector.onTouchEvent(event);
    }
    @Override
    public boolean onDown(MotionEvent e) {
        Log.e(TAG, "onDown: action = " + e.getAction());
        return false;
    }
    @Override
    public void onShowPress(MotionEvent e) {
        Log.e(TAG, "onShowPress:");
    }
    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        Log.e(TAG, "onSingleTapUp: " + e.getAction());
        return false;
    }
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        Log.e(TAG, "onScroll: e1.action = " + e1.getAction() + ", e2.action = " + e2.getAction());
        return false;
    }
    @Override
    public void onLongPress(MotionEvent e) {
        Log.e(TAG, "onLongPress: action = " + e.getAction());
    }
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        Log.e(TAG, "onFling: e1.action = " + e1.getAction() + ", e2.action = " + e2.getAction());
        return false;
    }
    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        Log.e(TAG, "onSingleTapConfirmed: action = " + e.getAction());
        return false;
    }
    @Override
    public boolean onDoubleTap(MotionEvent e) {
        Log.e(TAG, "onDoubleTap: action = " + e.getAction());
        return false;
    }
    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        Log.e(TAG, "onDoubleTapEvent: action = " + e.getAction());
        return false;
    }
}

然後在佈局中讓它佔滿屏幕。

tips:
action = 0DOWN 事件
action = 1UP 事件
action = 2MOVE 事件

運行程序,我們執行一次單擊,一次長按單擊,然後雙擊一次,發下打印日誌如下:

//第一次單擊
TestGestureDetector: onDown: action = 0
TestGestureDetector: onShowPress:
TestGestureDetector: onLongPress: action = 0
//第一次長按單擊
TestGestureDetector: onDown: action = 0
TestGestureDetector: onShowPress:
TestGestureDetector: onLongPress: action = 0
//第一次雙擊
TestGestureDetector: onDown: action = 0
TestGestureDetector: onShowPress:
TestGestureDetector: onDown: action = 0
TestGestureDetector: onShowPress:
TestGestureDetector: onLongPress: action = 0

通過上面的日誌信息我們可以知道 :
一次 單擊長按單擊 操作會觸發 onDownonShowPressonLongPress三個回調。
雙擊 操作則會依次觸發 onDownonShowPressonDownonShowPressonLongPress 五次回調。

顯示單擊出現 onLongPress 是不合理的,我們可以通過 gestureDetector.setIsLongpressEnabled(false) 禁用掉,而且我們也沒有監聽到 單機和雙擊等其他回調,這是爲什麼呢?

這是因爲我們 沒有消耗掉 DOWN 事件,這涉及到事件分發相關的知識了,這裏先不說,後面會寫文章單獨講解。那怎麼消耗掉 DOWN 事件呢?很簡單,只要在 onDown 中返回 true
修改上述代碼如下,只貼出修改的部分,

public class TestGestureDetector extends View implements GestureDetector.OnGestureListener,
        GestureDetector.OnDoubleTapListener {
    ...
    public TestGestureDetector(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        gestureDetector = new GestureDetector(context, this);
        gestureDetector.setIsLongpressEnabled(false);
    }
    @Override
    public boolean onDown(MotionEvent e) {
        Log.e(TAG, "onDown: action = " + e.getAction());
        return true;
    }
    ...
}

運行程序,在執行一次單擊,一次長按單擊和一次雙擊,日誌如下:

//第一次單擊
TestGestureDetector: onDown: action = 0
TestGestureDetector: onSingleTapUp: 1
TestGestureDetector: onSingleTapConfirmed: action = 0
//第一次長按單擊
TestGestureDetector: onDown: action = 0
TestGestureDetector: onShowPress:
TestGestureDetector: onSingleTapUp: 1
TestGestureDetector: onSingleTapConfirmed: action = 1
//第一次雙擊
TestGestureDetector: onDown: action = 0
TestGestureDetector: onSingleTapUp: 1
TestGestureDetector: onDoubleTap: action = 0
TestGestureDetector: onDoubleTapEvent: action = 0
TestGestureDetector: onDown: action = 0
TestGestureDetector: onDoubleTapEvent: action = 1

我們可以看到現在一次單擊則會觸發onDownonSingleTapUponSingleTapConfirmed 這三個回調。
一次長按單擊則會觸發onDownonShowPressonSingleTapUponSingleTapConfirmed 這四個回調。
一次雙擊則會一次觸發onDownonSingleTapUponDoubleTaponDoubleTapEventonDownonDoubleTapEvent 這六個回調。

而我們在屏幕上快速滑動時,則會觸發 onDownonShowPressonScrollonScrollonFling這五個回調,onShowPress 取決於你在按下和開始滑動之前的時間間隔,短的話就不會有, 是否有 onFling 取決於滑動的距離和速度

TestGestureDetector: onDown: action = 0
TestGestureDetector: onShowPress:
TestGestureDetector: onScroll: e1.action = 0, e2.action = 2
TestGestureDetector: onScroll: e1.action = 0, e2.action = 2
TestGestureDetector: onFling: e1.action = 0, e2.action = 1

下面我們來統一介紹下這些回調具體的含義把:

方法名 描述 所屬接口
onDown 觸摸View的瞬間,由一個 DOWN 觸發 OnGestureListener
onShowPress 觸摸View未鬆開或者滑動時觸發 OnGestureListener
onSingleTapUp 觸摸後鬆開,在onDown的基礎上加了個 UP 事件,
屬於單擊行爲
OnGestureListener
onScroll 按下並拖動,由一個 DOWN 和 多個 MOVE 組成,
屬於拖動行爲
OnGestureListener
onLongPress 長按事件 OnGestureListener
onFling 快速滑動後鬆開,需要滑動一定的距離 OnGestureListener
onSingleTapConfirmed 嚴格的單擊行爲,onSingleTapUp之後只能是onSingleTapConfirmed 或 onDoubleTap 中 的一個 OnDoubleTapListener
onDoubleTap 雙擊行爲,和 onSingleTapConfirmed 不共存 OnDoubleTapListener
onDoubleTapEvent 表示雙擊行爲的發生,
一次雙擊行爲會觸發多次onDoubleTapEvent
OnDoubleTapListener

Scroller

Scroller 用於實現View的彈性滑動,當我們使用View的 scrollToscrollBy 方法進行滑動時,滑動時瞬間完成的,沒有過渡效果使得用戶體驗不好,這個時候就可以使用 Scroler 來解決這一用戶體驗差的問題。
Scroller本身無法讓View彈性滑動,需要配合View的 computeScroll 方法。

那如果使用Scroller呢? 它的典型代碼是固定的,如下所示。
至於爲什麼能夠實現,我們下篇文章介紹 View的滑動 的時候再具體分析。

public class TestScroller extends TextView {
    private static final String TAG = "TestScroller";
    Scroller mScroller;
    public TestScroller(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
    }
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }
    public void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int scrollY = getScrollY();
        int deltaX = destX - scrollX;
        int deltaY = destY - scrollY;
        mScroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000);
        invalidate();
    }
}
//activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <com.lxk.viewdemo.TestScroller
        android:id="@+id/tv"
        android:layout_width="320dp"
        android:layout_height="320dp"
        android:layout_margin="8dp"
        android:background="@color/colorAccent"
        android:gravity="center"
        android:padding="8dp"
        android:text="Hello World!" />
</LinearLayout>
//MainActivity.java
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final TestScroller scroller = findViewById(R.id.tv);
        scroller.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                scroller.smoothScrollTo(200, 200);
            }
        });
}

運行看看,可以看到點擊之後,內容在 1s 內往左上方各平移了 200px
Scroll


如果覺得不錯的話,請幫忙點個讚唄。

以上


掃描下面的二維碼,關注我的公衆號 Android1024, 點關注,不迷路。
Android1024

`

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章