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 17package com.android.launcher3; 18 19import android.animation.ObjectAnimator; 20import android.annotation.TargetApi; 21import android.content.Context; 22import android.content.res.ColorStateList; 23import android.content.res.Resources; 24import android.content.res.Resources.Theme; 25import android.content.res.TypedArray; 26import android.graphics.Bitmap; 27import android.graphics.Canvas; 28import android.graphics.Paint; 29import android.graphics.Region; 30import android.graphics.drawable.Drawable; 31import android.os.Build; 32import android.util.AttributeSet; 33import android.util.SparseArray; 34import android.util.TypedValue; 35import android.view.KeyEvent; 36import android.view.MotionEvent; 37import android.view.View; 38import android.view.ViewConfiguration; 39import android.view.ViewParent; 40import android.view.animation.AccelerateInterpolator; 41import android.view.animation.DecelerateInterpolator; 42import android.widget.TextView; 43 44import com.android.launcher3.IconCache.IconLoadRequest; 45import com.android.launcher3.model.PackageItemInfo; 46 47/** 48 * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan 49 * because we want to make the bubble taller than the text and TextView's clip is 50 * too aggressive. 51 */ 52public class BubbleTextView extends TextView 53 implements BaseRecyclerViewFastScrollBar.FastScrollFocusableView { 54 55 private static SparseArray<Theme> sPreloaderThemes = new SparseArray<Theme>(2); 56 57 private static final float SHADOW_LARGE_RADIUS = 4.0f; 58 private static final float SHADOW_SMALL_RADIUS = 1.75f; 59 private static final float SHADOW_Y_OFFSET = 2.0f; 60 private static final int SHADOW_LARGE_COLOUR = 0xDD000000; 61 private static final int SHADOW_SMALL_COLOUR = 0xCC000000; 62 63 private static final int DISPLAY_WORKSPACE = 0; 64 private static final int DISPLAY_ALL_APPS = 1; 65 66 private static final float FAST_SCROLL_FOCUS_MAX_SCALE = 1.15f; 67 private static final int FAST_SCROLL_FOCUS_MODE_NONE = 0; 68 private static final int FAST_SCROLL_FOCUS_MODE_SCALE_ICON = 1; 69 private static final int FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG = 2; 70 private static final int FAST_SCROLL_FOCUS_FADE_IN_DURATION = 175; 71 private static final int FAST_SCROLL_FOCUS_FADE_OUT_DURATION = 125; 72 73 private final Launcher mLauncher; 74 private Drawable mIcon; 75 private final Drawable mBackground; 76 private final CheckLongPressHelper mLongPressHelper; 77 private final HolographicOutlineHelper mOutlineHelper; 78 private final StylusEventHelper mStylusEventHelper; 79 80 private boolean mBackgroundSizeChanged; 81 82 private Bitmap mPressedBackground; 83 84 private float mSlop; 85 86 private final boolean mDeferShadowGenerationOnTouch; 87 private final boolean mCustomShadowsEnabled; 88 private final boolean mLayoutHorizontal; 89 private final int mIconSize; 90 private int mTextColor; 91 92 private boolean mStayPressed; 93 private boolean mIgnorePressedStateChange; 94 private boolean mDisableRelayout = false; 95 96 private ObjectAnimator mFastScrollFocusAnimator; 97 private Paint mFastScrollFocusBgPaint; 98 private float mFastScrollFocusFraction; 99 private boolean mFastScrollFocused; 100 private final int mFastScrollMode = FAST_SCROLL_FOCUS_MODE_SCALE_ICON; 101 102 private IconLoadRequest mIconLoadRequest; 103 104 public BubbleTextView(Context context) { 105 this(context, null, 0); 106 } 107 108 public BubbleTextView(Context context, AttributeSet attrs) { 109 this(context, attrs, 0); 110 } 111 112 public BubbleTextView(Context context, AttributeSet attrs, int defStyle) { 113 super(context, attrs, defStyle); 114 mLauncher = (Launcher) context; 115 DeviceProfile grid = mLauncher.getDeviceProfile(); 116 117 TypedArray a = context.obtainStyledAttributes(attrs, 118 R.styleable.BubbleTextView, defStyle, 0); 119 mCustomShadowsEnabled = a.getBoolean(R.styleable.BubbleTextView_customShadows, true); 120 mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false); 121 mDeferShadowGenerationOnTouch = 122 a.getBoolean(R.styleable.BubbleTextView_deferShadowGeneration, false); 123 124 int display = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE); 125 int defaultIconSize = grid.iconSizePx; 126 if (display == DISPLAY_WORKSPACE) { 127 setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); 128 } else if (display == DISPLAY_ALL_APPS) { 129 setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx); 130 defaultIconSize = grid.allAppsIconSizePx; 131 } 132 133 mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride, 134 defaultIconSize); 135 136 a.recycle(); 137 138 if (mCustomShadowsEnabled) { 139 // Draw the background itself as the parent is drawn twice. 140 mBackground = getBackground(); 141 setBackground(null); 142 } else { 143 mBackground = null; 144 } 145 146 mLongPressHelper = new CheckLongPressHelper(this); 147 mStylusEventHelper = new StylusEventHelper(this); 148 149 mOutlineHelper = HolographicOutlineHelper.obtain(getContext()); 150 if (mCustomShadowsEnabled) { 151 setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR); 152 } 153 154 if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG) { 155 mFastScrollFocusBgPaint = new Paint(); 156 mFastScrollFocusBgPaint.setAntiAlias(true); 157 mFastScrollFocusBgPaint.setColor( 158 getResources().getColor(R.color.container_fastscroll_thumb_active_color)); 159 } 160 161 setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate()); 162 } 163 164 public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache) { 165 applyFromShortcutInfo(info, iconCache, false); 166 } 167 168 public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache, 169 boolean promiseStateChanged) { 170 Bitmap b = info.getIcon(iconCache); 171 172 FastBitmapDrawable iconDrawable = mLauncher.createIconDrawable(b); 173 iconDrawable.setGhostModeEnabled(info.isDisabled != 0); 174 175 setIcon(iconDrawable, mIconSize); 176 if (info.contentDescription != null) { 177 setContentDescription(info.contentDescription); 178 } 179 setText(info.title); 180 setTag(info); 181 182 if (promiseStateChanged || info.isPromise()) { 183 applyState(promiseStateChanged); 184 } 185 } 186 187 public void applyFromApplicationInfo(AppInfo info) { 188 setIcon(mLauncher.createIconDrawable(info.iconBitmap), mIconSize); 189 setText(info.title); 190 if (info.contentDescription != null) { 191 setContentDescription(info.contentDescription); 192 } 193 // We don't need to check the info since it's not a ShortcutInfo 194 super.setTag(info); 195 196 // Verify high res immediately 197 verifyHighRes(); 198 } 199 200 public void applyFromPackageItemInfo(PackageItemInfo info) { 201 setIcon(mLauncher.createIconDrawable(info.iconBitmap), mIconSize); 202 setText(info.title); 203 if (info.contentDescription != null) { 204 setContentDescription(info.contentDescription); 205 } 206 // We don't need to check the info since it's not a ShortcutInfo 207 super.setTag(info); 208 209 // Verify high res immediately 210 verifyHighRes(); 211 } 212 213 /** 214 * Overrides the default long press timeout. 215 */ 216 public void setLongPressTimeout(int longPressTimeout) { 217 mLongPressHelper.setLongPressTimeout(longPressTimeout); 218 } 219 220 @Override 221 protected boolean setFrame(int left, int top, int right, int bottom) { 222 if (getLeft() != left || getRight() != right || getTop() != top || getBottom() != bottom) { 223 mBackgroundSizeChanged = true; 224 } 225 return super.setFrame(left, top, right, bottom); 226 } 227 228 @Override 229 protected boolean verifyDrawable(Drawable who) { 230 return who == mBackground || super.verifyDrawable(who); 231 } 232 233 @Override 234 public void setTag(Object tag) { 235 if (tag != null) { 236 LauncherModel.checkItemInfo((ItemInfo) tag); 237 } 238 super.setTag(tag); 239 } 240 241 @Override 242 public void setPressed(boolean pressed) { 243 super.setPressed(pressed); 244 245 if (!mIgnorePressedStateChange) { 246 updateIconState(); 247 } 248 } 249 250 /** Returns the icon for this view. */ 251 public Drawable getIcon() { 252 return mIcon; 253 } 254 255 /** Returns whether the layout is horizontal. */ 256 public boolean isLayoutHorizontal() { 257 return mLayoutHorizontal; 258 } 259 260 private void updateIconState() { 261 if (mIcon instanceof FastBitmapDrawable) { 262 ((FastBitmapDrawable) mIcon).setPressed(isPressed() || mStayPressed); 263 } 264 } 265 266 @Override 267 public boolean onTouchEvent(MotionEvent event) { 268 // Call the superclass onTouchEvent first, because sometimes it changes the state to 269 // isPressed() on an ACTION_UP 270 boolean result = super.onTouchEvent(event); 271 272 // Check for a stylus button press, if it occurs cancel any long press checks. 273 if (mStylusEventHelper.checkAndPerformStylusEvent(event)) { 274 mLongPressHelper.cancelLongPress(); 275 result = true; 276 } 277 278 switch (event.getAction()) { 279 case MotionEvent.ACTION_DOWN: 280 // So that the pressed outline is visible immediately on setStayPressed(), 281 // we pre-create it on ACTION_DOWN (it takes a small but perceptible amount of time 282 // to create it) 283 if (!mDeferShadowGenerationOnTouch && mPressedBackground == null) { 284 mPressedBackground = mOutlineHelper.createMediumDropShadow(this); 285 } 286 287 // If we're in a stylus button press, don't check for long press. 288 if (!mStylusEventHelper.inStylusButtonPressed()) { 289 mLongPressHelper.postCheckForLongPress(); 290 } 291 break; 292 case MotionEvent.ACTION_CANCEL: 293 case MotionEvent.ACTION_UP: 294 // If we've touched down and up on an item, and it's still not "pressed", then 295 // destroy the pressed outline 296 if (!isPressed()) { 297 mPressedBackground = null; 298 } 299 300 mLongPressHelper.cancelLongPress(); 301 break; 302 case MotionEvent.ACTION_MOVE: 303 if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) { 304 mLongPressHelper.cancelLongPress(); 305 } 306 break; 307 } 308 return result; 309 } 310 311 void setStayPressed(boolean stayPressed) { 312 mStayPressed = stayPressed; 313 if (!stayPressed) { 314 mPressedBackground = null; 315 } else { 316 if (mPressedBackground == null) { 317 mPressedBackground = mOutlineHelper.createMediumDropShadow(this); 318 } 319 } 320 321 // Only show the shadow effect when persistent pressed state is set. 322 ViewParent parent = getParent(); 323 if (parent != null && parent.getParent() instanceof BubbleTextShadowHandler) { 324 ((BubbleTextShadowHandler) parent.getParent()).setPressedIcon( 325 this, mPressedBackground); 326 } 327 328 updateIconState(); 329 } 330 331 void clearPressedBackground() { 332 setPressed(false); 333 setStayPressed(false); 334 } 335 336 @Override 337 public boolean onKeyDown(int keyCode, KeyEvent event) { 338 if (super.onKeyDown(keyCode, event)) { 339 // Pre-create shadow so show immediately on click. 340 if (mPressedBackground == null) { 341 mPressedBackground = mOutlineHelper.createMediumDropShadow(this); 342 } 343 return true; 344 } 345 return false; 346 } 347 348 @Override 349 public boolean onKeyUp(int keyCode, KeyEvent event) { 350 // Unlike touch events, keypress event propagate pressed state change immediately, 351 // without waiting for onClickHandler to execute. Disable pressed state changes here 352 // to avoid flickering. 353 mIgnorePressedStateChange = true; 354 boolean result = super.onKeyUp(keyCode, event); 355 356 mPressedBackground = null; 357 mIgnorePressedStateChange = false; 358 updateIconState(); 359 return result; 360 } 361 362 @Override 363 public void draw(Canvas canvas) { 364 if (!mCustomShadowsEnabled) { 365 // Draw the fast scroll focus bg if we have one 366 if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG && 367 mFastScrollFocusFraction > 0f) { 368 DeviceProfile grid = mLauncher.getDeviceProfile(); 369 int iconCenterX = getScrollX() + (getWidth() / 2); 370 int iconCenterY = getScrollY() + getPaddingTop() + (grid.iconSizePx / 2); 371 canvas.drawCircle(iconCenterX, iconCenterY, 372 mFastScrollFocusFraction * (getWidth() / 2), mFastScrollFocusBgPaint); 373 } 374 375 super.draw(canvas); 376 377 return; 378 } 379 380 final Drawable background = mBackground; 381 if (background != null) { 382 final int scrollX = getScrollX(); 383 final int scrollY = getScrollY(); 384 385 if (mBackgroundSizeChanged) { 386 background.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop()); 387 mBackgroundSizeChanged = false; 388 } 389 390 if ((scrollX | scrollY) == 0) { 391 background.draw(canvas); 392 } else { 393 canvas.translate(scrollX, scrollY); 394 background.draw(canvas); 395 canvas.translate(-scrollX, -scrollY); 396 } 397 } 398 399 // If text is transparent, don't draw any shadow 400 if (getCurrentTextColor() == getResources().getColor(android.R.color.transparent)) { 401 getPaint().clearShadowLayer(); 402 super.draw(canvas); 403 return; 404 } 405 406 // We enhance the shadow by drawing the shadow twice 407 getPaint().setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR); 408 super.draw(canvas); 409 canvas.save(Canvas.CLIP_SAVE_FLAG); 410 canvas.clipRect(getScrollX(), getScrollY() + getExtendedPaddingTop(), 411 getScrollX() + getWidth(), 412 getScrollY() + getHeight(), Region.Op.INTERSECT); 413 getPaint().setShadowLayer(SHADOW_SMALL_RADIUS, 0.0f, 0.0f, SHADOW_SMALL_COLOUR); 414 super.draw(canvas); 415 canvas.restore(); 416 } 417 418 @Override 419 protected void onAttachedToWindow() { 420 super.onAttachedToWindow(); 421 422 if (mBackground != null) mBackground.setCallback(this); 423 424 if (mIcon instanceof PreloadIconDrawable) { 425 ((PreloadIconDrawable) mIcon).applyPreloaderTheme(getPreloaderTheme()); 426 } 427 mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 428 } 429 430 @Override 431 protected void onDetachedFromWindow() { 432 super.onDetachedFromWindow(); 433 if (mBackground != null) mBackground.setCallback(null); 434 } 435 436 @Override 437 public void setTextColor(int color) { 438 mTextColor = color; 439 super.setTextColor(color); 440 } 441 442 @Override 443 public void setTextColor(ColorStateList colors) { 444 mTextColor = colors.getDefaultColor(); 445 super.setTextColor(colors); 446 } 447 448 public void setTextVisibility(boolean visible) { 449 Resources res = getResources(); 450 if (visible) { 451 super.setTextColor(mTextColor); 452 } else { 453 super.setTextColor(res.getColor(android.R.color.transparent)); 454 } 455 } 456 457 @Override 458 public void cancelLongPress() { 459 super.cancelLongPress(); 460 461 mLongPressHelper.cancelLongPress(); 462 } 463 464 public void applyState(boolean promiseStateChanged) { 465 if (getTag() instanceof ShortcutInfo) { 466 ShortcutInfo info = (ShortcutInfo) getTag(); 467 final boolean isPromise = info.isPromise(); 468 final int progressLevel = isPromise ? 469 ((info.hasStatusFlag(ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE) ? 470 info.getInstallProgress() : 0)) : 100; 471 472 if (mIcon != null) { 473 final PreloadIconDrawable preloadDrawable; 474 if (mIcon instanceof PreloadIconDrawable) { 475 preloadDrawable = (PreloadIconDrawable) mIcon; 476 } else { 477 preloadDrawable = new PreloadIconDrawable(mIcon, getPreloaderTheme()); 478 setIcon(preloadDrawable, mIconSize); 479 } 480 481 preloadDrawable.setLevel(progressLevel); 482 if (promiseStateChanged) { 483 preloadDrawable.maybePerformFinishedAnimation(); 484 } 485 } 486 } 487 } 488 489 private Theme getPreloaderTheme() { 490 Object tag = getTag(); 491 int style = ((tag != null) && (tag instanceof ShortcutInfo) && 492 (((ShortcutInfo) tag).container >= 0)) ? R.style.PreloadIcon_Folder 493 : R.style.PreloadIcon; 494 Theme theme = sPreloaderThemes.get(style); 495 if (theme == null) { 496 theme = getResources().newTheme(); 497 theme.applyStyle(style, true); 498 sPreloaderThemes.put(style, theme); 499 } 500 return theme; 501 } 502 503 /** 504 * Sets the icon for this view based on the layout direction. 505 */ 506 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 507 private Drawable setIcon(Drawable icon, int iconSize) { 508 mIcon = icon; 509 if (iconSize != -1) { 510 mIcon.setBounds(0, 0, iconSize, iconSize); 511 } 512 if (mLayoutHorizontal) { 513 if (Utilities.ATLEAST_JB_MR1) { 514 setCompoundDrawablesRelative(mIcon, null, null, null); 515 } else { 516 setCompoundDrawables(mIcon, null, null, null); 517 } 518 } else { 519 setCompoundDrawables(null, mIcon, null, null); 520 } 521 return icon; 522 } 523 524 @Override 525 public void requestLayout() { 526 if (!mDisableRelayout) { 527 super.requestLayout(); 528 } 529 } 530 531 /** 532 * Applies the item info if it is same as what the view is pointing to currently. 533 */ 534 public void reapplyItemInfo(final ItemInfo info) { 535 if (getTag() == info) { 536 mIconLoadRequest = null; 537 mDisableRelayout = true; 538 if (info instanceof AppInfo) { 539 applyFromApplicationInfo((AppInfo) info); 540 } else if (info instanceof ShortcutInfo) { 541 applyFromShortcutInfo((ShortcutInfo) info, 542 LauncherAppState.getInstance().getIconCache()); 543 if ((info.rank < FolderIcon.NUM_ITEMS_IN_PREVIEW) && (info.container >= 0)) { 544 View folderIcon = 545 mLauncher.getWorkspace().getHomescreenIconByItemId(info.container); 546 if (folderIcon != null) { 547 folderIcon.invalidate(); 548 } 549 } 550 } else if (info instanceof PackageItemInfo) { 551 applyFromPackageItemInfo((PackageItemInfo) info); 552 } 553 mDisableRelayout = false; 554 } 555 } 556 557 /** 558 * Verifies that the current icon is high-res otherwise posts a request to load the icon. 559 */ 560 public void verifyHighRes() { 561 if (mIconLoadRequest != null) { 562 mIconLoadRequest.cancel(); 563 mIconLoadRequest = null; 564 } 565 if (getTag() instanceof AppInfo) { 566 AppInfo info = (AppInfo) getTag(); 567 if (info.usingLowResIcon) { 568 mIconLoadRequest = LauncherAppState.getInstance().getIconCache() 569 .updateIconInBackground(BubbleTextView.this, info); 570 } 571 } else if (getTag() instanceof ShortcutInfo) { 572 ShortcutInfo info = (ShortcutInfo) getTag(); 573 if (info.usingLowResIcon) { 574 mIconLoadRequest = LauncherAppState.getInstance().getIconCache() 575 .updateIconInBackground(BubbleTextView.this, info); 576 } 577 } else if (getTag() instanceof PackageItemInfo) { 578 PackageItemInfo info = (PackageItemInfo) getTag(); 579 if (info.usingLowResIcon) { 580 mIconLoadRequest = LauncherAppState.getInstance().getIconCache() 581 .updateIconInBackground(BubbleTextView.this, info); 582 } 583 } 584 } 585 586 // Setters & getters for the animation 587 public void setFastScrollFocus(float fraction) { 588 mFastScrollFocusFraction = fraction; 589 if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_SCALE_ICON) { 590 setScaleX(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f)); 591 setScaleY(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f)); 592 } else { 593 invalidate(); 594 } 595 } 596 597 public float getFastScrollFocus() { 598 return mFastScrollFocusFraction; 599 } 600 601 @Override 602 public void setFastScrollFocused(final boolean focused, boolean animated) { 603 if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_NONE) { 604 return; 605 } 606 607 if (mFastScrollFocused != focused) { 608 mFastScrollFocused = focused; 609 610 if (animated) { 611 // Clean up the previous focus animator 612 if (mFastScrollFocusAnimator != null) { 613 mFastScrollFocusAnimator.cancel(); 614 } 615 mFastScrollFocusAnimator = ObjectAnimator.ofFloat(this, "fastScrollFocus", 616 focused ? 1f : 0f); 617 if (focused) { 618 mFastScrollFocusAnimator.setInterpolator(new DecelerateInterpolator()); 619 } else { 620 mFastScrollFocusAnimator.setInterpolator(new AccelerateInterpolator()); 621 } 622 mFastScrollFocusAnimator.setDuration(focused ? 623 FAST_SCROLL_FOCUS_FADE_IN_DURATION : FAST_SCROLL_FOCUS_FADE_OUT_DURATION); 624 mFastScrollFocusAnimator.start(); 625 } else { 626 mFastScrollFocusFraction = focused ? 1f : 0f; 627 } 628 } 629 } 630 631 /** 632 * Interface to be implemented by the grand parent to allow click shadow effect. 633 */ 634 public static interface BubbleTextShadowHandler { 635 void setPressedIcon(BubbleTextView icon, Bitmap background); 636 } 637} 638