第一章 摘要
在Linux內核支持的基礎上, Android 在其 2.0 源碼中加入多點觸摸功能。由此觸摸屏在 Android 的 frameworks 被完全分爲 2 種實現途徑:單點觸摸屏的單點方式,多點觸摸屏的單點和多點方式。
第二章 軟件位
在Linux的 input.h中,多點觸摸功能依賴於以下幾個主要的軟件位:
……………………….. #define SYN_REPORT0 #define SYN_CONFIG1 #define SYN_MT_REPORT2 ………………………... #define ABS_MT_TOUCH_MAJOR0x30/* Major axis of touching ellipse */ #define ABS_MT_TOUCH_MINOR0x31/* Minor axis (omit if circular) */ #define ABS_MT_WIDTH_MAJOR0x32/* Major axis of approaching ellipse */ #define ABS_MT_WIDTH_MINOR0x33/* Minor axis (omit if circular) */ #define ABS_MT_ORIENTATION0x34/* Ellipse orientation */ #define ABS_MT_POSITION_X0x35/* Center X ellipse position */ #define ABS_MT_POSITION_Y0x36/* Center Y ellipse position */ #define ABS_MT_TOOL_TYPE0x37/* Type of touching device */ #define ABS_MT_BLOB_ID0x38/* 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 在軌跡球和單點屏事件方式中也全使用了新的變量名,以方便多點屏事件同樣能使用這些變量,所以修改時還需要注意許多細節方面。
轉載自:“炸水生菜 ” 博客:http://yuesun.blog.51cto.com