| /* |
| * 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); |
| } |
| } |
| } |