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.content.Context;
     20 import android.content.res.ColorStateList;
     21 import android.content.res.Resources;
     22 import android.content.res.Resources.Theme;
     23 import android.content.res.TypedArray;
     24 import android.graphics.Bitmap;
     25 import android.graphics.Canvas;
     26 import android.graphics.Region;
     27 import android.graphics.drawable.Drawable;
     28 import android.util.AttributeSet;
     29 import android.util.SparseArray;
     30 import android.util.TypedValue;
     31 import android.view.KeyEvent;
     32 import android.view.MotionEvent;
     33 import android.view.ViewConfiguration;
     34 import android.widget.TextView;
     35 
     36 /**
     37  * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan
     38  * because we want to make the bubble taller than the text and TextView's clip is
     39  * too aggressive.
     40  */
     41 public class BubbleTextView extends TextView {
     42 
     43     private static SparseArray<Theme> sPreloaderThemes = new SparseArray<>(2);
     44 
     45     private static final float SHADOW_LARGE_RADIUS = 4.0f;
     46     private static final float SHADOW_SMALL_RADIUS = 1.75f;
     47     private static final float SHADOW_Y_OFFSET = 2.0f;
     48     private static final int SHADOW_LARGE_COLOUR = 0xDD000000;
     49     private static final int SHADOW_SMALL_COLOUR = 0xCC000000;
     50     static final float PADDING_V = 3.0f;
     51 
     52     private HolographicOutlineHelper mOutlineHelper;
     53     private Bitmap mPressedBackground;
     54 
     55     private float mSlop;
     56 
     57     private int mTextColor;
     58     private final boolean mCustomShadowsEnabled;
     59     private boolean mIsTextVisible;
     60 
     61     // TODO: Remove custom background handling code, as no instance of BubbleTextView use any
     62     // background.
     63     private boolean mBackgroundSizeChanged;
     64     private final Drawable mBackground;
     65 
     66     private boolean mStayPressed;
     67     private boolean mIgnorePressedStateChange;
     68     private CheckLongPressHelper mLongPressHelper;
     69 
     70     public BubbleTextView(Context context) {
     71         this(context, null, 0);
     72     }
     73 
     74     public BubbleTextView(Context context, AttributeSet attrs) {
     75         this(context, attrs, 0);
     76     }
     77 
     78     public BubbleTextView(Context context, AttributeSet attrs, int defStyle) {
     79         super(context, attrs, defStyle);
     80 
     81         TypedArray a = context.obtainStyledAttributes(attrs,
     82                 R.styleable.BubbleTextView, defStyle, 0);
     83         mCustomShadowsEnabled = a.getBoolean(R.styleable.BubbleTextView_customShadows, true);
     84         a.recycle();
     85 
     86         if (mCustomShadowsEnabled) {
     87             // Draw the background itself as the parent is drawn twice.
     88             mBackground = getBackground();
     89             setBackground(null);
     90         } else {
     91             mBackground = null;
     92         }
     93         init();
     94     }
     95 
     96     public void onFinishInflate() {
     97         super.onFinishInflate();
     98 
     99         // Ensure we are using the right text size
    100         LauncherAppState app = LauncherAppState.getInstance();
    101         DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
    102         setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx);
    103     }
    104 
    105     private void init() {
    106         mLongPressHelper = new CheckLongPressHelper(this);
    107 
    108         mOutlineHelper = HolographicOutlineHelper.obtain(getContext());
    109         if (mCustomShadowsEnabled) {
    110             setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR);
    111         }
    112     }
    113 
    114     public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache,
    115             boolean setDefaultPadding) {
    116         applyFromShortcutInfo(info, iconCache, setDefaultPadding, false);
    117     }
    118 
    119     public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache,
    120             boolean setDefaultPadding, boolean promiseStateChanged) {
    121         Bitmap b = info.getIcon(iconCache);
    122         LauncherAppState app = LauncherAppState.getInstance();
    123 
    124         FastBitmapDrawable iconDrawable = Utilities.createIconDrawable(b);
    125         iconDrawable.setGhostModeEnabled(info.isDisabled);
    126 
    127         setCompoundDrawables(null, iconDrawable, null, null);
    128         if (setDefaultPadding) {
    129             DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
    130             setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
    131         }
    132         if (info.contentDescription != null) {
    133             setContentDescription(info.contentDescription);
    134         }
    135         setText(info.title);
    136         setTag(info);
    137 
    138         if (promiseStateChanged || info.isPromise()) {
    139             applyState(promiseStateChanged);
    140         }
    141     }
    142 
    143     public void applyFromApplicationInfo(AppInfo info) {
    144         LauncherAppState app = LauncherAppState.getInstance();
    145         DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
    146 
    147         Drawable topDrawable = Utilities.createIconDrawable(info.iconBitmap);
    148         topDrawable.setBounds(0, 0, grid.allAppsIconSizePx, grid.allAppsIconSizePx);
    149         setCompoundDrawables(null, topDrawable, null, null);
    150         setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
    151         setText(info.title);
    152         if (info.contentDescription != null) {
    153             setContentDescription(info.contentDescription);
    154         }
    155         setTag(info);
    156     }
    157 
    158 
    159     @Override
    160     protected boolean setFrame(int left, int top, int right, int bottom) {
    161         if (getLeft() != left || getRight() != right || getTop() != top || getBottom() != bottom) {
    162             mBackgroundSizeChanged = true;
    163         }
    164         return super.setFrame(left, top, right, bottom);
    165     }
    166 
    167     @Override
    168     protected boolean verifyDrawable(Drawable who) {
    169         return who == mBackground || super.verifyDrawable(who);
    170     }
    171 
    172     @Override
    173     public void setTag(Object tag) {
    174         if (tag != null) {
    175             LauncherModel.checkItemInfo((ItemInfo) tag);
    176         }
    177         super.setTag(tag);
    178     }
    179 
    180     @Override
    181     public void setPressed(boolean pressed) {
    182         super.setPressed(pressed);
    183 
    184         if (!mIgnorePressedStateChange) {
    185             updateIconState();
    186         }
    187     }
    188 
    189     private void updateIconState() {
    190         Drawable top = getCompoundDrawables()[1];
    191         if (top instanceof FastBitmapDrawable) {
    192             ((FastBitmapDrawable) top).setPressed(isPressed() || mStayPressed);
    193         }
    194     }
    195 
    196     @Override
    197     public boolean onTouchEvent(MotionEvent event) {
    198         // Call the superclass onTouchEvent first, because sometimes it changes the state to
    199         // isPressed() on an ACTION_UP
    200         boolean result = super.onTouchEvent(event);
    201 
    202         switch (event.getAction()) {
    203             case MotionEvent.ACTION_DOWN:
    204                 // So that the pressed outline is visible immediately on setStayPressed(),
    205                 // we pre-create it on ACTION_DOWN (it takes a small but perceptible amount of time
    206                 // to create it)
    207                 if (mPressedBackground == null) {
    208                     mPressedBackground = mOutlineHelper.createMediumDropShadow(this);
    209                 }
    210 
    211                 mLongPressHelper.postCheckForLongPress();
    212                 break;
    213             case MotionEvent.ACTION_CANCEL:
    214             case MotionEvent.ACTION_UP:
    215                 // If we've touched down and up on an item, and it's still not "pressed", then
    216                 // destroy the pressed outline
    217                 if (!isPressed()) {
    218                     mPressedBackground = null;
    219                 }
    220 
    221                 mLongPressHelper.cancelLongPress();
    222                 break;
    223             case MotionEvent.ACTION_MOVE:
    224                 if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) {
    225                     mLongPressHelper.cancelLongPress();
    226                 }
    227                 break;
    228         }
    229         return result;
    230     }
    231 
    232     void setStayPressed(boolean stayPressed) {
    233         mStayPressed = stayPressed;
    234         if (!stayPressed) {
    235             mPressedBackground = null;
    236         }
    237 
    238         // Only show the shadow effect when persistent pressed state is set.
    239         if (getParent() instanceof ShortcutAndWidgetContainer) {
    240             CellLayout layout = (CellLayout) getParent().getParent();
    241             layout.setPressedIcon(this, mPressedBackground, mOutlineHelper.shadowBitmapPadding);
    242         }
    243 
    244         updateIconState();
    245     }
    246 
    247     void clearPressedBackground() {
    248         setPressed(false);
    249         setStayPressed(false);
    250     }
    251 
    252     @Override
    253     public boolean onKeyDown(int keyCode, KeyEvent event) {
    254         if (super.onKeyDown(keyCode, event)) {
    255             // Pre-create shadow so show immediately on click.
    256             if (mPressedBackground == null) {
    257                 mPressedBackground = mOutlineHelper.createMediumDropShadow(this);
    258             }
    259             return true;
    260         }
    261         return false;
    262     }
    263 
    264     @Override
    265     public boolean onKeyUp(int keyCode, KeyEvent event) {
    266         // Unlike touch events, keypress event propagate pressed state change immediately,
    267         // without waiting for onClickHandler to execute. Disable pressed state changes here
    268         // to avoid flickering.
    269         mIgnorePressedStateChange = true;
    270         boolean result = super.onKeyUp(keyCode, event);
    271 
    272         mPressedBackground = null;
    273         mIgnorePressedStateChange = false;
    274         updateIconState();
    275         return result;
    276     }
    277 
    278     @Override
    279     public void draw(Canvas canvas) {
    280         if (!mCustomShadowsEnabled) {
    281             super.draw(canvas);
    282             return;
    283         }
    284 
    285         final Drawable background = mBackground;
    286         if (background != null) {
    287             final int scrollX = getScrollX();
    288             final int scrollY = getScrollY();
    289 
    290             if (mBackgroundSizeChanged) {
    291                 background.setBounds(0, 0,  getRight() - getLeft(), getBottom() - getTop());
    292                 mBackgroundSizeChanged = false;
    293             }
    294 
    295             if ((scrollX | scrollY) == 0) {
    296                 background.draw(canvas);
    297             } else {
    298                 canvas.translate(scrollX, scrollY);
    299                 background.draw(canvas);
    300                 canvas.translate(-scrollX, -scrollY);
    301             }
    302         }
    303 
    304         // If text is transparent, don't draw any shadow
    305         if (getCurrentTextColor() == getResources().getColor(android.R.color.transparent)) {
    306             getPaint().clearShadowLayer();
    307             super.draw(canvas);
    308             return;
    309         }
    310 
    311         // We enhance the shadow by drawing the shadow twice
    312         getPaint().setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR);
    313         super.draw(canvas);
    314         canvas.save(Canvas.CLIP_SAVE_FLAG);
    315         canvas.clipRect(getScrollX(), getScrollY() + getExtendedPaddingTop(),
    316                 getScrollX() + getWidth(),
    317                 getScrollY() + getHeight(), Region.Op.INTERSECT);
    318         getPaint().setShadowLayer(SHADOW_SMALL_RADIUS, 0.0f, 0.0f, SHADOW_SMALL_COLOUR);
    319         super.draw(canvas);
    320         canvas.restore();
    321     }
    322 
    323     @Override
    324     protected void onAttachedToWindow() {
    325         super.onAttachedToWindow();
    326 
    327         if (mBackground != null) mBackground.setCallback(this);
    328         Drawable top = getCompoundDrawables()[1];
    329 
    330         if (top instanceof PreloadIconDrawable) {
    331             ((PreloadIconDrawable) top).applyTheme(getPreloaderTheme());
    332         }
    333         mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    334     }
    335 
    336     @Override
    337     protected void onDetachedFromWindow() {
    338         super.onDetachedFromWindow();
    339         if (mBackground != null) mBackground.setCallback(null);
    340     }
    341 
    342     @Override
    343     public void setTextColor(int color) {
    344         mTextColor = color;
    345         super.setTextColor(color);
    346     }
    347 
    348     @Override
    349     public void setTextColor(ColorStateList colors) {
    350         mTextColor = colors.getDefaultColor();
    351         super.setTextColor(colors);
    352     }
    353 
    354     public void setTextVisibility(boolean visible) {
    355         Resources res = getResources();
    356         if (visible) {
    357             super.setTextColor(mTextColor);
    358         } else {
    359             super.setTextColor(res.getColor(android.R.color.transparent));
    360         }
    361         mIsTextVisible = visible;
    362     }
    363 
    364     public boolean isTextVisible() {
    365         return mIsTextVisible;
    366     }
    367 
    368     @Override
    369     protected boolean onSetAlpha(int alpha) {
    370         return true;
    371     }
    372 
    373     @Override
    374     public void cancelLongPress() {
    375         super.cancelLongPress();
    376 
    377         mLongPressHelper.cancelLongPress();
    378     }
    379 
    380     public void applyState(boolean promiseStateChanged) {
    381         if (getTag() instanceof ShortcutInfo) {
    382             ShortcutInfo info = (ShortcutInfo) getTag();
    383             final boolean isPromise = info.isPromise();
    384             final int progressLevel = isPromise ?
    385                     ((info.hasStatusFlag(ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE) ?
    386                             info.getInstallProgress() : 0)) : 100;
    387 
    388             Drawable[] drawables = getCompoundDrawables();
    389             Drawable top = drawables[1];
    390             if (top != null) {
    391                 final PreloadIconDrawable preloadDrawable;
    392                 if (top instanceof PreloadIconDrawable) {
    393                     preloadDrawable = (PreloadIconDrawable) top;
    394                 } else {
    395                     preloadDrawable = new PreloadIconDrawable(top, getPreloaderTheme());
    396                     setCompoundDrawables(drawables[0], preloadDrawable, drawables[2], drawables[3]);
    397                 }
    398 
    399                 preloadDrawable.setLevel(progressLevel);
    400                 if (promiseStateChanged) {
    401                     preloadDrawable.maybePerformFinishedAnimation();
    402                 }
    403             }
    404         }
    405     }
    406 
    407     private Theme getPreloaderTheme() {
    408         Object tag = getTag();
    409         int style = ((tag != null) && (tag instanceof ShortcutInfo) &&
    410                 (((ShortcutInfo) tag).container >= 0)) ? R.style.PreloadIcon_Folder
    411                         : R.style.PreloadIcon;
    412         Theme theme = sPreloaderThemes.get(style);
    413         if (theme == null) {
    414             theme = getResources().newTheme();
    415             theme.applyStyle(style, true);
    416             sPreloaderThemes.put(style, theme);
    417         }
    418         return theme;
    419     }
    420 }
    421