android 触屏反馈原理

在用户对软按键或者某些ui操作的时候会反馈振动,达到让用户感知操作ok的效果。
在情景模式(Audio Profile)的选取之后,将会出现对特定情景模式设置的界面(Edit Profile),在这里面就可以设置是否启动振动器
和反馈功能。
情景模式对于的代码在:packages/apps/Settings/src/com/android/settings/audioprofile下,
其中文件:AudioProfileSettings.java是情景模式选择;Editprofile.java是情景模式编辑界面。

如果用户在界面上点击选中了反馈的功能,那么将会调用到函数:setHapticFeedbackEnabled()
@ frameworks/base/core/java/android/view/View.java
public void setHapticFeedbackEnabled(boolean hapticFeedbackEnabled) {
        setFlags(hapticFeedbackEnabled ? HAPTIC_FEEDBACK_ENABLED: 0, HAPTIC_FEEDBACK_ENABLED);
    }
   
先来看看打电话界面的软键盘的触摸反馈功能的实现:
@packages/apps/Phone/src/com/android/phone
进入这个目录一眼就可以看到文件HapticFeedback.java,很显然,它就是实现振动反馈功能的,该文件代码比较简单:
该文件有一段较为详细的注释,说明了如何在代码中添加这个功能,使用它们的函数接口
/**
* Handles the haptic feedback: a light buzz happening when the user
* presses a soft key (UI button or capacitive key(电容虚拟按键)).
*
* The haptic feedback is controlled by:
* - a system resource for the pattern
*   The pattern used is tuned per device and stored in an internal
*   resource (config_virtualKeyVibePattern.)
*
* - a system setting HAPTIC_FEEDBACK_ENABLED.(这个就是情景模式中设置的反馈使能)
*   HAPTIC_FEEDBACK_ENABLED can be changed by the user using the
*   system Settings activity. It must be rechecked each time the
*   activity comes in the foreground (onResume).
*
* //////后面的代码可以看出,必须要这两个条件同时满足才可以调用振动器的接口。
*
* This class is not thread safe. It assumes it'll be called from the
* UI thead.
*
* Typical usage(典型用法):
* --------------
*   static private final boolean HAPTIC_ENABLED = true;
*   private HapticFeedback mHaptic = new HapticFeedback();
*
*   protected void onCreate(Bundle icicle) {
*     mHaptic.init((Context)this, HAPTIC_ENABLED);
*   }
*
*   protected void onResume() {
*     // Refresh the system setting.
*     mHaptic.checkSystemSetting();
*   }
*
*   public void foo() {
*     mHaptic.vibrate();
*   }
*
*/

public class HapticFeedback {
    private static final int VIBRATION_PATTERN_ID =
            com.android.internal.R.array.config_virtualKeyVibePattern;
    /** If no pattern was found, vibrate for a small amount of time. */
    private static final long DURATION = 10;  // millisec.
    /** Play the haptic pattern only once. */
    private static final int NO_REPEAT = -1;
   
    ...
     private Context mContext;
    private long[] mHapticPattern;
    private Vibrator mVibrator;

    private boolean mEnabled;
    private Settings.System mSystemSettings;
    private ContentResolver mContentResolver;
    private boolean mSettingEnabled;
   
    public void init(Context context, boolean enabled) {
        mEnabled = enabled;
        if (enabled) {
            mVibrator = new Vibrator();
            if (!loadHapticSystemPattern(context.getResources())) {
                mHapticPattern = new long[] {0, DURATION, 2 * DURATION, 3 * DURATION};
                // 设置一个默认的振动模式
            }
            mSystemSettings = new Settings.System();
            mContentResolver = context.getContentResolver();
        }
    }
   
