| /* |
| * Copyright (C) 2011 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 com.android.inputmethod.keyboard; |
| |
| import android.animation.AnimatorInflater; |
| import android.animation.ObjectAnimator; |
| import android.content.Context; |
| import android.content.SharedPreferences; |
| import android.content.pm.PackageManager; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Paint; |
| import android.graphics.Paint.Align; |
| import android.graphics.Typeface; |
| import android.graphics.drawable.Drawable; |
| import android.os.Message; |
| import android.os.SystemClock; |
| import android.preference.PreferenceManager; |
| import android.util.AttributeSet; |
| import android.util.DisplayMetrics; |
| import android.util.Log; |
| import android.util.SparseArray; |
| import android.util.TypedValue; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewGroup; |
| import android.view.inputmethod.InputMethodSubtype; |
| import android.widget.TextView; |
| |
| import com.android.inputmethod.accessibility.AccessibilityUtils; |
| import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; |
| import com.android.inputmethod.annotations.ExternallyReferenced; |
| import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy; |
| import com.android.inputmethod.keyboard.PointerTracker.TimerProxy; |
| import com.android.inputmethod.keyboard.internal.GestureFloatingPreviewText; |
| import com.android.inputmethod.keyboard.internal.GestureTrailsPreview; |
| import com.android.inputmethod.keyboard.internal.KeyDrawParams; |
| import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams; |
| import com.android.inputmethod.keyboard.internal.PreviewPlacerView; |
| import com.android.inputmethod.keyboard.internal.SlidingKeyInputPreview; |
| import com.android.inputmethod.keyboard.internal.TouchScreenRegulator; |
| import com.android.inputmethod.latin.CollectionUtils; |
| import com.android.inputmethod.latin.Constants; |
| import com.android.inputmethod.latin.CoordinateUtils; |
| import com.android.inputmethod.latin.DebugSettings; |
| import com.android.inputmethod.latin.LatinIME; |
| import com.android.inputmethod.latin.LatinImeLogger; |
| import com.android.inputmethod.latin.R; |
| import com.android.inputmethod.latin.ResourceUtils; |
| import com.android.inputmethod.latin.Settings; |
| import com.android.inputmethod.latin.StaticInnerHandlerWrapper; |
| import com.android.inputmethod.latin.StringUtils; |
| import com.android.inputmethod.latin.SubtypeLocale; |
| import com.android.inputmethod.latin.SuggestedWords; |
| import com.android.inputmethod.latin.Utils.UsabilityStudyLogUtils; |
| import com.android.inputmethod.latin.define.ProductionFlag; |
| import com.android.inputmethod.research.ResearchLogger; |
| |
| import java.util.Locale; |
| import java.util.WeakHashMap; |
| |
| /** |
| * A view that is responsible for detecting key presses and touch movements. |
| * |
| * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedEnabled |
| * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedIcon |
| * @attr ref R.styleable#MainKeyboardView_spacebarTextRatio |
| * @attr ref R.styleable#MainKeyboardView_spacebarTextColor |
| * @attr ref R.styleable#MainKeyboardView_spacebarTextShadowColor |
| * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFinalAlpha |
| * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFadeoutAnimator |
| * @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator |
| * @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator |
| * @attr ref R.styleable#MainKeyboardView_keyHysteresisDistance |
| * @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdTime |
| * @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdDistance |
| * @attr ref R.styleable#MainKeyboardView_slidingKeyInputEnable |
| * @attr ref R.styleable#MainKeyboardView_keyRepeatStartTimeout |
| * @attr ref R.styleable#MainKeyboardView_keyRepeatInterval |
| * @attr ref R.styleable#MainKeyboardView_longPressKeyTimeout |
| * @attr ref R.styleable#MainKeyboardView_longPressShiftKeyTimeout |
| * @attr ref R.styleable#MainKeyboardView_ignoreAltCodeKeyTimeout |
| * @attr ref R.styleable#MainKeyboardView_keyPreviewLayout |
| * @attr ref R.styleable#MainKeyboardView_keyPreviewOffset |
| * @attr ref R.styleable#MainKeyboardView_keyPreviewHeight |
| * @attr ref R.styleable#MainKeyboardView_keyPreviewLingerTimeout |
| * @attr ref R.styleable#MainKeyboardView_moreKeysKeyboardLayout |
| * @attr ref R.styleable#MainKeyboardView_backgroundDimAlpha |
| * @attr ref R.styleable#MainKeyboardView_showMoreKeysKeyboardAtTouchPoint |
| * @attr ref R.styleable#MainKeyboardView_gestureFloatingPreviewTextLingerTimeout |
| * @attr ref R.styleable#MainKeyboardView_gestureStaticTimeThresholdAfterFastTyping |
| * @attr ref R.styleable#MainKeyboardView_gestureDetectFastMoveSpeedThreshold |
| * @attr ref R.styleable#MainKeyboardView_gestureDynamicThresholdDecayDuration |
| * @attr ref R.styleable#MainKeyboardView_gestureDynamicTimeThresholdFrom |
| * @attr ref R.styleable#MainKeyboardView_gestureDynamicTimeThresholdTo |
| * @attr ref R.styleable#MainKeyboardView_gestureDynamicDistanceThresholdFrom |
| * @attr ref R.styleable#MainKeyboardView_gestureDynamicDistanceThresholdTo |
| * @attr ref R.styleable#MainKeyboardView_gestureSamplingMinimumDistance |
| * @attr ref R.styleable#MainKeyboardView_gestureRecognitionMinimumTime |
| * @attr ref R.styleable#MainKeyboardView_gestureRecognitionSpeedThreshold |
| * @attr ref R.styleable#MainKeyboardView_suppressKeyPreviewAfterBatchInputDuration |
| */ |
| public final class MainKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler, |
| PointerTracker.DrawingProxy, MoreKeysPanel.Controller, |
| TouchScreenRegulator.ProcessMotionEvent { |
| private static final String TAG = MainKeyboardView.class.getSimpleName(); |
| |
| // TODO: Kill process when the usability study mode was changed. |
| private static final boolean ENABLE_USABILITY_STUDY_LOG = LatinImeLogger.sUsabilityStudy; |
| |
| /** Listener for {@link KeyboardActionListener}. */ |
| private KeyboardActionListener mKeyboardActionListener; |
| |
| /* Space key and its icons */ |
| private Key mSpaceKey; |
| private Drawable mSpaceIcon; |
| // Stuff to draw language name on spacebar. |
| private final int mLanguageOnSpacebarFinalAlpha; |
| private ObjectAnimator mLanguageOnSpacebarFadeoutAnimator; |
| private boolean mNeedsToDisplayLanguage; |
| private boolean mHasMultipleEnabledIMEsOrSubtypes; |
| private int mLanguageOnSpacebarAnimAlpha = Constants.Color.ALPHA_OPAQUE; |
| private final float mSpacebarTextRatio; |
| private float mSpacebarTextSize; |
| private final int mSpacebarTextColor; |
| private final int mSpacebarTextShadowColor; |
| // The minimum x-scale to fit the language name on spacebar. |
| private static final float MINIMUM_XSCALE_OF_LANGUAGE_NAME = 0.8f; |
| // Stuff to draw auto correction LED on spacebar. |
| private boolean mAutoCorrectionSpacebarLedOn; |
| private final boolean mAutoCorrectionSpacebarLedEnabled; |
| private final Drawable mAutoCorrectionSpacebarLedIcon; |
| private static final int SPACE_LED_LENGTH_PERCENT = 80; |
| |
| // Stuff to draw altCodeWhileTyping keys. |
| private ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator; |
| private ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator; |
| private int mAltCodeKeyWhileTypingAnimAlpha = Constants.Color.ALPHA_OPAQUE; |
| |
| // Preview placer view |
| private final PreviewPlacerView mPreviewPlacerView; |
| private final int[] mOriginCoords = CoordinateUtils.newInstance(); |
| private final GestureFloatingPreviewText mGestureFloatingPreviewText; |
| private final GestureTrailsPreview mGestureTrailsPreview; |
| private final SlidingKeyInputPreview mSlidingKeyInputPreview; |
| |
| // Key preview |
| private static final int PREVIEW_ALPHA = 240; |
| private final int mKeyPreviewLayoutId; |
| private final int mKeyPreviewOffset; |
| private final int mKeyPreviewHeight; |
| private final SparseArray<TextView> mKeyPreviewTexts = CollectionUtils.newSparseArray(); |
| private final KeyPreviewDrawParams mKeyPreviewDrawParams = new KeyPreviewDrawParams(); |
| private boolean mShowKeyPreviewPopup = true; |
| private int mKeyPreviewLingerTimeout; |
| |
| // More keys keyboard |
| private final Paint mBackgroundDimAlphaPaint = new Paint(); |
| private boolean mNeedsToDimEntireKeyboard; |
| private final View mMoreKeysKeyboardContainer; |
| private final WeakHashMap<Key, Keyboard> mMoreKeysKeyboardCache = |
| CollectionUtils.newWeakHashMap(); |
| private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint; |
| // More keys panel (used by both more keys keyboard and more suggestions view) |
| // TODO: Consider extending to support multiple more keys panels |
| private MoreKeysPanel mMoreKeysPanel; |
| |
| // Gesture floating preview text |
| // TODO: Make this parameter customizable by user via settings. |
| private int mGestureFloatingPreviewTextLingerTimeout; |
| |
| private final TouchScreenRegulator mTouchScreenRegulator; |
| |
| private KeyDetector mKeyDetector; |
| private final boolean mHasDistinctMultitouch; |
| private int mOldPointerCount = 1; |
| private Key mOldKey; |
| |
| private final KeyTimerHandler mKeyTimerHandler; |
| |
| private static final class KeyTimerHandler extends StaticInnerHandlerWrapper<MainKeyboardView> |
| implements TimerProxy { |
| private static final int MSG_TYPING_STATE_EXPIRED = 0; |
| private static final int MSG_REPEAT_KEY = 1; |
| private static final int MSG_LONGPRESS_KEY = 2; |
| private static final int MSG_DOUBLE_TAP = 3; |
| private static final int MSG_UPDATE_BATCH_INPUT = 4; |
| |
| private final int mKeyRepeatStartTimeout; |
| private final int mKeyRepeatInterval; |
| private final int mLongPressShiftLockTimeout; |
| private final int mIgnoreAltCodeKeyTimeout; |
| private final int mGestureRecognitionUpdateTime; |
| |
| public KeyTimerHandler(final MainKeyboardView outerInstance, |
| final TypedArray mainKeyboardViewAttr) { |
| super(outerInstance); |
| |
| mKeyRepeatStartTimeout = mainKeyboardViewAttr.getInt( |
| R.styleable.MainKeyboardView_keyRepeatStartTimeout, 0); |
| mKeyRepeatInterval = mainKeyboardViewAttr.getInt( |
| R.styleable.MainKeyboardView_keyRepeatInterval, 0); |
| mLongPressShiftLockTimeout = mainKeyboardViewAttr.getInt( |
| R.styleable.MainKeyboardView_longPressShiftLockTimeout, 0); |
| mIgnoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt( |
| R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0); |
| mGestureRecognitionUpdateTime = mainKeyboardViewAttr.getInt( |
| R.styleable.MainKeyboardView_gestureRecognitionUpdateTime, 0); |
| } |
| |
| @Override |
| public void handleMessage(final Message msg) { |
| final MainKeyboardView keyboardView = getOuterInstance(); |
| if (keyboardView == null) { |
| return; |
| } |
| final PointerTracker tracker = (PointerTracker) msg.obj; |
| switch (msg.what) { |
| case MSG_TYPING_STATE_EXPIRED: |
| startWhileTypingFadeinAnimation(keyboardView); |
| break; |
| case MSG_REPEAT_KEY: |
| final Key currentKey = tracker.getKey(); |
| if (currentKey != null && currentKey.mCode == msg.arg1) { |
| tracker.onRegisterKey(currentKey); |
| startKeyRepeatTimer(tracker, mKeyRepeatInterval); |
| } |
| break; |
| case MSG_LONGPRESS_KEY: |
| if (tracker != null) { |
| keyboardView.onLongPress(tracker); |
| } else { |
| KeyboardSwitcher.getInstance().onLongPressTimeout(msg.arg1); |
| } |
| break; |
| case MSG_UPDATE_BATCH_INPUT: |
| tracker.updateBatchInputByTimer(SystemClock.uptimeMillis()); |
| startUpdateBatchInputTimer(tracker); |
| break; |
| } |
| } |
| |
| private void startKeyRepeatTimer(final PointerTracker tracker, final long delay) { |
| final Key key = tracker.getKey(); |
| if (key == null) { |
| return; |
| } |
| sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, key.mCode, 0, tracker), delay); |
| } |
| |
| @Override |
| public void startKeyRepeatTimer(final PointerTracker tracker) { |
| startKeyRepeatTimer(tracker, mKeyRepeatStartTimeout); |
| } |
| |
| public void cancelKeyRepeatTimer() { |
| removeMessages(MSG_REPEAT_KEY); |
| } |
| |
| // TODO: Suppress layout changes in key repeat mode |
| public boolean isInKeyRepeat() { |
| return hasMessages(MSG_REPEAT_KEY); |
| } |
| |
| @Override |
| public void startLongPressTimer(final int code) { |
| cancelLongPressTimer(); |
| final int delay; |
| switch (code) { |
| case Constants.CODE_SHIFT: |
| delay = mLongPressShiftLockTimeout; |
| break; |
| default: |
| delay = 0; |
| break; |
| } |
| if (delay > 0) { |
| sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, code, 0), delay); |
| } |
| } |
| |
| @Override |
| public void startLongPressTimer(final PointerTracker tracker) { |
| cancelLongPressTimer(); |
| if (tracker == null) { |
| return; |
| } |
| final Key key = tracker.getKey(); |
| final int delay; |
| switch (key.mCode) { |
| case Constants.CODE_SHIFT: |
| delay = mLongPressShiftLockTimeout; |
| break; |
| default: |
| final int longpressTimeout = |
| Settings.getInstance().getCurrent().mKeyLongpressTimeout; |
| if (KeyboardSwitcher.getInstance().isInMomentarySwitchState()) { |
| // We use longer timeout for sliding finger input started from the symbols |
| // mode key. |
| delay = longpressTimeout * 3; |
| } else { |
| delay = longpressTimeout; |
| } |
| break; |
| } |
| if (delay > 0) { |
| sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, tracker), delay); |
| } |
| } |
| |
| @Override |
| public void cancelLongPressTimer() { |
| removeMessages(MSG_LONGPRESS_KEY); |
| } |
| |
| private static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel, |
| final ObjectAnimator animatorToStart) { |
| if (animatorToCancel == null || animatorToStart == null) { |
| // TODO: Stop using null as a no-operation animator. |
| return; |
| } |
| float startFraction = 0.0f; |
| if (animatorToCancel.isStarted()) { |
| animatorToCancel.cancel(); |
| startFraction = 1.0f - animatorToCancel.getAnimatedFraction(); |
| } |
| final long startTime = (long)(animatorToStart.getDuration() * startFraction); |
| animatorToStart.start(); |
| animatorToStart.setCurrentPlayTime(startTime); |
| } |
| |
| private static void startWhileTypingFadeinAnimation(final MainKeyboardView keyboardView) { |
| cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator, |
| keyboardView.mAltCodeKeyWhileTypingFadeinAnimator); |
| } |
| |
| private static void startWhileTypingFadeoutAnimation(final MainKeyboardView keyboardView) { |
| cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeinAnimator, |
| keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator); |
| } |
| |
| @Override |
| public void startTypingStateTimer(final Key typedKey) { |
| if (typedKey.isModifier() || typedKey.altCodeWhileTyping()) { |
| return; |
| } |
| |
| final boolean isTyping = isTypingState(); |
| removeMessages(MSG_TYPING_STATE_EXPIRED); |
| final MainKeyboardView keyboardView = getOuterInstance(); |
| |
| // When user hits the space or the enter key, just cancel the while-typing timer. |
| final int typedCode = typedKey.mCode; |
| if (typedCode == Constants.CODE_SPACE || typedCode == Constants.CODE_ENTER) { |
| if (isTyping) { |
| startWhileTypingFadeinAnimation(keyboardView); |
| } |
| return; |
| } |
| |
| sendMessageDelayed( |
| obtainMessage(MSG_TYPING_STATE_EXPIRED), mIgnoreAltCodeKeyTimeout); |
| if (isTyping) { |
| return; |
| } |
| startWhileTypingFadeoutAnimation(keyboardView); |
| } |
| |
| @Override |
| public boolean isTypingState() { |
| return hasMessages(MSG_TYPING_STATE_EXPIRED); |
| } |
| |
| @Override |
| public void startDoubleTapTimer() { |
| sendMessageDelayed(obtainMessage(MSG_DOUBLE_TAP), |
| ViewConfiguration.getDoubleTapTimeout()); |
| } |
| |
| @Override |
| public void cancelDoubleTapTimer() { |
| removeMessages(MSG_DOUBLE_TAP); |
| } |
| |
| @Override |
| public boolean isInDoubleTapTimeout() { |
| return hasMessages(MSG_DOUBLE_TAP); |
| } |
| |
| @Override |
| public void cancelKeyTimers() { |
| cancelKeyRepeatTimer(); |
| cancelLongPressTimer(); |
| } |
| |
| @Override |
| public void startUpdateBatchInputTimer(final PointerTracker tracker) { |
| if (mGestureRecognitionUpdateTime <= 0) { |
| return; |
| } |
| removeMessages(MSG_UPDATE_BATCH_INPUT, tracker); |
| sendMessageDelayed(obtainMessage(MSG_UPDATE_BATCH_INPUT, tracker), |
| mGestureRecognitionUpdateTime); |
| } |
| |
| @Override |
| public void cancelUpdateBatchInputTimer(final PointerTracker tracker) { |
| removeMessages(MSG_UPDATE_BATCH_INPUT, tracker); |
| } |
| |
| @Override |
| public void cancelAllUpdateBatchInputTimers() { |
| removeMessages(MSG_UPDATE_BATCH_INPUT); |
| } |
| |
| public void cancelAllMessages() { |
| cancelKeyTimers(); |
| cancelAllUpdateBatchInputTimers(); |
| } |
| } |
| |
| private final DrawingHandler mDrawingHandler = new DrawingHandler(this); |
| |
| public static class DrawingHandler extends StaticInnerHandlerWrapper<MainKeyboardView> { |
| private static final int MSG_DISMISS_KEY_PREVIEW = 0; |
| private static final int MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; |
| |
| public DrawingHandler(final MainKeyboardView outerInstance) { |
| super(outerInstance); |
| } |
| |
| @Override |
| public void handleMessage(final Message msg) { |
| final MainKeyboardView mainKeyboardView = getOuterInstance(); |
| if (mainKeyboardView == null) return; |
| final PointerTracker tracker = (PointerTracker) msg.obj; |
| switch (msg.what) { |
| case MSG_DISMISS_KEY_PREVIEW: |
| final TextView previewText = mainKeyboardView.mKeyPreviewTexts.get( |
| tracker.mPointerId); |
| if (previewText != null) { |
| previewText.setVisibility(INVISIBLE); |
| } |
| break; |
| case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT: |
| mainKeyboardView.showGestureFloatingPreviewText(SuggestedWords.EMPTY); |
| break; |
| } |
| } |
| |
| public void dismissKeyPreview(final long delay, final PointerTracker tracker) { |
| sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay); |
| } |
| |
| public void cancelDismissKeyPreview(final PointerTracker tracker) { |
| removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker); |
| } |
| |
| private void cancelAllDismissKeyPreviews() { |
| removeMessages(MSG_DISMISS_KEY_PREVIEW); |
| } |
| |
| public void dismissGestureFloatingPreviewText(final long delay) { |
| sendMessageDelayed(obtainMessage(MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT), delay); |
| } |
| |
| public void cancelAllMessages() { |
| cancelAllDismissKeyPreviews(); |
| } |
| } |
| |
| public MainKeyboardView(final Context context, final AttributeSet attrs) { |
| this(context, attrs, R.attr.mainKeyboardViewStyle); |
| } |
| |
| public MainKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) { |
| super(context, attrs, defStyle); |
| |
| mTouchScreenRegulator = new TouchScreenRegulator(context, this); |
| |
| final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); |
| final boolean forceNonDistinctMultitouch = prefs.getBoolean( |
| DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH, false); |
| final boolean hasDistinctMultitouch = context.getPackageManager() |
| .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT); |
| mHasDistinctMultitouch = hasDistinctMultitouch && !forceNonDistinctMultitouch; |
| final Resources res = getResources(); |
| final boolean needsPhantomSuddenMoveEventHack = Boolean.parseBoolean( |
| ResourceUtils.getDeviceOverrideValue( |
| res, R.array.phantom_sudden_move_event_device_list)); |
| PointerTracker.init(needsPhantomSuddenMoveEventHack); |
| mPreviewPlacerView = new PreviewPlacerView(context, attrs); |
| |
| final TypedArray mainKeyboardViewAttr = context.obtainStyledAttributes( |
| attrs, R.styleable.MainKeyboardView, defStyle, R.style.MainKeyboardView); |
| final int backgroundDimAlpha = mainKeyboardViewAttr.getInt( |
| R.styleable.MainKeyboardView_backgroundDimAlpha, 0); |
| mBackgroundDimAlphaPaint.setColor(Color.BLACK); |
| mBackgroundDimAlphaPaint.setAlpha(backgroundDimAlpha); |
| mAutoCorrectionSpacebarLedEnabled = mainKeyboardViewAttr.getBoolean( |
| R.styleable.MainKeyboardView_autoCorrectionSpacebarLedEnabled, false); |
| mAutoCorrectionSpacebarLedIcon = mainKeyboardViewAttr.getDrawable( |
| R.styleable.MainKeyboardView_autoCorrectionSpacebarLedIcon); |
| mSpacebarTextRatio = mainKeyboardViewAttr.getFraction( |
| R.styleable.MainKeyboardView_spacebarTextRatio, 1, 1, 1.0f); |
| mSpacebarTextColor = mainKeyboardViewAttr.getColor( |
| R.styleable.MainKeyboardView_spacebarTextColor, 0); |
| mSpacebarTextShadowColor = mainKeyboardViewAttr.getColor( |
| R.styleable.MainKeyboardView_spacebarTextShadowColor, 0); |
| mLanguageOnSpacebarFinalAlpha = mainKeyboardViewAttr.getInt( |
| R.styleable.MainKeyboardView_languageOnSpacebarFinalAlpha, |
| Constants.Color.ALPHA_OPAQUE); |
| final int languageOnSpacebarFadeoutAnimatorResId = mainKeyboardViewAttr.getResourceId( |
| R.styleable.MainKeyboardView_languageOnSpacebarFadeoutAnimator, 0); |
| final int altCodeKeyWhileTypingFadeoutAnimatorResId = mainKeyboardViewAttr.getResourceId( |
| R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator, 0); |
| final int altCodeKeyWhileTypingFadeinAnimatorResId = mainKeyboardViewAttr.getResourceId( |
| R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0); |
| |
| final float keyHysteresisDistance = mainKeyboardViewAttr.getDimension( |
| R.styleable.MainKeyboardView_keyHysteresisDistance, 0.0f); |
| final float keyHysteresisDistanceForSlidingModifier = mainKeyboardViewAttr.getDimension( |
| R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0.0f); |
| mKeyDetector = new KeyDetector( |
| keyHysteresisDistance, keyHysteresisDistanceForSlidingModifier); |
| mKeyTimerHandler = new KeyTimerHandler(this, mainKeyboardViewAttr); |
| mKeyPreviewOffset = mainKeyboardViewAttr.getDimensionPixelOffset( |
| R.styleable.MainKeyboardView_keyPreviewOffset, 0); |
| mKeyPreviewHeight = mainKeyboardViewAttr.getDimensionPixelSize( |
| R.styleable.MainKeyboardView_keyPreviewHeight, 0); |
| mKeyPreviewLingerTimeout = mainKeyboardViewAttr.getInt( |
| R.styleable.MainKeyboardView_keyPreviewLingerTimeout, 0); |
| mKeyPreviewLayoutId = mainKeyboardViewAttr.getResourceId( |
| R.styleable.MainKeyboardView_keyPreviewLayout, 0); |
| if (mKeyPreviewLayoutId == 0) { |
| mShowKeyPreviewPopup = false; |
| } |
| final int moreKeysKeyboardLayoutId = mainKeyboardViewAttr.getResourceId( |
| R.styleable.MainKeyboardView_moreKeysKeyboardLayout, 0); |
| mConfigShowMoreKeysKeyboardAtTouchedPoint = mainKeyboardViewAttr.getBoolean( |
| R.styleable.MainKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false); |
| |
| mGestureFloatingPreviewTextLingerTimeout = mainKeyboardViewAttr.getInt( |
| R.styleable.MainKeyboardView_gestureFloatingPreviewTextLingerTimeout, 0); |
| PointerTracker.setParameters(mainKeyboardViewAttr); |
| |
| mGestureFloatingPreviewText = new GestureFloatingPreviewText( |
| mPreviewPlacerView, mainKeyboardViewAttr); |
| mPreviewPlacerView.addPreview(mGestureFloatingPreviewText); |
| |
| mGestureTrailsPreview = new GestureTrailsPreview( |
| mPreviewPlacerView, mainKeyboardViewAttr); |
| mPreviewPlacerView.addPreview(mGestureTrailsPreview); |
| |
| mSlidingKeyInputPreview = new SlidingKeyInputPreview( |
| mPreviewPlacerView, mainKeyboardViewAttr); |
| mPreviewPlacerView.addPreview(mSlidingKeyInputPreview); |
| mainKeyboardViewAttr.recycle(); |
| |
| mMoreKeysKeyboardContainer = LayoutInflater.from(getContext()) |
| .inflate(moreKeysKeyboardLayoutId, null); |
| mLanguageOnSpacebarFadeoutAnimator = loadObjectAnimator( |
| languageOnSpacebarFadeoutAnimatorResId, this); |
| mAltCodeKeyWhileTypingFadeoutAnimator = loadObjectAnimator( |
| altCodeKeyWhileTypingFadeoutAnimatorResId, this); |
| mAltCodeKeyWhileTypingFadeinAnimator = loadObjectAnimator( |
| altCodeKeyWhileTypingFadeinAnimatorResId, this); |
| } |
| |
| private ObjectAnimator loadObjectAnimator(final int resId, final Object target) { |
| if (resId == 0) { |
| // TODO: Stop returning null. |
| return null; |
| } |
| final ObjectAnimator animator = (ObjectAnimator)AnimatorInflater.loadAnimator( |
| getContext(), resId); |
| if (animator != null) { |
| animator.setTarget(target); |
| } |
| return animator; |
| } |
| |
| @ExternallyReferenced |
| public int getLanguageOnSpacebarAnimAlpha() { |
| return mLanguageOnSpacebarAnimAlpha; |
| } |
| |
| @ExternallyReferenced |
| public void setLanguageOnSpacebarAnimAlpha(final int alpha) { |
| mLanguageOnSpacebarAnimAlpha = alpha; |
| invalidateKey(mSpaceKey); |
| } |
| |
| @ExternallyReferenced |
| public int getAltCodeKeyWhileTypingAnimAlpha() { |
| return mAltCodeKeyWhileTypingAnimAlpha; |
| } |
| |
| @ExternallyReferenced |
| public void setAltCodeKeyWhileTypingAnimAlpha(final int alpha) { |
| if (mAltCodeKeyWhileTypingAnimAlpha == alpha) { |
| return; |
| } |
| // Update the visual of alt-code-key-while-typing. |
| mAltCodeKeyWhileTypingAnimAlpha = alpha; |
| final Keyboard keyboard = getKeyboard(); |
| if (keyboard == null) { |
| return; |
| } |
| for (final Key key : keyboard.mAltCodeKeysWhileTyping) { |
| invalidateKey(key); |
| } |
| } |
| |
| public void setKeyboardActionListener(final KeyboardActionListener listener) { |
| mKeyboardActionListener = listener; |
| PointerTracker.setKeyboardActionListener(listener); |
| } |
| |
| /** |
| * Returns the {@link KeyboardActionListener} object. |
| * @return the listener attached to this keyboard |
| */ |
| @Override |
| public KeyboardActionListener getKeyboardActionListener() { |
| return mKeyboardActionListener; |
| } |
| |
| @Override |
| public KeyDetector getKeyDetector() { |
| return mKeyDetector; |
| } |
| |
| @Override |
| public DrawingProxy getDrawingProxy() { |
| return this; |
| } |
| |
| @Override |
| public TimerProxy getTimerProxy() { |
| return mKeyTimerHandler; |
| } |
| |
| /** |
| * Attaches a keyboard to this view. The keyboard can be switched at any time and the |
| * view will re-layout itself to accommodate the keyboard. |
| * @see Keyboard |
| * @see #getKeyboard() |
| * @param keyboard the keyboard to display in this view |
| */ |
| @Override |
| public void setKeyboard(final Keyboard keyboard) { |
| // Remove any pending messages, except dismissing preview and key repeat. |
| mKeyTimerHandler.cancelLongPressTimer(); |
| super.setKeyboard(keyboard); |
| mKeyDetector.setKeyboard( |
| keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection()); |
| PointerTracker.setKeyDetector(mKeyDetector); |
| mTouchScreenRegulator.setKeyboardGeometry(keyboard.mOccupiedWidth); |
| mMoreKeysKeyboardCache.clear(); |
| |
| mSpaceKey = keyboard.getKey(Constants.CODE_SPACE); |
| mSpaceIcon = (mSpaceKey != null) |
| ? mSpaceKey.getIcon(keyboard.mIconsSet, Constants.Color.ALPHA_OPAQUE) : null; |
| final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap; |
| mSpacebarTextSize = keyHeight * mSpacebarTextRatio; |
| if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { |
| ResearchLogger.mainKeyboardView_setKeyboard(keyboard); |
| } |
| |
| // This always needs to be set since the accessibility state can |
| // potentially change without the keyboard being set again. |
| AccessibleKeyboardViewProxy.getInstance().setKeyboard(); |
| } |
| |
| /** |
| * Enables or disables the key feedback popup. This is a popup that shows a magnified |
| * version of the depressed key. By default the preview is enabled. |
| * @param previewEnabled whether or not to enable the key feedback preview |
| * @param delay the delay after which the preview is dismissed |
| * @see #isKeyPreviewPopupEnabled() |
| */ |
| public void setKeyPreviewPopupEnabled(final boolean previewEnabled, final int delay) { |
| mShowKeyPreviewPopup = previewEnabled; |
| mKeyPreviewLingerTimeout = delay; |
| } |
| |
| |
| private void locatePreviewPlacerView() { |
| if (mPreviewPlacerView.getParent() != null) { |
| return; |
| } |
| final int width = getWidth(); |
| final int height = getHeight(); |
| if (width == 0 || height == 0) { |
| // In transient state. |
| return; |
| } |
| getLocationInWindow(mOriginCoords); |
| final DisplayMetrics dm = getResources().getDisplayMetrics(); |
| if (CoordinateUtils.y(mOriginCoords) < dm.heightPixels / 4) { |
| // In transient state. |
| return; |
| } |
| final View rootView = getRootView(); |
| if (rootView == null) { |
| Log.w(TAG, "Cannot find root view"); |
| return; |
| } |
| final ViewGroup windowContentView = (ViewGroup)rootView.findViewById(android.R.id.content); |
| // Note: It'd be very weird if we get null by android.R.id.content. |
| if (windowContentView == null) { |
| Log.w(TAG, "Cannot find android.R.id.content view to add PreviewPlacerView"); |
| } else { |
| windowContentView.addView(mPreviewPlacerView); |
| mPreviewPlacerView.setKeyboardViewGeometry(mOriginCoords, width, height); |
| } |
| } |
| |
| /** |
| * Returns the enabled state of the key feedback preview |
| * @return whether or not the key feedback preview is enabled |
| * @see #setKeyPreviewPopupEnabled(boolean, int) |
| */ |
| public boolean isKeyPreviewPopupEnabled() { |
| return mShowKeyPreviewPopup; |
| } |
| |
| private void addKeyPreview(final TextView keyPreview) { |
| locatePreviewPlacerView(); |
| mPreviewPlacerView.addView( |
| keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacerView, 0, 0)); |
| } |
| |
| private TextView getKeyPreviewText(final int pointerId) { |
| TextView previewText = mKeyPreviewTexts.get(pointerId); |
| if (previewText != null) { |
| return previewText; |
| } |
| final Context context = getContext(); |
| if (mKeyPreviewLayoutId != 0) { |
| previewText = (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null); |
| } else { |
| previewText = new TextView(context); |
| } |
| mKeyPreviewTexts.put(pointerId, previewText); |
| return previewText; |
| } |
| |
| private void dismissAllKeyPreviews() { |
| final int pointerCount = mKeyPreviewTexts.size(); |
| for (int id = 0; id < pointerCount; id++) { |
| final TextView previewText = mKeyPreviewTexts.get(id); |
| if (previewText != null) { |
| previewText.setVisibility(INVISIBLE); |
| } |
| } |
| PointerTracker.setReleasedKeyGraphicsToAllKeys(); |
| } |
| |
| // Background state set |
| private static final int[][][] KEY_PREVIEW_BACKGROUND_STATE_TABLE = { |
| { // STATE_MIDDLE |
| EMPTY_STATE_SET, |
| { R.attr.state_has_morekeys } |
| }, |
| { // STATE_LEFT |
| { R.attr.state_left_edge }, |
| { R.attr.state_left_edge, R.attr.state_has_morekeys } |
| }, |
| { // STATE_RIGHT |
| { R.attr.state_right_edge }, |
| { R.attr.state_right_edge, R.attr.state_has_morekeys } |
| } |
| }; |
| private static final int STATE_MIDDLE = 0; |
| private static final int STATE_LEFT = 1; |
| private static final int STATE_RIGHT = 2; |
| private static final int STATE_NORMAL = 0; |
| private static final int STATE_HAS_MOREKEYS = 1; |
| private static final int[] KEY_PREVIEW_BACKGROUND_DEFAULT_STATE = |
| KEY_PREVIEW_BACKGROUND_STATE_TABLE[STATE_MIDDLE][STATE_NORMAL]; |
| |
| @Override |
| public void showKeyPreview(final PointerTracker tracker) { |
| final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams; |
| final Keyboard keyboard = getKeyboard(); |
| if (!mShowKeyPreviewPopup) { |
| previewParams.mPreviewVisibleOffset = -keyboard.mVerticalGap; |
| return; |
| } |
| |
| final TextView previewText = getKeyPreviewText(tracker.mPointerId); |
| // If the key preview has no parent view yet, add it to the ViewGroup which can place |
| // key preview absolutely in SoftInputWindow. |
| if (previewText.getParent() == null) { |
| addKeyPreview(previewText); |
| } |
| |
| mDrawingHandler.cancelDismissKeyPreview(tracker); |
| final Key key = tracker.getKey(); |
| // If key is invalid or IME is already closed, we must not show key preview. |
| // Trying to show key preview while root window is closed causes |
| // WindowManager.BadTokenException. |
| if (key == null) { |
| return; |
| } |
| |
| final KeyDrawParams drawParams = mKeyDrawParams; |
| previewText.setTextColor(drawParams.mPreviewTextColor); |
| final Drawable background = previewText.getBackground(); |
| if (background != null) { |
| background.setState(KEY_PREVIEW_BACKGROUND_DEFAULT_STATE); |
| background.setAlpha(PREVIEW_ALPHA); |
| } |
| final String label = key.getPreviewLabel(); |
| // What we show as preview should match what we show on a key top in onDraw(). |
| if (label != null) { |
| // TODO Should take care of temporaryShiftLabel here. |
| previewText.setCompoundDrawables(null, null, null, null); |
| previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, |
| key.selectPreviewTextSize(drawParams)); |
| previewText.setTypeface(key.selectPreviewTypeface(drawParams)); |
| previewText.setText(label); |
| } else { |
| previewText.setCompoundDrawables(null, null, null, |
| key.getPreviewIcon(keyboard.mIconsSet)); |
| previewText.setText(null); |
| } |
| |
| previewText.measure( |
| ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); |
| final int keyDrawWidth = key.getDrawWidth(); |
| final int previewWidth = previewText.getMeasuredWidth(); |
| final int previewHeight = mKeyPreviewHeight; |
| // The width and height of visible part of the key preview background. The content marker |
| // of the background 9-patch have to cover the visible part of the background. |
| previewParams.mPreviewVisibleWidth = previewWidth - previewText.getPaddingLeft() |
| - previewText.getPaddingRight(); |
| previewParams.mPreviewVisibleHeight = previewHeight - previewText.getPaddingTop() |
| - previewText.getPaddingBottom(); |
| // The distance between the top edge of the parent key and the bottom of the visible part |
| // of the key preview background. |
| previewParams.mPreviewVisibleOffset = mKeyPreviewOffset - previewText.getPaddingBottom(); |
| getLocationInWindow(mOriginCoords); |
| // The key preview is horizontally aligned with the center of the visible part of the |
| // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and |
| // the left/right background is used if such background is specified. |
| final int statePosition; |
| int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2 |
| + CoordinateUtils.x(mOriginCoords); |
| if (previewX < 0) { |
| previewX = 0; |
| statePosition = STATE_LEFT; |
| } else if (previewX > getWidth() - previewWidth) { |
| previewX = getWidth() - previewWidth; |
| statePosition = STATE_RIGHT; |
| } else { |
| statePosition = STATE_MIDDLE; |
| } |
| // The key preview is placed vertically above the top edge of the parent key with an |
| // arbitrary offset. |
| final int previewY = key.mY - previewHeight + mKeyPreviewOffset |
| + CoordinateUtils.y(mOriginCoords); |
| |
| if (background != null) { |
| final int hasMoreKeys = (key.mMoreKeys != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL; |
| background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]); |
| } |
| ViewLayoutUtils.placeViewAt( |
| previewText, previewX, previewY, previewWidth, previewHeight); |
| previewText.setVisibility(VISIBLE); |
| } |
| |
| @Override |
| public void dismissKeyPreview(final PointerTracker tracker) { |
| mDrawingHandler.dismissKeyPreview(mKeyPreviewLingerTimeout, tracker); |
| } |
| |
| public void setSlidingKeyInputPreviewEnabled(final boolean enabled) { |
| mSlidingKeyInputPreview.setPreviewEnabled(enabled); |
| } |
| |
| @Override |
| public void showSlidingKeyInputPreview(final PointerTracker tracker) { |
| locatePreviewPlacerView(); |
| mSlidingKeyInputPreview.setPreviewPosition(tracker); |
| } |
| |
| @Override |
| public void dismissSlidingKeyInputPreview() { |
| mSlidingKeyInputPreview.dismissSlidingKeyInputPreview(); |
| } |
| |
| public void setGesturePreviewMode(final boolean drawsGesturePreviewTrail, |
| final boolean drawsGestureFloatingPreviewText) { |
| mGestureFloatingPreviewText.setPreviewEnabled(drawsGestureFloatingPreviewText); |
| mGestureTrailsPreview.setPreviewEnabled(drawsGesturePreviewTrail); |
| } |
| |
| public void showGestureFloatingPreviewText(final SuggestedWords suggestedWords) { |
| locatePreviewPlacerView(); |
| mGestureFloatingPreviewText.setSuggetedWords(suggestedWords); |
| } |
| |
| public void dismissGestureFloatingPreviewText() { |
| locatePreviewPlacerView(); |
| mDrawingHandler.dismissGestureFloatingPreviewText(mGestureFloatingPreviewTextLingerTimeout); |
| } |
| |
| @Override |
| public void showGesturePreviewTrail(final PointerTracker tracker) { |
| locatePreviewPlacerView(); |
| mGestureFloatingPreviewText.setPreviewPosition(tracker); |
| mGestureTrailsPreview.setPreviewPosition(tracker); |
| } |
| |
| // Note that this method is called from a non-UI thread. |
| public void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) { |
| PointerTracker.setMainDictionaryAvailability(mainDictionaryAvailable); |
| } |
| |
| public void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) { |
| PointerTracker.setGestureHandlingEnabledByUser(gestureHandlingEnabledByUser); |
| } |
| |
| @Override |
| protected void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| // Notify the ResearchLogger (development only diagnostics) that the keyboard view has |
| // been attached. This is needed to properly show the splash screen, which requires that |
| // the window token of the KeyboardView be non-null. |
| if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { |
| ResearchLogger.getInstance().mainKeyboardView_onAttachedToWindow(this); |
| } |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| mPreviewPlacerView.removeAllViews(); |
| // Notify the ResearchLogger (development only diagnostics) that the keyboard view has |
| // been detached. This is needed to invalidate the reference of {@link MainKeyboardView} |
| // to null. |
| if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { |
| ResearchLogger.getInstance().mainKeyboardView_onDetachedFromWindow(); |
| } |
| } |
| |
| private MoreKeysPanel onCreateMoreKeysPanel(final Key key, final Context context) { |
| if (key.mMoreKeys == null) { |
| return null; |
| } |
| Keyboard moreKeysKeyboard = mMoreKeysKeyboardCache.get(key); |
| if (moreKeysKeyboard == null) { |
| moreKeysKeyboard = new MoreKeysKeyboard.Builder( |
| context, key, this, mKeyPreviewDrawParams).build(); |
| mMoreKeysKeyboardCache.put(key, moreKeysKeyboard); |
| } |
| |
| final View container = mMoreKeysKeyboardContainer; |
| final MoreKeysKeyboardView moreKeysKeyboardView = |
| (MoreKeysKeyboardView)container.findViewById(R.id.more_keys_keyboard_view); |
| moreKeysKeyboardView.setKeyboard(moreKeysKeyboard); |
| container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); |
| return moreKeysKeyboardView; |
| } |
| |
| /** |
| * Called when a key is long pressed. |
| * @param tracker the pointer tracker which pressed the parent key |
| * @return true if the long press is handled, false otherwise. Subclasses should call the |
| * method on the base class if the subclass doesn't wish to handle the call. |
| */ |
| private boolean onLongPress(final PointerTracker tracker) { |
| if (isShowingMoreKeysPanel()) { |
| return false; |
| } |
| final Key key = tracker.getKey(); |
| if (key == null) { |
| return false; |
| } |
| if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { |
| ResearchLogger.mainKeyboardView_onLongPress(); |
| } |
| final int code = key.mCode; |
| if (key.hasEmbeddedMoreKey()) { |
| final int embeddedCode = key.mMoreKeys[0].mCode; |
| tracker.onLongPressed(); |
| invokeCodeInput(embeddedCode); |
| invokeReleaseKey(code); |
| KeyboardSwitcher.getInstance().hapticAndAudioFeedback(code); |
| return true; |
| } |
| if (code == Constants.CODE_SPACE || code == Constants.CODE_LANGUAGE_SWITCH) { |
| // Long pressing the space key invokes IME switcher dialog. |
| if (invokeCustomRequest(LatinIME.CODE_SHOW_INPUT_METHOD_PICKER)) { |
| tracker.onLongPressed(); |
| invokeReleaseKey(code); |
| return true; |
| } |
| } |
| return openMoreKeysPanel(key, tracker); |
| } |
| |
| private boolean invokeCustomRequest(final int requestCode) { |
| return mKeyboardActionListener.onCustomRequest(requestCode); |
| } |
| |
| private void invokeCodeInput(final int code) { |
| mKeyboardActionListener.onCodeInput( |
| code, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); |
| } |
| |
| private void invokeReleaseKey(final int code) { |
| mKeyboardActionListener.onReleaseKey(code, false); |
| } |
| |
| private boolean openMoreKeysPanel(final Key key, final PointerTracker tracker) { |
| final MoreKeysPanel moreKeysPanel = onCreateMoreKeysPanel(key, getContext()); |
| if (moreKeysPanel == null) { |
| return false; |
| } |
| |
| final int[] lastCoords = CoordinateUtils.newInstance(); |
| tracker.getLastCoordinates(lastCoords); |
| final boolean keyPreviewEnabled = isKeyPreviewPopupEnabled() && !key.noKeyPreview(); |
| // The more keys keyboard is usually horizontally aligned with the center of the parent key. |
| // If showMoreKeysKeyboardAtTouchedPoint is true and the key preview is disabled, the more |
| // keys keyboard is placed at the touch point of the parent key. |
| final int pointX = (mConfigShowMoreKeysKeyboardAtTouchedPoint && !keyPreviewEnabled) |
| ? CoordinateUtils.x(lastCoords) |
| : key.mX + key.mWidth / 2; |
| // The more keys keyboard is usually vertically aligned with the top edge of the parent key |
| // (plus vertical gap). If the key preview is enabled, the more keys keyboard is vertically |
| // aligned with the bottom edge of the visible part of the key preview. |
| // {@code mPreviewVisibleOffset} has been set appropriately in |
| // {@link KeyboardView#showKeyPreview(PointerTracker)}. |
| final int pointY = key.mY + mKeyPreviewDrawParams.mPreviewVisibleOffset; |
| moreKeysPanel.showMoreKeysPanel(this, this, pointX, pointY, mKeyboardActionListener); |
| final int translatedX = moreKeysPanel.translateX(CoordinateUtils.x(lastCoords)); |
| final int translatedY = moreKeysPanel.translateY(CoordinateUtils.y(lastCoords)); |
| tracker.onShowMoreKeysPanel(translatedX, translatedY, moreKeysPanel); |
| return true; |
| } |
| |
| public boolean isInSlidingKeyInput() { |
| if (isShowingMoreKeysPanel()) { |
| return true; |
| } |
| return PointerTracker.isAnyInSlidingKeyInput(); |
| } |
| |
| @Override |
| public void onShowMoreKeysPanel(final MoreKeysPanel panel) { |
| locatePreviewPlacerView(); |
| if (isShowingMoreKeysPanel()) { |
| onDismissMoreKeysPanel(); |
| } |
| mPreviewPlacerView.addView(panel.getContainerView()); |
| mMoreKeysPanel = panel; |
| dimEntireKeyboard(true /* dimmed */); |
| } |
| |
| public boolean isShowingMoreKeysPanel() { |
| return mMoreKeysPanel != null && mMoreKeysPanel.isShowingInParent(); |
| } |
| |
| @Override |
| public void onCancelMoreKeysPanel() { |
| PointerTracker.dismissAllMoreKeysPanels(); |
| } |
| |
| @Override |
| public boolean onDismissMoreKeysPanel() { |
| dimEntireKeyboard(false /* dimmed */); |
| if (isShowingMoreKeysPanel()) { |
| mPreviewPlacerView.removeView(mMoreKeysPanel.getContainerView()); |
| mMoreKeysPanel = null; |
| return true; |
| } |
| return false; |
| } |
| |
| public int getPointerCount() { |
| return mOldPointerCount; |
| } |
| |
| @Override |
| public boolean dispatchTouchEvent(MotionEvent event) { |
| if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { |
| return AccessibleKeyboardViewProxy.getInstance().dispatchTouchEvent(event); |
| } |
| return super.dispatchTouchEvent(event); |
| } |
| |
| @Override |
| public boolean onTouchEvent(final MotionEvent me) { |
| if (getKeyboard() == null) { |
| return false; |
| } |
| return mTouchScreenRegulator.onTouchEvent(me); |
| } |
| |
| @Override |
| public boolean processMotionEvent(final MotionEvent me) { |
| final boolean nonDistinctMultitouch = !mHasDistinctMultitouch; |
| final int action = me.getActionMasked(); |
| final int pointerCount = me.getPointerCount(); |
| final int oldPointerCount = mOldPointerCount; |
| mOldPointerCount = pointerCount; |
| |
| // TODO: cleanup this code into a multi-touch to single-touch event converter class? |
| // If the device does not have distinct multi-touch support panel, ignore all multi-touch |
| // events except a transition from/to single-touch. |
| if (nonDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) { |
| return true; |
| } |
| |
| final long eventTime = me.getEventTime(); |
| final int index = me.getActionIndex(); |
| final int id = me.getPointerId(index); |
| final int x = (int)me.getX(index); |
| final int y = (int)me.getY(index); |
| |
| // TODO: This might be moved to the tracker.processMotionEvent() call below. |
| if (ENABLE_USABILITY_STUDY_LOG && action != MotionEvent.ACTION_MOVE) { |
| writeUsabilityStudyLog(me, action, eventTime, index, id, x, y); |
| } |
| // TODO: This should be moved to the tracker.processMotionEvent() call below. |
| // Currently the same "move" event is being logged twice. |
| if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { |
| ResearchLogger.mainKeyboardView_processMotionEvent( |
| me, action, eventTime, index, id, x, y); |
| } |
| |
| if (mKeyTimerHandler.isInKeyRepeat()) { |
| final PointerTracker tracker = PointerTracker.getPointerTracker(id, this); |
| // Key repeating timer will be canceled if 2 or more keys are in action, and current |
| // event (UP or DOWN) is non-modifier key. |
| if (pointerCount > 1 && !tracker.isModifier()) { |
| mKeyTimerHandler.cancelKeyRepeatTimer(); |
| } |
| // Up event will pass through. |
| } |
| |
| // TODO: cleanup this code into a multi-touch to single-touch event converter class? |
| // Translate mutli-touch event to single-touch events on the device that has no distinct |
| // multi-touch panel. |
| if (nonDistinctMultitouch) { |
| // Use only main (id=0) pointer tracker. |
| final PointerTracker tracker = PointerTracker.getPointerTracker(0, this); |
| if (pointerCount == 1 && oldPointerCount == 2) { |
| // Multi-touch to single touch transition. |
| // Send a down event for the latest pointer if the key is different from the |
| // previous key. |
| final Key newKey = tracker.getKeyOn(x, y); |
| if (mOldKey != newKey) { |
| tracker.onDownEvent(x, y, eventTime, this); |
| if (action == MotionEvent.ACTION_UP) { |
| tracker.onUpEvent(x, y, eventTime); |
| } |
| } |
| } else if (pointerCount == 2 && oldPointerCount == 1) { |
| // Single-touch to multi-touch transition. |
| // Send an up event for the last pointer. |
| final int[] lastCoords = CoordinateUtils.newInstance(); |
| mOldKey = tracker.getKeyOn( |
| CoordinateUtils.x(lastCoords), CoordinateUtils.y(lastCoords)); |
| tracker.onUpEvent( |
| CoordinateUtils.x(lastCoords), CoordinateUtils.y(lastCoords), eventTime); |
| } else if (pointerCount == 1 && oldPointerCount == 1) { |
| tracker.processMotionEvent(action, x, y, eventTime, this); |
| } else { |
| Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount |
| + " (old " + oldPointerCount + ")"); |
| } |
| return true; |
| } |
| |
| if (action == MotionEvent.ACTION_MOVE) { |
| for (int i = 0; i < pointerCount; i++) { |
| final int pointerId = me.getPointerId(i); |
| final PointerTracker tracker = PointerTracker.getPointerTracker( |
| pointerId, this); |
| final int px = (int)me.getX(i); |
| final int py = (int)me.getY(i); |
| tracker.onMoveEvent(px, py, eventTime, me); |
| if (ENABLE_USABILITY_STUDY_LOG) { |
| writeUsabilityStudyLog(me, action, eventTime, i, pointerId, px, py); |
| } |
| // TODO: This seems to be no longer necessary, and confusing because it leads to |
| // duplicate MotionEvents being recorded. |
| // if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { |
| // ResearchLogger.mainKeyboardView_processMotionEvent( |
| // me, action, eventTime, i, pointerId, px, py); |
| // } |
| } |
| } else { |
| final PointerTracker tracker = PointerTracker.getPointerTracker(id, this); |
| tracker.processMotionEvent(action, x, y, eventTime, this); |
| } |
| |
| return true; |
| } |
| |
| private static void writeUsabilityStudyLog(final MotionEvent me, final int action, |
| final long eventTime, final int index, final int id, final int x, final int y) { |
| final String eventTag; |
| switch (action) { |
| case MotionEvent.ACTION_UP: |
| eventTag = "[Up]"; |
| break; |
| case MotionEvent.ACTION_DOWN: |
| eventTag = "[Down]"; |
| break; |
| case MotionEvent.ACTION_POINTER_UP: |
| eventTag = "[PointerUp]"; |
| break; |
| case MotionEvent.ACTION_POINTER_DOWN: |
| eventTag = "[PointerDown]"; |
| break; |
| case MotionEvent.ACTION_MOVE: |
| eventTag = "[Move]"; |
| break; |
| default: |
| eventTag = "[Action" + action + "]"; |
| break; |
| } |
| final float size = me.getSize(index); |
| final float pressure = me.getPressure(index); |
| UsabilityStudyLogUtils.getInstance().write( |
| eventTag + eventTime + "," + id + "," + x + "," + y + "," + size + "," + pressure); |
| } |
| |
| public void cancelAllMessages() { |
| mKeyTimerHandler.cancelAllMessages(); |
| mDrawingHandler.cancelAllMessages(); |
| } |
| |
| public void closing() { |
| dismissAllKeyPreviews(); |
| cancelAllMessages(); |
| onDismissMoreKeysPanel(); |
| mMoreKeysKeyboardCache.clear(); |
| } |
| |
| /** |
| * Receives hover events from the input framework. |
| * |
| * @param event The motion event to be dispatched. |
| * @return {@code true} if the event was handled by the view, {@code false} |
| * otherwise |
| */ |
| @Override |
| public boolean dispatchHoverEvent(final MotionEvent event) { |
| if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { |
| final PointerTracker tracker = PointerTracker.getPointerTracker(0, this); |
| return AccessibleKeyboardViewProxy.getInstance().dispatchHoverEvent(event, tracker); |
| } |
| |
| // Reflection doesn't support calling superclass methods. |
| return false; |
| } |
| |
| public void updateShortcutKey(final boolean available) { |
| final Keyboard keyboard = getKeyboard(); |
| if (keyboard == null) { |
| return; |
| } |
| final Key shortcutKey = keyboard.getKey(Constants.CODE_SHORTCUT); |
| if (shortcutKey == null) { |
| return; |
| } |
| shortcutKey.setEnabled(available); |
| invalidateKey(shortcutKey); |
| } |
| |
| public void startDisplayLanguageOnSpacebar(final boolean subtypeChanged, |
| final boolean needsToDisplayLanguage, final boolean hasMultipleEnabledIMEsOrSubtypes) { |
| mNeedsToDisplayLanguage = needsToDisplayLanguage; |
| mHasMultipleEnabledIMEsOrSubtypes = hasMultipleEnabledIMEsOrSubtypes; |
| final ObjectAnimator animator = mLanguageOnSpacebarFadeoutAnimator; |
| if (animator == null) { |
| mNeedsToDisplayLanguage = false; |
| } else { |
| if (subtypeChanged && needsToDisplayLanguage) { |
| setLanguageOnSpacebarAnimAlpha(Constants.Color.ALPHA_OPAQUE); |
| if (animator.isStarted()) { |
| animator.cancel(); |
| } |
| animator.start(); |
| } else { |
| if (!animator.isStarted()) { |
| mLanguageOnSpacebarAnimAlpha = mLanguageOnSpacebarFinalAlpha; |
| } |
| } |
| } |
| invalidateKey(mSpaceKey); |
| } |
| |
| public void updateAutoCorrectionState(final boolean isAutoCorrection) { |
| if (!mAutoCorrectionSpacebarLedEnabled) { |
| return; |
| } |
| mAutoCorrectionSpacebarLedOn = isAutoCorrection; |
| invalidateKey(mSpaceKey); |
| } |
| |
| private void dimEntireKeyboard(final boolean dimmed) { |
| final boolean needsRedrawing = mNeedsToDimEntireKeyboard != dimmed; |
| mNeedsToDimEntireKeyboard = dimmed; |
| if (needsRedrawing) { |
| invalidateAllKeys(); |
| } |
| } |
| |
| @Override |
| protected void onDraw(final Canvas canvas) { |
| super.onDraw(canvas); |
| |
| // Overlay a dark rectangle to dim. |
| if (mNeedsToDimEntireKeyboard) { |
| canvas.drawRect(0.0f, 0.0f, getWidth(), getHeight(), mBackgroundDimAlphaPaint); |
| } |
| } |
| |
| @Override |
| protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint, |
| final KeyDrawParams params) { |
| if (key.altCodeWhileTyping() && key.isEnabled()) { |
| params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha; |
| } |
| if (key.mCode == Constants.CODE_SPACE) { |
| drawSpacebar(key, canvas, paint); |
| // Whether space key needs to show the "..." popup hint for special purposes |
| if (key.isLongPressEnabled() && mHasMultipleEnabledIMEsOrSubtypes) { |
| drawKeyPopupHint(key, canvas, paint, params); |
| } |
| } else if (key.mCode == Constants.CODE_LANGUAGE_SWITCH) { |
| super.onDrawKeyTopVisuals(key, canvas, paint, params); |
| drawKeyPopupHint(key, canvas, paint, params); |
| } else { |
| super.onDrawKeyTopVisuals(key, canvas, paint, params); |
| } |
| } |
| |
| private static boolean fitsTextIntoWidth(final int width, final String text, |
| final Paint paint) { |
| paint.setTextScaleX(1.0f); |
| final float textWidth = TypefaceUtils.getLabelWidth(text, paint); |
| if (textWidth < width) { |
| return true; |
| } |
| |
| final float scaleX = width / textWidth; |
| if (scaleX < MINIMUM_XSCALE_OF_LANGUAGE_NAME) { |
| return false; |
| } |
| |
| paint.setTextScaleX(scaleX); |
| return TypefaceUtils.getLabelWidth(text, paint) < width; |
| } |
| |
| // Layout language name on spacebar. |
| private static String layoutLanguageOnSpacebar(final Paint paint, |
| final InputMethodSubtype subtype, final int width) { |
| // Choose appropriate language name to fit into the width. |
| final String fullText = getFullDisplayName(subtype); |
| if (fitsTextIntoWidth(width, fullText, paint)) { |
| return fullText; |
| } |
| |
| final String middleText = getMiddleDisplayName(subtype); |
| if (fitsTextIntoWidth(width, middleText, paint)) { |
| return middleText; |
| } |
| |
| final String shortText = getShortDisplayName(subtype); |
| if (fitsTextIntoWidth(width, shortText, paint)) { |
| return shortText; |
| } |
| |
| return ""; |
| } |
| |
| private void drawSpacebar(final Key key, final Canvas canvas, final Paint paint) { |
| final int width = key.mWidth; |
| final int height = key.mHeight; |
| |
| // If input language are explicitly selected. |
| if (mNeedsToDisplayLanguage) { |
| paint.setTextAlign(Align.CENTER); |
| paint.setTypeface(Typeface.DEFAULT); |
| paint.setTextSize(mSpacebarTextSize); |
| final InputMethodSubtype subtype = getKeyboard().mId.mSubtype; |
| final String language = layoutLanguageOnSpacebar(paint, subtype, width); |
| // Draw language text with shadow |
| final float descent = paint.descent(); |
| final float textHeight = -paint.ascent() + descent; |
| final float baseline = height / 2 + textHeight / 2; |
| paint.setColor(mSpacebarTextShadowColor); |
| paint.setAlpha(mLanguageOnSpacebarAnimAlpha); |
| canvas.drawText(language, width / 2, baseline - descent - 1, paint); |
| paint.setColor(mSpacebarTextColor); |
| paint.setAlpha(mLanguageOnSpacebarAnimAlpha); |
| canvas.drawText(language, width / 2, baseline - descent, paint); |
| } |
| |
| // Draw the spacebar icon at the bottom |
| if (mAutoCorrectionSpacebarLedOn) { |
| final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100; |
| final int iconHeight = mAutoCorrectionSpacebarLedIcon.getIntrinsicHeight(); |
| int x = (width - iconWidth) / 2; |
| int y = height - iconHeight; |
| drawIcon(canvas, mAutoCorrectionSpacebarLedIcon, x, y, iconWidth, iconHeight); |
| } else if (mSpaceIcon != null) { |
| final int iconWidth = mSpaceIcon.getIntrinsicWidth(); |
| final int iconHeight = mSpaceIcon.getIntrinsicHeight(); |
| int x = (width - iconWidth) / 2; |
| int y = height - iconHeight; |
| drawIcon(canvas, mSpaceIcon, x, y, iconWidth, iconHeight); |
| } |
| } |
| |
| // InputMethodSubtype's display name for spacebar text in its locale. |
| // isAdditionalSubtype (T=true, F=false) |
| // locale layout | Short Middle Full |
| // ------ ------- - ---- --------- ---------------------- |
| // en_US qwerty F En English English (US) exception |
| // en_GB qwerty F En English English (UK) exception |
| // es_US spanish F Es Español Español (EE.UU.) exception |
| // fr azerty F Fr Français Français |
| // fr_CA qwerty F Fr Français Français (Canada) |
| // de qwertz F De Deutsch Deutsch |
| // zz qwerty F QWERTY QWERTY |
| // fr qwertz T Fr Français Français |
| // de qwerty T De Deutsch Deutsch |
| // en_US azerty T En English English (US) |
| // zz azerty T AZERTY AZERTY |
| |
| // Get InputMethodSubtype's full display name in its locale. |
| static String getFullDisplayName(final InputMethodSubtype subtype) { |
| if (SubtypeLocale.isNoLanguage(subtype)) { |
| return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype); |
| } |
| return SubtypeLocale.getSubtypeLocaleDisplayName(subtype.getLocale()); |
| } |
| |
| // Get InputMethodSubtype's short display name in its locale. |
| static String getShortDisplayName(final InputMethodSubtype subtype) { |
| if (SubtypeLocale.isNoLanguage(subtype)) { |
| return ""; |
| } |
| final Locale locale = SubtypeLocale.getSubtypeLocale(subtype); |
| return StringUtils.capitalizeFirstCodePoint(locale.getLanguage(), locale); |
| } |
| |
| // Get InputMethodSubtype's middle display name in its locale. |
| static String getMiddleDisplayName(final InputMethodSubtype subtype) { |
| if (SubtypeLocale.isNoLanguage(subtype)) { |
| return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype); |
| } |
| final Locale locale = SubtypeLocale.getSubtypeLocale(subtype); |
| return SubtypeLocale.getSubtypeLocaleDisplayName(locale.getLanguage()); |
| } |
| } |