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