android中触屏反馈原理
HOPE mt6516 android2.2 linux2.6.32
在用户对软按键或者某些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): <bool name="config_enable_dialer_key_vibration">true</bool>
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个类似的振动模式
<!-- Vibrator pattern for feedback about a long screen/key press -->
<integer-array name="config_longPressVibePattern">
<item>0</item>
<item>1</item>
<item>20</item>
<item>21</item>
</integer-array>
<!-- Vibrator pattern for feedback about touching a virtual key -->
<integer-array name="config_virtualKeyVibePattern">
<item>0</item>
<item>10</item>
<item>20</item>
<item>30</item>
</integer-array>
<!-- Vibrator pattern for a very short but reliable vibration for soft keyboard tap -->
<integer-array name="config_keyboardTapVibePattern">
<item>40</item>
</integer-array>
<!-- Vibrator pattern for feedback about booting with safe mode disabled -->
<integer-array name="config_safeModeDisabledVibePattern">
<item>0</item>
<item>1</item>
<item>20</item>
<item>21</item>
</integer-array>
<!-- Vibrator pattern for feedback about booting with safe mode disabled -->
<integer-array name="config_safeModeEnabledVibePattern">
<item>0</item>
<item>1</item>
<item>20</item>
<item>21</item>
<item>500</item>
<item>600</item>
</integer-array>
那么上面类似的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<EventHub> 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)