Android多點觸摸實現
第一章 摘要
在Linux 內核支持的基礎上, Android 在其 2.0 源碼中加入多點觸摸功能。由此觸摸屏在 Android 的 frameworks 被完全分爲 2 種實現途徑:單點觸摸屏的單點方式,多點觸摸屏的單點和多點方式。
第二章 軟件位
在Linux 的 input.h 中,多點觸摸功能依賴於以下幾個主要的軟件位:
……………………… ..
- define SYN_REPORT 0
- define SYN_CONFIG 1
- define SYN_MT_REPORT 2
……………………… ...
- define ABS_MT_TOUCH_MAJOR 0x30 /* Major axis of touching ellipse */
- define ABS_MT_TOUCH_MINOR 0x31 /* Minor axis (omit if circular) */
- define ABS_MT_WIDTH_MAJOR 0x32 /* Major axis of approaching ellipse */
- define ABS_MT_WIDTH_MINOR 0x33 /* Minor axis (omit if circular) */
- define ABS_MT_ORIENTATION 0x34 /* Ellipse orientation */
- define ABS_MT_POSITION_X 0x35 /* Center X ellipse position */
- define ABS_MT_POSITION_Y 0x36 /* Center Y ellipse position */
- define ABS_MT_TOOL_TYPE 0x37 /* Type of touching device */
- define ABS_MT_BLOB_ID 0x38 /* Group a set of packets as a blob */
…………………………
在Android 中對應的軟件位定義在 RawInputEvent.java 中 :
………………… ..
public class RawInputEvent {
……………… .
public static final int CLASS_TOUCHSCREEN_MT = 0x00000010;
……………… ..
public static final int ABS_MT_TOUCH_MAJOR = 0x30;
public static final int ABS_MT_TOUCH_MINOR = 0x31;
public static final int ABS_MT_WIDTH_MAJOR = 0x32;
public static final int ABS_MT_WIDTH_MINOR = 0x33;
public static final int ABS_MT_ORIENTATION = 0x34;
public static final int ABS_MT_POSITION_X = 0x35;
public static final int ABS_MT_POSITION_Y = 0x36;
public static final int ABS_MT_TOOL_TYPE = 0x37;
public static final int ABS_MT_BLOB_ID = 0x38;
………………… .
public static final int SYN_REPORT = 0;
public static final int SYN_CONFIG = 1;
public static final int SYN_MT_REPORT = 2;
……………… ..
在Android 中,多點觸摸的實現方法在具體的代碼實現中和單點是完全區分開的。在 Android 代碼的 EventHub.cpp 中,單點屏和多點屏由如下代碼段來判定:
int EventHub::open_device(const char *deviceName)
{
………………………
if (test_bit(ABS_MT_TOUCH_MAJOR, abs_bitmask)
&& test_bit(ABS_MT_POSITION_X, abs_bitmask)
&& test_bit(ABS_MT_POSITION_Y, abs_bitmask)) {
device->classes |= CLASS_TOUCHSCREEN | CLASS_TOUCHSCREEN_MT;
// LOGI("It is a multi-touch screen!");
}
//single-touch?
else if (test_bit(BTN_TOUCH, key_bitmask)
&& test_bit(ABS_X, abs_bitmask)
&& test_bit(ABS_Y, abs_bitmask)) {
device->classes |= CLASS_TOUCHSCREEN;
// LOGI("It is a single-touch screen!");
}
……………… ..
}
我們知道,在觸摸屏驅動中,通常在probe 函數中會調用 input_set_abs_params 給設備的input_dev 結構體初始化,這些 input_dev 的參數會在 Android 的 EventHub.cpp 中被讀取。如上可知,如果我們的觸摸屏想被當成多點屏被處理,只需要在驅動中給 input_dev 額外增加以下幾個參數即可:
input_set_abs_params(mcs_data.input, ABS_MT_POSITION_X, pdata->abs_x_min, pdata->abs_x_max, 0, 0);
input_set_abs_params(mcs_data.input, ABS_MT_POSITION_Y, pdata->abs_y_min, pdata->abs_y_max, 0, 0);
input_set_abs_params(mcs_data.input, ABS_MT_TOUCH_MAJOR, 0, 15, 0, 0);
//相當於單點屏的 ABX_PRESSURE
input_set_abs_params(mcs_data.input, ABS_MT_WIDTH_MAJOR, 0, 15, 0, 0);
//相當於單點屏的 ABS_TOOL_WIDTH
注:
爲了讓我們的驅動代碼支持所有的Android 版本,無論是多點屏還是單點屏,一般都會保留單點屏的事件,如 ABS_TOUCH, ABS_PRESSURE, ABS_X, ABS_Y 等。另外,由於在 Android2.0 前支持多點的 frameworks 大多是用 HAT0X,HAT0Y 來實現的,所以一般也會上報這 2 個事件。
第三章 同步方式
由於多點觸摸技術需要採集到多個點,然後再一起處理這些點,所以在軟件實現中需要保證每一波點的準確性和完整性。因此,Linux 內核提供了 input_mt_sync(struct input_dev * input) 函數。在每波的每個點上報後需要緊跟一句 input_mt_sync(), 當這波所有點上報後再使用 input_sync() 進行同步。例如一波要上報 3 個點:
/* 上報點 1*/
…………… ..
input_mt_sync(input);
/* 上報點 2*/
…………… ..
input_mt_sync(input);
/* 上報點 3*/
…………… ..
input_mt_sync(input);
input_sync(input);
注:即使是僅上報一個點的單點事件,也需要一次input_my_sync 。
在Android 的 KeyInputQueue.java 中,系統創建了一個線程,然後把所有的 Input 事件放入一個隊列:
public abstract class KeyInputQueue {
……………………
Thread mThread = new Thread("InputDeviceReader") {
public void run() {
android.os.Process.setThreadPriority(
android.os.Process.THREAD_PRIORITY_URGENT_DISPLAY);
try {
RawInputEvent ev = new RawInputEvent();
while (true) {
InputDevice di;
// block, doesn't release the monitor
readEvent(ev);
if (ev.type == RawInputEvent.EV_DEVICE_ADDED) {
synchronized (mFirst) {
di = newInputDevice(ev.deviceId);
mDevices.put(ev.deviceId, di);
configChanged = true;
}
} else if (ev.type == RawInputEvent.EV_DEVICE_REMOVED) {
synchronized (mFirst) {
Log.i(TAG, "Device removed: id=0x"
+ Integer.toHexString(ev.deviceId));
di = mDevices.get(ev.deviceId);
if (di != null) {
mDevices.delete(ev.deviceId);
configChanged = true;
} else {
Log.w(TAG, "Bad device id: " + ev.deviceId);
}
}
} else {
di = getInputDevice(ev.deviceId);
// first crack at it
send = preprocessEvent(di, ev);
if (ev.type == RawInputEvent.EV_KEY) {
di.mMetaKeysState = makeMetaState(ev.keycode,
ev.value != 0, di.mMetaKeysState);
mHaveGlobalMetaState = false;
}
}
if (di == null) {
continue;
}
if (configChanged) {
synchronized (mFirst) {
addLocked(di, SystemClock.uptimeMillis(), 0,
RawInputEvent.CLASS_CONFIGURATION_CHANGED,
null);
}
}
if (!send) {
continue;
}
synchronized (mFirst) {
……………………… .
if (type == RawInputEvent.EV_KEY &&
(classes&RawInputEvent.CLASS_KEYBOARD) != 0 &&
(scancode < RawInputEvent.BTN_FIRST ||
scancode > RawInputEvent.BTN_LAST)) {
/* 鍵盤按鍵事件 */
…………………… .
} else if (ev.type == RawInputEvent.EV_KEY) {
/* 下面是 EV_KEY 事件分支,只支持單點的觸摸屏有按鍵事件,
* 而支持多點的觸摸屏沒有按鍵事件,只有絕對座標事件
- /
if (ev.scancode == RawInputEvent.BTN_TOUCH &&
(classes&(RawInputEvent.CLASS_TOUCHSCREEN
|RawInputEvent.CLASS_TOUCHSCREEN_MT))
== RawInputEvent.CLASS_TOUCHSCREEN) {
/* 只支持單點的觸摸屏的按鍵事件 */
…………………………………
} else if (ev.scancode == RawInputEvent.BTN_MOUSE &&
(classes&RawInputEvent.CLASS_TRACKBALL) != 0) {
/* 鼠標和軌跡球 */
……………………… .
} else if (ev.type == RawInputEvent.EV_ABS &&
(classes&RawInputEvent.CLASS_TOUCHSCREEN_MT) != 0) {
/* 下面纔是多點觸摸屏上報的事件 */
if (ev.scancode == RawInputEvent.ABS_MT_TOUCH_MAJOR) {
di.mAbs.changed = true;
di.mAbs.mNextData[di.mAbs.mAddingPointerOffset
+ MotionEvent.SAMPLE_PRESSURE] = ev.value;
} else if (ev.scancode == RawInputEvent.ABS_MT_POSITION_X) {
di.mAbs.changed = true;
di.mAbs.mNextData[di.mAbs.mAddingPointerOffset
+ MotionEvent.SAMPLE_X] = ev.value;
} else if (ev.scancode == RawInputEvent.ABS_MT_POSITION_Y) {
di.mAbs.changed = true;
di.mAbs.mNextData[di.mAbs.mAddingPointerOffset
+ MotionEvent.SAMPLE_Y] = ev.value;
} else if (ev.scancode == RawInputEvent.ABS_MT_WIDTH_MAJOR) {
di.mAbs.changed = true;
di.mAbs.mNextData[di.mAbs.mAddingPointerOffset
+ MotionEvent.SAMPLE_SIZE] = ev.value;
}
/* 上面這段就是多點觸摸屏要用到的事件上報部分 ;
* 使用一個數組 mNextData 來保存,其中 di.mAbs.mAddingPointerOffset
* 是當前點的偏移量,在每個點中還在 MotionEvent 中定義了 X,Y,PRESSURE
* SIZE等偏移量,多點觸摸屏的壓力值由絕對座標事件 ABS_MT_TOUCH_MAJOR 確定。
*/
} else if (ev.type == RawInputEvent.EV_ABS &&
(classes&RawInputEvent.CLASS_TOUCHSCREEN) != 0) {
/* 這裏是對單點觸摸屏上報座標事件的新的處理方法,同樣使用了數組來保存 */
if (ev.scancode == RawInputEvent.ABS_X) {
di.mAbs.changed = true;
di.curTouchVals[MotionEvent.SAMPLE_X] = ev.value;
} else if (ev.scancode == RawInputEvent.ABS_Y) {
di.mAbs.changed = true;
di.curTouchVals[MotionEvent.SAMPLE_Y] = ev.value;
} else if (ev.scancode == RawInputEvent.ABS_PRESSURE) {
di.mAbs.changed = true;
di.curTouchVals[MotionEvent.SAMPLE_PRESSURE] = ev.value;
di.curTouchVals[MotionEvent.NUM_SAMPLE_DATA
+ MotionEvent.SAMPLE_PRESSURE] = ev.value;
} else if (ev.scancode == RawInputEvent.ABS_TOOL_WIDTH) {
di.mAbs.changed = true;
di.curTouchVals[MotionEvent.SAMPLE_SIZE] = ev.value;
di.curTouchVals[MotionEvent.NUM_SAMPLE_DATA
+ MotionEvent.SAMPLE_SIZE] = ev.value;
}
…………………………………………… .}
/* 下面是關鍵的同步處理方法 */
if (ev.type == RawInputEvent.EV_SYN
&& ev.scancode == RawInputEvent.SYN_MT_REPORT
&& di.mAbs != null) {
/* 在這裏實現了對 SYN_MT_REPORT 事件的處理,
* 改變了 di.mAbs.mAddingPointerOffset 的值,從而將
* 新增的點的參數保存到下一組偏移量的位置。
*/
…………………… .
final int newOffset = (num <= InputDevice.MAX_POINTERS)
? (num * MotionEvent.NUM_SAMPLE_DATA)
: (InputDevice.MAX_POINTERS *
MotionEvent.NUM_SAMPLE_DATA);
di.mAbs.mAddingPointerOffset = newOffset;
di.mAbs.mNextData[newOffset
+ MotionEvent.SAMPLE_PRESSURE] = 0;
}
……………… .
} else if (send || (ev.type == RawInputEvent.EV_SYN
&& ev.scancode == RawInputEvent.SYN_REPORT)) {
/* 這裏實現了對 SYN_REPORT 事件的處理
* 如果是單點觸摸屏,即使用 di.curTouchVals 數組保存的點
* 轉化爲多點觸摸屏的 mNextData 數組保存
* 最後是調用 InputDevice 中的 generateAbsMotion 處理這個數組。這個函數
* 的具體實現方法將在後面補充
*/
………………………… ..
ms.finish(); //重置所有點和偏移量
…………………… ..
}
由於上層的代碼仍然使用ABS_X, ABS_Y 這些事件,爲了使多點觸摸屏代碼有良好的兼容性,在 KeyInputQueue.java 的最後,我們將多點事件類型轉化爲單點事件類型,返回一個新的 InputDevice:
private InputDevice newInputDevice(int deviceId) {
int classes = getDeviceClasses(deviceId);
String name = getDeviceName(deviceId);
InputDevice.AbsoluteInfo absX;
InputDevice.AbsoluteInfo absY;
InputDevice.AbsoluteInfo absPressure;
InputDevice.AbsoluteInfo absSize;
if ((classes&RawInputEvent.CLASS_TOUCHSCREEN_MT) != 0) {
absX = loadAbsoluteInfo(deviceId,
RawInputEvent.ABS_MT_POSITION_X, "X");
absY = loadAbsoluteInfo(deviceId,
RawInputEvent.ABS_MT_POSITION_Y, "Y");
absPressure = loadAbsoluteInfo(deviceId,
RawInputEvent.ABS_MT_TOUCH_MAJOR, "Pressure");
absSize = loadAbsoluteInfo(deviceId,
RawInputEvent.ABS_MT_WIDTH_MAJOR, "Size");
} else if ((classes&RawInputEvent.CLASS_TOUCHSCREEN) != 0) {
absX = loadAbsoluteInfo(deviceId,
RawInputEvent.ABS_X, "X");
absY = loadAbsoluteInfo(deviceId,
RawInputEvent.ABS_Y, "Y");
absPressure = loadAbsoluteInfo(deviceId,
RawInputEvent.ABS_PRESSURE, "Pressure");
absSize = loadAbsoluteInfo(deviceId,
RawInputEvent.ABS_TOOL_WIDTH, "Size");
} else {
absX = null;
absY = null;
absPressure = null;
absSize = null;
}
return new InputDevice(deviceId, classes, name, absX, absY, absPressure, absSize);
}
第四章 觸摸事件 數組的處理
上面我們曾說到 generateAbsMotion 這個方法,它們在InputDevice 類的內部類 MotionState 中實現,該類被定義爲 InputDevice 類的靜態成員類 (static class) ,調用它們可以直接使用:
InputDeviceClass.MotionStateClass.generateAbsMotion()。
public class InputDevice {
……………………………
static class MotionState { //下面是這個內部類的幾個函數
……………………………… .
/* mLastNumPointers 爲上一個動作在觸屏上按鍵的個數 */
int mLastNumPointers = 0;
final int[] mLastData = new int[MotionEvent.NUM_SAMPLE_DATA * MAX_POINTERS];
/* mNextNumPointers 爲下一個動作在觸屏上按鍵的個數 */
/* 通過對這 2 個值大小的判斷,可以確認新的動作方式 */
int mNextNumPointers = 0;
final int[] mNextData = new int[(MotionEvent.NUM_SAMPLE_DATA * MAX_POINTERS)
+ MotionEvent.NUM_SAMPLE_DATA];
………………………………… .
int[] generateAveragedData(int upOrDownPointer, int lastNumPointers,
int nextNumPointers) { //平滑處理
…………………………………… .
}
private boolean assignPointer(int nextIndex, boolean allowOverlap) { //指派按鍵
……………………………………
}
private int updatePointerIdentifiers() { //更新按鍵 ID
………………………………… .
}
void removeOldPointer(int index) {
……………………………………
}
MotionEvent generateAbsMotion(InputDevice device, long curTime,
long curTimeNano, Display display, int orientation,
int metaState) {
……………………………………
int upOrDownPointer = updatePointerIdentifiers();
final int numPointers = mLastNumPointers;
………………………………………
/* 對行爲的判斷 */
if (nextNumPointers != lastNumPointers) { //前後在觸屏上點個數不同,說明有手指 up 或 down
if (nextNumPointers > lastNumPointers) {
if (lastNumPointers == 0) { //上次觸屏上沒有按鍵,新值又大,說明有按鍵按下
action = MotionEvent.ACTION_DOWN;
mDownTime = curTime;
} else { //有新點按下,分配給新點 ID 號
action = MotionEvent.ACTION_POINTER_DOWN
| (upOrDownPointer << MotionEvent.ACTION_POINTER_ID_SHIFT);
}
} else { //新動作比原來 pointer 數量少
if (numPointers == 1) { //原來只有 1 個點按下,所以現在的動作是全部按鍵 up
action = MotionEvent.ACTION_UP;
} else { //原來有多點按下,現在是 ACTION_POINTER_UP 動作,
action = MotionEvent.ACTION_POINTER_UP
| (upOrDownPointer << MotionEvent.ACTION_POINTER_ID_SHIFT);
}
}
currentMove = null;
} else { //前後觸屏 pointer 個數相同,所以是移動動作 ACTION_MOVE
action = MotionEvent.ACTION_MOVE;
}
/* 後面則是根據屏幕的 height 和 width 以及屏幕方向 orientation 對這些點進行二次處理 */
……………………………………
}
MotionEvent generateRelMotion(InputDevice device, long curTime,
long curTimeNano, int orientation, int metaState) {
/* 軌跡球等的處理方式 */
………………………………………… ..
}
void finish() { //結束這輪動作
mNextNumPointers = mAddingPointerOffset = 0;
mNextData[MotionEvent.SAMPLE_PRESSURE] = 0;
}
…………………………………… .
}
……………………………… .
……………………………………
}
第五章 接口
我們平時所看到的用2 個手指對圖片放大縮小、旋轉等手勢都是由應用程序編寫瀏覽器實現的。這些應用程序大多會使用 Android2.0 以上的在 MotionEvent.java 中實現的新的接口。所以,我們還需要給 MotionEvent 類補充儘量全的接口。這裏可以完全參照 google 新的 android 代碼。
第六章 總結
綜上,在硬件支持基礎上,Android1.6 如果要實現多點觸摸功能,主要工作可簡述爲以下幾個方面:
1、 驅動中,除了增加多點的事件上報方式,還要完全更改單點的事件上報方式。
2、 Android的 Frameworks 層需要修改的文件有: EventHub.cpp , RawInputEvent.java , KeyInputQueue.java , InputDevice.java , MotionEvent.java 。
3、 編寫新的支持多點觸摸功能的多媒體瀏覽器。
4、 爲了代碼簡練,android2.0 在軌跡球和單點屏事件方式中也全使用了新的變量名,以方便多點屏事件同樣能使用這些變量,所以修改時還需要注意許多細節方面。