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