轉載請以鏈接形式標明出處:
本文出自:103style的博客
《Android開發藝術探索》 學習記錄
可以帶着以下問題來看本文:
- View的座標系和座標,平移等動畫改變的是什麼屬性?
- View有哪些事件?
- 如果獲取系統可識別的最短滑動距離?
- 如果計算滑動的速度?
- 單擊、雙擊、長按等事件的監聽?
- 彈性滑動的實現?
目錄
- View 與 ViewGroup
- View 的位置參數
- MotionEvent 和 TouchSlop
- VelocityTracker
- GestureDetector
- Scroller
View與ViewGroup
public class View
extendsObject
implementsDrawable.Callback
,KeyEvent.Callback
,AccessibilityEventSource
java.lang.Object
↳android.view.View
Known direct subclasses
AnalogClock
,ImageView
,KeyboardView
,MediaRouteButton
,ProgressBar
,Space
,SurfaceView
,TextView
,TextureView
,ViewGroup
,ViewStub
.
Known indirect subclasses
AbsListView
,AbsSeekBar
,AbsSpinner
,AbsoluteLayout
,AutoCompleteTextView
,Button
,CalendarView
,CheckBox
,CheckedTextView
,Chronometer
,and 57 others..
public abstract class
ViewGroup
extendsView
implementsViewParent
,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的座標系如下圖:
左上角爲原點O(0,0),X、Y軸分別向右向下遞增。
圖中 View 和 ViewGroup 的位置由其四個頂點決定,以View爲例,分別對應四個屬性:Left
、Top
、Right
、Bottom
.
所以 Width = Right - Left
, Height = Bottom - Top
.
在 Android 3.0
開始,View又增加了 x
、y
、translationX
、translationY
四個參數。
x
、y
即爲上圖中的A點,分別對應A點在View座標系中的X、Y軸上的座標。
translationX
、translationY
則爲相對於父容器ViewGroup的偏移量,默認爲 0
。
他們的關係爲: x = left + tranlastionX
、y = 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.OnGestureListener
和 GestureDetector.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 = 0 爲 DOWN 事件
action = 1 爲 UP 事件
action = 2 爲 MOVE 事件
運行程序,我們執行一次單擊,一次長按單擊,然後雙擊一次,發下打印日誌如下:
//第一次單擊
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
通過上面的日誌信息我們可以知道 :
一次 單擊 和 長按單擊 操作會觸發 onDown
、onShowPress
、onLongPress
三個回調。
雙擊 操作則會依次觸發 onDown
、onShowPress
、onDown
、onShowPress
、onLongPress
五次回調。
顯示單擊出現 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
我們可以看到現在一次單擊則會觸發onDown
、onSingleTapUp
、onSingleTapConfirmed
這三個回調。
一次長按單擊則會觸發onDown
、onShowPress
、onSingleTapUp
、onSingleTapConfirmed
這四個回調。
一次雙擊則會一次觸發onDown
、onSingleTapUp
、onDoubleTap
、onDoubleTapEvent
、onDown
、onDoubleTapEvent
這六個回調。
而我們在屏幕上快速滑動時,則會觸發 onDown
、onShowPress
、onScroll
、onScroll
、onFling
這五個回調,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的 scrollTo
、scrollBy
方法進行滑動時,滑動時瞬間完成的,沒有過渡效果使得用戶體驗不好,這個時候就可以使用 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
。
如果覺得不錯的話,請幫忙點個讚唄。
以上
掃描下面的二維碼,關注我的公衆號 Android1024, 點關注,不迷路。
`