Android長按及拖動事件探究

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 #setLongClickable(boolean)
*/
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)) {
// This is a second tap
mIsDoubleTapping = true;
// Give a callback with the first tap of the double-tap
handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
// Give a callback with down event of the double-tap
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
} else {
// This is a first tap
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) {
// Give the move events of the double-tap
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) {
// Finally, give the up event of the double-tap
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) {
// A fling must travel the minimum tap distance
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();
}
// Hold the event we obtained above - listeners may have changed the original.
mPreviousUpEvent = currentUpEvent;
if (mVelocityTracker != null) {
// This may have been cleared when we called out to the
// application above.
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件事:

  1. 判斷雙擊事件(咱們不關心是否雙擊,由於沒有設置這個監聽器,也不是本文討論的重點);
  2. 進行初始化操做.包括:
    • mDownFocusX = mLastFocusX = focusX;
      mDownFocusY = mLastFocusY = focusY; // 記錄焦點座標,用於判斷在按下的過程當中是否發生了手指的移動
    • mAlwaysInTapRegion = true; // 按下了相應的區域,判斷單擊事件並制定後來的事件響應機制
    • mAlwaysInBiggerTapRegion = true; // 按下了相應的大區域,判斷雙擊事件
    • mStillDown = true; // 用於判斷用戶是輕輕觸摸了一下仍是一直按下
    • mInLongPress = false; // 判斷是否正在長按
    • mDeferConfirmSingleTap = false; // 用於處理是不是一次TAP事件
  3. 經過發送延時消息來判斷觸不觸發長按事件:
      
      
      
      
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. 若是正在長按或者是在上下文中點擊,則跳出循環;

            
            
            
            
    1
    2
    3
            
            
            
            
    if (mInLongPress || mInContextClick) {
    break;
    }
  2. 判斷是否是雙擊事件(mAlwaysInTapRegion);

  3. 若是不是雙擊事件,則判斷是否是還在以前觸摸的那個區域(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); // 移除全部觸摸相關的消息事件
  4. 若是以上兩項都不符合,那麼則肯定爲滾動事件,並重置焦點:
    • 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();
// Default long press time threshold.
private static final long LONG_PRESS_TIME_THRESHOLD = 500;
// Long press event message handler.
private Handler mHandler = new Handler();
// The long press time threshold.
private long mPressTimeThreshold;
// Record start point and end point to judge whether user has moved while performing long press event.
private DoublePoint mTouchStartPoint = new DoublePoint();
private DoublePoint mTouchEndPoint = new DoublePoint();
// The long press thread.
private final LongPressThread mLongPressThread = new LongPressThread();
// Inset in pixels to look for touchable content when the user touches the edge of the screen.
private final float mTouchSlop;
// The long press callback.
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 user is pressing and dragging, then we make a callback.
if (mLongPressThread.mLongPressing) {
resetLongPressEvent();
if (listener != null) {
return listener.onLongPressed(event);
}
break;
}
// If user has moved before activating long press event, then the event should be reset.
if (calculateDistanceBetween(mTouchStartPoint, mTouchEndPoint) > mTouchSlop) {
resetLongPressEvent();
}
break;
case MotionEvent.ACTION_UP:
if (mLongPressThread.mLongPressing) {
resetLongPressEvent();
// Must set true and left the child know we have handled this event.
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 {
// A flag to set whether the long press event happens.
boolean mLongPressing = false;
// A flag to set whether this thread has been added to the handler.
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.OnDragListenerView.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");
// Do nothing
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");
// Do nothing
break;
case DragEvent. ACTION_DROP:
Log.d(TAG, "ACTION_DROP event");
// Do nothing
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);
// Instantiates the drag shadow builder.
View.DragShadowBuilder myShadow = new View.DragShadowBuilder(v);
// Starts the drag
v.startDrag(dragData, // the data to be dragged
myShadow, // the drag shadow builder
null, // no need to use local data
0 // flags (not currently used, set to 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分離時,進入這個狀態,此狀態只會在不重疊的一瞬間調用一次.

  • DragEvent.ACTION_DROP

當用戶在一個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 user is pressing and dragging, then we make a callback.
if (mLongPressThread.mLongPressing) {
// 註銷下面這行,實現長期監聽.
// resetLongPressEvent();
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

相關文章
相關標籤/搜索