Home | History | Annotate | Download | only in pinyin
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.inputmethod.pinyin;
     18 
     19 import android.content.Context;
     20 import android.graphics.Canvas;
     21 import android.graphics.Paint;
     22 import android.graphics.Rect;
     23 import android.graphics.Paint.FontMetricsInt;
     24 import android.graphics.drawable.ColorDrawable;
     25 import android.graphics.drawable.Drawable;
     26 import android.os.Handler;
     27 import android.view.Gravity;
     28 import android.view.View;
     29 import android.view.View.MeasureSpec;
     30 import android.widget.PopupWindow;
     31 
     32 /**
     33  * Subclass of PopupWindow used as the feedback when user presses on a soft key
     34  * or a candidate.
     35  */
     36 public class BalloonHint extends PopupWindow {
     37     /**
     38      * Delayed time to show the balloon hint.
     39      */
     40     public static final int TIME_DELAY_SHOW = 0;
     41 
     42     /**
     43      * Delayed time to dismiss the balloon hint.
     44      */
     45     public static final int TIME_DELAY_DISMISS = 200;
     46 
     47     /**
     48      * The padding information of the balloon. Because PopupWindow's background
     49      * can not be changed unless it is dismissed and shown again, we set the
     50      * real background drawable to the content view, and make the PopupWindow's
     51      * background transparent. So actually this padding information is for the
     52      * content view.
     53      */
     54     private Rect mPaddingRect = new Rect();
     55 
     56     /**
     57      * The context used to create this balloon hint object.
     58      */
     59     private Context mContext;
     60 
     61     /**
     62      * Parent used to show the balloon window.
     63      */
     64     private View mParent;
     65 
     66     /**
     67      * The content view of the balloon.
     68      */
     69     BalloonView mBalloonView;
     70 
     71     /**
     72      * The measuring specification used to determine its size. Key-press
     73      * balloons and candidates balloons have different measuring specifications.
     74      */
     75     private int mMeasureSpecMode;
     76 
     77     /**
     78      * Used to indicate whether the balloon needs to be dismissed forcibly.
     79      */
     80     private boolean mForceDismiss;
     81 
     82     /**
     83      * Timer used to show/dismiss the balloon window with some time delay.
     84      */
     85     private BalloonTimer mBalloonTimer;
     86 
     87     private int mParentLocationInWindow[] = new int[2];
     88 
     89     public BalloonHint(Context context, View parent, int measureSpecMode) {
     90         super(context);
     91         mParent = parent;
     92         mMeasureSpecMode = measureSpecMode;
     93 
     94         setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
     95         setTouchable(false);
     96         setBackgroundDrawable(new ColorDrawable(0));
     97 
     98         mBalloonView = new BalloonView(context);
     99         mBalloonView.setClickable(false);
    100         setContentView(mBalloonView);
    101 
    102         mBalloonTimer = new BalloonTimer();
    103     }
    104 
    105     public Context getContext() {
    106         return mContext;
    107     }
    108 
    109     public Rect getPadding() {
    110         return mPaddingRect;
    111     }
    112 
    113     public void setBalloonBackground(Drawable drawable) {
    114         // We usually pick up a background from a soft keyboard template,
    115         // and the object may has been set to this balloon before.
    116         if (mBalloonView.getBackground() == drawable) return;
    117         mBalloonView.setBackgroundDrawable(drawable);
    118 
    119         if (null != drawable) {
    120             drawable.getPadding(mPaddingRect);
    121         } else {
    122             mPaddingRect.set(0, 0, 0, 0);
    123         }
    124     }
    125 
    126     /**
    127      * Set configurations to show text label in this balloon.
    128      *
    129      * @param label The text label to show in the balloon.
    130      * @param textSize The text size used to show label.
    131      * @param textBold Used to indicate whether the label should be bold.
    132      * @param textColor The text color used to show label.
    133      * @param width The desired width of the balloon. The real width is
    134      *        determined by the desired width and balloon's measuring
    135      *        specification.
    136      * @param height The desired width of the balloon. The real width is
    137      *        determined by the desired width and balloon's measuring
    138      *        specification.
    139      */
    140     public void setBalloonConfig(String label, float textSize,
    141             boolean textBold, int textColor, int width, int height) {
    142         mBalloonView.setTextConfig(label, textSize, textBold, textColor);
    143         setBalloonSize(width, height);
    144     }
    145 
    146     /**
    147      * Set configurations to show text label in this balloon.
    148      *
    149      * @param icon The icon used to shown in this balloon.
    150      * @param width The desired width of the balloon. The real width is
    151      *        determined by the desired width and balloon's measuring
    152      *        specification.
    153      * @param height The desired width of the balloon. The real width is
    154      *        determined by the desired width and balloon's measuring
    155      *        specification.
    156      */
    157     public void setBalloonConfig(Drawable icon, int width, int height) {
    158         mBalloonView.setIcon(icon);
    159         setBalloonSize(width, height);
    160     }
    161 
    162 
    163     public boolean needForceDismiss() {
    164         return mForceDismiss;
    165     }
    166 
    167     public int getPaddingLeft() {
    168         return mPaddingRect.left;
    169     }
    170 
    171     public int getPaddingTop() {
    172         return mPaddingRect.top;
    173     }
    174 
    175     public int getPaddingRight() {
    176         return mPaddingRect.right;
    177     }
    178 
    179     public int getPaddingBottom() {
    180         return mPaddingRect.bottom;
    181     }
    182 
    183     public void delayedShow(long delay, int locationInParent[]) {
    184         if (mBalloonTimer.isPending()) {
    185             mBalloonTimer.removeTimer();
    186         }
    187         if (delay <= 0) {
    188             mParent.getLocationInWindow(mParentLocationInWindow);
    189             showAtLocation(mParent, Gravity.LEFT | Gravity.TOP,
    190                     locationInParent[0], locationInParent[1]
    191                             + mParentLocationInWindow[1]);
    192         } else {
    193             mBalloonTimer.startTimer(delay, BalloonTimer.ACTION_SHOW,
    194                     locationInParent, -1, -1);
    195         }
    196     }
    197 
    198     public void delayedUpdate(long delay, int locationInParent[],
    199             int width, int height) {
    200         mBalloonView.invalidate();
    201         if (mBalloonTimer.isPending()) {
    202             mBalloonTimer.removeTimer();
    203         }
    204         if (delay <= 0) {
    205             mParent.getLocationInWindow(mParentLocationInWindow);
    206             update(locationInParent[0], locationInParent[1]
    207                     + mParentLocationInWindow[1], width, height);
    208         } else {
    209             mBalloonTimer.startTimer(delay, BalloonTimer.ACTION_UPDATE,
    210                     locationInParent, width, height);
    211         }
    212     }
    213 
    214     public void delayedDismiss(long delay) {
    215         if (mBalloonTimer.isPending()) {
    216             mBalloonTimer.removeTimer();
    217             int pendingAction = mBalloonTimer.getAction();
    218             if (0 != delay && BalloonTimer.ACTION_HIDE != pendingAction) {
    219                 mBalloonTimer.run();
    220             }
    221         }
    222         if (delay <= 0) {
    223             dismiss();
    224         } else {
    225             mBalloonTimer.startTimer(delay, BalloonTimer.ACTION_HIDE, null, -1,
    226                     -1);
    227         }
    228     }
    229 
    230     public void removeTimer() {
    231         if (mBalloonTimer.isPending()) {
    232             mBalloonTimer.removeTimer();
    233         }
    234     }
    235 
    236     private void setBalloonSize(int width, int height) {
    237         int widthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
    238                 mMeasureSpecMode);
    239         int heightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
    240                 mMeasureSpecMode);
    241         mBalloonView.measure(widthMeasureSpec, heightMeasureSpec);
    242 
    243         int oldWidth = getWidth();
    244         int oldHeight = getHeight();
    245         int newWidth = mBalloonView.getMeasuredWidth() + getPaddingLeft()
    246                 + getPaddingRight();
    247         int newHeight = mBalloonView.getMeasuredHeight() + getPaddingTop()
    248                 + getPaddingBottom();
    249         setWidth(newWidth);
    250         setHeight(newHeight);
    251 
    252         // If update() is called to update both size and position, the system
    253         // will first MOVE the PopupWindow to the new position, and then
    254         // perform a size-updating operation, so there will be a flash in
    255         // PopupWindow if user presses a key and moves finger to next one whose
    256         // size is different.
    257         // PopupWindow will handle the updating issue in one go in the future,
    258         // but before that, if we find the size is changed, a mandatory dismiss
    259         // operation is required. In our UI design, normal QWERTY keys' width
    260         // can be different in 1-pixel, and we do not dismiss the balloon when
    261         // user move between QWERTY keys.
    262         mForceDismiss = false;
    263         if (isShowing()) {
    264             mForceDismiss = oldWidth - newWidth > 1 || newWidth - oldWidth > 1;
    265         }
    266     }
    267 
    268 
    269     private class BalloonTimer extends Handler implements Runnable {
    270         public static final int ACTION_SHOW = 1;
    271         public static final int ACTION_HIDE = 2;
    272         public static final int ACTION_UPDATE = 3;
    273 
    274         /**
    275          * The pending action.
    276          */
    277         private int mAction;
    278 
    279         private int mPositionInParent[] = new int[2];
    280         private int mWidth;
    281         private int mHeight;
    282 
    283         private boolean mTimerPending = false;
    284 
    285         public void startTimer(long time, int action, int positionInParent[],
    286                 int width, int height) {
    287             mAction = action;
    288             if (ACTION_HIDE != action) {
    289                 mPositionInParent[0] = positionInParent[0];
    290                 mPositionInParent[1] = positionInParent[1];
    291             }
    292             mWidth = width;
    293             mHeight = height;
    294             postDelayed(this, time);
    295             mTimerPending = true;
    296         }
    297 
    298         public boolean isPending() {
    299             return mTimerPending;
    300         }
    301 
    302         public boolean removeTimer() {
    303             if (mTimerPending) {
    304                 mTimerPending = false;
    305                 removeCallbacks(this);
    306                 return true;
    307             }
    308 
    309             return false;
    310         }
    311 
    312         public int getAction() {
    313             return mAction;
    314         }
    315 
    316         public void run() {
    317             switch (mAction) {
    318             case ACTION_SHOW:
    319                 mParent.getLocationInWindow(mParentLocationInWindow);
    320                 showAtLocation(mParent, Gravity.LEFT | Gravity.TOP,
    321                         mPositionInParent[0], mPositionInParent[1]
    322                                 + mParentLocationInWindow[1]);
    323                 break;
    324             case ACTION_HIDE:
    325                 dismiss();
    326                 break;
    327             case ACTION_UPDATE:
    328                 mParent.getLocationInWindow(mParentLocationInWindow);
    329                 update(mPositionInParent[0], mPositionInParent[1]
    330                         + mParentLocationInWindow[1], mWidth, mHeight);
    331             }
    332             mTimerPending = false;
    333         }
    334     }
    335 
    336     private class BalloonView extends View {
    337         /**
    338          * Suspension points used to display long items.
    339          */
    340         private static final String SUSPENSION_POINTS = "...";
    341 
    342         /**
    343          * The icon to be shown. If it is not null, {@link #mLabel} will be
    344          * ignored.
    345          */
    346         private Drawable mIcon;
    347 
    348         /**
    349          * The label to be shown. It is enabled only if {@link #mIcon} is null.
    350          */
    351         private String mLabel;
    352 
    353         private int mLabeColor = 0xff000000;
    354         private Paint mPaintLabel;
    355         private FontMetricsInt mFmi;
    356 
    357         /**
    358          * The width to show suspension points.
    359          */
    360         private float mSuspensionPointsWidth;
    361 
    362 
    363         public BalloonView(Context context) {
    364             super(context);
    365             mPaintLabel = new Paint();
    366             mPaintLabel.setColor(mLabeColor);
    367             mPaintLabel.setAntiAlias(true);
    368             mPaintLabel.setFakeBoldText(true);
    369             mFmi = mPaintLabel.getFontMetricsInt();
    370         }
    371 
    372         public void setIcon(Drawable icon) {
    373             mIcon = icon;
    374         }
    375 
    376         public void setTextConfig(String label, float fontSize,
    377                 boolean textBold, int textColor) {
    378             // Icon should be cleared so that the label will be enabled.
    379             mIcon = null;
    380             mLabel = label;
    381             mPaintLabel.setTextSize(fontSize);
    382             mPaintLabel.setFakeBoldText(textBold);
    383             mPaintLabel.setColor(textColor);
    384             mFmi = mPaintLabel.getFontMetricsInt();
    385             mSuspensionPointsWidth = mPaintLabel.measureText(SUSPENSION_POINTS);
    386         }
    387 
    388         @Override
    389         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    390             final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    391             final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    392             final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    393             final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    394 
    395             if (widthMode == MeasureSpec.EXACTLY) {
    396                 setMeasuredDimension(widthSize, heightSize);
    397                 return;
    398             }
    399 
    400             int measuredWidth = mPaddingLeft + mPaddingRight;
    401             int measuredHeight = mPaddingTop + mPaddingBottom;
    402             if (null != mIcon) {
    403                 measuredWidth += mIcon.getIntrinsicWidth();
    404                 measuredHeight += mIcon.getIntrinsicHeight();
    405             } else if (null != mLabel) {
    406                 measuredWidth += (int) (mPaintLabel.measureText(mLabel));
    407                 measuredHeight += mFmi.bottom - mFmi.top;
    408             }
    409             if (widthSize > measuredWidth || widthMode == MeasureSpec.AT_MOST) {
    410                 measuredWidth = widthSize;
    411             }
    412 
    413             if (heightSize > measuredHeight
    414                     || heightMode == MeasureSpec.AT_MOST) {
    415                 measuredHeight = heightSize;
    416             }
    417 
    418             int maxWidth = Environment.getInstance().getScreenWidth() -
    419                     mPaddingLeft - mPaddingRight;
    420             if (measuredWidth > maxWidth) {
    421                 measuredWidth = maxWidth;
    422             }
    423             setMeasuredDimension(measuredWidth, measuredHeight);
    424         }
    425 
    426         @Override
    427         protected void onDraw(Canvas canvas) {
    428             int width = getWidth();
    429             int height = getHeight();
    430             if (null != mIcon) {
    431                 int marginLeft = (width - mIcon.getIntrinsicWidth()) / 2;
    432                 int marginRight = width - mIcon.getIntrinsicWidth()
    433                         - marginLeft;
    434                 int marginTop = (height - mIcon.getIntrinsicHeight()) / 2;
    435                 int marginBottom = height - mIcon.getIntrinsicHeight()
    436                         - marginTop;
    437                 mIcon.setBounds(marginLeft, marginTop, width - marginRight,
    438                         height - marginBottom);
    439                 mIcon.draw(canvas);
    440             } else if (null != mLabel) {
    441                 float labelMeasuredWidth = mPaintLabel.measureText(mLabel);
    442                 float x = mPaddingLeft;
    443                 x += (width - labelMeasuredWidth - mPaddingLeft - mPaddingRight) / 2.0f;
    444                 String labelToDraw = mLabel;
    445                 if (x < mPaddingLeft) {
    446                     x = mPaddingLeft;
    447                     labelToDraw = getLimitedLabelForDrawing(mLabel,
    448                             width - mPaddingLeft - mPaddingRight);
    449                 }
    450 
    451                 int fontHeight = mFmi.bottom - mFmi.top;
    452                 float marginY = (height - fontHeight) / 2.0f;
    453                 float y = marginY - mFmi.top;
    454                 canvas.drawText(labelToDraw, x, y, mPaintLabel);
    455             }
    456         }
    457 
    458         private String getLimitedLabelForDrawing(String rawLabel,
    459                 float widthToDraw) {
    460             int subLen = rawLabel.length();
    461             if (subLen <= 1) return rawLabel;
    462             do {
    463                 subLen--;
    464                 float width = mPaintLabel.measureText(rawLabel, 0, subLen);
    465                 if (width + mSuspensionPointsWidth <= widthToDraw || 1 >= subLen) {
    466                     return rawLabel.substring(0, subLen) +
    467                             SUSPENSION_POINTS;
    468                 }
    469             } while (true);
    470         }
    471     }
    472 }
    473