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