1 /* 2 * Copyright (C) 2008 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.launcher3; 18 19 import android.animation.ObjectAnimator; 20 import android.content.Context; 21 import android.content.res.ColorStateList; 22 import android.content.res.TypedArray; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.Paint; 26 import android.graphics.Point; 27 import android.graphics.Rect; 28 import android.graphics.drawable.ColorDrawable; 29 import android.graphics.drawable.Drawable; 30 import android.support.v4.graphics.ColorUtils; 31 import android.text.TextUtils.TruncateAt; 32 import android.util.AttributeSet; 33 import android.util.Property; 34 import android.util.TypedValue; 35 import android.view.KeyEvent; 36 import android.view.MotionEvent; 37 import android.view.View; 38 import android.view.ViewConfiguration; 39 import android.view.ViewDebug; 40 import android.widget.TextView; 41 42 import com.android.launcher3.IconCache.IconLoadRequest; 43 import com.android.launcher3.IconCache.ItemInfoUpdateReceiver; 44 import com.android.launcher3.Launcher.OnResumeCallback; 45 import com.android.launcher3.badge.BadgeInfo; 46 import com.android.launcher3.badge.BadgeRenderer; 47 import com.android.launcher3.folder.FolderIcon; 48 import com.android.launcher3.graphics.DrawableFactory; 49 import com.android.launcher3.graphics.IconPalette; 50 import com.android.launcher3.graphics.PreloadIconDrawable; 51 import com.android.launcher3.model.PackageItemInfo; 52 53 import java.text.NumberFormat; 54 55 /** 56 * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan 57 * because we want to make the bubble taller than the text and TextView's clip is 58 * too aggressive. 59 */ 60 public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, OnResumeCallback { 61 62 private static final int DISPLAY_WORKSPACE = 0; 63 private static final int DISPLAY_ALL_APPS = 1; 64 private static final int DISPLAY_FOLDER = 2; 65 66 private static final int[] STATE_PRESSED = new int[] {android.R.attr.state_pressed}; 67 68 69 private static final Property<BubbleTextView, Float> BADGE_SCALE_PROPERTY 70 = new Property<BubbleTextView, Float>(Float.TYPE, "badgeScale") { 71 @Override 72 public Float get(BubbleTextView bubbleTextView) { 73 return bubbleTextView.mBadgeScale; 74 } 75 76 @Override 77 public void set(BubbleTextView bubbleTextView, Float value) { 78 bubbleTextView.mBadgeScale = value; 79 bubbleTextView.invalidate(); 80 } 81 }; 82 83 public static final Property<BubbleTextView, Float> TEXT_ALPHA_PROPERTY 84 = new Property<BubbleTextView, Float>(Float.class, "textAlpha") { 85 @Override 86 public Float get(BubbleTextView bubbleTextView) { 87 return bubbleTextView.mTextAlpha; 88 } 89 90 @Override 91 public void set(BubbleTextView bubbleTextView, Float alpha) { 92 bubbleTextView.setTextAlpha(alpha); 93 } 94 }; 95 96 private final BaseDraggingActivity mActivity; 97 private Drawable mIcon; 98 private final boolean mCenterVertically; 99 100 private final CheckLongPressHelper mLongPressHelper; 101 private final StylusEventHelper mStylusEventHelper; 102 private final float mSlop; 103 104 private final boolean mLayoutHorizontal; 105 private final int mIconSize; 106 107 @ViewDebug.ExportedProperty(category = "launcher") 108 private boolean mIsIconVisible = true; 109 @ViewDebug.ExportedProperty(category = "launcher") 110 private int mTextColor; 111 @ViewDebug.ExportedProperty(category = "launcher") 112 private float mTextAlpha = 1; 113 114 private BadgeInfo mBadgeInfo; 115 private BadgeRenderer mBadgeRenderer; 116 private int mBadgeColor; 117 private float mBadgeScale; 118 private boolean mForceHideBadge; 119 private Point mTempSpaceForBadgeOffset = new Point(); 120 private Rect mTempIconBounds = new Rect(); 121 122 @ViewDebug.ExportedProperty(category = "launcher") 123 private boolean mStayPressed; 124 @ViewDebug.ExportedProperty(category = "launcher") 125 private boolean mIgnorePressedStateChange; 126 @ViewDebug.ExportedProperty(category = "launcher") 127 private boolean mDisableRelayout = false; 128 129 private IconLoadRequest mIconLoadRequest; 130 131 public BubbleTextView(Context context) { 132 this(context, null, 0); 133 } 134 135 public BubbleTextView(Context context, AttributeSet attrs) { 136 this(context, attrs, 0); 137 } 138 139 public BubbleTextView(Context context, AttributeSet attrs, int defStyle) { 140 super(context, attrs, defStyle); 141 mActivity = BaseDraggingActivity.fromContext(context); 142 DeviceProfile grid = mActivity.getDeviceProfile(); 143 mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 144 145 TypedArray a = context.obtainStyledAttributes(attrs, 146 R.styleable.BubbleTextView, defStyle, 0); 147 mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false); 148 149 int display = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE); 150 int defaultIconSize = grid.iconSizePx; 151 if (display == DISPLAY_WORKSPACE) { 152 setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); 153 setCompoundDrawablePadding(grid.iconDrawablePaddingPx); 154 } else if (display == DISPLAY_ALL_APPS) { 155 setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx); 156 setCompoundDrawablePadding(grid.allAppsIconDrawablePaddingPx); 157 defaultIconSize = grid.allAppsIconSizePx; 158 } else if (display == DISPLAY_FOLDER) { 159 setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.folderChildTextSizePx); 160 setCompoundDrawablePadding(grid.folderChildDrawablePaddingPx); 161 defaultIconSize = grid.folderChildIconSizePx; 162 } 163 mCenterVertically = a.getBoolean(R.styleable.BubbleTextView_centerVertically, false); 164 165 mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride, 166 defaultIconSize); 167 a.recycle(); 168 169 mLongPressHelper = new CheckLongPressHelper(this); 170 mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this); 171 172 setEllipsize(TruncateAt.END); 173 setAccessibilityDelegate(mActivity.getAccessibilityDelegate()); 174 setTextAlpha(1f); 175 } 176 177 @Override 178 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 179 // Disable marques when not focused to that, so that updating text does not cause relayout. 180 setEllipsize(focused ? TruncateAt.MARQUEE : TruncateAt.END); 181 super.onFocusChanged(focused, direction, previouslyFocusedRect); 182 } 183 184 /** 185 * Resets the view so it can be recycled. 186 */ 187 public void reset() { 188 mBadgeInfo = null; 189 mBadgeColor = Color.TRANSPARENT; 190 mBadgeScale = 0f; 191 mForceHideBadge = false; 192 } 193 194 public void applyFromShortcutInfo(ShortcutInfo info) { 195 applyFromShortcutInfo(info, false); 196 } 197 198 public void applyFromShortcutInfo(ShortcutInfo info, boolean promiseStateChanged) { 199 applyIconAndLabel(info); 200 setTag(info); 201 if (promiseStateChanged || (info.hasPromiseIconUi())) { 202 applyPromiseState(promiseStateChanged); 203 } 204 205 applyBadgeState(info, false /* animate */); 206 } 207 208 public void applyFromApplicationInfo(AppInfo info) { 209 applyIconAndLabel(info); 210 211 // We don't need to check the info since it's not a ShortcutInfo 212 super.setTag(info); 213 214 // Verify high res immediately 215 verifyHighRes(); 216 217 if (info instanceof PromiseAppInfo) { 218 PromiseAppInfo promiseAppInfo = (PromiseAppInfo) info; 219 applyProgressLevel(promiseAppInfo.level); 220 } 221 applyBadgeState(info, false /* animate */); 222 } 223 224 public void applyFromPackageItemInfo(PackageItemInfo info) { 225 applyIconAndLabel(info); 226 // We don't need to check the info since it's not a ShortcutInfo 227 super.setTag(info); 228 229 // Verify high res immediately 230 verifyHighRes(); 231 } 232 233 private void applyIconAndLabel(ItemInfoWithIcon info) { 234 FastBitmapDrawable iconDrawable = DrawableFactory.get(getContext()).newIcon(info); 235 mBadgeColor = IconPalette.getMutedColor(info.iconColor, 0.54f); 236 237 setIcon(iconDrawable); 238 setText(info.title); 239 if (info.contentDescription != null) { 240 setContentDescription(info.isDisabled() 241 ? getContext().getString(R.string.disabled_app_label, info.contentDescription) 242 : info.contentDescription); 243 } 244 } 245 246 /** 247 * Overrides the default long press timeout. 248 */ 249 public void setLongPressTimeout(int longPressTimeout) { 250 mLongPressHelper.setLongPressTimeout(longPressTimeout); 251 } 252 253 @Override 254 public void setTag(Object tag) { 255 if (tag != null) { 256 LauncherModel.checkItemInfo((ItemInfo) tag); 257 } 258 super.setTag(tag); 259 } 260 261 @Override 262 public void refreshDrawableState() { 263 if (!mIgnorePressedStateChange) { 264 super.refreshDrawableState(); 265 } 266 } 267 268 @Override 269 protected int[] onCreateDrawableState(int extraSpace) { 270 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 271 if (mStayPressed) { 272 mergeDrawableStates(drawableState, STATE_PRESSED); 273 } 274 return drawableState; 275 } 276 277 /** Returns the icon for this view. */ 278 public Drawable getIcon() { 279 return mIcon; 280 } 281 282 @Override 283 public boolean onTouchEvent(MotionEvent event) { 284 // Call the superclass onTouchEvent first, because sometimes it changes the state to 285 // isPressed() on an ACTION_UP 286 boolean result = super.onTouchEvent(event); 287 288 // Check for a stylus button press, if it occurs cancel any long press checks. 289 if (mStylusEventHelper.onMotionEvent(event)) { 290 mLongPressHelper.cancelLongPress(); 291 result = true; 292 } 293 294 switch (event.getAction()) { 295 case MotionEvent.ACTION_DOWN: 296 // If we're in a stylus button press, don't check for long press. 297 if (!mStylusEventHelper.inStylusButtonPressed()) { 298 mLongPressHelper.postCheckForLongPress(); 299 } 300 break; 301 case MotionEvent.ACTION_CANCEL: 302 case MotionEvent.ACTION_UP: 303 mLongPressHelper.cancelLongPress(); 304 break; 305 case MotionEvent.ACTION_MOVE: 306 if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) { 307 mLongPressHelper.cancelLongPress(); 308 } 309 break; 310 } 311 return result; 312 } 313 314 void setStayPressed(boolean stayPressed) { 315 mStayPressed = stayPressed; 316 refreshDrawableState(); 317 } 318 319 @Override 320 public void onLauncherResume() { 321 // Reset the pressed state of icon that was locked in the press state while activity 322 // was launching 323 setStayPressed(false); 324 } 325 326 void clearPressedBackground() { 327 setPressed(false); 328 setStayPressed(false); 329 } 330 331 @Override 332 public boolean onKeyUp(int keyCode, KeyEvent event) { 333 // Unlike touch events, keypress event propagate pressed state change immediately, 334 // without waiting for onClickHandler to execute. Disable pressed state changes here 335 // to avoid flickering. 336 mIgnorePressedStateChange = true; 337 boolean result = super.onKeyUp(keyCode, event); 338 mIgnorePressedStateChange = false; 339 refreshDrawableState(); 340 return result; 341 } 342 343 @SuppressWarnings("wrongcall") 344 protected void drawWithoutBadge(Canvas canvas) { 345 super.onDraw(canvas); 346 } 347 348 @Override 349 public void onDraw(Canvas canvas) { 350 super.onDraw(canvas); 351 drawBadgeIfNecessary(canvas); 352 } 353 354 /** 355 * Draws the icon badge in the top right corner of the icon bounds. 356 * @param canvas The canvas to draw to. 357 */ 358 protected void drawBadgeIfNecessary(Canvas canvas) { 359 if (!mForceHideBadge && (hasBadge() || mBadgeScale > 0)) { 360 getIconBounds(mTempIconBounds); 361 mTempSpaceForBadgeOffset.set((getWidth() - mIconSize) / 2, getPaddingTop()); 362 final int scrollX = getScrollX(); 363 final int scrollY = getScrollY(); 364 canvas.translate(scrollX, scrollY); 365 mBadgeRenderer.draw(canvas, mBadgeColor, mTempIconBounds, mBadgeScale, 366 mTempSpaceForBadgeOffset); 367 canvas.translate(-scrollX, -scrollY); 368 } 369 } 370 371 public void forceHideBadge(boolean forceHideBadge) { 372 if (mForceHideBadge == forceHideBadge) { 373 return; 374 } 375 mForceHideBadge = forceHideBadge; 376 377 if (forceHideBadge) { 378 invalidate(); 379 } else if (hasBadge()) { 380 ObjectAnimator.ofFloat(this, BADGE_SCALE_PROPERTY, 0, 1).start(); 381 } 382 } 383 384 private boolean hasBadge() { 385 return mBadgeInfo != null; 386 } 387 388 public void getIconBounds(Rect outBounds) { 389 int top = getPaddingTop(); 390 int left = (getWidth() - mIconSize) / 2; 391 int right = left + mIconSize; 392 int bottom = top + mIconSize; 393 outBounds.set(left, top, right, bottom); 394 } 395 396 @Override 397 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 398 if (mCenterVertically) { 399 Paint.FontMetrics fm = getPaint().getFontMetrics(); 400 int cellHeightPx = mIconSize + getCompoundDrawablePadding() + 401 (int) Math.ceil(fm.bottom - fm.top); 402 int height = MeasureSpec.getSize(heightMeasureSpec); 403 setPadding(getPaddingLeft(), (height - cellHeightPx) / 2, getPaddingRight(), 404 getPaddingBottom()); 405 } 406 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 407 } 408 409 @Override 410 public void setTextColor(int color) { 411 mTextColor = color; 412 super.setTextColor(getModifiedColor()); 413 } 414 415 @Override 416 public void setTextColor(ColorStateList colors) { 417 mTextColor = colors.getDefaultColor(); 418 if (Float.compare(mTextAlpha, 1) == 0) { 419 super.setTextColor(colors); 420 } else { 421 super.setTextColor(getModifiedColor()); 422 } 423 } 424 425 public boolean shouldTextBeVisible() { 426 // Text should be visible everywhere but the hotseat. 427 Object tag = getParent() instanceof FolderIcon ? ((View) getParent()).getTag() : getTag(); 428 ItemInfo info = tag instanceof ItemInfo ? (ItemInfo) tag : null; 429 return info == null || info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT; 430 } 431 432 public void setTextVisibility(boolean visible) { 433 setTextAlpha(visible ? 1 : 0); 434 } 435 436 private void setTextAlpha(float alpha) { 437 mTextAlpha = alpha; 438 super.setTextColor(getModifiedColor()); 439 } 440 441 private int getModifiedColor() { 442 if (mTextAlpha == 0) { 443 // Special case to prevent text shadows in high contrast mode 444 return Color.TRANSPARENT; 445 } 446 return ColorUtils.setAlphaComponent( 447 mTextColor, Math.round(Color.alpha(mTextColor) * mTextAlpha)); 448 } 449 450 /** 451 * Creates an animator to fade the text in or out. 452 * @param fadeIn Whether the text should fade in or fade out. 453 */ 454 public ObjectAnimator createTextAlphaAnimator(boolean fadeIn) { 455 float toAlpha = shouldTextBeVisible() && fadeIn ? 1 : 0; 456 return ObjectAnimator.ofFloat(this, TEXT_ALPHA_PROPERTY, toAlpha); 457 } 458 459 @Override 460 public void cancelLongPress() { 461 super.cancelLongPress(); 462 463 mLongPressHelper.cancelLongPress(); 464 } 465 466 public void applyPromiseState(boolean promiseStateChanged) { 467 if (getTag() instanceof ShortcutInfo) { 468 ShortcutInfo info = (ShortcutInfo) getTag(); 469 final boolean isPromise = info.hasPromiseIconUi(); 470 final int progressLevel = isPromise ? 471 ((info.hasStatusFlag(ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE) ? 472 info.getInstallProgress() : 0)) : 100; 473 474 PreloadIconDrawable preloadDrawable = applyProgressLevel(progressLevel); 475 if (preloadDrawable != null && promiseStateChanged) { 476 preloadDrawable.maybePerformFinishedAnimation(); 477 } 478 } 479 } 480 481 public PreloadIconDrawable applyProgressLevel(int progressLevel) { 482 if (getTag() instanceof ItemInfoWithIcon) { 483 ItemInfoWithIcon info = (ItemInfoWithIcon) getTag(); 484 if (progressLevel >= 100) { 485 setContentDescription(info.contentDescription != null 486 ? info.contentDescription : ""); 487 } else if (progressLevel > 0) { 488 setContentDescription(getContext() 489 .getString(R.string.app_downloading_title, info.title, 490 NumberFormat.getPercentInstance().format(progressLevel * 0.01))); 491 } else { 492 setContentDescription(getContext() 493 .getString(R.string.app_waiting_download_title, info.title)); 494 } 495 if (mIcon != null) { 496 final PreloadIconDrawable preloadDrawable; 497 if (mIcon instanceof PreloadIconDrawable) { 498 preloadDrawable = (PreloadIconDrawable) mIcon; 499 preloadDrawable.setLevel(progressLevel); 500 } else { 501 preloadDrawable = DrawableFactory.get(getContext()) 502 .newPendingIcon(info, getContext()); 503 preloadDrawable.setLevel(progressLevel); 504 setIcon(preloadDrawable); 505 } 506 return preloadDrawable; 507 } 508 } 509 return null; 510 } 511 512 public void applyBadgeState(ItemInfo itemInfo, boolean animate) { 513 if (mIcon instanceof FastBitmapDrawable) { 514 boolean wasBadged = mBadgeInfo != null; 515 mBadgeInfo = mActivity.getBadgeInfoForItem(itemInfo); 516 boolean isBadged = mBadgeInfo != null; 517 float newBadgeScale = isBadged ? 1f : 0; 518 mBadgeRenderer = mActivity.getDeviceProfile().mBadgeRenderer; 519 if (wasBadged || isBadged) { 520 // Animate when a badge is first added or when it is removed. 521 if (animate && (wasBadged ^ isBadged) && isShown()) { 522 ObjectAnimator.ofFloat(this, BADGE_SCALE_PROPERTY, newBadgeScale).start(); 523 } else { 524 mBadgeScale = newBadgeScale; 525 invalidate(); 526 } 527 } 528 if (itemInfo.contentDescription != null) { 529 if (hasBadge()) { 530 int count = mBadgeInfo.getNotificationCount(); 531 setContentDescription(getContext().getResources().getQuantityString( 532 R.plurals.badged_app_label, count, itemInfo.contentDescription, count)); 533 } else { 534 setContentDescription(itemInfo.contentDescription); 535 } 536 } 537 } 538 } 539 540 /** 541 * Sets the icon for this view based on the layout direction. 542 */ 543 private void setIcon(Drawable icon) { 544 if (mIsIconVisible) { 545 applyCompoundDrawables(icon); 546 } 547 mIcon = icon; 548 } 549 550 public void setIconVisible(boolean visible) { 551 mIsIconVisible = visible; 552 Drawable icon = visible ? mIcon : new ColorDrawable(Color.TRANSPARENT); 553 applyCompoundDrawables(icon); 554 } 555 556 protected void applyCompoundDrawables(Drawable icon) { 557 // If we had already set an icon before, disable relayout as the icon size is the 558 // same as before. 559 mDisableRelayout = mIcon != null; 560 561 icon.setBounds(0, 0, mIconSize, mIconSize); 562 if (mLayoutHorizontal) { 563 setCompoundDrawablesRelative(icon, null, null, null); 564 } else { 565 setCompoundDrawables(null, icon, null, null); 566 } 567 mDisableRelayout = false; 568 } 569 570 @Override 571 public void requestLayout() { 572 if (!mDisableRelayout) { 573 super.requestLayout(); 574 } 575 } 576 577 /** 578 * Applies the item info if it is same as what the view is pointing to currently. 579 */ 580 @Override 581 public void reapplyItemInfo(ItemInfoWithIcon info) { 582 if (getTag() == info) { 583 mIconLoadRequest = null; 584 mDisableRelayout = true; 585 586 // Optimization: Starting in N, pre-uploads the bitmap to RenderThread. 587 info.iconBitmap.prepareToDraw(); 588 589 if (info instanceof AppInfo) { 590 applyFromApplicationInfo((AppInfo) info); 591 } else if (info instanceof ShortcutInfo) { 592 applyFromShortcutInfo((ShortcutInfo) info); 593 mActivity.invalidateParent(info); 594 } else if (info instanceof PackageItemInfo) { 595 applyFromPackageItemInfo((PackageItemInfo) info); 596 } 597 598 mDisableRelayout = false; 599 } 600 } 601 602 /** 603 * Verifies that the current icon is high-res otherwise posts a request to load the icon. 604 */ 605 public void verifyHighRes() { 606 if (mIconLoadRequest != null) { 607 mIconLoadRequest.cancel(); 608 mIconLoadRequest = null; 609 } 610 if (getTag() instanceof ItemInfoWithIcon) { 611 ItemInfoWithIcon info = (ItemInfoWithIcon) getTag(); 612 if (info.usingLowResIcon) { 613 mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache() 614 .updateIconInBackground(BubbleTextView.this, info); 615 } 616 } 617 } 618 619 public int getIconSize() { 620 return mIconSize; 621 } 622 } 623