Android多點觸摸實現

Android多點觸摸實現

第一章 摘要

在Linux 內核支持的基礎上, Android 在其 2.0 源碼中加入多點觸摸功能。由此觸摸屏在 Android 的 frameworks 被完全分爲 2 種實現途徑:單點觸摸屏的單點方式,多點觸摸屏的單點和多點方式。

第二章 軟件位

在Linux 的 input.h 中,多點觸摸功能依賴於以下幾個主要的軟件位:

……………………… ..

  1. define SYN_REPORT 0
  1. define SYN_CONFIG 1
  1. define SYN_MT_REPORT 2

……………………… ...

  1. define ABS_MT_TOUCH_MAJOR 0x30 /* Major axis of touching ellipse */
  1. define ABS_MT_TOUCH_MINOR 0x31 /* Minor axis (omit if circular) */
  1. define ABS_MT_WIDTH_MAJOR 0x32 /* Major axis of approaching ellipse */
  1. define ABS_MT_WIDTH_MINOR 0x33 /* Minor axis (omit if circular) */
  1. define ABS_MT_ORIENTATION 0x34 /* Ellipse orientation */
  1. define ABS_MT_POSITION_X 0x35 /* Center X ellipse position */
  1. define ABS_MT_POSITION_Y 0x36 /* Center Y ellipse position */
  1. define ABS_MT_TOOL_TYPE 0x37 /* Type of touching device */
  1. 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 在軌跡球和單點屏事件方式中也全使用了新的變量名,以方便多點屏事件同樣能使用這些變量,所以修改時還需要注意許多細節方面。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章