    // Reload the system settings to check if the user enabled the haptic feedback.
    // called in onResume()
    public void checkSystemSetting() {
        if (!mEnabled) {
            return;
        }
        try {
            int val = mSystemSettings.getInt(mContentResolver, System.HAPTIC_FEEDBACK_ENABLED, 0);
            mSettingEnabled = val != 0; /////////////////
        } catch (Resources.NotFoundException nfe) {
            Log.e(TAG, "Could not retrieve system setting.", nfe);
            mSettingEnabled = false;
        }
    }
   
    public void vibrate() {
        if (!mEnabled || !mSettingEnabled) {
            return;  // 看得出,只有调用init时传入enable和用户设置使能反馈后才能调用振动器的接口
        }
        mVibrator.vibrate(mHapticPattern, NO_REPEAT);
    }
 
  /**
     * @return true If the system haptic pattern was found.
     * @加载振动模式
     */
    private boolean loadHapticSystemPattern(Resources r) {
        int[] pattern;

        mHapticPattern = null;
        try {
            pattern = r.getIntArray(VIBRATION_PATTERN_ID);
        } catch (Resources.NotFoundException nfe) {
            Log.e(TAG, "Vibrate pattern missing.", nfe);
            return false;
        }

        if (null == pattern || pattern.length == 0) {
            Log.e(TAG, "Haptic pattern is null or empty.");
            return false;
        }

        // int[] to long[] conversion.
        mHapticPattern = new long[pattern.length];
        for (int i = 0; i < pattern.length; i++) {
            mHapticPattern[i] = pattern[i];
        }
        return true;
    }
}

在打电话界面有两个文件中使用到这类API:EmergencyDialer.java (packages\apps\phone\src\com\android\phone)
             TwelveKeyDialer.java (packages\apps\contacts\src\com\android\contacts)
现在以拨号键盘为例:TwelveKeyDialer.java,搜索mHaptic.字符串即可找到。
mHaptic.init(this, r.getBoolean(R.bool.config_enable_dialer_key_vibration))
config_enable_dialer_key_vibration在下列文件中定义:
Config.xml (packages\apps\phone\res\values):    true

public void onClick(View view) {
        switch (view.getId()) {
            case R.id.one: {
                keyPressed(KeyEvent.KEYCODE_1);
                playTone(ToneGenerator.TONE_DTMF_1);
                mHaptic.vibrate();
                return;
            }
            ...
        }
    }
    
    protected void onResume() {
     ...
     // Retrieve the haptic feedback setting.
     mHaptic.checkSystemSetting();
     ...
    }

从上面的init函数调用和Config.xml可以看出,config_virtualKeyVibePattern模式并没有在phone的资源中定义,
直接使用的默认模式。
那么全局搜索了config_virtualKeyVibePattern这个字符串,发现只有在文件
Config.xml (frameworks\base\core\res\res\values)中有定义,而起还有另外4个类似的振动模式

   
        0
        1
        20
        21
   

   
   
        0
        10
        20
        30
   

   
   
        40
   

   
   
        0
        1
        20
        21
   

   
   
        0
        1
        20
        21
        500
        600
   


那么上面类似的5中模式在哪里有使用呢?在刚刚的搜索结果中可以看到赫然出现一个文件:
PhoneWindowManager.java (frameworks\policies\base\phone\com\android\internal\policy\impl)
这个文件中的代码实现系统所有窗口的统一管理,那么显而易见大部分窗口振动都是在这里操作的。
public void init(Context context, IWindowManager windowManager,
            LocalPowerManager powerManager) {
      ...
       mVibrator = new Vibrator();
        mLongPressVibePattern = getLongIntArray(mContext.getResources(),
                com.android.internal.R.array.config_longPressVibePattern);
        mVirtualKeyVibePattern = getLongIntArray(mContext.getResources(),
                com.android.internal.R.array.config_virtualKeyVibePattern);
        mKeyboardTapVibePattern = getLongIntArray(mContext.getResources(),
                com.android.internal.R.array.config_keyboardTapVibePattern);
        mSafeModeDisabledVibePattern = getLongIntArray(mContext.getResources(),
                com.android.internal.R.array.config_safeModeDisabledVibePattern);
        mSafeModeEnabledVibePattern = getLongIntArray(mContext.getResources(),
                com.android.internal.R.array.config_safeModeEnabledVibePattern);
      // 取出振动模式数据,放在long []类型数组中。
     
     }

