/*
 * Copyright (C) 2008-2009 Google Inc.
 *
 * 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;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.inputmethodservice.Keyboard.Key;
import android.media.AudioManager;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup.LayoutParams;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.PopupWindow;
import android.widget.TextView;

import com.android.internal.R;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

A view that renders a virtual Keyboard. It handles rendering of keys and detecting key presses and touch movements.
@attrref android.R.styleable#KeyboardView_keyBackground
@attrref android.R.styleable#KeyboardView_keyPreviewLayout
@attrref android.R.styleable#KeyboardView_keyPreviewOffset
@attrref android.R.styleable#KeyboardView_keyPreviewHeight
@attrref android.R.styleable#KeyboardView_labelTextSize
@attrref android.R.styleable#KeyboardView_keyTextSize
@attrref android.R.styleable#KeyboardView_keyTextColor
@attrref android.R.styleable#KeyboardView_verticalCorrection
@attrref android.R.styleable#KeyboardView_popupLayout
/** * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and * detecting key presses and touch movements. * * @attr ref android.R.styleable#KeyboardView_keyBackground * @attr ref android.R.styleable#KeyboardView_keyPreviewLayout * @attr ref android.R.styleable#KeyboardView_keyPreviewOffset * @attr ref android.R.styleable#KeyboardView_keyPreviewHeight * @attr ref android.R.styleable#KeyboardView_labelTextSize * @attr ref android.R.styleable#KeyboardView_keyTextSize * @attr ref android.R.styleable#KeyboardView_keyTextColor * @attr ref android.R.styleable#KeyboardView_verticalCorrection * @attr ref android.R.styleable#KeyboardView_popupLayout */
public class KeyboardView extends View implements View.OnClickListener {
Listener for virtual keyboard events.
/** * Listener for virtual keyboard events. */
public interface OnKeyboardActionListener {
Called when the user presses a key. This is sent before the onKey is called. For keys that repeat, this is only called once.
Params:
  • primaryCode – the unicode of the key being pressed. If the touch is not on a valid key, the value will be zero.
/** * Called when the user presses a key. This is sent before the {@link #onKey} is called. * For keys that repeat, this is only called once. * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid * key, the value will be zero. */
void onPress(int primaryCode);
Called when the user releases a key. This is sent after the onKey is called. For keys that repeat, this is only called once.
Params:
  • primaryCode – the code of the key that was released
/** * Called when the user releases a key. This is sent after the {@link #onKey} is called. * For keys that repeat, this is only called once. * @param primaryCode the code of the key that was released */
void onRelease(int primaryCode);
Send a key press to the listener.
Params:
  • primaryCode – this is the key that was pressed
  • keyCodes – the codes for all the possible alternative keys with the primary code being the first. If the primary key code is a single character such as an alphabet or number or symbol, the alternatives will include other characters that may be on the same key or adjacent keys. These codes are useful to correct for accidental presses of a key adjacent to the intended key.
/** * Send a key press to the listener. * @param primaryCode this is the key that was pressed * @param keyCodes the codes for all the possible alternative keys * with the primary code being the first. If the primary key code is * a single character such as an alphabet or number or symbol, the alternatives * will include other characters that may be on the same key or adjacent keys. * These codes are useful to correct for accidental presses of a key adjacent to * the intended key. */
void onKey(int primaryCode, int[] keyCodes);
Sends a sequence of characters to the listener.
Params:
  • text – the sequence of characters to be displayed.
/** * Sends a sequence of characters to the listener. * @param text the sequence of characters to be displayed. */
void onText(CharSequence text);
Called when the user quickly moves the finger from right to left.
/** * Called when the user quickly moves the finger from right to left. */
void swipeLeft();
Called when the user quickly moves the finger from left to right.
/** * Called when the user quickly moves the finger from left to right. */
void swipeRight();
Called when the user quickly moves the finger from up to down.
/** * Called when the user quickly moves the finger from up to down. */
void swipeDown();
Called when the user quickly moves the finger from down to up.
/** * Called when the user quickly moves the finger from down to up. */
void swipeUp(); } private static final boolean DEBUG = false; private static final int NOT_A_KEY = -1; private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE }; private static final int[] LONG_PRESSABLE_STATE_SET = { R.attr.state_long_pressable }; private Keyboard mKeyboard; private int mCurrentKeyIndex = NOT_A_KEY; private int mLabelTextSize; private int mKeyTextSize; private int mKeyTextColor; private float mShadowRadius; private int mShadowColor; private float mBackgroundDimAmount; private TextView mPreviewText; private PopupWindow mPreviewPopup; private int mPreviewTextSizeLarge; private int mPreviewOffset; private int mPreviewHeight; // Working variable private final int[] mCoordinates = new int[2]; private PopupWindow mPopupKeyboard; private View mMiniKeyboardContainer; private KeyboardView mMiniKeyboard; private boolean mMiniKeyboardOnScreen; private View mPopupParent; private int mMiniKeyboardOffsetX; private int mMiniKeyboardOffsetY; private Map<Key,View> mMiniKeyboardCache; private Key[] mKeys; /** Listener for {@link OnKeyboardActionListener}. */ private OnKeyboardActionListener mKeyboardActionListener; private static final int MSG_SHOW_PREVIEW = 1; private static final int MSG_REMOVE_PREVIEW = 2; private static final int MSG_REPEAT = 3; private static final int MSG_LONGPRESS = 4; private static final int DELAY_BEFORE_PREVIEW = 0; private static final int DELAY_AFTER_PREVIEW = 70; private static final int DEBOUNCE_TIME = 70; private int mVerticalCorrection; private int mProximityThreshold; private boolean mPreviewCentered = false; private boolean mShowPreview = true; private boolean mShowTouchPoints = true; private int mPopupPreviewX; private int mPopupPreviewY; private int mLastX; private int mLastY; private int mStartX; private int mStartY; private boolean mProximityCorrectOn; private Paint mPaint; private Rect mPadding; private long mDownTime; private long mLastMoveTime; private int mLastKey; private int mLastCodeX; private int mLastCodeY; private int mCurrentKey = NOT_A_KEY; private int mDownKey = NOT_A_KEY; private long mLastKeyTime; private long mCurrentKeyTime; private int[] mKeyIndices = new int[12]; private GestureDetector mGestureDetector; private int mPopupX; private int mPopupY; private int mRepeatKeyIndex = NOT_A_KEY; private int mPopupLayout; private boolean mAbortKey; private Key mInvalidatedKey; private Rect mClipRegion = new Rect(0, 0, 0, 0); private boolean mPossiblePoly; private SwipeTracker mSwipeTracker = new SwipeTracker(); private int mSwipeThreshold; private boolean mDisambiguateSwipe; // Variables for dealing with multiple pointers private int mOldPointerCount = 1; private float mOldPointerX; private float mOldPointerY; private Drawable mKeyBackground; private static final int REPEAT_INTERVAL = 50; // ~20 keys per second private static final int REPEAT_START_DELAY = 400; private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); private static int MAX_NEARBY_KEYS = 12; private int[] mDistances = new int[MAX_NEARBY_KEYS]; // For multi-tap private int mLastSentIndex; private int mTapCount; private long mLastTapTime; private boolean mInMultiTap; private static final int MULTITAP_INTERVAL = 800; // milliseconds private StringBuilder mPreviewLabel = new StringBuilder(1);
Whether the keyboard bitmap needs to be redrawn before it's blitted.
/** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/
private boolean mDrawPending;
The dirty region in the keyboard bitmap
/** The dirty region in the keyboard bitmap */
private Rect mDirtyRect = new Rect();
The keyboard bitmap for faster updates
/** The keyboard bitmap for faster updates */
private Bitmap mBuffer;
Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer.
/** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */
private boolean mKeyboardChanged;
The canvas for the above mutable keyboard bitmap
/** The canvas for the above mutable keyboard bitmap */
private Canvas mCanvas;
The accessibility manager for accessibility support
/** The accessibility manager for accessibility support */
private AccessibilityManager mAccessibilityManager;
The audio manager for accessibility support
/** The audio manager for accessibility support */
private AudioManager mAudioManager;
Whether the requirement of a headset to hear passwords if accessibility is enabled is announced.
/** Whether the requirement of a headset to hear passwords if accessibility is enabled is announced. */
private boolean mHeadsetRequiredToHearPasswordsAnnounced; Handler mHandler; public KeyboardView(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.keyboardViewStyle); } public KeyboardView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public KeyboardView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); TypedArray a = context.obtainStyledAttributes( attrs, android.R.styleable.KeyboardView, defStyleAttr, defStyleRes); LayoutInflater inflate = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); int previewLayout = 0; int keyTextSize = 0; int n = a.getIndexCount(); for (int i = 0; i < n; i++) { int attr = a.getIndex(i); switch (attr) { case com.android.internal.R.styleable.KeyboardView_keyBackground: mKeyBackground = a.getDrawable(attr); break; case com.android.internal.R.styleable.KeyboardView_verticalCorrection: mVerticalCorrection = a.getDimensionPixelOffset(attr, 0); break; case com.android.internal.R.styleable.KeyboardView_keyPreviewLayout: previewLayout = a.getResourceId(attr, 0); break; case com.android.internal.R.styleable.KeyboardView_keyPreviewOffset: mPreviewOffset = a.getDimensionPixelOffset(attr, 0); break; case com.android.internal.R.styleable.KeyboardView_keyPreviewHeight: mPreviewHeight = a.getDimensionPixelSize(attr, 80); break; case com.android.internal.R.styleable.KeyboardView_keyTextSize: mKeyTextSize = a.getDimensionPixelSize(attr, 18); break; case com.android.internal.R.styleable.KeyboardView_keyTextColor: mKeyTextColor = a.getColor(attr, 0xFF000000); break; case com.android.internal.R.styleable.KeyboardView_labelTextSize: mLabelTextSize = a.getDimensionPixelSize(attr, 14); break; case com.android.internal.R.styleable.KeyboardView_popupLayout: mPopupLayout = a.getResourceId(attr, 0); break; case com.android.internal.R.styleable.KeyboardView_shadowColor: mShadowColor = a.getColor(attr, 0); break; case com.android.internal.R.styleable.KeyboardView_shadowRadius: mShadowRadius = a.getFloat(attr, 0f); break; } } a = mContext.obtainStyledAttributes( com.android.internal.R.styleable.Theme); mBackgroundDimAmount = a.getFloat(android.R.styleable.Theme_backgroundDimAmount, 0.5f); mPreviewPopup = new PopupWindow(context); if (previewLayout != 0) { mPreviewText = (TextView) inflate.inflate(previewLayout, null); mPreviewTextSizeLarge = (int) mPreviewText.getTextSize(); mPreviewPopup.setContentView(mPreviewText); mPreviewPopup.setBackgroundDrawable(null); } else { mShowPreview = false; } mPreviewPopup.setTouchable(false); mPopupKeyboard = new PopupWindow(context); mPopupKeyboard.setBackgroundDrawable(null); //mPopupKeyboard.setClippingEnabled(false); mPopupParent = this; //mPredicting = true; mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setTextSize(keyTextSize); mPaint.setTextAlign(Align.CENTER); mPaint.setAlpha(255); mPadding = new Rect(0, 0, 0, 0); mMiniKeyboardCache = new HashMap<Key,View>(); mKeyBackground.getPadding(mPadding); mSwipeThreshold = (int) (500 * getResources().getDisplayMetrics().density); mDisambiguateSwipe = getResources().getBoolean( com.android.internal.R.bool.config_swipeDisambiguation); mAccessibilityManager = AccessibilityManager.getInstance(context); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); resetMultiTap(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); initGestureDetector(); if (mHandler == null) { mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_SHOW_PREVIEW: showKey(msg.arg1); break; case MSG_REMOVE_PREVIEW: mPreviewText.setVisibility(INVISIBLE); break; case MSG_REPEAT: if (repeatKey()) { Message repeat = Message.obtain(this, MSG_REPEAT); sendMessageDelayed(repeat, REPEAT_INTERVAL); } break; case MSG_LONGPRESS: openPopupIfRequired((MotionEvent) msg.obj); break; } } }; } } private void initGestureDetector() { if (mGestureDetector == null) { mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { @Override public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX, float velocityY) { if (mPossiblePoly) return false; final float absX = Math.abs(velocityX); final float absY = Math.abs(velocityY); float deltaX = me2.getX() - me1.getX(); float deltaY = me2.getY() - me1.getY(); int travelX = getWidth() / 2; // Half the keyboard width int travelY = getHeight() / 2; // Half the keyboard height mSwipeTracker.computeCurrentVelocity(1000); final float endingVelocityX = mSwipeTracker.getXVelocity(); final float endingVelocityY = mSwipeTracker.getYVelocity(); boolean sendDownKey = false; if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) { if (mDisambiguateSwipe && endingVelocityX < velocityX / 4) { sendDownKey = true; } else { swipeRight(); return true; } } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) { if (mDisambiguateSwipe && endingVelocityX > velocityX / 4) { sendDownKey = true; } else { swipeLeft(); return true; } } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) { if (mDisambiguateSwipe && endingVelocityY > velocityY / 4) { sendDownKey = true; } else { swipeUp(); return true; } } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) { if (mDisambiguateSwipe && endingVelocityY < velocityY / 4) { sendDownKey = true; } else { swipeDown(); return true; } } if (sendDownKey) { detectAndSendKey(mDownKey, mStartX, mStartY, me1.getEventTime()); } return false; } }); mGestureDetector.setIsLongpressEnabled(false); } } public void setOnKeyboardActionListener(OnKeyboardActionListener listener) { mKeyboardActionListener = listener; }
Returns the OnKeyboardActionListener object.
Returns:the listener attached to this keyboard
/** * Returns the {@link OnKeyboardActionListener} object. * @return the listener attached to this keyboard */
protected OnKeyboardActionListener getOnKeyboardActionListener() { return mKeyboardActionListener; }
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.
Params:
  • keyboard – the keyboard to display in this view
