/* * Copyright (C) 2009 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.pinyin; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Paint.FontMetricsInt; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Handler; import android.view.Gravity; import android.view.View; import android.view.View.MeasureSpec; import android.widget.PopupWindow; /** * Subclass of PopupWindow used as the feedback when user presses on a soft key * or a candidate. */ public class BalloonHint extends PopupWindow { /** * Delayed time to show the balloon hint. */ public static final int TIME_DELAY_SHOW = 0; /** * Delayed time to dismiss the balloon hint. */ public static final int TIME_DELAY_DISMISS = 200; /** * The padding information of the balloon. Because PopupWindow's background * can not be changed unless it is dismissed and shown again, we set the * real background drawable to the content view, and make the PopupWindow's * background transparent. So actually this padding information is for the * content view. */ private Rect mPaddingRect = new Rect(); /** * The context used to create this balloon hint object. */ private Context mContext; /** * Parent used to show the balloon window. */ private View mParent; /** * The content view of the balloon. */ BalloonView mBalloonView; /** * The measuring specification used to determine its size. Key-press * balloons and candidates balloons have different measuring specifications. */ private int mMeasureSpecMode; /** * Used to indicate whether the balloon needs to be dismissed forcibly. */ private boolean mForceDismiss; /** * Timer used to show/dismiss the balloon window with some time delay. */ private BalloonTimer mBalloonTimer; private int mParentLocationInWindow[] = new int[2]; public BalloonHint(Context context, View parent, int measureSpecMode) { super(context); mParent = parent; mMeasureSpecMode = measureSpecMode; setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); setTouchable(false); setBackgroundDrawable(new ColorDrawable(0)); mBalloonView = new BalloonView(context); mBalloonView.setClickable(false); setContentView(mBalloonView); mBalloonTimer = new BalloonTimer(); } public Context getContext() { return mContext; } public Rect getPadding() { return mPaddingRect; } public void setBalloonBackground(Drawable drawable) { // We usually pick up a background from a soft keyboard template, // and the object may has been set to this balloon before. if (mBalloonView.getBackground() == drawable) return; mBalloonView.setBackgroundDrawable(drawable); if (null != drawable) { drawable.getPadding(mPaddingRect); } else { mPaddingRect.set(0, 0, 0, 0); } } /** * Set configurations to show text label in this balloon. * * @param label The text label to show in the balloon. * @param textSize The text size used to show label. * @param textBold Used to indicate whether the label should be bold. * @param textColor The text color used to show label. * @param width The desired width of the balloon. The real width is * determined by the desired width and balloon's measuring * specification. * @param height The desired width of the balloon. The real width is * determined by the desired width and balloon's measuring * specification. */ public void setBalloonConfig(String label, float textSize, boolean textBold, int textColor, int width, int height) { mBalloonView.setTextConfig(label, textSize, textBold, textColor); setBalloonSize(width, height); } /** * Set configurations to show text label in this balloon. * * @param icon The icon used to shown in this balloon. * @param width The desired width of the balloon. The real width is * determined by the desired width and balloon's measuring * specification. * @param height The desired width of the balloon. The real width is * determined by the desired width and balloon's measuring * specification. */ public void setBalloonConfig(Drawable icon, int width, int height) { mBalloonView.setIcon(icon); setBalloonSize(width, height); } public boolean needForceDismiss() { return mForceDismiss; } public int getPaddingLeft() { return mPaddingRect.left; } public int getPaddingTop() { return mPaddingRect.top; } public int getPaddingRight() { return mPaddingRect.right; } public int getPaddingBottom() { return mPaddingRect.bottom; } public void delayedShow(long delay, int locationInParent[]) { if (mBalloonTimer.isPending()) { mBalloonTimer.removeTimer(); } if (delay <= 0) { mParent.getLocationInWindow(mParentLocationInWindow); showAtLocation(mParent, Gravity.LEFT | Gravity.TOP, locationInParent[0], locationInParent[1] + mParentLocationInWindow[1]); } else { mBalloonTimer.startTimer(delay, BalloonTimer.ACTION_SHOW, locationInParent, -1, -1); } } public void delayedUpdate(long delay, int locationInParent[], int width, int height) { mBalloonView.invalidate(); if (mBalloonTimer.isPending()) { mBalloonTimer.removeTimer(); } if (delay <= 0) { mParent.getLocationInWindow(mParentLocationInWindow); update(locationInParent[0], locationInParent[1] + mParentLocationInWindow[1], width, height); } else { mBalloonTimer.startTimer(delay, BalloonTimer.ACTION_UPDATE, locationInParent, width, height); } } public void delayedDismiss(long delay) { if (mBalloonTimer.isPending()) { mBalloonTimer.removeTimer(); int pendingAction = mBalloonTimer.getAction(); if (0 != delay && BalloonTimer.ACTION_HIDE != pendingAction) { mBalloonTimer.run(); } } if (delay <= 0) { dismiss(); } else { mBalloonTimer.startTimer(delay, BalloonTimer.ACTION_HIDE, null, -1, -1); } } public void removeTimer() { if (mBalloonTimer.isPending()) { mBalloonTimer.removeTimer(); } } private void setBalloonSize(int width, int height) { int widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, mMeasureSpecMode); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, mMeasureSpecMode); mBalloonView.measure(widthMeasureSpec, heightMeasureSpec); int oldWidth = getWidth(); int oldHeight = getHeight(); int newWidth = mBalloonView.getMeasuredWidth() + getPaddingLeft() + getPaddingRight(); int newHeight = mBalloonView.getMeasuredHeight() + getPaddingTop() + getPaddingBottom(); setWidth(newWidth); setHeight(newHeight); // If update() is called to update both size and position, the system // will first MOVE the PopupWindow to the new position, and then // perform a size-updating operation, so there will be a flash in // PopupWindow if user presses a key and moves finger to next one whose // size is different. // PopupWindow will handle the updating issue in one go in the future, // but before that, if we find the size is changed, a mandatory dismiss // operation is required. In our UI design, normal QWERTY keys' width // can be different in 1-pixel, and we do not dismiss the balloon when // user move between QWERTY keys. mForceDismiss = false; if (isShowing()) { mForceDismiss = oldWidth - newWidth > 1 || newWidth - oldWidth > 1; } } private class BalloonTimer extends Handler implements Runnable { public static final int ACTION_SHOW = 1; public static final int ACTION_HIDE = 2; public static final int ACTION_UPDATE = 3; /** * The pending action. */ private int mAction; private int mPositionInParent[] = new int[2]; private int mWidth; private int mHeight; private boolean mTimerPending = false; public void startTimer(long time, int action, int positionInParent[], int width, int height) { mAction = action; if (ACTION_HIDE != action) { mPositionInParent[0] = positionInParent[0]; mPositionInParent[1] = positionInParent[1]; } mWidth = width; mHeight = height; postDelayed(this, time); mTimerPending = true; } public boolean isPending() { return mTimerPending; } public boolean removeTimer() { if (mTimerPending) { mTimerPending = false; removeCallbacks(this); return true; } return false; } public int getAction() { return mAction; } public void run() { switch (mAction) { case ACTION_SHOW: mParent.getLocationInWindow(mParentLocationInWindow); showAtLocation(mParent, Gravity.LEFT | Gravity.TOP, mPositionInParent[0], mPositionInParent[1] + mParentLocationInWindow[1]); break; case ACTION_HIDE: dismiss(); break; case ACTION_UPDATE: mParent.getLocationInWindow(mParentLocationInWindow); update(mPositionInParent[0], mPositionInParent[1] + mParentLocationInWindow[1], mWidth, mHeight); } mTimerPending = false; } } private class BalloonView extends View { /** * Suspension points used to display long items. */ private static final String SUSPENSION_POINTS = "..."; /** * The icon to be shown. If it is not null, {@link #mLabel} will be * ignored. */ private Drawable mIcon; /** * The label to be shown. It is enabled only if {@link #mIcon} is null. */ private String mLabel; private int mLabeColor = 0xff000000; private Paint mPaintLabel; private FontMetricsInt mFmi; /** * The width to show suspension points. */ private float mSuspensionPointsWidth; public BalloonView(Context context) { super(context); mPaintLabel = new Paint(); mPaintLabel.setColor(mLabeColor); mPaintLabel.setAntiAlias(true); mPaintLabel.setFakeBoldText(true); mFmi = mPaintLabel.getFontMetricsInt(); } public void setIcon(Drawable icon) { mIcon = icon; } public void setTextConfig(String label, float fontSize, boolean textBold, int textColor) { // Icon should be cleared so that the label will be enabled. mIcon = null; mLabel = label; mPaintLabel.setTextSize(fontSize); mPaintLabel.setFakeBoldText(textBold); mPaintLabel.setColor(textColor); mFmi = mPaintLabel.getFontMetricsInt(); mSuspensionPointsWidth = mPaintLabel.measureText(SUSPENSION_POINTS); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (widthMode == MeasureSpec.EXACTLY) { setMeasuredDimension(widthSize, heightSize); return; } int measuredWidth = mPaddingLeft + mPaddingRight; int measuredHeight = mPaddingTop + mPaddingBottom; if (null != mIcon) { measuredWidth += mIcon.getIntrinsicWidth(); measuredHeight += mIcon.getIntrinsicHeight(); } else if (null != mLabel) { measuredWidth += (int) (mPaintLabel.measureText(mLabel)); measuredHeight += mFmi.bottom - mFmi.top; } if (widthSize > measuredWidth || widthMode == MeasureSpec.AT_MOST) { measuredWidth = widthSize; } if (heightSize > measuredHeight || heightMode == MeasureSpec.AT_MOST) { measuredHeight = heightSize; } int maxWidth = Environment.getInstance().getScreenWidth() - mPaddingLeft - mPaddingRight; if (measuredWidth > maxWidth) { measuredWidth = maxWidth; } setMeasuredDimension(measuredWidth, measuredHeight); } @Override protected void onDraw(Canvas canvas) { int width = getWidth(); int height = getHeight(); if (null != mIcon) { int marginLeft = (width - mIcon.getIntrinsicWidth()) / 2; int marginRight = width - mIcon.getIntrinsicWidth() - marginLeft; int marginTop = (height - mIcon.getIntrinsicHeight()) / 2; int marginBottom = height - mIcon.getIntrinsicHeight() - marginTop; mIcon.setBounds(marginLeft, marginTop, width - marginRight, height - marginBottom); mIcon.draw(canvas); } else if (null != mLabel) { float labelMeasuredWidth = mPaintLabel.measureText(mLabel); float x = mPaddingLeft; x += (width - labelMeasuredWidth - mPaddingLeft - mPaddingRight) / 2.0f; String labelToDraw = mLabel; if (x < mPaddingLeft) { x = mPaddingLeft; labelToDraw = getLimitedLabelForDrawing(mLabel, width - mPaddingLeft - mPaddingRight); } int fontHeight = mFmi.bottom - mFmi.top; float marginY = (height - fontHeight) / 2.0f; float y = marginY - mFmi.top; canvas.drawText(labelToDraw, x, y, mPaintLabel); } } private String getLimitedLabelForDrawing(String rawLabel, float widthToDraw) { int subLen = rawLabel.length(); if (subLen <= 1) return rawLabel; do { subLen--; float width = mPaintLabel.measureText(rawLabel, 0, subLen); if (width + mSuspensionPointsWidth <= widthToDraw || 1 >= subLen) { return rawLabel.substring(0, subLen) + SUSPENSION_POINTS; } } while (true); } } }