public boolean performHapticFeedbackLw(WindowState win, int effectId,
            boolean always) {
        final boolean hapticsDisabled = Settings.System.getInt(mContext
                .getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED,
                0) == 0;  // 再次获取用户设置
        if (!always
                && (hapticsDisabled || mKeyguardMediator
                        .isShowingAndNotHidden())) {
            return false;  //
        }
        long[] pattern = null;
        switch (effectId) {  // effectId这个值是觉得采用哪种模式的,长按,还是按了软按键产生等等。
        case HapticFeedbackConstants.LONG_PRESS:
            pattern = mLongPressVibePattern;
            break;
        case HapticFeedbackConstants.VIRTUAL_KEY:
            pattern = mVirtualKeyVibePattern;
            break;
        case HapticFeedbackConstants.KEYBOARD_TAP:
            pattern = mKeyboardTapVibePattern;
            break;
        case HapticFeedbackConstants.SAFE_MODE_DISABLED:
            pattern = mSafeModeDisabledVibePattern;
            break;
        case HapticFeedbackConstants.SAFE_MODE_ENABLED:
            pattern = mSafeModeEnabledVibePattern;
            break;
        default:
            return false;
        }
        // 调用振动器接口开始振动
        if (pattern.length == 1) {
            // One-shot vibration
            mVibrator.vibrate(pattern[0]);
        } else {
            // Pattern vibration
            mVibrator.vibrate(pattern, -1);
        }
        return true;
    }
   
    public void keyFeedbackFromInput(KeyEvent event) {
        if (event.getAction() == KeyEvent.ACTION_DOWN
                && (event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
                // 这里有对按键类型做判断看是否是虚拟的硬按键,就对于这虚拟的软按键。
            performHapticFeedbackLw(null, HapticFeedbackConstants.VIRTUAL_KEY,
                    false);
        }
    }

到这里,基本上大致明白了振动反馈的原理,现在要引入一个实际面临的问题。

本来Virtual Key是只的触摸屏上划出一块区域来做几个按键的这类按键,这类按键在原始系统上就是可以进行振动反馈的,
不过呢,现在我们的系统上有2个触摸按键MENU和BACK,他们虽然也是在触摸屏上,不过他们是按照按键上报,而不是按触摸
坐标上报之后生成的虚拟keyevent,所以这样的话,上层就会将他们视为物理按键,但是他们确实是触摸型的。
所以为了让这类按键触摸之后也有振动反馈,所以就需要查找按键上报的流程,然后进行修改。

获取按键的类型flags: event.getFlags(), KeyEvent.java (frameworks\base\core\java\android\view)
想要知道这个flags是如何来的,那么就需要从kernel内核之上开始顺藤摸瓜将按键上报流程缕一下:

EventHub.cpp (frameworks\base\libs\ui)
这个文件中的函数EventHub::getEvent()会从内核中获取input子系统的event事件,其中也包括key事件。
bool EventHub::getEvent(int32_t* outDeviceId, int32_t* outType,
        int32_t* outScancode, int32_t* outKeycode, uint32_t *outFlags,
        int32_t* outValue, nsecs_t* outWhen)
{
  ...
  while(1) {
   ...
   pollres = poll(mFDs, mFDCount, -1);
  
   ...
   for(i = 1; i < mFDCount; i++) {
            if(mFDs[i].revents) {
               
                if(mFDs[i].revents & POLLIN) {
                    res = read(mFDs[i].fd, &iev, sizeof(iev));
                    if (res == sizeof(iev)) {
                      
                        *outDeviceId = mDevices[i]->id;
                        if (*outDeviceId == mFirstKeyboardId) *outDeviceId = 0;
                        *outType = iev.type;
                        *outScancode = iev.code;
                        if (iev.type == EV_KEY) {
                            err = mDevices[i]->layoutMap->map(iev.code, outKeycode, outFlags);
                            // 从*.kl文件中进行键值映射,也得到想要flags值。kernel键值映射到android键值。

       ...
                        } else {
                            *outKeycode = iev.code;
                        }
                        *outValue = iev.value;
                        *outWhen = s2ns(iev.time.tv_sec) + us2ns(iev.time.tv_usec);
                        return true;
                    }
                    ...
                    }
                }
            }
        }
  ...
  }
}

