• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
BalloonHint(Context context, View parent, int measureSpecMode)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 
getContext()105     public Context getContext() {
106         return mContext;
107     }
108 
getPadding()109     public Rect getPadding() {
110         return mPaddingRect;
111     }
112 
setBalloonBackground(Drawable drawable)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      */
setBalloonConfig(String label, float textSize, boolean textBold, int textColor, int width, int height)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      */
setBalloonConfig(Drawable icon, int width, int height)157     public void setBalloonConfig(Drawable icon, int width, int height) {
158         mBalloonView.setIcon(icon);
159         setBalloonSize(width, height);
160     }
161 
162 
needForceDismiss()163     public boolean needForceDismiss() {
164         return mForceDismiss;
165     }
166 
getPaddingLeft()167     public int getPaddingLeft() {
168         return mPaddingRect.left;
169     }
170 
getPaddingTop()171     public int getPaddingTop() {
172         return mPaddingRect.top;
173     }
174 
getPaddingRight()175     public int getPaddingRight() {
176         return mPaddingRect.right;
177     }
178 
getPaddingBottom()179     public int getPaddingBottom() {
180         return mPaddingRect.bottom;
181     }
182 
delayedShow(long delay, int locationInParent[])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 
delayedUpdate(long delay, int locationInParent[], int width, int height)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 
delayedDismiss(long delay)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 
removeTimer()230     public void removeTimer() {
231         if (mBalloonTimer.isPending()) {
232             mBalloonTimer.removeTimer();
233         }
234     }
235 
setBalloonSize(int width, int height)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 
startTimer(long time, int action, int positionInParent[], int width, int height)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 
isPending()298         public boolean isPending() {
299             return mTimerPending;
300         }
301 
removeTimer()302         public boolean removeTimer() {
303             if (mTimerPending) {
304                 mTimerPending = false;
305                 removeCallbacks(this);
306                 return true;
307             }
308 
309             return false;
310         }
311 
getAction()312         public int getAction() {
313             return mAction;
314         }
315 
run()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 
BalloonView(Context context)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 
setIcon(Drawable icon)372         public void setIcon(Drawable icon) {
373             mIcon = icon;
374         }
375 
setTextConfig(String label, float fontSize, boolean textBold, int textColor)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onDraw(Canvas canvas)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 
getLimitedLabelForDrawing(String rawLabel, float widthToDraw)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