Android中長按拖動仍是比較常見的.好比Launcher中的圖標拖動及屏幕切換,ListView中item順序的改變,新聞類App中新聞類別的順序改變等.下面就這個事件作一下分析.php
就目前而言,Android中實現長按事件響應有幾種方式,包括:html
- 設置View.OnLongClickListener監聽器
- 經過GestureDetector.OnGestureListener間接獲取長按事件
- 實現View.OnTouchListener,而後在回調中經過MotionEvent判斷是否觸發了長按事件
下面分別介紹這三種方式.java
View.OnLongClickListener
對於Android中的任何一個View,均可以實現長按事件監聽,並回調這個事件.在View
類裏,定義了OnLongClickListener
.android
1
2
3
4
5
6
7
8
9
10
11
12
13
|
* Interface definition for a callback to be invoked when a view has been clicked and held.
*/
public
interface {
* Called when a view has been clicked and held.
*
*
@param v The view that was clicked and held.
*
*
@return true if the callback consumed the long click, false otherwise.
*/
boolean onLongClick(View v);
}
|
默認狀況下,View類是不支持長按的,由LONG_CLICKABLE
這個標記控制.若是設置了監聽器,則會默認打開支持長按的開關,並回調上面的boolean onLongClick(View v)
方法.從註釋的返回值中能夠看到,若是這個回調消費了長按事件,則返回true
,不然返回false
.這和View
類中的各類觸摸事件傳遞是一致的.git
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/**
* Register a callback
to be invoked when this view
is clicked
and held. If this view
is
not
* long clickable,
it becomes long clickable.
*
* @param l The callback
that will
run
*
* @see
*/
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(
true);
}
getListenerInfo().mOnLongClickListener = l;
}
|
其中, getListenerInfo()
返回一個包含了一個View類中全部的監聽器事件的靜態內部類ListenerInfo
.github
簡單實例
1
2
3
4
5
6
7
8
9
|
ImageView imageView =
new ImageView(
this);
imageView.setImageResource(R.drawable.ic_launcher);
imageView.setOnLongClickListener(
new View.OnLongClickListener() {
public
boolean onLongClick(View v) {
Log.v(TAG,
"perform long click.");
return
false;
}
});
|
GestureDetector.OnGestureListener
GestureDetector
提供了豐富的手勢識別功能.除了支持長按事件監聽外,還支持多種手勢事件監聽.在GestureDetector.OnGestureListener
這個監聽器中,提供了6種手勢監聽回調:ruby
- boolean onDown(MotionEvent e);
- void onShowPress(MotionEvent e);
- boolean onSingleTapUp(MotionEvent e);
- boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
- void onLongPress(MotionEvent e);
- boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
幾乎包含了一次界面觸摸操做所能想到的全部操做.其中,能夠經過void onLongPress(MotionEvent e)
來實現長按監聽.併發
簡單實例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
package com.amap.mock.activity;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import com.amap.mock.R;
* Created by xingli on 8/19/15.
*
* An example of performing click event.
*/
public
class GestureActivity extends Activity implements GestureDetector.OnGestureListener {
private
static
final String TAG = GestureActivity.class.getSimpleName();
private GestureDetector gestureDetector;
protected
void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
gestureDetector =
new GestureDetector(
this,
this);
}
public
boolean onTouchEvent(MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
public
boolean onDown(MotionEvent e) {
Log.v(TAG,
"onDown");
return
false;
}
public
void onShowPress(MotionEvent e) {
Log.v(TAG,
"onShowPress");
}
@Override
public
boolean onSingleTapUp(MotionEvent e) {
Log.v(TAG,
"onSingleTapUp");
return
false;
}
@Override
public
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
Log.v(TAG,
"onScroll");
return
false;
}
@Override
public
void onLongPress(MotionEvent e) {
Log.v(TAG,
"onLongPress");
}
@Override
public
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.v(TAG,
"onFling");
return
false;
}
}
|
GestureDetector長按事件原理解析
在上面的例子中,咱們看到在GestureDetector
這個類中,實現了onTouchEvent()
方法,直接代替View
類中的onTouchEvent()
方法,便可實現觸摸事件的檢測.下面是GestureDetector.onTouchEvent()
的部分關鍵源碼:app
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
|
public
boolean onTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier !=
null) {
mInputEventConsistencyVerifier.onTouchEvent(ev,
0);
}
final
int action = ev.getAction();
...
boolean handled =
false;
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.
ACTION_POINTER_DOWN:
...
break;
case MotionEvent.
ACTION_POINTER_UP:
...
break;
case MotionEvent.
ACTION_DOWN:
if (mDoubleTapListener !=
null) {
boolean hadTapMessage = mHandler.hasMessages(TAP);
if (hadTapMessage) mHandler.removeMessages(TAP);
if ((mCurrentDownEvent !=
null) && (mPreviousUpEvent !=
null) && hadTapMessage &&
isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
mIsDoubleTapping =
true;
handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
}
else {
mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
}
}
mDownFocusX = mLastFocusX = focusX;
mDownFocusY = mLastFocusY = focusY;
if (mCurrentDownEvent !=
null) {
mCurrentDownEvent.recycle();
}
mCurrentDownEvent = MotionEvent.obtain(ev);
mAlwaysInTapRegion =
true;
mAlwaysInBiggerTapRegion =
true;
mStillDown =
true;
mInLongPress =
false;
mDeferConfirmSingleTap =
false;
if (mIsLongpressEnabled) {
mHandler.removeMessages(LONG_PRESS);
mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
+ TAP_TIMEOUT + LONGPRESS_TIMEOUT);
}
mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
handled |= mListener.onDown(ev);
break;
case MotionEvent.
ACTION_MOVE:
if (mInLongPress || mInContextClick) {
break;
}
final
float scrollX = mLastFocusX - focusX;
final
float scrollY = mLastFocusY - focusY;
if (mIsDoubleTapping) {
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
}
else
if (mAlwaysInTapRegion) {
final
int deltaX = (
int) (focusX - mDownFocusX);
final
int deltaY = (
int) (focusY - mDownFocusY);
int distance = (deltaX * deltaX) + (deltaY * deltaY);
if (distance > mTouchSlopSquare) {
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
mLastFocusX = focusX;
mLastFocusY = focusY;
mAlwaysInTapRegion =
false;
mHandler.removeMessages(TAP);
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
}
if (distance > mDoubleTapTouchSlopSquare) {
mAlwaysInBiggerTapRegion =
false;
}
}
else
if ((Math.abs(scrollX) >=
1) || (Math.abs(scrollY) >=
1)) {
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
mLastFocusX = focusX;
mLastFocusY = focusY;
}
break;
case MotionEvent.
ACTION_UP:
mStillDown =
false;
MotionEvent currentUpEvent = MotionEvent.obtain(ev);
if (mIsDoubleTapping) {
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
}
else
if (mInLongPress) {
mHandler.removeMessages(TAP);
mInLongPress =
false;
}
else
if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
handled = mListener.onSingleTapUp(ev);
if (mDeferConfirmSingleTap && mDoubleTapListener !=
null) {
mDoubleTapListener.onSingleTapConfirmed(ev);
}
}
else
if (!mIgnoreNextUpEvent) {
final VelocityTracker velocityTracker = mVelocityTracker;
final
int pointerId = ev.getPointerId(
0);
velocityTracker.computeCurrentVelocity(
1000, mMaximumFlingVelocity);
final
float velocityY = velocityTracker.getYVelocity(pointerId);
final
float velocityX = velocityTracker.getXVelocity(pointerId);
if ((Math.abs(velocityY) > mMinimumFlingVelocity)
|| (Math.abs(velocityX) > mMinimumFlingVelocity)){
handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
}
}
if (mPreviousUpEvent !=
null) {
mPreviousUpEvent.recycle();
}
mPreviousUpEvent = currentUpEvent;
if (mVelocityTracker !=
null) {
mVelocityTracker.recycle();
mVelocityTracker =
null;
}
mIsDoubleTapping =
false;
mDeferConfirmSingleTap =
false;
mIgnoreNextUpEvent =
false;
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
break;
case MotionEvent.
ACTION_CANCEL:
cancel();
break;
}
if (!handled && mInputEventConsistencyVerifier !=
null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev,
0);
}
return handled;
}
|
在onTouchEvent()
方法中,很明顯是經過Handler來傳遞觸摸事件並觸發相關的回調的.由於Handler是經過一個串行的隊列來處理消息的,能夠防止併發觸摸操做時產生行爲邏輯的混亂.在此方法中,能夠看到對MotionEvent事件的處理,就長按事件來講,分爲:less
- MotionEvent.ACTION_DOWN
- MotionEvent.ACTION_MOVE
- MotionEvent.ACTION_UP
在MotionEvent.ACTION_DOWN
階段,程序作了3件事:
- 判斷雙擊事件(咱們不關心是否雙擊,由於沒有設置這個監聽器,也不是本文討論的重點);
- 進行初始化操做.包括:
- mDownFocusX = mLastFocusX = focusX;
mDownFocusY = mLastFocusY = focusY; // 記錄焦點座標,用於判斷在按下的過程當中是否發生了手指的移動
- mAlwaysInTapRegion = true; // 按下了相應的區域,判斷單擊事件並制定後來的事件響應機制
- mAlwaysInBiggerTapRegion = true; // 按下了相應的大區域,判斷雙擊事件
- mStillDown = true; // 用於判斷用戶是輕輕觸摸了一下仍是一直按下
- mInLongPress = false; // 判斷是否正在長按
- mDeferConfirmSingleTap = false; // 用於處理是不是一次
TAP
事件
- 經過發送延時消息來判斷觸不觸發長按事件:
1
2
3
4
5
|
if (mIsLongpressEnabled) {
mHandler
.removeMessages(LONG_PRESS);
mHandler
.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
+ TAP_TIMEOUT + LONGPRESS_TIMEOUT);
}
|
默認的TAP_TIMEOUT
是100ms,LONGPRESS_TIMEOUT
是500ms.這兩個參數在ViewConfiguration.java
類中有定義,而且暫時不提供API更改觸發值.
1
2
3
4
5
6
7
8
9
10
11
12
|
* Defines the default duration in milliseconds before a press turns into
* a long press
*/
private
static
final
int DEFAULT_LONG_PRESS_TIMEOUT =
500;
* Defines the duration in milliseconds we will wait to see if a touch event
* is a tap or a scroll. If the user does not move within this interval, it is
* considered to be a tap.
*/
private
static
final
int TAP_TIMEOUT =
100;
|
接下來,在MotionEvent.ACTION_MOVE
階段,程序判斷比較簡單.
若是正在長按或者是在上下文中點擊,則跳出循環;
1
2
3
|
if (mInLongPress
|| mInContextClick) {
break;
}
|
判斷是否是雙擊事件(mAlwaysInTapRegion
);
- 若是不是雙擊事件,則判斷是否是還在以前觸摸的那個區域(
mAlwaysInTapRegion
);若是是,因爲觸發了ACTION_MOVE
事件,那麼說明手指已經移動過了N個單位距離,這時候,須要判斷這個距離是否是大於某個閾值mTouchSlopSquare
,其中
mTouchSlopSquare=configuration.getScaledTouchSlop()^2
,
在配置文件中默認值爲8dip
.若是大於這個閾值,則說明移動確實發生了,這時候:
- handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); //設置滾動監聽回調
- mLastFocusX = focusX;
- mLastFocusY = focusY; // 從新設置觸摸焦點
- mAlwaysInTapRegion = false; // 重置觸摸區域判斷
- mHandler.removeMessages(TAP);
- mHandler.removeMessages(SHOW_PRESS);
- mHandler.removeMessages(LONG_PRESS); // 移除全部觸摸相關的消息事件
- 若是以上兩項都不符合,那麼則肯定爲滾動事件,並重置焦點:
- handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
- mLastFocusX = focusX;
- mLastFocusY = focusY;
處理完成移動事件後,到了MotionEvent.ACTION_UP
階段,程序主要判斷當前處於哪一個階段,而後分別針對這個階段作事件清除,資源回收,重置各類觸摸狀態.因爲程序比較簡單,就再也不詳細分析這個階段的消息了.
View.OnTouchListener
看了GestureDetector.onTouchEvent
的源碼後,是否是以爲長按事件檢測與處理很簡單?接下來的這種方法就是借鑑了第二種方法來實現的,主要原理就是利用Handler發送延時消息來判斷是否是觸發了長按事件.不過我是在MotionEvent.ACTION_MOVE
階段來判斷長按事件,這樣作的緣由留給後面來分析.先看代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
|
package com.amap.mock.activity;
import android.os.Handler;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
* Created by xingli on 9/7/15.
*
* A long press event detector.
*/
public
class LongPressHandler implements View.OnTouchListener {
private
static
final String TAG = LongPressHandler2.class.getSimpleName();
private
static
final
long LONG_PRESS_TIME_THRESHOLD =
500;
private Handler mHandler =
new Handler();
private
long mPressTimeThreshold;
private DoublePoint mTouchStartPoint =
new DoublePoint();
private DoublePoint mTouchEndPoint =
new DoublePoint();
private
final LongPressThread mLongPressThread =
new LongPressThread();
private
final
float mTouchSlop;
private OnLongPressListener listener;
public LongPressHandler2(View view) {
this(view, LONG_PRESS_TIME_THRESHOLD);
}
public LongPressHandler2(View view, long holdTime) {
view.setOnTouchListener(
this);
mTouchSl
大專欄
Android長按及拖動事件探究op = ViewConfiguration.get(view.getContext()).getScaledEdgeSlop();
Log.v(TAG,
"touch slop:" + mTouchSlop);
mPressTimeThreshold = holdTime;
}
@Override
public
boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mTouchStartPoint.x = event.getRawX();
mTouchStartPoint.y = event.getRawY();
addLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
mTouchEndPoint.x = event.getRawX();
mTouchEndPoint.y = event.getRawY();
if (mLongPressThread.mLongPressing) {
resetLongPressEvent();
if (listener !=
null) {
return listener.onLongPressed(event);
}
break;
}
if (calculateDistanceBetween(mTouchStartPoint, mTouchEndPoint) > mTouchSlop) {
resetLongPressEvent();
}
break;
case MotionEvent.ACTION_UP:
if (mLongPressThread.mLongPressing) {
resetLongPressEvent();
return
true;
}
default:
resetLongPressEvent();
break;
}
return
false;
}
public
void setOnLongPressListener(OnLongPressListener listener) {
this.listener = listener;
}
* Reset the long press event.
*/
private
void resetLongPressEvent() {
if (mLongPressThread.mAdded) {
mHandler.removeCallbacks(mLongPressThread);
mLongPressThread.mAdded =
false;
}
mLongPressThread.mLongPressing =
false;
}
* Add long press event handler.
*/
private
void addLongPressCallback() {
if (!mLongPressThread.mAdded) {
mLongPressThread.mLongPressing =
false;
mHandler.postDelayed(mLongPressThread, mPressTimeThreshold);
mLongPressThread.mAdded =
true;
}
}
* Calculate distance between two point.
*
*
@param before previous point
*
@param after next point
*
@return the distance
*/
private
double calculateDistanceBetween(DoublePoint before, DoublePoint after) {
return Math.sqrt(Math.pow((
before.x -
after.x),
2) + Math.pow((
before.y -
after.y),
2));
}
* Judge whether the long press event happens.
*
* The time threshold of default activated event is {
@see #LONG_PRESS_TIME_THRESHOLD}
*/
private
static
class LongPressThread implements Runnable {
boolean mLongPressing =
false;
boolean mAdded =
false;
@Override
public
void run() {
mLongPressing =
true;
}
}
private
static
class DoublePoint {
public
double x;
public
double y;
}
* The long press listener.
*/
public
interface OnLongPressListener {
* Notified when a long press occurs with the initial on down {
@link MotionEvent} that trigged it.
*/
boolean onLongPressed(MotionEvent event);
}
}
|
這段代碼可能沒有GestureDetector
這個類寫的這麼規範和完整,但至少可以實現長按觸發而且實現事件的回調.使用這個類有如下兩個限制:
- 在
LongPressHandler
這個類的構造函數中,設置了OnTouchListener
監聽器,所以若是這個View在其餘地方也設置了一樣的監聽器,有可能不起做用,以最後一個初始化該監聽器的類其做用爲標準;
- 必須設置
View
爲可點擊的.即View.setClickable(true)
.顯然,不可點擊的話就沒有長按事件了.
長按事件小結
通過上面的分析,咱們經過三種方式實現了長按事件的檢測及事件回調處理,分別是View.OnLongClickListener
,GestureDetector.OnGestureListener
以及View.OnTouchListener
.
若是僅僅是考慮長按事件,那麼直接設置View.OnLongClickListener
監聽器是最方便的實現;若是須要監聽多種觸摸事件,那麼顯然GestureDetector.OnGestureListener
是理想的選擇,而且在GestureDetector
類內部已經實現了一個簡單的監聽器實現GestureDetector.SimpleOnGestureListener
,這個類沒有實現任何功能,須要子類覆蓋相應的方法來響應事件回調;若是要實現長按拖拽呢,顯然以上兩個類是沒有辦法知足要求的,所以,擴展View.OnTouchListener
類是個不錯的選擇,在文章最後,會介紹如何擴展來實現長按拖拽事件.
Android Drag Event
拖拽事件和長按事件同樣,是直接獲得View
類支持的.在View.ListenerInfo
類中,定義了View.OnDragListener
監聽器,不過須要配合上邊的View.OnLongClickListener
來使用,不然單單有這個監聽器是不起做用的.目前,實現拖拽事件的方法有兩種:
- 設置
View.OnDragListener
和View.OnLongClickListener
監聽器,在長按事件響應時開始拖拽,經過回調判斷拖拽事件
- 經過
View.layout(int,int,int,int)
方法直接修改View的位置
View.OnDragListener
先來看看View.OnDragListener
的定義:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
* Interface definition for a callback to be invoked when a drag is being dispatched
* to this view. The callback will be invoked before the hosting view's own
* onDrag(event) method. If the listener wants to fall back to the hosting view's
* onDrag(event) behavior, it should return 'false' from this callback.
*/
public
interface OnDragListener {
* Called when a drag event is dispatched to a view. This allows listeners
* to get a chance to override base View behavior.
*
*
@param v The View that received the drag event.
*
@param event The {
@link android.view.DragEvent} object for the drag event.
*
@return {
@code true} if the drag event was handled successfully, or {
@code false}
* if the drag event was not handled. Note that {
@code false} will trigger the View
* to call its {
@link #onDragEvent(DragEvent) onDragEvent()} handler.
*/
boolean onDrag(View v, DragEvent event);
}
|
恩,雖然僅僅是個接口,然而有許多注意事項.這個接口會在屏幕響應拖拽事件時調用,而且會在View類中的View.onDrag(event)
方法以前調用.若是須要系統繼續調用View.onDrag(event)
方法,那麼這個監聽器回調應該返回false
,讓事件傳遞到下一層.
前面說了,僅僅設置View.OnDragListener
監聽器是不夠的,由於系統並不會主動去觸發這個事件監聽,而是經過View.startDrag(ClipData, DragShadowBuilder, Object, int)
這個方法,這個方法會在View類的頂層根視圖ViewRootImpl
中處理拖拽事件,注意,ViewRootImpl
並不是繼承自View
.下面是一個簡單的例子.
簡單實例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
package com.amap.mock.activity;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipDescription;
import android.os.Bundle;
import android.util.Log;
import android.view.DragEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import com.amap.mock.R;
* Created by xingli on 9/9/15.
*
* An example of performing drag event.
*/
public
class DragActivity extends Activity implements View.OnDragListener, View. {
private
static
final String TAG = DragActivity.
class.getSimpleName();
private ImageView mIvLogo;
@Override
protected
void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
mIvLogo = (ImageView) findViewById(R.id.iv_logo);
mIvLogo.setClickable(
true);
mIvLogo.setOnLongClickListener(
this);
mIvLogo.setOnDragListener(
this);
}
@Override
public
boolean onDrag(View v, DragEvent event) {
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) v.getLayoutParams();
switch (event.getAction()) {
case DragEvent.
ACTION_DRAG_STARTED:
Log.d(TAG,
"Action is DragEvent.ACTION_DRAG_STARTED");
break;
case DragEvent.
ACTION_DRAG_ENTERED:
Log.d(TAG,
"Action is DragEvent.ACTION_DRAG_ENTERED");
break;
case DragEvent.
ACTION_DRAG_EXITED:
Log.d(TAG,
"Action is DragEvent.ACTION_DRAG_EXITED");
break;
case DragEvent.
ACTION_DRAG_LOCATION:
Log.d(TAG,
"Action is DragEvent.ACTION_DRAG_LOCATION");
break;
case DragEvent.
ACTION_DRAG_ENDED:
Log.d(TAG,
"Action is DragEvent.ACTION_DRAG_ENDED");
break;
case DragEvent.
ACTION_DROP:
Log.d(TAG,
"ACTION_DROP event");
break;
default:
break;
}
return
true;
}
@Override
public
boolean onLongClick(View v) {
ClipData.Item item =
new ClipData.Item(
"");
String[] mimeTypes = { ClipDescription.MIMETYPE_TEXT_PLAIN };
ClipData dragData =
new ClipData(
"", mimeTypes, item);
View.DragShadowBuilder myShadow =
new View.DragShadowBuilder(v);
v.startDrag(dragData,
myShadow,
null,
0
);
return
true;
}
}
|
首先須要監聽長按事件,而後在觸發長按事件後,即可以開始拖動了.拖動的時候會回調View.onDrag()
方法.其中,在DragEvent
中定義了幾個動做,表示拖動過程:
DragEvent.ACTION_DRAG_STARTED
調用View.startDrag()
並得到拖動的陰影后進入這個階段
DragEvent.ACTION_DRAG_ENTERED
系統會把帶有這個類型的拖拽事件發送給當前佈局中全部的View對象的拖拽事件監聽器,若是要繼續接收拖拽事件,包括可能的放下事件,View對象的拖拽事件監聽器必須返回true.
DragEvent.ACTION_DRAG_LOCATION
當接收到ACTION_DRAG_ENTERED
事件,而且拖動的影子與原來的View還有重疊的區域時,進入這個狀態,只要還在拖動而且符合要求,則這個狀態是會被調用屢次的.
DragEvent.ACTION_DRAG_EXITED
當接收到ACTION_DRAG_ENTERED
事件及至少一次ACTION_DRAG_LOCATION
事件,而且拖動的影子與原來的View沒有重疊的區域,即影子與View分離時,進入這個狀態,此狀態只會在不重疊的一瞬間調用一次.
當用戶在一個View對象之上釋放了拖拽影子,這個對象的拖拽事件監聽器就會收到這種操做類型。若是這個監聽器在響應ACTION_DRAG_STARTED
拖拽事件中返回了true,那麼這種操做類型只會發送給一個View對象。若是用戶在沒有被註冊監聽器的View對象上釋放了拖拽影子,或者用戶沒有在當前佈局的任何部分釋放操做影子,這個操做類型就不會被髮送。若是View對象成功的處理放下事件,監聽器要返回true,不然應該返回false。
DragEvent.ACTION_DRAG_ENDED
當系統結束拖拽操做時,View對象拖拽監聽器會接收這種事件操做類型。這種操做類型以前不必定是ACTION_DROP
事件。若是系統發送了一個ACTION_DROP
事件,那麼接收ACTION_DRAG_ENDED
操做類型不意味着放下操做成功了。監聽器必須調用getResult()方法來得到響應ACTION_DROP
事件中的返回值。若是ACTION_DROP
事件沒有被髮送,那麼getResult()會返回false。
View.layout(int,int,int,int)
上面的方法有個致命的弱點,那就是圖標沒辦法放到指定拖動的點,而只能實現拖動的效果.由於Android系統在設計的時候,View.OnDragListener
並非用來進行圖標拖動的,而是文字的複製粘貼,咱們只是強行地將它做用於圖標的拖拽.但上面這種方案也是能夠解決這個問題的,那就是先移除原來的圖標,而後再在新的位置重繪圖標,不過這略顯麻煩了,對於內存吃緊的Android系統來講,這無疑是雪上加霜.
下面咱們經過從新佈局圖標的Layout來實現拖動效果.要實現這種效果,就要用到上面介紹的長按事件中的第三種方案,採用設置View.OnTouchListener
監聽器來監聽觸摸事件,並在MotionEvent.ACTION_MOVE
中處理拖動.
對於上面的LongPressHandler
類,還須要作小小的修改,由於上面這個類觸發了一次長按回調後,就順便移除了這個回調,後面的觸摸事件就接收不了監聽了.解決方案也很簡單,在下面這段代碼中,把移除監聽註銷掉就能夠了.
1
2
3
4
5
6
7
8
9
10
11
12
|
case MotionEvent.ACTION_MOVE:
mTouchEndPoint.x =
event.getRawX();
mTouchEndPoint.y =
event.getRawY();
if (mLongPressThread.mLongPressing) {
if (listener !=
null) {
return listener.onLongPressed(
event);
}
break;
}
|
而後,咱們在主類中調用這個方法的回調,在回調裏進行圖標的拖拽.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
package com.amap.mock.activity;
import android.app.Activity;
import android.os.Bundle;
import android.view.Gravity;
import android.view.MotionEvent;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.amap.mock.R;
* Created by xingli on 9/9/15.
*
* An example of performing long press and drag event.
*/
public
class DragActivity extends Activity {
private
static
final String TAG = DragActivity.class.getSimpleName();
private ImageView mIvLogo;
private LongPressHandler longPressHandler;
private
int statusBarHeight;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
mIvLogo = (ImageView) findViewById(R.id.iv_logo);
mIvLogo.setClickable(
true);
setDragEnable(
true);
}
public void setDragEnable(boolean enable) {
if (enable) {
if (longPressHandler ==
null) {
longPressHandler =
new LongPressHandler(mIvLogo);
}
longPressHandler.setOnLongPressListener(
new LongPressHandler.OnLongPressListener() {
@Override
public boolean onLongPressed(MotionEvent event) {
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mIvLogo.getLayoutParams();
params.gravity = Gravity.TOP | Gravity.LEFT;
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int x = (
int) event.getRawX();
int y = (
int) event.getRawY();
int width = mIvLogo.getMeasuredWidth();
int height = mIvLogo.getMeasuredHeight();
int l = x - width /
2;
int r = x + width /
2;
int t = y - height /
2 - getStatusBarHeight();
int b = y + height /
2 - getStatusBarHeight();
params.leftMargin = l;
params.topMargin = t;
mIvLogo.layout(l, t, r, b);
return
true;
}
return
false;
}
});
}
else {
if (longPressHandler !=
null) {
longPressHandler.setOnLongPressListener(
null);
longPressHandler =
null;
}
}
}
* Get the status bar height.
*
*
@return the height
*/
public int getStatusBarHeight() {
if (statusBarHeight ==
0) {
int resourceId = getResources().getIdentifier(
"status_bar_height",
"dimen",
"android");
if (resourceId >
0) {
statusBarHeight = getResources().getDimensionPixelSize(resourceId);
}
}
return statusBarHeight;
}
}
|
其中,Activity的佈局:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id=
"@+id/container"
android:layout_width=
"fill_parent"
android:layout_height=
"fill_parent"
android:orientation=
"vertical">
<ImageView
android:id=
"@+id/iv_logo"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:src=
"@drawable/ic_launcher" />
</FrameLayout>
|
這段代碼的關鍵思想在於,在拖動時,動態計算當前位置的座標,而後調用View.layout()
方法對這個View從新佈局.有幾個須要注意的地方:
- 記得算上動態欄的高度,能夠經過
getResources().getDimensionPixelSize(resourceId);
獲得某個資源的像素值;
- 須要計算圖像的中心點,不然移動的距離以你圖標的左上角來計算;
- 在上面的源碼中,我還作了一件事,就是獲取圖標的佈局,從新設置Margin值.其實若是你除了拖動之外不進行別的操做的話,不必進行這樣的設置.但若是你須要重繪這個按鈕的話,那麼重繪的時候是按照舊的LayoutParams來繪製的,這樣會形成圖標又回到了原來的地方.我在設置FloatingActionButton的時候就遇到了這樣的問題,具體代碼可參考Github源碼.
- 若是不想長按拖動,而是直接拖動,那麼修改長按觸發閾值
LONG_PRESS_TIME_THRESHOLD
,或者經過public LongPressHandler(View view, long holdTime)
構造函數來實例化LongPressHandler
就能夠了.
觸摸事件小結
通過上面的分析,咱們經過兩種方式實現了觸摸事件的實現及事件回調處理,分別是View.OnDragListener接口和View.layout()方法.
實際上觸摸事件是和長按事件分不開的,只是觸發時間的長短閾值設置不一樣罷了.在第一種方法中,經過調用View.startDrag()
方法觸發拖拽事件,經過設置View.OnDragListener
設置事件回調,即可以在回調中處理拖拽事件.可是這種方法的應用場景並非圖標拖拽,而是文字的複製粘貼,原始的視圖是不會移動的.通常咱們會經過覆蓋View.onTouchEvent()
或者設置View.OnTouchListener
監聽器來監聽滑動事件,並在MotionEvent.ACTION_MOVE
狀態中處理拖拽問題,這即是第二種方案的思想.
Source Code
長按拖拽的源碼在Github上,歡迎star&fork:)
Reference
http://grepcode.com/file/repo1.maven.org/maven2/org.robolectric/android-all/5.0.0_r2-robolectric-1/android/view/View.java
http://www.ablanxue.com/prone_4213_1.html
http://www.yiibai.com/android/android_drag_and_drop.html
http://developer.android.com/guide/topics/ui/drag-drop.html
http://www.jcodecraeer.com/a/anzhuokaifa/developer/2013/0311/1003.html