*.kl文件格式:
key 158   BACK
...
key 229   MENU
key 139   MENU
key 59    MENU
...
key 116   POWER             WAKE
...

其中第一列是key关键字,第二列是kernel的键值,第3列是android键值,第4列是flag
其中flags有以下几种,可以从文件:KeycodeLabels.h (frameworks\base\include\ui)中查看得到:
static const KeycodeLabel FLAGS[] = {
     { "WAKE", 0x00000001 },
     { "WAKE_DROPPED", 0x00000002 },
     { "SHIFT", 0x00000004 },
     { "CAPS_LOCK", 0x00000008 },
     { "ALT", 0x00000010 },
     { "ALT_GR", 0x00000020 },
     { "MENU", 0x00000040 },
     { "LAUNCHER", 0x00000080 },
     { NULL, 0 }
};

其中也可以看到MENU和BACK在android空间中对于的键值分别是:82和4.
static const KeycodeLabel KEYCODES[] = {
  ...
  { "BACK", 4 },
  ...
  { "MENU", 82 },
  ...
}

上面这些值都是KeyLayoutMap.cpp (frameworks\base\libs\ui)文件中函数
status_t KeyLayoutMap::load(const char* filename)在加载*.kl文件的时候根据文件和上面两个数组设置好了的,最后只需要用map()函数来获取这些值即可。

接下来按键值将会上传至JNI层:com_android_server_KeyInputQueue.cpp (frameworks\base\services\jni)
static jboolean  android_server_KeyInputQueue_readEvent(JNIEnv* env, jobject clazz, jobject event)
{
    gLock.lock();
    sp hub = gHub;
    if (hub == NULL) {
        hub = new EventHub;
        gHub = hub;
    }
    gLock.unlock();

    int32_t deviceId;
    int32_t type;
    int32_t scancode, keycode;
    uint32_t flags;
    int32_t value;
    nsecs_t when;
    bool res = hub->getEvent(&deviceId, &type, &scancode, &keycode, &flags, &value, &when);  // getEvent

    env->SetIntField(event, gInputOffsets.mDeviceId, (jint)deviceId);
    env->SetIntField(event, gInputOffsets.mType, (jint)type);
    env->SetIntField(event, gInputOffsets.mScancode, (jint)scancode);
    env->SetIntField(event, gInputOffsets.mKeycode, (jint)keycode);
    env->SetIntField(event, gInputOffsets.mFlags, (jint)flags);
    env->SetIntField(event, gInputOffsets.mValue, value);
    env->SetLongField(event, gInputOffsets.mWhen, (jlong)(nanoseconds_to_milliseconds(when)));
    // 将这些值传入java空间。

    return res;
}
最后在java代码中只需要调用jni接口readEvent即可。

KeyInputQueue.java (frameworks\base\services\java\com\android\server)文件中会创建一个线程来专门读取inputevent值

