338 lines
13 KiB
Java
338 lines
13 KiB
Java
![]() |
/*
|
||
|
* Copyright (C) 2022 The Android Open Source Project
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
|
||
|
package android.inputmethodservice.navigationbar;
|
||
|
|
||
|
import static android.view.Display.INVALID_DISPLAY;
|
||
|
import static android.view.KeyEvent.KEYCODE_BACK;
|
||
|
import static android.view.KeyEvent.KEYCODE_UNKNOWN;
|
||
|
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
|
||
|
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
|
||
|
|
||
|
import android.content.Context;
|
||
|
import android.graphics.Canvas;
|
||
|
import android.graphics.Paint;
|
||
|
import android.graphics.drawable.Drawable;
|
||
|
import android.inputmethodservice.InputMethodService;
|
||
|
import android.media.AudioManager;
|
||
|
import android.os.Bundle;
|
||
|
import android.os.SystemClock;
|
||
|
import android.util.AttributeSet;
|
||
|
import android.view.HapticFeedbackConstants;
|
||
|
import android.view.InputDevice;
|
||
|
import android.view.KeyCharacterMap;
|
||
|
import android.view.KeyEvent;
|
||
|
import android.view.MotionEvent;
|
||
|
import android.view.SoundEffectConstants;
|
||
|
import android.view.View;
|
||
|
import android.view.ViewConfiguration;
|
||
|
import android.view.accessibility.AccessibilityEvent;
|
||
|
import android.view.accessibility.AccessibilityNodeInfo;
|
||
|
import android.view.inputmethod.InputConnection;
|
||
|
import android.widget.ImageView;
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
public class KeyButtonView extends ImageView implements ButtonInterface {
|
||
|
private static final String TAG = KeyButtonView.class.getSimpleName();
|
||
|
|
||
|
private final boolean mPlaySounds;
|
||
|
private long mDownTime;
|
||
|
private boolean mTracking;
|
||
|
private int mCode;
|
||
|
private int mTouchDownX;
|
||
|
private int mTouchDownY;
|
||
|
private AudioManager mAudioManager;
|
||
|
private boolean mGestureAborted;
|
||
|
private OnClickListener mOnClickListener;
|
||
|
private final KeyButtonRipple mRipple;
|
||
|
private final Paint mOvalBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
||
|
private float mDarkIntensity;
|
||
|
private boolean mHasOvalBg = false;
|
||
|
|
||
|
public KeyButtonView(Context context, AttributeSet attrs) {
|
||
|
super(context, attrs);
|
||
|
|
||
|
// TODO(b/215443343): Figure out better place to set this.
|
||
|
switch (getId()) {
|
||
|
case com.android.internal.R.id.input_method_nav_back:
|
||
|
mCode = KEYCODE_BACK;
|
||
|
break;
|
||
|
default:
|
||
|
mCode = KEYCODE_UNKNOWN;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
mPlaySounds = true;
|
||
|
|
||
|
setClickable(true);
|
||
|
mAudioManager = context.getSystemService(AudioManager.class);
|
||
|
|
||
|
mRipple = new KeyButtonRipple(context, this,
|
||
|
com.android.internal.R.dimen.input_method_nav_key_button_ripple_max_width);
|
||
|
setBackground(mRipple);
|
||
|
setWillNotDraw(false);
|
||
|
forceHasOverlappingRendering(false);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean isClickable() {
|
||
|
return mCode != KEYCODE_UNKNOWN || super.isClickable();
|
||
|
}
|
||
|
|
||
|
public void setCode(int code) {
|
||
|
mCode = code;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setOnClickListener(OnClickListener onClickListener) {
|
||
|
super.setOnClickListener(onClickListener);
|
||
|
mOnClickListener = onClickListener;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
|
||
|
super.onInitializeAccessibilityNodeInfo(info);
|
||
|
if (mCode != KEYCODE_UNKNOWN) {
|
||
|
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, null));
|
||
|
if (isLongClickable()) {
|
||
|
info.addAction(
|
||
|
new AccessibilityNodeInfo.AccessibilityAction(ACTION_LONG_CLICK, null));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void onWindowVisibilityChanged(int visibility) {
|
||
|
super.onWindowVisibilityChanged(visibility);
|
||
|
if (visibility != View.VISIBLE) {
|
||
|
jumpDrawablesToCurrentState();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
|
||
|
if (action == ACTION_CLICK && mCode != KEYCODE_UNKNOWN) {
|
||
|
sendEvent(KeyEvent.ACTION_DOWN, 0, SystemClock.uptimeMillis());
|
||
|
sendEvent(KeyEvent.ACTION_UP, mTracking ? KeyEvent.FLAG_TRACKING : 0);
|
||
|
mTracking = false;
|
||
|
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||
|
playSoundEffect(SoundEffectConstants.CLICK);
|
||
|
return true;
|
||
|
} else if (action == ACTION_LONG_CLICK && mCode != KEYCODE_UNKNOWN) {
|
||
|
sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);
|
||
|
sendEvent(KeyEvent.ACTION_UP, mTracking ? KeyEvent.FLAG_TRACKING : 0);
|
||
|
mTracking = false;
|
||
|
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
|
||
|
return true;
|
||
|
}
|
||
|
return super.performAccessibilityActionInternal(action, arguments);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean onTouchEvent(MotionEvent ev) {
|
||
|
final boolean showSwipeUI = false; // mOverviewProxyService.shouldShowSwipeUpUI();
|
||
|
final int action = ev.getAction();
|
||
|
int x, y;
|
||
|
if (action == MotionEvent.ACTION_DOWN) {
|
||
|
mGestureAborted = false;
|
||
|
}
|
||
|
if (mGestureAborted) {
|
||
|
setPressed(false);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
switch (action) {
|
||
|
case MotionEvent.ACTION_DOWN:
|
||
|
mDownTime = SystemClock.uptimeMillis();
|
||
|
setPressed(true);
|
||
|
|
||
|
// Use raw X and Y to detect gestures in case a parent changes the x and y values
|
||
|
mTouchDownX = (int) ev.getRawX();
|
||
|
mTouchDownY = (int) ev.getRawY();
|
||
|
if (mCode != KEYCODE_UNKNOWN) {
|
||
|
sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);
|
||
|
} else {
|
||
|
// Provide the same haptic feedback that the system offers for virtual keys.
|
||
|
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
|
||
|
}
|
||
|
if (!showSwipeUI) {
|
||
|
playSoundEffect(SoundEffectConstants.CLICK);
|
||
|
}
|
||
|
break;
|
||
|
case MotionEvent.ACTION_MOVE:
|
||
|
x = (int) ev.getRawX();
|
||
|
y = (int) ev.getRawY();
|
||
|
|
||
|
float slop = getQuickStepTouchSlopPx(getContext());
|
||
|
if (Math.abs(x - mTouchDownX) > slop || Math.abs(y - mTouchDownY) > slop) {
|
||
|
// When quick step is enabled, prevent animating the ripple triggered by
|
||
|
// setPressed and decide to run it on touch up
|
||
|
setPressed(false);
|
||
|
}
|
||
|
break;
|
||
|
case MotionEvent.ACTION_CANCEL:
|
||
|
setPressed(false);
|
||
|
if (mCode != KEYCODE_UNKNOWN) {
|
||
|
sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
|
||
|
}
|
||
|
break;
|
||
|
case MotionEvent.ACTION_UP:
|
||
|
final boolean doIt = isPressed();
|
||
|
setPressed(false);
|
||
|
final boolean doHapticFeedback = (SystemClock.uptimeMillis() - mDownTime) > 150;
|
||
|
if (showSwipeUI) {
|
||
|
if (doIt) {
|
||
|
// Apply haptic feedback on touch up since there is none on touch down
|
||
|
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
|
||
|
playSoundEffect(SoundEffectConstants.CLICK);
|
||
|
}
|
||
|
} else if (doHapticFeedback) {
|
||
|
// Always send a release ourselves because it doesn't seem to be sent elsewhere
|
||
|
// and it feels weird to sometimes get a release haptic and other times not.
|
||
|
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE);
|
||
|
}
|
||
|
if (mCode != KEYCODE_UNKNOWN) {
|
||
|
if (doIt) {
|
||
|
sendEvent(KeyEvent.ACTION_UP, mTracking ? KeyEvent.FLAG_TRACKING : 0);
|
||
|
mTracking = false;
|
||
|
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||
|
} else {
|
||
|
sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
|
||
|
}
|
||
|
} else {
|
||
|
// no key code, just a regular ImageView
|
||
|
if (doIt && mOnClickListener != null) {
|
||
|
mOnClickListener.onClick(this);
|
||
|
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setImageDrawable(Drawable drawable) {
|
||
|
super.setImageDrawable(drawable);
|
||
|
|
||
|
if (drawable == null) {
|
||
|
return;
|
||
|
}
|
||
|
KeyButtonDrawable keyButtonDrawable = (KeyButtonDrawable) drawable;
|
||
|
keyButtonDrawable.setDarkIntensity(mDarkIntensity);
|
||
|
mHasOvalBg = keyButtonDrawable.hasOvalBg();
|
||
|
if (mHasOvalBg) {
|
||
|
mOvalBgPaint.setColor(keyButtonDrawable.getDrawableBackgroundColor());
|
||
|
}
|
||
|
mRipple.setType(keyButtonDrawable.hasOvalBg() ? KeyButtonRipple.Type.OVAL
|
||
|
: KeyButtonRipple.Type.ROUNDED_RECT);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void playSoundEffect(int soundConstant) {
|
||
|
if (!mPlaySounds) return;
|
||
|
mAudioManager.playSoundEffect(soundConstant);
|
||
|
}
|
||
|
|
||
|
private void sendEvent(int action, int flags) {
|
||
|
sendEvent(action, flags, SystemClock.uptimeMillis());
|
||
|
}
|
||
|
|
||
|
private void sendEvent(int action, int flags, long when) {
|
||
|
// TODO(b/215443343): Consolidate this logic to somewhere else.
|
||
|
if (mContext instanceof InputMethodService) {
|
||
|
final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
|
||
|
final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount,
|
||
|
0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
|
||
|
flags | KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
|
||
|
InputDevice.SOURCE_KEYBOARD);
|
||
|
int displayId = INVALID_DISPLAY;
|
||
|
|
||
|
// Make KeyEvent work on multi-display environment
|
||
|
if (getDisplay() != null) {
|
||
|
displayId = getDisplay().getDisplayId();
|
||
|
}
|
||
|
if (displayId != INVALID_DISPLAY) {
|
||
|
ev.setDisplayId(displayId);
|
||
|
}
|
||
|
final InputMethodService ims = (InputMethodService) mContext;
|
||
|
final boolean handled;
|
||
|
switch (action) {
|
||
|
case KeyEvent.ACTION_DOWN:
|
||
|
handled = ims.onKeyDown(ev.getKeyCode(), ev);
|
||
|
mTracking = handled && ev.getRepeatCount() == 0
|
||
|
&& (ev.getFlags() & KeyEvent.FLAG_START_TRACKING) != 0;
|
||
|
break;
|
||
|
case KeyEvent.ACTION_UP:
|
||
|
handled = ims.onKeyUp(ev.getKeyCode(), ev);
|
||
|
break;
|
||
|
default:
|
||
|
handled = false;
|
||
|
break;
|
||
|
}
|
||
|
if (!handled) {
|
||
|
final InputConnection ic = ims.getCurrentInputConnection();
|
||
|
if (ic != null) {
|
||
|
ic.sendKeyEvent(ev);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setDarkIntensity(float darkIntensity) {
|
||
|
mDarkIntensity = darkIntensity;
|
||
|
|
||
|
Drawable drawable = getDrawable();
|
||
|
if (drawable != null) {
|
||
|
((KeyButtonDrawable) drawable).setDarkIntensity(darkIntensity);
|
||
|
// Since we reuse the same drawable for multiple views, we need to invalidate the view
|
||
|
// manually.
|
||
|
invalidate();
|
||
|
}
|
||
|
mRipple.setDarkIntensity(darkIntensity);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setDelayTouchFeedback(boolean shouldDelay) {
|
||
|
mRipple.setDelayTouchFeedback(shouldDelay);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void draw(Canvas canvas) {
|
||
|
if (mHasOvalBg) {
|
||
|
int d = Math.min(getWidth(), getHeight());
|
||
|
canvas.drawOval(0, 0, d, d, mOvalBgPaint);
|
||
|
}
|
||
|
super.draw(canvas);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Ratio of quickstep touch slop (when system takes over the touch) to view touch slop
|
||
|
*/
|
||
|
public static final float QUICKSTEP_TOUCH_SLOP_RATIO = 3;
|
||
|
|
||
|
/**
|
||
|
* Touch slop for quickstep gesture
|
||
|
*/
|
||
|
private static float getQuickStepTouchSlopPx(Context context) {
|
||
|
return QUICKSTEP_TOUCH_SLOP_RATIO * ViewConfiguration.get(context).getScaledTouchSlop();
|
||
|
}
|
||
|
}
|