futokb/java/src/com/android/inputmethod/keyboard/KeyboardView.java
Tom Ouyang fa2d543785 Refactor more keys menu framework (part 1)
Eliminate use of slower popup window for rendering the more keys menu and more suggestions menu.
These menus are now drawn directly on the preview placer view.

Change-Id: I40638724a6ffde2be4b5d0341d2a5db7ff7be930
2012-12-07 16:34:25 -08:00

1065 lines
44 KiB
Java

/*
* Copyright (C) 2010 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.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Message;
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.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.android.inputmethod.keyboard.internal.KeyDrawParams;
import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams;
import com.android.inputmethod.keyboard.internal.KeyVisualAttributes;
import com.android.inputmethod.keyboard.internal.PreviewPlacerView;
import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.CoordinateUtils;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
import com.android.inputmethod.latin.StringUtils;
import com.android.inputmethod.latin.define.ProductionFlag;
import com.android.inputmethod.research.ResearchLogger;
import java.util.HashSet;
/**
* A view that renders a virtual {@link Keyboard}.
*
* @attr ref R.styleable#KeyboardView_keyBackground
* @attr ref R.styleable#KeyboardView_moreKeysLayout
* @attr ref R.styleable#KeyboardView_keyPreviewLayout
* @attr ref R.styleable#KeyboardView_keyPreviewOffset
* @attr ref R.styleable#KeyboardView_keyPreviewHeight
* @attr ref R.styleable#KeyboardView_keyPreviewLingerTimeout
* @attr ref R.styleable#KeyboardView_keyLabelHorizontalPadding
* @attr ref R.styleable#KeyboardView_keyHintLetterPadding
* @attr ref R.styleable#KeyboardView_keyPopupHintLetterPadding
* @attr ref R.styleable#KeyboardView_keyShiftedLetterHintPadding
* @attr ref R.styleable#KeyboardView_keyTextShadowRadius
* @attr ref R.styleable#KeyboardView_backgroundDimAlpha
* @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextSize
* @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextColor
* @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextOffset
* @attr ref R.styleable#KeyboardView_gestureFloatingPreviewColor
* @attr ref R.styleable#KeyboardView_gestureFloatingPreviewHorizontalPadding
* @attr ref R.styleable#KeyboardView_gestureFloatingPreviewVerticalPadding
* @attr ref R.styleable#KeyboardView_gestureFloatingPreviewRoundRadius
* @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextLingerTimeout
* @attr ref R.styleable#KeyboardView_gesturePreviewTrailFadeoutStartDelay
* @attr ref R.styleable#KeyboardView_gesturePreviewTrailFadeoutDuration
* @attr ref R.styleable#KeyboardView_gesturePreviewTrailUpdateInterval
* @attr ref R.styleable#KeyboardView_gesturePreviewTrailColor
* @attr ref R.styleable#KeyboardView_gesturePreviewTrailWidth
* @attr ref R.styleable#KeyboardView_verticalCorrection
* @attr ref R.styleable#Keyboard_Key_keyTypeface
* @attr ref R.styleable#Keyboard_Key_keyLetterSize
* @attr ref R.styleable#Keyboard_Key_keyLabelSize
* @attr ref R.styleable#Keyboard_Key_keyLargeLetterRatio
* @attr ref R.styleable#Keyboard_Key_keyLargeLabelRatio
* @attr ref R.styleable#Keyboard_Key_keyHintLetterRatio
* @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintRatio
* @attr ref R.styleable#Keyboard_Key_keyHintLabelRatio
* @attr ref R.styleable#Keyboard_Key_keyPreviewTextRatio
* @attr ref R.styleable#Keyboard_Key_keyTextColor
* @attr ref R.styleable#Keyboard_Key_keyTextColorDisabled
* @attr ref R.styleable#Keyboard_Key_keyTextShadowColor
* @attr ref R.styleable#Keyboard_Key_keyHintLetterColor
* @attr ref R.styleable#Keyboard_Key_keyHintLabelColor
* @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintInactivatedColor
* @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintActivatedColor
* @attr ref R.styleable#Keyboard_Key_keyPreviewTextColor
*/
public class KeyboardView extends View implements PointerTracker.DrawingProxy,
MoreKeysPanel.Controller {
private static final String TAG = KeyboardView.class.getSimpleName();
// XML attributes
protected final KeyVisualAttributes mKeyVisualAttributes;
private final int mKeyLabelHorizontalPadding;
private final float mKeyHintLetterPadding;
private final float mKeyPopupHintLetterPadding;
private final float mKeyShiftedLetterHintPadding;
private final float mKeyTextShadowRadius;
protected final float mVerticalCorrection;
protected final int mMoreKeysLayout;
protected final Drawable mKeyBackground;
protected final Rect mKeyBackgroundPadding = new Rect();
private final int mBackgroundDimAlpha;
// HORIZONTAL ELLIPSIS "...", character for popup hint.
private static final String POPUP_HINT_CHAR = "\u2026";
// Margin between the label and the icon on a key that has both of them.
// Specified by the fraction of the key width.
// TODO: Use resource parameter for this value.
private static final float LABEL_ICON_MARGIN = 0.05f;
// The maximum key label width in the proportion to the key width.
private static final float MAX_LABEL_RATIO = 0.90f;
// Main keyboard
private Keyboard mKeyboard;
protected final KeyDrawParams mKeyDrawParams = new KeyDrawParams();
// Preview placer view
private final PreviewPlacerView mPreviewPlacerView;
private final int[] mOriginCoords = CoordinateUtils.newInstance();
// More keys panel (used by both more keys keyboard and more suggestions view)
// TODO: Consider extending to support multiple more keys panels
protected MoreKeysPanel mMoreKeysPanel;
// Key preview
private static final int PREVIEW_ALPHA = 240;
private final int mKeyPreviewLayoutId;
private final int mPreviewOffset;
private final int mPreviewHeight;
private final int mPreviewLingerTimeout;
private final SparseArray<TextView> mKeyPreviewTexts = CollectionUtils.newSparseArray();
protected final KeyPreviewDrawParams mKeyPreviewDrawParams = new KeyPreviewDrawParams();
private boolean mShowKeyPreviewPopup = true;
private int mDelayAfterPreview;
// 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];
// Drawing
/** True if the entire keyboard needs to be dimmed. */
private boolean mNeedsToDimEntireKeyboard;
/** True if all keys should be drawn */
private boolean mInvalidateAllKeys;
/** The keys that should be drawn */
private final HashSet<Key> mInvalidatedKeys = CollectionUtils.newHashSet();
/** The working rectangle variable */
private final Rect mWorkingRect = new Rect();
/** The keyboard bitmap buffer for faster updates */
/** The clip region to draw keys */
private final Region mClipRegion = new Region();
private Bitmap mOffscreenBuffer;
/** The canvas for the above mutable keyboard bitmap */
private final Canvas mOffscreenCanvas = new Canvas();
private final Paint mPaint = new Paint();
private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics();
// This sparse array caches key label text height in pixel indexed by key label text size.
private static final SparseArray<Float> sTextHeightCache = CollectionUtils.newSparseArray();
// This sparse array caches key label text width in pixel indexed by key label text size.
private static final SparseArray<Float> sTextWidthCache = CollectionUtils.newSparseArray();
private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' };
private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' };
private final DrawingHandler mDrawingHandler = new DrawingHandler(this);
public static class DrawingHandler extends StaticInnerHandlerWrapper<KeyboardView> {
private static final int MSG_DISMISS_KEY_PREVIEW = 0;
public DrawingHandler(final KeyboardView outerInstance) {
super(outerInstance);
}
@Override
public void handleMessage(final Message msg) {
final KeyboardView keyboardView = getOuterInstance();
if (keyboardView == null) return;
final PointerTracker tracker = (PointerTracker) msg.obj;
switch (msg.what) {
case MSG_DISMISS_KEY_PREVIEW:
final TextView previewText = keyboardView.mKeyPreviewTexts.get(tracker.mPointerId);
if (previewText != null) {
previewText.setVisibility(INVISIBLE);
}
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 cancelAllMessages() {
cancelAllDismissKeyPreviews();
}
}
public KeyboardView(final Context context, final AttributeSet attrs) {
this(context, attrs, R.attr.keyboardViewStyle);
}
public KeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
mKeyBackground = keyboardViewAttr.getDrawable(R.styleable.KeyboardView_keyBackground);
mKeyBackground.getPadding(mKeyBackgroundPadding);
mPreviewOffset = keyboardViewAttr.getDimensionPixelOffset(
R.styleable.KeyboardView_keyPreviewOffset, 0);
mPreviewHeight = keyboardViewAttr.getDimensionPixelSize(
R.styleable.KeyboardView_keyPreviewHeight, 80);
mPreviewLingerTimeout = keyboardViewAttr.getInt(
R.styleable.KeyboardView_keyPreviewLingerTimeout, 0);
mDelayAfterPreview = mPreviewLingerTimeout;
mKeyLabelHorizontalPadding = keyboardViewAttr.getDimensionPixelOffset(
R.styleable.KeyboardView_keyLabelHorizontalPadding, 0);
mKeyHintLetterPadding = keyboardViewAttr.getDimension(
R.styleable.KeyboardView_keyHintLetterPadding, 0);
mKeyPopupHintLetterPadding = keyboardViewAttr.getDimension(
R.styleable.KeyboardView_keyPopupHintLetterPadding, 0);
mKeyShiftedLetterHintPadding = keyboardViewAttr.getDimension(
R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0);
mKeyTextShadowRadius = keyboardViewAttr.getFloat(
R.styleable.KeyboardView_keyTextShadowRadius, 0.0f);
mKeyPreviewLayoutId = keyboardViewAttr.getResourceId(
R.styleable.KeyboardView_keyPreviewLayout, 0);
if (mKeyPreviewLayoutId == 0) {
mShowKeyPreviewPopup = false;
}
mVerticalCorrection = keyboardViewAttr.getDimension(
R.styleable.KeyboardView_verticalCorrection, 0);
mMoreKeysLayout = keyboardViewAttr.getResourceId(
R.styleable.KeyboardView_moreKeysLayout, 0);
mBackgroundDimAlpha = keyboardViewAttr.getInt(
R.styleable.KeyboardView_backgroundDimAlpha, 0);
keyboardViewAttr.recycle();
final TypedArray keyAttr = context.obtainStyledAttributes(attrs,
R.styleable.Keyboard_Key, defStyle, R.style.KeyboardView);
mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
keyAttr.recycle();
mPreviewPlacerView = new PreviewPlacerView(context, attrs);
mPaint.setAntiAlias(true);
}
private static void blendAlpha(final Paint paint, final int alpha) {
final int color = paint.getColor();
paint.setARGB((paint.getAlpha() * alpha) / Constants.Color.ALPHA_OPAQUE,
Color.red(color), Color.green(color), Color.blue(color));
}
/**
* 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(final Keyboard keyboard) {
mKeyboard = keyboard;
LatinImeLogger.onSetKeyboard(keyboard);
requestLayout();
invalidateAllKeys();
final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes);
mKeyDrawParams.updateParams(keyHeight, keyboard.mKeyVisualAttributes);
}
/**
* Returns the current keyboard being displayed by this view.
* @return the currently attached keyboard
* @see #setKeyboard(Keyboard)
*/
public Keyboard getKeyboard() {
return mKeyboard;
}
/**
* 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;
mDelayAfterPreview = delay;
}
/**
* 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;
}
public void setGesturePreviewMode(final boolean drawsGesturePreviewTrail,
final boolean drawsGestureFloatingPreviewText) {
mPreviewPlacerView.setGesturePreviewMode(
drawsGesturePreviewTrail, drawsGestureFloatingPreviewText);
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
if (mKeyboard != null) {
// The main keyboard expands to the display width.
final int height = mKeyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(widthMeasureSpec, height);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
public void onDraw(final Canvas canvas) {
super.onDraw(canvas);
if (canvas.isHardwareAccelerated()) {
onDrawKeyboard(canvas);
return;
}
final boolean bufferNeedsUpdates = mInvalidateAllKeys || !mInvalidatedKeys.isEmpty();
if (bufferNeedsUpdates || mOffscreenBuffer == null) {
if (maybeAllocateOffscreenBuffer()) {
mInvalidateAllKeys = true;
// TODO: Stop using the offscreen canvas even when in software rendering
mOffscreenCanvas.setBitmap(mOffscreenBuffer);
}
onDrawKeyboard(mOffscreenCanvas);
}
canvas.drawBitmap(mOffscreenBuffer, 0, 0, null);
}
private boolean maybeAllocateOffscreenBuffer() {
final int width = getWidth();
final int height = getHeight();
if (width == 0 || height == 0) {
return false;
}
if (mOffscreenBuffer != null && mOffscreenBuffer.getWidth() == width
&& mOffscreenBuffer.getHeight() == height) {
return false;
}
freeOffscreenBuffer();
mOffscreenBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
return true;
}
private void freeOffscreenBuffer() {
if (mOffscreenBuffer != null) {
mOffscreenBuffer.recycle();
mOffscreenBuffer = null;
}
}
private void onDrawKeyboard(final Canvas canvas) {
if (mKeyboard == null) return;
final int width = getWidth();
final int height = getHeight();
final Paint paint = mPaint;
// Calculate clip region and set.
final boolean drawAllKeys = mInvalidateAllKeys || mInvalidatedKeys.isEmpty();
final boolean isHardwareAccelerated = canvas.isHardwareAccelerated();
// TODO: Confirm if it's really required to draw all keys when hardware acceleration is on.
if (drawAllKeys || isHardwareAccelerated) {
mClipRegion.set(0, 0, width, height);
} else {
mClipRegion.setEmpty();
for (final Key key : mInvalidatedKeys) {
if (mKeyboard.hasKey(key)) {
final int x = key.mX + getPaddingLeft();
final int y = key.mY + getPaddingTop();
mWorkingRect.set(x, y, x + key.mWidth, y + key.mHeight);
mClipRegion.union(mWorkingRect);
}
}
}
if (!isHardwareAccelerated) {
canvas.clipRegion(mClipRegion, Region.Op.REPLACE);
// Draw keyboard background.
canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
final Drawable background = getBackground();
if (background != null) {
background.draw(canvas);
}
}
// TODO: Confirm if it's really required to draw all keys when hardware acceleration is on.
if (drawAllKeys || isHardwareAccelerated) {
// Draw all keys.
for (final Key key : mKeyboard.mKeys) {
onDrawKey(key, canvas, paint);
}
} else {
// Draw invalidated keys.
for (final Key key : mInvalidatedKeys) {
if (mKeyboard.hasKey(key)) {
onDrawKey(key, canvas, paint);
}
}
}
// Overlay a dark rectangle to dim.
if (mNeedsToDimEntireKeyboard) {
paint.setColor(Color.BLACK);
paint.setAlpha(mBackgroundDimAlpha);
// Note: clipRegion() above is in effect if it was called.
canvas.drawRect(0, 0, width, height, paint);
}
// ResearchLogging indicator.
// TODO: Reimplement using a keyboard background image specific to the ResearchLogger,
// and remove this call.
if (ProductionFlag.IS_EXPERIMENTAL) {
ResearchLogger.getInstance().paintIndicator(this, paint, canvas, width, height);
}
mInvalidatedKeys.clear();
mInvalidateAllKeys = false;
}
public void dimEntireKeyboard(final boolean dimmed) {
final boolean needsRedrawing = mNeedsToDimEntireKeyboard != dimmed;
mNeedsToDimEntireKeyboard = dimmed;
if (needsRedrawing) {
invalidateAllKeys();
}
}
private void onDrawKey(final Key key, final Canvas canvas, final Paint paint) {
final int keyDrawX = key.getDrawX() + getPaddingLeft();
final int keyDrawY = key.mY + getPaddingTop();
canvas.translate(keyDrawX, keyDrawY);
final int keyHeight = mKeyboard.mMostCommonKeyHeight - mKeyboard.mVerticalGap;
final KeyVisualAttributes attr = key.mKeyVisualAttributes;
final KeyDrawParams params = mKeyDrawParams.mayCloneAndUpdateParams(keyHeight, attr);
params.mAnimAlpha = Constants.Color.ALPHA_OPAQUE;
if (!key.isSpacer()) {
onDrawKeyBackground(key, canvas);
}
onDrawKeyTopVisuals(key, canvas, paint, params);
canvas.translate(-keyDrawX, -keyDrawY);
}
// Draw key background.
protected void onDrawKeyBackground(final Key key, final Canvas canvas) {
final Rect padding = mKeyBackgroundPadding;
final int bgWidth = key.getDrawWidth() + padding.left + padding.right;
final int bgHeight = key.mHeight + padding.top + padding.bottom;
final int bgX = -padding.left;
final int bgY = -padding.top;
final int[] drawableState = key.getCurrentDrawableState();
final Drawable background = mKeyBackground;
background.setState(drawableState);
final Rect bounds = background.getBounds();
if (bgWidth != bounds.right || bgHeight != bounds.bottom) {
background.setBounds(0, 0, bgWidth, bgHeight);
}
canvas.translate(bgX, bgY);
background.draw(canvas);
if (LatinImeLogger.sVISUALDEBUG) {
drawRectangle(canvas, 0, 0, bgWidth, bgHeight, 0x80c00000, new Paint());
}
canvas.translate(-bgX, -bgY);
}
// Draw key top visuals.
protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint,
final KeyDrawParams params) {
final int keyWidth = key.getDrawWidth();
final int keyHeight = key.mHeight;
final float centerX = keyWidth * 0.5f;
final float centerY = keyHeight * 0.5f;
if (LatinImeLogger.sVISUALDEBUG) {
drawRectangle(canvas, 0, 0, keyWidth, keyHeight, 0x800000c0, new Paint());
}
// Draw key label.
final Drawable icon = key.getIcon(mKeyboard.mIconsSet, params.mAnimAlpha);
float positionX = centerX;
if (key.mLabel != null) {
final String label = key.mLabel;
paint.setTypeface(key.selectTypeface(params));
paint.setTextSize(key.selectTextSize(params));
final float labelCharHeight = getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint);
final float labelCharWidth = getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint);
// Vertical label text alignment.
final float baseline = centerY + labelCharHeight / 2;
// Horizontal label text alignment
float labelWidth = 0;
if (key.isAlignLeft()) {
positionX = mKeyLabelHorizontalPadding;
paint.setTextAlign(Align.LEFT);
} else if (key.isAlignRight()) {
positionX = keyWidth - mKeyLabelHorizontalPadding;
paint.setTextAlign(Align.RIGHT);
} else if (key.isAlignLeftOfCenter()) {
// TODO: Parameterise this?
positionX = centerX - labelCharWidth * 7 / 4;
paint.setTextAlign(Align.LEFT);
} else if (key.hasLabelWithIconLeft() && icon != null) {
labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth()
+ LABEL_ICON_MARGIN * keyWidth;
positionX = centerX + labelWidth / 2;
paint.setTextAlign(Align.RIGHT);
} else if (key.hasLabelWithIconRight() && icon != null) {
labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth()
+ LABEL_ICON_MARGIN * keyWidth;
positionX = centerX - labelWidth / 2;
paint.setTextAlign(Align.LEFT);
} else {
positionX = centerX;
paint.setTextAlign(Align.CENTER);
}
if (key.needsXScale()) {
paint.setTextScaleX(
Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / getLabelWidth(label, paint)));
}
paint.setColor(key.selectTextColor(params));
if (key.isEnabled()) {
// Set a drop shadow for the text
paint.setShadowLayer(mKeyTextShadowRadius, 0, 0, params.mTextShadowColor);
} else {
// Make label invisible
paint.setColor(Color.TRANSPARENT);
}
blendAlpha(paint, params.mAnimAlpha);
canvas.drawText(label, 0, label.length(), positionX, baseline, paint);
// Turn off drop shadow and reset x-scale.
paint.setShadowLayer(0, 0, 0, 0);
paint.setTextScaleX(1.0f);
if (icon != null) {
final int iconWidth = icon.getIntrinsicWidth();
final int iconHeight = icon.getIntrinsicHeight();
final int iconY = (keyHeight - iconHeight) / 2;
if (key.hasLabelWithIconLeft()) {
final int iconX = (int)(centerX - labelWidth / 2);
drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
} else if (key.hasLabelWithIconRight()) {
final int iconX = (int)(centerX + labelWidth / 2 - iconWidth);
drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
}
}
if (LatinImeLogger.sVISUALDEBUG) {
final Paint line = new Paint();
drawHorizontalLine(canvas, baseline, keyWidth, 0xc0008000, line);
drawVerticalLine(canvas, positionX, keyHeight, 0xc0800080, line);
}
}
// Draw hint label.
if (key.mHintLabel != null) {
final String hintLabel = key.mHintLabel;
paint.setTextSize(key.selectHintTextSize(params));
paint.setColor(key.selectHintTextColor(params));
blendAlpha(paint, params.mAnimAlpha);
final float hintX, hintY;
if (key.hasHintLabel()) {
// The hint label is placed just right of the key label. Used mainly on
// "phone number" layout.
// TODO: Generalize the following calculations.
hintX = positionX + getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) * 2;
hintY = centerY + getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
paint.setTextAlign(Align.LEFT);
} else if (key.hasShiftedLetterHint()) {
// The hint label is placed at top-right corner of the key. Used mainly on tablet.
hintX = keyWidth - mKeyShiftedLetterHintPadding
- getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
paint.getFontMetrics(mFontMetrics);
hintY = -mFontMetrics.top;
paint.setTextAlign(Align.CENTER);
} else { // key.hasHintLetter()
// The hint letter is placed at top-right corner of the key. Used mainly on phone.
hintX = keyWidth - mKeyHintLetterPadding
- getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint) / 2;
hintY = -paint.ascent();
paint.setTextAlign(Align.CENTER);
}
canvas.drawText(hintLabel, 0, hintLabel.length(), hintX, hintY, paint);
if (LatinImeLogger.sVISUALDEBUG) {
final Paint line = new Paint();
drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line);
drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line);
}
}
// Draw key icon.
if (key.mLabel == null && icon != null) {
final int iconWidth = Math.min(icon.getIntrinsicWidth(), keyWidth);
final int iconHeight = icon.getIntrinsicHeight();
final int iconX, alignX;
final int iconY = (keyHeight - iconHeight) / 2;
if (key.isAlignLeft()) {
iconX = mKeyLabelHorizontalPadding;
alignX = iconX;
} else if (key.isAlignRight()) {
iconX = keyWidth - mKeyLabelHorizontalPadding - iconWidth;
alignX = iconX + iconWidth;
} else { // Align center
iconX = (keyWidth - iconWidth) / 2;
alignX = iconX + iconWidth / 2;
}
drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
if (LatinImeLogger.sVISUALDEBUG) {
final Paint line = new Paint();
drawVerticalLine(canvas, alignX, keyHeight, 0xc0800080, line);
drawRectangle(canvas, iconX, iconY, iconWidth, iconHeight, 0x80c00000, line);
}
}
if (key.hasPopupHint() && key.mMoreKeys != null && key.mMoreKeys.length > 0) {
drawKeyPopupHint(key, canvas, paint, params);
}
}
// Draw popup hint "..." at the bottom right corner of the key.
protected void drawKeyPopupHint(final Key key, final Canvas canvas, final Paint paint,
final KeyDrawParams params) {
final int keyWidth = key.getDrawWidth();
final int keyHeight = key.mHeight;
paint.setTypeface(params.mTypeface);
paint.setTextSize(params.mHintLetterSize);
paint.setColor(params.mHintLabelColor);
paint.setTextAlign(Align.CENTER);
final float hintX = keyWidth - mKeyHintLetterPadding
- getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
final float hintY = keyHeight - mKeyPopupHintLetterPadding;
canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint);
if (LatinImeLogger.sVISUALDEBUG) {
final Paint line = new Paint();
drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line);
drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line);
}
}
private static int getCharGeometryCacheKey(final char referenceChar, final Paint paint) {
final int labelSize = (int)paint.getTextSize();
final Typeface face = paint.getTypeface();
final int codePointOffset = referenceChar << 15;
if (face == Typeface.DEFAULT) {
return codePointOffset + labelSize;
} else if (face == Typeface.DEFAULT_BOLD) {
return codePointOffset + labelSize + 0x1000;
} else if (face == Typeface.MONOSPACE) {
return codePointOffset + labelSize + 0x2000;
} else {
return codePointOffset + labelSize;
}
}
// Working variable for the following methods.
private final Rect mTextBounds = new Rect();
private float getCharHeight(final char[] referenceChar, final Paint paint) {
final int key = getCharGeometryCacheKey(referenceChar[0], paint);
final Float cachedValue = sTextHeightCache.get(key);
if (cachedValue != null)
return cachedValue;
paint.getTextBounds(referenceChar, 0, 1, mTextBounds);
final float height = mTextBounds.height();
sTextHeightCache.put(key, height);
return height;
}
private float getCharWidth(final char[] referenceChar, final Paint paint) {
final int key = getCharGeometryCacheKey(referenceChar[0], paint);
final Float cachedValue = sTextWidthCache.get(key);
if (cachedValue != null)
return cachedValue;
paint.getTextBounds(referenceChar, 0, 1, mTextBounds);
final float width = mTextBounds.width();
sTextWidthCache.put(key, width);
return width;
}
// TODO: Remove this method.
public float getLabelWidth(final String label, final Paint paint) {
paint.getTextBounds(label, 0, label.length(), mTextBounds);
return mTextBounds.width();
}
protected static void drawIcon(final Canvas canvas, final Drawable icon, final int x,
final int y, final int width, final int height) {
canvas.translate(x, y);
icon.setBounds(0, 0, width, height);
icon.draw(canvas);
canvas.translate(-x, -y);
}
private static void drawHorizontalLine(final Canvas canvas, final float y, final float w,
final int color, final Paint paint) {
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(1.0f);
paint.setColor(color);
canvas.drawLine(0, y, w, y, paint);
}
private static void drawVerticalLine(final Canvas canvas, final float x, final float h,
final int color, final Paint paint) {
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(1.0f);
paint.setColor(color);
canvas.drawLine(x, 0, x, h, paint);
}
private static void drawRectangle(final Canvas canvas, final float x, final float y,
final float w, final float h, final int color, final Paint paint) {
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(1.0f);
paint.setColor(color);
canvas.translate(x, y);
canvas.drawRect(0, 0, w, h, paint);
canvas.translate(-x, -y);
}
public Paint newDefaultLabelPaint() {
final Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setTypeface(mKeyDrawParams.mTypeface);
paint.setTextSize(mKeyDrawParams.mLabelSize);
return paint;
}
public void cancelAllMessages() {
mDrawingHandler.cancelAllMessages();
}
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();
}
@Override
public void dismissKeyPreview(final PointerTracker tracker) {
mDrawingHandler.dismissKeyPreview(mDelayAfterPreview, tracker);
}
private void addKeyPreview(final TextView keyPreview) {
locatePreviewPlacerView();
mPreviewPlacerView.addView(
keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacerView, 0, 0));
}
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);
}
}
@Override
public void showSlidingKeyInputPreview(final PointerTracker tracker) {
locatePreviewPlacerView();
mPreviewPlacerView.showSlidingKeyInputPreview(tracker);
}
@Override
public void dismissSlidingKeyInputPreview() {
mPreviewPlacerView.dismissSlidingKeyInputPreview();
}
public void showGestureFloatingPreviewText(final String gestureFloatingPreviewText) {
locatePreviewPlacerView();
mPreviewPlacerView.setGestureFloatingPreviewText(gestureFloatingPreviewText);
}
public void dismissGestureFloatingPreviewText() {
locatePreviewPlacerView();
mPreviewPlacerView.dismissGestureFloatingPreviewText();
}
@Override
public void showGesturePreviewTrail(final PointerTracker tracker,
final boolean isOldestTracker) {
locatePreviewPlacerView();
mPreviewPlacerView.invalidatePointer(tracker, isOldestTracker);
}
@Override
public void showKeyPreview(final PointerTracker tracker) {
final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams;
if (!mShowKeyPreviewPopup) {
previewParams.mPreviewVisibleOffset = -mKeyboard.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.isShiftedLetterActivated() ? key.mHintLabel : key.mLabel;
// 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);
if (StringUtils.codePointCount(label) > 1) {
previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawParams.mLetterSize);
previewText.setTypeface(Typeface.DEFAULT_BOLD);
} else {
previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawParams.mPreviewTextSize);
previewText.setTypeface(key.selectTypeface(drawParams));
}
previewText.setText(label);
} else {
previewText.setCompoundDrawables(null, null, null,
key.getPreviewIcon(mKeyboard.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 = mPreviewHeight;
// 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 = mPreviewOffset - 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 + mPreviewOffset
+ 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);
}
/**
* 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(Key)
*/
public void invalidateAllKeys() {
mInvalidatedKeys.clear();
mInvalidateAllKeys = 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.
* @param key key in the attached {@link Keyboard}.
* @see #invalidateAllKeys
*/
@Override
public void invalidateKey(final Key key) {
if (mInvalidateAllKeys) return;
if (key == null) return;
mInvalidatedKeys.add(key);
final int x = key.mX + getPaddingLeft();
final int y = key.mY + getPaddingTop();
invalidate(x, y, x + key.mWidth, y + key.mHeight);
}
public void closing() {
dismissAllKeyPreviews();
cancelAllMessages();
mInvalidateAllKeys = true;
requestLayout();
}
@Override
public void onShowMoreKeysPanel(final MoreKeysPanel panel) {
if (isShowingMoreKeysPanel()) {
onDismissMoreKeysPanel();
}
mMoreKeysPanel = panel;
mPreviewPlacerView.addView(mMoreKeysPanel.getContainerView());
}
public boolean isShowingMoreKeysPanel() {
return (mMoreKeysPanel != null);
}
@Override
public boolean dismissMoreKeysPanel() {
if (isShowingMoreKeysPanel()) {
return mMoreKeysPanel.dismissMoreKeysPanel();
}
return false;
}
@Override
public boolean onDismissMoreKeysPanel() {
if (isShowingMoreKeysPanel()) {
mPreviewPlacerView.removeView(mMoreKeysPanel.getContainerView());
mMoreKeysPanel = null;
return true;
}
return false;
}
public void purgeKeyboardAndClosing() {
mKeyboard = null;
closing();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
closing();
mPreviewPlacerView.removeAllViews();
freeOffscreenBuffer();
}
}