See Also:
/** * 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 */
public void setKeyboard(Keyboard keyboard) { if (mKeyboard != null) { showPreview(NOT_A_KEY); } // Remove any pending messages removeMessages(); mKeyboard = keyboard; List<Key> keys = mKeyboard.getKeys(); mKeys = keys.toArray(new Key[keys.size()]); requestLayout(); // Hint to reallocate the buffer if the size changed mKeyboardChanged = true; invalidateAllKeys(); computeProximityThreshold(keyboard); mMiniKeyboardCache.clear(); // Not really necessary to do every time, but will free up views // Switching to a different keyboard should abort any pending keys so that the key up // doesn't get delivered to the old or new keyboard mAbortKey = true; // Until the next ACTION_DOWN }
Returns the current keyboard being displayed by this view.
See Also:
Returns:the currently attached keyboard
/** * Returns the current keyboard being displayed by this view. * @return the currently attached keyboard * @see #setKeyboard(Keyboard) */
public Keyboard getKeyboard() { return mKeyboard; }
Sets the state of the shift key of the keyboard, if any.
Params:
  • shifted – whether or not to enable the state of the shift key
See Also:
Returns:true if the shift key state changed, false if there was no change
/** * Sets the state of the shift key of the keyboard, if any. * @param shifted whether or not to enable the state of the shift key * @return true if the shift key state changed, false if there was no change * @see KeyboardView#isShifted() */
public boolean setShifted(boolean shifted) { if (mKeyboard != null) { if (mKeyboard.setShifted(shifted)) { // The whole keyboard probably needs to be redrawn invalidateAllKeys(); return true; } } return false; }
Returns the state of the shift key of the keyboard, if any.
See Also:
Returns:true if the shift is in a pressed state, false otherwise. If there is no shift key on the keyboard or there is no keyboard attached, it returns false.
/** * Returns the state of the shift key of the keyboard, if any. * @return true if the shift is in a pressed state, false otherwise. If there is * no shift key on the keyboard or there is no keyboard attached, it returns false. * @see KeyboardView#setShifted(boolean) */
public boolean isShifted() { if (mKeyboard != null) { return mKeyboard.isShifted(); } return false; }
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.
Params:
  • previewEnabled – whether or not to enable the key feedback popup
See Also:
/** * 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 popup * @see #isPreviewEnabled() */
public void setPreviewEnabled(boolean previewEnabled) { mShowPreview = previewEnabled; }
Returns the enabled state of the key feedback popup.
See Also:
Returns:whether or not the key feedback popup is enabled
/** * Returns the enabled state of the key feedback popup. * @return whether or not the key feedback popup is enabled * @see #setPreviewEnabled(boolean) */
public boolean isPreviewEnabled() { return mShowPreview; } public void setVerticalCorrection(int verticalOffset) { } public void setPopupParent(View v) { mPopupParent = v; } public void setPopupOffset(int x, int y) { mMiniKeyboardOffsetX = x; mMiniKeyboardOffsetY = y; if (mPreviewPopup.isShowing()) { mPreviewPopup.dismiss(); } }
When enabled, calls to OnKeyboardActionListener.onKey will include key codes for adjacent keys. When disabled, only the primary key code will be reported.
Params:
  • enabled – whether or not the proximity correction is enabled
/** * When enabled, calls to {@link OnKeyboardActionListener#onKey} will include key * codes for adjacent keys. When disabled, only the primary key code will be * reported. * @param enabled whether or not the proximity correction is enabled */
public void setProximityCorrectionEnabled(boolean enabled) { mProximityCorrectOn = enabled; }
Returns true if proximity correction is enabled.
/** * Returns true if proximity correction is enabled. */
public boolean isProximityCorrectionEnabled() { return mProximityCorrectOn; }
Popup keyboard close button clicked.
@hide
/** * Popup keyboard close button clicked. * @hide */
public void onClick(View v) { dismissPopupKeyboard(); } private CharSequence adjustCase(CharSequence label) { if (mKeyboard.isShifted() && label != null && label.length() < 3 && Character.isLowerCase(label.charAt(0))) { label = label.toString().toUpperCase(); } return label; } @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Round up a little if (mKeyboard == null) { setMeasuredDimension(mPaddingLeft + mPaddingRight, mPaddingTop + mPaddingBottom); } else { int width = mKeyboard.getMinWidth() + mPaddingLeft + mPaddingRight; if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) { width = MeasureSpec.getSize(widthMeasureSpec); } setMeasuredDimension(width, mKeyboard.getHeight() + mPaddingTop + mPaddingBottom); } }
Compute the average distance between adjacent keys (horizontally and vertically) and square it to get the proximity threshold. We use a square here and in computing the touch distance from a key's center to avoid taking a square root.
Params:
  • keyboard –
/** * Compute the average distance between adjacent keys (horizontally and vertically) * and square it to get the proximity threshold. We use a square here and in computing * the touch distance from a key's center to avoid taking a square root. * @param keyboard */
private void computeProximityThreshold(Keyboard keyboard) { if (keyboard == null) return; final Key[] keys = mKeys; if (keys == null) return; int length = keys.length; int dimensionSum = 0; for (int i = 0; i < length; i++) { Key key = keys[i]; dimensionSum += Math.min(key.width, key.height) + key.gap; } if (dimensionSum < 0 || length == 0) return; mProximityThreshold = (int) (dimensionSum * 1.4f / length); mProximityThreshold *= mProximityThreshold; // Square it } @Override public void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (mKeyboard != null) { mKeyboard.resize(w, h); } // Release the buffer, if any and it will be reallocated on the next draw mBuffer = null; } @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); if (mDrawPending || mBuffer == null || mKeyboardChanged) { onBufferDraw(); } canvas.drawBitmap(mBuffer, 0, 0, null); } private void onBufferDraw() { if (mBuffer == null || mKeyboardChanged) { if (mBuffer == null || mKeyboardChanged && (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) { // Make sure our bitmap is at least 1x1 final int width = Math.max(1, getWidth()); final int height = Math.max(1, getHeight()); mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); mCanvas = new Canvas(mBuffer); } invalidateAllKeys(); mKeyboardChanged = false; } if (mKeyboard == null) return; mCanvas.save(); final Canvas canvas = mCanvas; canvas.clipRect(mDirtyRect); final Paint paint = mPaint; final Drawable keyBackground = mKeyBackground; final Rect clipRegion = mClipRegion; final Rect padding = mPadding; final int kbdPaddingLeft = mPaddingLeft; final int kbdPaddingTop = mPaddingTop; final Key[] keys = mKeys; final Key invalidKey = mInvalidatedKey; paint.setColor(mKeyTextColor); boolean drawSingleKey = false; if (invalidKey != null && canvas.getClipBounds(clipRegion)) { // Is clipRegion completely contained within the invalidated key? if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left && invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top && invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right && invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) { drawSingleKey = true; } } canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR); final int keyCount = keys.length; for (int i = 0; i < keyCount; i++) { final Key key = keys[i]; if (drawSingleKey && invalidKey != key) { continue; } int[] drawableState = key.getCurrentDrawableState(); keyBackground.setState(drawableState); // Switch the character to uppercase if shift is pressed String label = key.label == null? null : adjustCase(key.label).toString(); final Rect bounds = keyBackground.getBounds(); if (key.width != bounds.right || key.height != bounds.bottom) { keyBackground.setBounds(0, 0, key.width, key.height); } canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop); keyBackground.draw(canvas); if (label != null) { // For characters, use large font. For labels like "Done", use small font. if (label.length() > 1 && key.codes.length < 2) { paint.setTextSize(mLabelTextSize); paint.setTypeface(Typeface.DEFAULT_BOLD); } else { paint.setTextSize(mKeyTextSize); paint.setTypeface(Typeface.DEFAULT); } // Draw a drop shadow for the text paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor); // Draw the text canvas.drawText(label, (key.width - padding.left - padding.right) / 2 + padding.left, (key.height - padding.top - padding.bottom) / 2 + (paint.getTextSize() - paint.descent()) / 2 + padding.top, paint); // Turn off drop shadow paint.setShadowLayer(0, 0, 0, 0); } else if (key.icon != null) { final int drawableX = (key.width - padding.left - padding.right - key.icon.getIntrinsicWidth()) / 2 + padding.left; final int drawableY = (key.height - padding.top - padding.bottom - key.icon.getIntrinsicHeight()) / 2 + padding.top; canvas.translate(drawableX, drawableY); key.icon.setBounds(0, 0, key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight()); key.icon.draw(canvas); canvas.translate(-drawableX, -drawableY); } canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop); } mInvalidatedKey = null; // Overlay a dark rectangle to dim the keyboard if (mMiniKeyboardOnScreen) { paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); canvas.drawRect(0, 0, getWidth(), getHeight(), paint); } if (DEBUG && mShowTouchPoints) { paint.setAlpha(128); paint.setColor(0xFFFF0000); canvas.drawCircle(mStartX, mStartY, 3, paint); canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint); paint.setColor(0xFF0000FF); canvas.drawCircle(mLastX, mLastY, 3, paint); paint.setColor(0xFF00FF00); canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint); } mCanvas.restore(); mDrawPending = false; mDirtyRect.setEmpty(); } private int getKeyIndices(int x, int y, int[] allKeys) { final Key[] keys = mKeys; int primaryIndex = NOT_A_KEY; int closestKey = NOT_A_KEY; int closestKeyDist = mProximityThreshold + 1; java.util.Arrays.fill(mDistances, Integer.MAX_VALUE); int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y); final int keyCount = nearestKeyIndices.length; for (int i = 0; i < keyCount; i++) { final Key key = keys[nearestKeyIndices[i]]; int dist = 0; boolean isInside = key.isInside(x,y); if (isInside) { primaryIndex = nearestKeyIndices[i]; } if (((mProximityCorrectOn && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold) || isInside) && key.codes[0] > 32) { // Find insertion point final int nCodes = key.codes.length; if (dist < closestKeyDist) { closestKeyDist = dist; closestKey = nearestKeyIndices[i]; } if (allKeys == null) continue; for (int j = 0; j < mDistances.length; j++) { if (mDistances[j] > dist) { // Make space for nCodes codes System.arraycopy(mDistances, j, mDistances, j + nCodes, mDistances.length - j - nCodes); System.arraycopy(allKeys, j, allKeys, j + nCodes, allKeys.length - j - nCodes); for (int c = 0; c < nCodes; c++) { allKeys[j + c] = key.codes[c]; mDistances[j + c] = dist; } break; } } } } if (primaryIndex == NOT_A_KEY) { primaryIndex = closestKey; } return primaryIndex; } private void detectAndSendKey(int index, int x, int y, long eventTime) { if (index != NOT_A_KEY && index < mKeys.length) { final Key key = mKeys[index]; if (key.text != null) { mKeyboardActionListener.onText(key.text); mKeyboardActionListener.onRelease(NOT_A_KEY); } else { int code = key.codes[0]; //TextEntryState.keyPressedAt(key, x, y); int[] codes = new int[MAX_NEARBY_KEYS]; Arrays.fill(codes, NOT_A_KEY); getKeyIndices(x, y, codes); // Multi-tap if (mInMultiTap) { if (mTapCount != -1) { mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE); } else { mTapCount = 0; } code = key.codes[mTapCount]; } mKeyboardActionListener.onKey(code, codes); mKeyboardActionListener.onRelease(code); } mLastSentIndex = index; mLastTapTime = eventTime; } }
Handle multi-tap keys by producing the key label for the current multi-tap state.
/** * Handle multi-tap keys by producing the key label for the current multi-tap state. */
private CharSequence getPreviewText(Key key) { if (mInMultiTap) { // Multi-tap mPreviewLabel.setLength(0); mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]); return adjustCase(mPreviewLabel); } else { return adjustCase(key.label); } } private void showPreview(int keyIndex) { int oldKeyIndex = mCurrentKeyIndex; final PopupWindow previewPopup = mPreviewPopup; mCurrentKeyIndex = keyIndex; // Release the old key and press the new key final Key[] keys = mKeys; if (oldKeyIndex != mCurrentKeyIndex) { if (oldKeyIndex != NOT_A_KEY && keys.length > oldKeyIndex) { Key oldKey = keys[oldKeyIndex]; oldKey.onReleased(mCurrentKeyIndex == NOT_A_KEY); invalidateKey(oldKeyIndex); final int keyCode = oldKey.codes[0]; sendAccessibilityEventForUnicodeCharacter(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT, keyCode); // TODO: We need to implement AccessibilityNodeProvider for this view. sendAccessibilityEventForUnicodeCharacter( AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED, keyCode); } if (mCurrentKeyIndex != NOT_A_KEY && keys.length > mCurrentKeyIndex) { Key newKey = keys[mCurrentKeyIndex]; newKey.onPressed(); invalidateKey(mCurrentKeyIndex); final int keyCode = newKey.codes[0]; sendAccessibilityEventForUnicodeCharacter(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, keyCode); // TODO: We need to implement AccessibilityNodeProvider for this view. sendAccessibilityEventForUnicodeCharacter( AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, keyCode); } } // If key changed and preview is on ... if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) { mHandler.removeMessages(MSG_SHOW_PREVIEW); if (previewPopup.isShowing()) { if (keyIndex == NOT_A_KEY) { mHandler.sendMessageDelayed(mHandler .obtainMessage(MSG_REMOVE_PREVIEW), DELAY_AFTER_PREVIEW); } } if (keyIndex != NOT_A_KEY) { if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) { // Show right away, if it's already visible and finger is moving around showKey(keyIndex); } else { mHandler.sendMessageDelayed( mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0), DELAY_BEFORE_PREVIEW); } } } } private void showKey(final int keyIndex) { final PopupWindow previewPopup = mPreviewPopup; final Key[] keys = mKeys; if (keyIndex < 0 || keyIndex >= mKeys.length) return; Key key = keys[keyIndex]; if (key.icon != null) { mPreviewText.setCompoundDrawables(null, null, null, key.iconPreview != null ? key.iconPreview : key.icon); mPreviewText.setText(null); } else { mPreviewText.setCompoundDrawables(null, null, null, null); mPreviewText.setText(getPreviewText(key)); if (key.label.length() > 1 && key.codes.length < 2) { mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize); mPreviewText.setTypeface(Typeface.DEFAULT_BOLD); } else { mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge); mPreviewText.setTypeface(Typeface.DEFAULT); } } mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight()); final int popupHeight = mPreviewHeight; LayoutParams lp = mPreviewText.getLayoutParams(); if (lp != null) { lp.width = popupWidth; lp.height = popupHeight; } if (!mPreviewCentered) { mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + mPaddingLeft; mPopupPreviewY = key.y - popupHeight + mPreviewOffset; } else { // TODO: Fix this if centering is brought back mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2; mPopupPreviewY = - mPreviewText.getMeasuredHeight(); } mHandler.removeMessages(MSG_REMOVE_PREVIEW); getLocationInWindow(mCoordinates); mCoordinates[0] += mMiniKeyboardOffsetX; // Offset may be zero mCoordinates[1] += mMiniKeyboardOffsetY; // Offset may be zero // Set the preview background state mPreviewText.getBackground().setState( key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); mPopupPreviewX += mCoordinates[0]; mPopupPreviewY += mCoordinates[1]; // If the popup cannot be shown above the key, put it on the side getLocationOnScreen(mCoordinates); if (mPopupPreviewY + mCoordinates[1] < 0) { // If the key you're pressing is on the left side of the keyboard, show the popup on // the right, offset by enough to see at least one key to the left/right. if (key.x + key.width <= getWidth() / 2) { mPopupPreviewX += (int) (key.width * 2.5); } else { mPopupPreviewX -= (int) (key.width * 2.5); } mPopupPreviewY += popupHeight; } if (previewPopup.isShowing()) { previewPopup.update(mPopupPreviewX, mPopupPreviewY, popupWidth, popupHeight); } else { previewPopup.setWidth(popupWidth); previewPopup.setHeight(popupHeight); previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY, mPopupPreviewX, mPopupPreviewY); } mPreviewText.setVisibility(VISIBLE); } private void sendAccessibilityEventForUnicodeCharacter(int eventType, int code) { if (mAccessibilityManager.isEnabled()) { AccessibilityEvent event = AccessibilityEvent.obtain(eventType); onInitializeAccessibilityEvent(event); final String text; switch (code) { case Keyboard.KEYCODE_ALT: text = mContext.getString(R.string.keyboardview_keycode_alt); break; case Keyboard.KEYCODE_CANCEL: text = mContext.getString(R.string.keyboardview_keycode_cancel); break; case Keyboard.KEYCODE_DELETE: text = mContext.getString(R.string.keyboardview_keycode_delete); break; case Keyboard.KEYCODE_DONE: text = mContext.getString(R.string.keyboardview_keycode_done); break; case Keyboard.KEYCODE_MODE_CHANGE: text = mContext.getString(R.string.keyboardview_keycode_mode_change); break; case Keyboard.KEYCODE_SHIFT: text = mContext.getString(R.string.keyboardview_keycode_shift); break; case '\n': text = mContext.getString(R.string.keyboardview_keycode_enter); break; default: text = String.valueOf((char) code); } event.getText().add(text); mAccessibilityManager.sendAccessibilityEvent(event); } }
Requests a redraw of the entire keyboard. Calling View.invalidate is not sufficient because the keyboard renders the keys to an off-screen buffer and an invalidate() only draws the cached buffer.
See Also:
/** * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient * because the keyboard renders the keys to an off-screen buffer and an invalidate() only * draws the cached buffer. * @see #invalidateKey(int) */
public void invalidateAllKeys() { mDirtyRect.union(0, 0, getWidth(), getHeight()); mDrawPending = true; invalidate(); }
Invalidates a key so that it will be redrawn on the next repaint. Use this method if only one key is changing it's content. Any changes that affect the position or size of the key may not be honored.
Params:
  • keyIndex – the index of the key in the attached Keyboard.
See Also:
/** * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only * one key is changing it's content. Any changes that affect the position or size of the key * may not be honored. * @param keyIndex the index of the key in the attached {@link Keyboard}. * @see #invalidateAllKeys */
public void invalidateKey(int keyIndex) { if (mKeys == null) return; if (keyIndex < 0 || keyIndex >= mKeys.length) { return; } final Key key = mKeys[keyIndex]; mInvalidatedKey = key; mDirtyRect.union(key.x + mPaddingLeft, key.y + mPaddingTop, key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop); onBufferDraw(); invalidate(key.x + mPaddingLeft, key.y + mPaddingTop, key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop); } private boolean openPopupIfRequired(MotionEvent me) { // Check if we have a popup layout specified first. if (mPopupLayout == 0) { return false; } if (mCurrentKey < 0 || mCurrentKey >= mKeys.length) { return false; } Key popupKey = mKeys[mCurrentKey]; boolean result = onLongPress(popupKey); if (result) { mAbortKey = true; showPreview(NOT_A_KEY); } return result; }
Called when a key is long pressed. By default this will open any popup keyboard associated with this key through the attributes popupLayout and popupCharacters.
Params:
  • popupKey – the key that was long pressed
Returns: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.
/** * Called when a key is long pressed. By default this will open any popup keyboard associated * with this key through the attributes popupLayout and popupCharacters. * @param popupKey the key that was long pressed * @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. */
protected boolean onLongPress(Key popupKey) { int popupKeyboardId = popupKey.popupResId; if (popupKeyboardId != 0) { mMiniKeyboardContainer = mMiniKeyboardCache.get(popupKey); if (mMiniKeyboardContainer == null) { LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( Context.LAYOUT_INFLATER_SERVICE); mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null); mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById( com.android.internal.R.id.keyboardView); View closeButton = mMiniKeyboardContainer.findViewById( com.android.internal.R.id.closeButton); if (closeButton != null) closeButton.setOnClickListener(this); mMiniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() { public void onKey(int primaryCode, int[] keyCodes) { mKeyboardActionListener.onKey(primaryCode, keyCodes); dismissPopupKeyboard(); } public void onText(CharSequence text) { mKeyboardActionListener.onText(text); dismissPopupKeyboard(); } public void swipeLeft() { } public void swipeRight() { } public void swipeUp() { } public void swipeDown() { } public void onPress(int primaryCode) { mKeyboardActionListener.onPress(primaryCode); } public void onRelease(int primaryCode) { mKeyboardActionListener.onRelease(primaryCode); } }); //mInputView.setSuggest(mSuggest); Keyboard keyboard; if (popupKey.popupCharacters != null) { keyboard = new Keyboard(getContext(), popupKeyboardId, popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight()); } else { keyboard = new Keyboard(getContext(), popupKeyboardId); } mMiniKeyboard.setKeyboard(keyboard); mMiniKeyboard.setPopupParent(this); mMiniKeyboardContainer.measure( MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); mMiniKeyboardCache.put(popupKey, mMiniKeyboardContainer); } else { mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById( com.android.internal.R.id.keyboardView); } getLocationInWindow(mCoordinates); mPopupX = popupKey.x + mPaddingLeft; mPopupY = popupKey.y + mPaddingTop; mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth(); mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight(); final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mCoordinates[0]; final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mCoordinates[1]; mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y); mMiniKeyboard.setShifted(isShifted()); mPopupKeyboard.setContentView(mMiniKeyboardContainer); mPopupKeyboard.setWidth(mMiniKeyboardContainer.getMeasuredWidth()); mPopupKeyboard.setHeight(mMiniKeyboardContainer.getMeasuredHeight()); mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y); mMiniKeyboardOnScreen = true; //mMiniKeyboard.onTouchEvent(getTranslatedEvent(me)); invalidateAllKeys(); return true; } return false; } @Override public boolean onHoverEvent(MotionEvent event) { if (mAccessibilityManager.isTouchExplorationEnabled() && event.getPointerCount() == 1) { final int action = event.getAction(); switch (action) { case MotionEvent.ACTION_HOVER_ENTER: { event.setAction(MotionEvent.ACTION_DOWN); } break; case MotionEvent.ACTION_HOVER_MOVE: { event.setAction(MotionEvent.ACTION_MOVE); } break; case MotionEvent.ACTION_HOVER_EXIT: { event.setAction(MotionEvent.ACTION_UP); } break; } return onTouchEvent(event); } return true; } @Override public boolean onTouchEvent(MotionEvent me) { // Convert multi-pointer up/down events to single up/down events to // deal with the typical multi-pointer behavior of two-thumb typing final int pointerCount = me.getPointerCount(); final int action = me.getAction(); boolean result = false; final long now = me.getEventTime(); if (pointerCount != mOldPointerCount) { if (pointerCount == 1) { // Send a down event for the latest pointer MotionEvent down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, me.getX(), me.getY(), me.getMetaState()); result = onModifiedTouchEvent(down, false); down.recycle(); // If it's an up action, then deliver the up as well. if (action == MotionEvent.ACTION_UP) { result = onModifiedTouchEvent(me, true); } } else { // Send an up event for the last pointer MotionEvent up = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, mOldPointerX, mOldPointerY, me.getMetaState()); result = onModifiedTouchEvent(up, true); up.recycle(); } } else { if (pointerCount == 1) { result = onModifiedTouchEvent(me, false); mOldPointerX = me.getX(); mOldPointerY = me.getY(); } else { // Don't do anything when 2 pointers are down and moving. result = true; } } mOldPointerCount = pointerCount; return result; } private boolean onModifiedTouchEvent(MotionEvent me, boolean possiblePoly) { int touchX = (int) me.getX() - mPaddingLeft; int touchY = (int) me.getY() - mPaddingTop; if (touchY >= -mVerticalCorrection) touchY += mVerticalCorrection; final int action = me.getAction(); final long eventTime = me.getEventTime(); int keyIndex = getKeyIndices(touchX, touchY, null); mPossiblePoly = possiblePoly; // Track the last few movements to look for spurious swipes. if (action == MotionEvent.ACTION_DOWN) mSwipeTracker.clear(); mSwipeTracker.addMovement(me); // Ignore all motion events until a DOWN. if (mAbortKey && action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_CANCEL) { return true; } if (mGestureDetector.onTouchEvent(me)) { showPreview(NOT_A_KEY); mHandler.removeMessages(MSG_REPEAT); mHandler.removeMessages(MSG_LONGPRESS); return true; } // Needs to be called after the gesture detector gets a turn, as it may have // displayed the mini keyboard if (mMiniKeyboardOnScreen && action != MotionEvent.ACTION_CANCEL) { return true; } switch (action) { case MotionEvent.ACTION_DOWN: mAbortKey = false; mStartX = touchX; mStartY = touchY; mLastCodeX = touchX; mLastCodeY = touchY; mLastKeyTime = 0; mCurrentKeyTime = 0; mLastKey = NOT_A_KEY; mCurrentKey = keyIndex; mDownKey = keyIndex; mDownTime = me.getEventTime(); mLastMoveTime = mDownTime; checkMultiTap(eventTime, keyIndex); mKeyboardActionListener.onPress(keyIndex != NOT_A_KEY ? mKeys[keyIndex].codes[0] : 0); if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) { mRepeatKeyIndex = mCurrentKey; Message msg = mHandler.obtainMessage(MSG_REPEAT); mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY); repeatKey(); // Delivering the key could have caused an abort if (mAbortKey) { mRepeatKeyIndex = NOT_A_KEY; break; } } if (mCurrentKey != NOT_A_KEY) { Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me); mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT); } showPreview(keyIndex); break; case MotionEvent.ACTION_MOVE: boolean continueLongPress = false; if (keyIndex != NOT_A_KEY) { if (mCurrentKey == NOT_A_KEY) { mCurrentKey = keyIndex; mCurrentKeyTime = eventTime - mDownTime; } else { if (keyIndex == mCurrentKey) { mCurrentKeyTime += eventTime - mLastMoveTime; continueLongPress = true; } else if (mRepeatKeyIndex == NOT_A_KEY) { resetMultiTap(); mLastKey = mCurrentKey; mLastCodeX = mLastX; mLastCodeY = mLastY; mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime; mCurrentKey = keyIndex; mCurrentKeyTime = 0; } } } if (!continueLongPress) { // Cancel old longpress mHandler.removeMessages(MSG_LONGPRESS); // Start new longpress if key has changed if (keyIndex != NOT_A_KEY) { Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me); mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT); } } showPreview(mCurrentKey); mLastMoveTime = eventTime; break; case MotionEvent.ACTION_UP: removeMessages(); if (keyIndex == mCurrentKey) { mCurrentKeyTime += eventTime - mLastMoveTime; } else { resetMultiTap(); mLastKey = mCurrentKey; mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime; mCurrentKey = keyIndex; mCurrentKeyTime = 0; } if (mCurrentKeyTime < mLastKeyTime && mCurrentKeyTime < DEBOUNCE_TIME && mLastKey != NOT_A_KEY) { mCurrentKey = mLastKey; touchX = mLastCodeX; touchY = mLastCodeY; } showPreview(NOT_A_KEY); Arrays.fill(mKeyIndices, NOT_A_KEY); // If we're not on a repeating key (which sends on a DOWN event) if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) { detectAndSendKey(mCurrentKey, touchX, touchY, eventTime); } invalidateKey(keyIndex); mRepeatKeyIndex = NOT_A_KEY; break; case MotionEvent.ACTION_CANCEL: removeMessages(); dismissPopupKeyboard(); mAbortKey = true; showPreview(NOT_A_KEY); invalidateKey(mCurrentKey); break; } mLastX = touchX; mLastY = touchY; return true; } private boolean repeatKey() { Key key = mKeys[mRepeatKeyIndex]; detectAndSendKey(mCurrentKey, key.x, key.y, mLastTapTime); return true; } protected void swipeRight() { mKeyboardActionListener.swipeRight(); } protected void swipeLeft() { mKeyboardActionListener.swipeLeft(); } protected void swipeUp() { mKeyboardActionListener.swipeUp(); } protected void swipeDown() { mKeyboardActionListener.swipeDown(); } public void closing() { if (mPreviewPopup.isShowing()) { mPreviewPopup.dismiss(); } removeMessages(); dismissPopupKeyboard(); mBuffer = null; mCanvas = null; mMiniKeyboardCache.clear(); } private void removeMessages() { if (mHandler != null) { mHandler.removeMessages(MSG_REPEAT); mHandler.removeMessages(MSG_LONGPRESS); mHandler.removeMessages(MSG_SHOW_PREVIEW); } } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); closing(); } private void dismissPopupKeyboard() { if (mPopupKeyboard.isShowing()) { mPopupKeyboard.dismiss(); mMiniKeyboardOnScreen = false; invalidateAllKeys(); } } public boolean handleBack() { if (mPopupKeyboard.isShowing()) { dismissPopupKeyboard(); return true; } return false; } private void resetMultiTap() { mLastSentIndex = NOT_A_KEY; mTapCount = 0; mLastTapTime = -1; mInMultiTap = false; } private void checkMultiTap(long eventTime, int keyIndex) { if (keyIndex == NOT_A_KEY) return; Key key = mKeys[keyIndex]; if (key.codes.length > 1) { mInMultiTap = true; if (eventTime < mLastTapTime + MULTITAP_INTERVAL && keyIndex == mLastSentIndex) { mTapCount = (mTapCount + 1) % key.codes.length; return; } else { mTapCount = -1; return; } } if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) { resetMultiTap(); } } private static class SwipeTracker { static final int NUM_PAST = 4; static final int LONGEST_PAST_TIME = 200; final float mPastX[] = new float[NUM_PAST]; final float mPastY[] = new float[NUM_PAST]; final long mPastTime[] = new long[NUM_PAST]; float mYVelocity; float mXVelocity; public void clear() { mPastTime[0] = 0; } public void addMovement(MotionEvent ev) { long time = ev.getEventTime(); final int N = ev.getHistorySize(); for (int i=0; i<N; i++) { addPoint(ev.getHistoricalX(i), ev.getHistoricalY(i), ev.getHistoricalEventTime(i)); } addPoint(ev.getX(), ev.getY(), time); } private void addPoint(float x, float y, long time) { int drop = -1; int i; final long[] pastTime = mPastTime; for (i=0; i<NUM_PAST; i++) { if (pastTime[i] == 0) { break; } else if (pastTime[i] < time-LONGEST_PAST_TIME) { drop = i; } } if (i == NUM_PAST && drop < 0) { drop = 0; } if (drop == i) drop--; final float[] pastX = mPastX; final float[] pastY = mPastY; if (drop >= 0) { final int start = drop+1; final int count = NUM_PAST-drop-1; System.arraycopy(pastX, start, pastX, 0, count); System.arraycopy(pastY, start, pastY, 0, count); System.arraycopy(pastTime, start, pastTime, 0, count); i -= (drop+1); } pastX[i] = x; pastY[i] = y; pastTime[i] = time; i++; if (i < NUM_PAST) { pastTime[i] = 0; } } public void computeCurrentVelocity(int units) { computeCurrentVelocity(units, Float.MAX_VALUE); } public void computeCurrentVelocity(int units, float maxVelocity) { final float[] pastX = mPastX; final float[] pastY = mPastY; final long[] pastTime = mPastTime; final float oldestX = pastX[0]; final float oldestY = pastY[0]; final long oldestTime = pastTime[0]; float accumX = 0; float accumY = 0; int N=0; while (N < NUM_PAST) { if (pastTime[N] == 0) { break; } N++; } for (int i=1; i < N; i++) { final int dur = (int)(pastTime[i] - oldestTime); if (dur == 0) continue; float dist = pastX[i] - oldestX; float vel = (dist/dur) * units; // pixels/frame. if (accumX == 0) accumX = vel; else accumX = (accumX + vel) * .5f; dist = pastY[i] - oldestY; vel = (dist/dur) * units; // pixels/frame. if (accumY == 0) accumY = vel; else accumY = (accumY + vel) * .5f; } mXVelocity = accumX < 0.0f ? Math.max(accumX, -maxVelocity) : Math.min(accumX, maxVelocity); mYVelocity = accumY < 0.0f ? Math.max(accumY, -maxVelocity) : Math.min(accumY, maxVelocity); } public float getXVelocity() { return mXVelocity; } public float getYVelocity() { return mYVelocity; } } }