Thread mThread = new Thread("InputDeviceReader") {
  public void run() {
    ...
    RawInputEvent ev = new RawInputEvent();
         while (true) {
           try {
               InputDevice di;

               // block, doesn't release the monitor
               readEvent(ev);
              
               ...
              
               }
    }
   
    ...
    // Is it a key event?
         if (type == RawInputEvent.EV_KEY &&
             (classes&RawInputEvent.CLASS_KEYBOARD) != 0 &&
             (scancode < RawInputEvent.BTN_FIRST ||
              scancode > RawInputEvent.BTN_LAST)) {
                  boolean down;
                  if (ev.value != 0) {
                  down = true;
                  di.mKeyDownTime = curTime;
                  } else {
                     down = false;
                  }
                  int keycode = rotateKeyCodeLocked(ev.keycode);
                  addLocked(di, curTimeNano, ev.flags,
                            RawInputEvent.CLASS_KEYBOARD,
                            newKeyEvent(di, di.mKeyDownTime, curTime, down,
                                       keycode, 0, scancode,
                                       ((ev.flags & WindowManagerPolicy.FLAG_WOKE_HERE) != 0)
                                       ? KeyEvent.FLAG_WOKE_HERE : 0));
         }
   
    ...
  
  }
}

generateVirtualKeyDown()根据上报的触摸点坐标等信息生成虚拟硬按键事件。
KeyEvent event = newKeyEvent(di, di.mKeyDownTime, curTime, true,
                    vk.lastKeycode, 0, vk.scancode,
                    KeyEvent.FLAG_VIRTUAL_HARD_KEY);
    mHapticFeedbackCallback.virtualKeyFeedback(event);
    addLocked(di, curTimeNano, ev.flags, RawInputEvent.CLASS_KEYBOARD,
                    event);




newKeyEvent()生成一个KeyEvent实例,如下:
public static KeyEvent newKeyEvent(InputDevice device, long downTime,
            long eventTime, boolean down, int keycode, int repeatCount,
            int scancode, int flags) {
           
        return new KeyEvent(
                downTime, eventTime,
                down ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP,
                keycode, repeatCount,
                device != null ? device.mMetaKeysState : 0,
                device != null ? device.id : -1, scancode,
                flags | KeyEvent.FLAG_FROM_SYSTEM);
    }
   
   
    ////***********************88   错误的修改  *******************************/
所以为了实现MENU和BACK按键能产生震动反馈,那么可以直接修改这个函数newKeyEvent(),如下:
public static KeyEvent newKeyEvent(InputDevice device, long downTime,
            long eventTime, boolean down, int keycode, int repeatCount,
            int scancode, int flags) {
+ int nflags = flags;
 
    +   if(keycode == 4 || keycode == 82)  // BACK and MENU
    +    nflags |= KeyEvent.FLAG_VIRTUAL_HARD_KEY;
        return new KeyEvent(
                downTime, eventTime,
                down ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP,
                keycode, repeatCount,
                device != null ? device.mMetaKeysState : 0,
                device != null ? device.id : -1, scancode,
    +/-         nflags | KeyEvent.FLAG_FROM_SYSTEM);
    }
////***********************88   错误的修改  *******************************/


int keycode = rotateKeyCodeLocked(ev.keycode);
       /* Begin: lizhiguo, 2011-10-20, modifiled for BACK and MENU key HapticFeedback.*/
       if(1){
        int nflags = ((ev.flags & WindowManagerPolicy.FLAG_WOKE_HERE) != 0)
                                              ? KeyEvent.FLAG_WOKE_HERE : 0;
        if(keycode == 4 || keycode == 82)   // BACK and MENU
         nflags |= KeyEvent.FLAG_VIRTUAL_HARD_KEY;

        KeyEvent event = newKeyEvent(di, di.mKeyDownTime, curTime, down,
                                             keycode, 0, scancode, nflags);

        if(keycode == 4 || keycode == 82)   // BACK and MENU
         mHapticFeedbackCallback.virtualKeyFeedback(event);
       
                             addLocked(di, curTimeNano, ev.flags,
                                     RawInputEvent.CLASS_KEYBOARD, event);
                            }
       else{
        addLocked(di, curTimeNano, ev.flags,
                               RawInputEvent.CLASS_KEYBOARD,
                               newKeyEvent(di, di.mKeyDownTime, curTime, down,
                                          keycode, 0, scancode,
                                          ((ev.flags & WindowManagerPolicy.FLAG_WOKE_HERE) != 0)
                                          ? KeyEvent.FLAG_WOKE_HERE : 0));
       }
       /* End: lizhiguo, 2011-10-20 */


