/* * 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.SuggestedWords; 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 mKeyPreviewOffset; private final int mKeyPreviewHeight; private final SparseArray mKeyPreviewTexts = CollectionUtils.newSparseArray(); protected final KeyPreviewDrawParams mKeyPreviewDrawParams = new KeyPreviewDrawParams(); private boolean mShowKeyPreviewPopup = true; private int mKeyPreviewLingerTimeout; // Gesture floating preview text // TODO: Make this parameter customizable by user via settings. private int mGestureFloatingPreviewTextLingerTimeout; // 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 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 sTextHeightCache = CollectionUtils.newSparseArray(); // This sparse array caches key label text width in pixel indexed by key label text size. private static final SparseArray 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 { private static final int MSG_DISMISS_KEY_PREVIEW = 0; private static final int MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; 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; case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT: keyboardView.mPreviewPlacerView.setGestureFloatingPreviewText(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 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); mKeyPreviewOffset = keyboardViewAttr.getDimensionPixelOffset( R.styleable.KeyboardView_keyPreviewOffset, 0); mKeyPreviewHeight = keyboardViewAttr.getDimensionPixelSize( R.styleable.KeyboardView_keyPreviewHeight, 80); mKeyPreviewLingerTimeout = keyboardViewAttr.getInt( R.styleable.KeyboardView_keyPreviewLingerTimeout, 0); 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); mGestureFloatingPreviewTextLingerTimeout = keyboardViewAttr.getInt( R.styleable.KeyboardView_gestureFloatingPreviewTextLingerTimeout, 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; mKeyPreviewLingerTimeout = 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(mKeyPreviewLingerTimeout, 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 SuggestedWords suggestedWords) { locatePreviewPlacerView(); mPreviewPlacerView.setGestureFloatingPreviewText(suggestedWords); } public void dismissGestureFloatingPreviewText() { locatePreviewPlacerView(); mDrawingHandler.dismissGestureFloatingPreviewText(mGestureFloatingPreviewTextLingerTimeout); } @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 = 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); } /** * 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(); onCancelMoreKeysPanel(); 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 void onCancelMoreKeysPanel() { if (isShowingMoreKeysPanel()) { mMoreKeysPanel.dismissMoreKeysPanel(); } } @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(); } }