diff options
Diffstat (limited to 'src/com/android/inputmethod/pinyin/BalloonHint.java')
-rw-r--r-- | src/com/android/inputmethod/pinyin/BalloonHint.java | 472 |
1 files changed, 472 insertions, 0 deletions
diff --git a/src/com/android/inputmethod/pinyin/BalloonHint.java b/src/com/android/inputmethod/pinyin/BalloonHint.java new file mode 100644 index 0000000..919dbf1 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/BalloonHint.java @@ -0,0 +1,472 @@ +/* + * 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); + } + } +} |