完!


源码列表:
1  KeyInputQueue.java (frameworks\base\services\java\com\android\server)
2  RawInputEvent.java (frameworks\base\core\java\android\view)
3  KeyEvent.java (frameworks\base\core\java\android\view)
4  KeyInputQueue.java (frameworks\base\services\java\com\android\server):2
5  WindowManagerPolicy.java (frameworks\base\core\java\android\view)
6  com_android_server_KeyInputQueue.cpp (frameworks\base\services\jni)
7  EventHub.cpp (frameworks\base\libs\ui)
8  EventHub.cpp (frameworks\base\libs\ui):2
9  KeyLayoutMap.cpp (frameworks\base\libs\ui)
0  KeycodeLabels.h (frameworks\base\include\ui)
A  TwelveKeyDialer.java (packages\apps\contacts\src\com\android\contacts)
B  PhoneWindowManager.java (frameworks\policies\base\phone\com\android\internal\policy\impl)
C  KeyEvent.java (frameworks\base\awt\java\awt\event)
D  HapticFeedbackConstants.java (frameworks\base\core\java\android\view)
E  PasswordUnlockScreen.java (frameworks\policies\base\phone\com\android\internal\policy\impl)
F  LockPatternView.java (frameworks\base\core\java\com\android\internal\widget)
G  HapticFeedback.java (packages\apps\phone\src\com\android\phone)
H  Donottranslate_config.xml (packages\apps\contacts\res\values)
I  EmergencyDialer.java (packages\apps\phone\src\com\android\phone)
J  Config.xml (frameworks\base\core\res\res\values)
K  Vibrator.java (frameworks\base\core\java\android\os)
L  View.java (frameworks\base\core\java\android\view)
M  Settings.java (frameworks\base\core\java\android\provider)
N  ContentResolver.java (frameworks\base\core\java\android\content)
O  System.java (dalvik\libcore\luni-kernel\src\main\java\java\lang)
P  Editprofile.java (packages\apps\settings\src\com\android\settings\audioprofile)
Q  CheckBoxPreference.java (frameworks\base\core\java\android\preference)
R  AudioProfileImpl.java (frameworks\mtk\extensions\media\audio\java\com\mediatek\audioprofile)
S  ProfileState.java (frameworks\mtk\extensions\media\audio\java\com\mediatek\audioprofile)
T  AudioProfileSettings.java (packages\apps\settings\src\com\android\settings\audioprofile)
U  Config.xml (packages\apps\phone\res\values)
V  TextKeyListenerTest.java (cts\tests\tests\text\src\android\text\method\cts)
W  WindowManagerService.java (frameworks\base\services\java\com\android\server)
X  KeyEventTest.java (cts\tests\tests\view\src\android\view\cts)
Y  KeyLayoutMap.h (frameworks\base\libs\ui)
Z  User_comments_for_4_to_5.xml (frameworks\base\docs\html\sdk\api_diff\5)
   missingSinces.txt (frameworks\base\docs\html\sdk\api_diff\5)
   5.xml (frameworks\base\api)
   MidWindowManager.java (frameworks\policies\base\mid\com\android\internal\policy\impl)
   EventHub.h (frameworks\base\include\ui)
   Evdev.c (kernel\drivers\input)
   Input.h (kernel\include\linux)
   Input.c (kernel\drivers\input)
   Preference.java (frameworks\base\core\java\android\preference)

你可能感兴趣的:(android)