Home | History | Annotate | Download | only in launcher3
      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 
     91     public BubbleTextView(Context context) {
     92         this(context, null, 0);
     93     }
     94 
     95     public BubbleTextView(Context context, AttributeSet attrs) {
     96         this(context, attrs, 0);
     97     }
     98 
     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 
    144     public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache) {
    145         applyFromShortcutInfo(info, iconCache, false);
    146     }
    147 
    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 
    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 
    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      */
    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      */
    210     public void setLongPressTimeout(int longPressTimeout) {
    211         mLongPressHelper.setLongPressTimeout(longPressTimeout);
    212     }
    213 
    214     @Override
    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
    223     protected boolean verifyDrawable(Drawable who) {
    224         return who == mBackground || super.verifyDrawable(who);
    225     }
    226 
    227     @Override
    228     public void setTag(Object tag) {
    229         if (tag != null) {
    230             LauncherModel.checkItemInfo((ItemInfo) tag);
    231         }
    232         super.setTag(tag);
    233     }
    234 
    235     @Override
    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. */
    245     public Drawable getIcon() {
    246         return mIcon;
    247     }
    248 
    249     /** Returns whether the layout is horizontal. */
    250     public boolean isLayoutHorizontal() {
    251         return mLayoutHorizontal;
    252     }
    253 
    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
    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 
    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 
    333     void clearPressedBackground() {
    334         setPressed(false);
    335         setStayPressed(false);
    336     }
    337 
    338     @Override
    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
    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
    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
    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
    422     protected void onDetachedFromWindow() {
    423         super.onDetachedFromWindow();
    424         if (mBackground != null) mBackground.setCallback(null);
    425     }
    426 
    427     @Override
    428     public void setTextColor(int color) {
    429         mTextColor = color;
    430         super.setTextColor(color);
    431     }
    432 
    433     @Override
    434     public void setTextColor(ColorStateList colors) {
    435         mTextColor = colors.getDefaultColor();
    436         super.setTextColor(colors);
    437     }
    438 
    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
    449     public void cancelLongPress() {
    450         super.cancelLongPress();
    451 
    452         mLongPressHelper.cancelLongPress();
    453     }
    454 
    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 
    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)
    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
    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      */
    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      */
    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
    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      */
    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 {
    641         void setPressedIcon(BubbleTextView icon, Bitmap background);
    642     }
    643 }
    644