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.Resources;
     21 import android.graphics.Bitmap;
     22 import android.graphics.Canvas;
     23 import android.graphics.Color;
     24 import android.graphics.Rect;
     25 import android.graphics.Region;
     26 import android.graphics.Region.Op;
     27 import android.graphics.drawable.Drawable;
     28 import android.util.AttributeSet;
     29 import android.util.TypedValue;
     30 import android.view.MotionEvent;
     31 import android.widget.TextView;
     32 
     33 /**
     34  * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan
     35  * because we want to make the bubble taller than the text and TextView's clip is
     36  * too aggressive.
     37  */
     38 public class BubbleTextView extends TextView {
     39     static final float SHADOW_LARGE_RADIUS = 4.0f;
     40     static final float SHADOW_SMALL_RADIUS = 1.75f;
     41     static final float SHADOW_Y_OFFSET = 2.0f;
     42     static final int SHADOW_LARGE_COLOUR = 0xDD000000;
     43     static final int SHADOW_SMALL_COLOUR = 0xCC000000;
     44     static final float PADDING_H = 8.0f;
     45     static final float PADDING_V = 3.0f;
     46 
     47     private int mPrevAlpha = -1;
     48 
     49     private HolographicOutlineHelper mOutlineHelper;
     50     private final Canvas mTempCanvas = new Canvas();
     51     private final Rect mTempRect = new Rect();
     52     private boolean mDidInvalidateForPressedState;
     53     private Bitmap mPressedOrFocusedBackground;
     54     private int mFocusedOutlineColor;
     55     private int mFocusedGlowColor;
     56     private int mPressedOutlineColor;
     57     private int mPressedGlowColor;
     58 
     59     private int mTextColor;
     60     private boolean mShadowsEnabled = true;
     61     private boolean mIsTextVisible;
     62 
     63     private boolean mBackgroundSizeChanged;
     64     private Drawable mBackground;
     65 
     66     private boolean mStayPressed;
     67     private CheckLongPressHelper mLongPressHelper;
     68 
     69     public BubbleTextView(Context context) {
     70         super(context);
     71         init();
     72     }
     73 
     74     public BubbleTextView(Context context, AttributeSet attrs) {
     75         super(context, attrs);
     76         init();
     77     }
     78 
     79     public BubbleTextView(Context context, AttributeSet attrs, int defStyle) {
     80         super(context, attrs, defStyle);
     81         init();
     82     }
     83 
     84     public void onFinishInflate() {
     85         super.onFinishInflate();
     86 
     87         // Ensure we are using the right text size
     88         LauncherAppState app = LauncherAppState.getInstance();
     89         DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
     90         setTextSize(TypedValue.COMPLEX_UNIT_SP, grid.iconTextSize);
     91         setTextColor(getResources().getColor(R.color.workspace_icon_text_color));
     92     }
     93 
     94     private void init() {
     95         mLongPressHelper = new CheckLongPressHelper(this);
     96         mBackground = getBackground();
     97 
     98         mOutlineHelper = HolographicOutlineHelper.obtain(getContext());
     99 
    100         final Resources res = getContext().getResources();
    101         mFocusedOutlineColor = mFocusedGlowColor = mPressedOutlineColor = mPressedGlowColor =
    102             res.getColor(R.color.outline_color);
    103 
    104         setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR);
    105     }
    106 
    107     public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache) {
    108         Bitmap b = info.getIcon(iconCache);
    109         LauncherAppState app = LauncherAppState.getInstance();
    110         DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
    111 
    112         setCompoundDrawables(null,
    113                 Utilities.createIconDrawable(b), null, null);
    114         setCompoundDrawablePadding((int) ((grid.folderIconSizePx - grid.iconSizePx) / 2f));
    115         setText(info.title);
    116         setTag(info);
    117     }
    118 
    119     @Override
    120     protected boolean setFrame(int left, int top, int right, int bottom) {
    121         if (getLeft() != left || getRight() != right || getTop() != top || getBottom() != bottom) {
    122             mBackgroundSizeChanged = true;
    123         }
    124         return super.setFrame(left, top, right, bottom);
    125     }
    126 
    127     @Override
    128     protected boolean verifyDrawable(Drawable who) {
    129         return who == mBackground || super.verifyDrawable(who);
    130     }
    131 
    132     @Override
    133     public void setTag(Object tag) {
    134         if (tag != null) {
    135             LauncherModel.checkItemInfo((ItemInfo) tag);
    136         }
    137         super.setTag(tag);
    138     }
    139 
    140     @Override
    141     protected void drawableStateChanged() {
    142         if (isPressed()) {
    143             // In this case, we have already created the pressed outline on ACTION_DOWN,
    144             // so we just need to do an invalidate to trigger draw
    145             if (!mDidInvalidateForPressedState) {
    146                 setCellLayoutPressedOrFocusedIcon();
    147             }
    148         } else {
    149             // Otherwise, either clear the pressed/focused background, or create a background
    150             // for the focused state
    151             final boolean backgroundEmptyBefore = mPressedOrFocusedBackground == null;
    152             if (!mStayPressed) {
    153                 mPressedOrFocusedBackground = null;
    154             }
    155             if (isFocused()) {
    156                 if (getLayout() == null) {
    157                     // In some cases, we get focus before we have been layed out. Set the
    158                     // background to null so that it will get created when the view is drawn.
    159                     mPressedOrFocusedBackground = null;
    160                 } else {
    161                     mPressedOrFocusedBackground = createGlowingOutline(
    162                             mTempCanvas, mFocusedGlowColor, mFocusedOutlineColor);
    163                 }
    164                 mStayPressed = false;
    165                 setCellLayoutPressedOrFocusedIcon();
    166             }
    167             final boolean backgroundEmptyNow = mPressedOrFocusedBackground == null;
    168             if (!backgroundEmptyBefore && backgroundEmptyNow) {
    169                 setCellLayoutPressedOrFocusedIcon();
    170             }
    171         }
    172 
    173         Drawable d = mBackground;
    174         if (d != null && d.isStateful()) {
    175             d.setState(getDrawableState());
    176         }
    177         super.drawableStateChanged();
    178     }
    179 
    180     /**
    181      * Draw this BubbleTextView into the given Canvas.
    182      *
    183      * @param destCanvas the canvas to draw on
    184      * @param padding the horizontal and vertical padding to use when drawing
    185      */
    186     private void drawWithPadding(Canvas destCanvas, int padding) {
    187         final Rect clipRect = mTempRect;
    188         getDrawingRect(clipRect);
    189 
    190         // adjust the clip rect so that we don't include the text label
    191         clipRect.bottom =
    192             getExtendedPaddingTop() - (int) BubbleTextView.PADDING_V + getLayout().getLineTop(0);
    193 
    194         // Draw the View into the bitmap.
    195         // The translate of scrollX and scrollY is necessary when drawing TextViews, because
    196         // they set scrollX and scrollY to large values to achieve centered text
    197         destCanvas.save();
    198         destCanvas.scale(getScaleX(), getScaleY(),
    199                 (getWidth() + padding) / 2, (getHeight() + padding) / 2);
    200         destCanvas.translate(-getScrollX() + padding / 2, -getScrollY() + padding / 2);
    201         destCanvas.clipRect(clipRect, Op.REPLACE);
    202         draw(destCanvas);
    203         destCanvas.restore();
    204     }
    205 
    206     /**
    207      * Returns a new bitmap to be used as the object outline, e.g. to visualize the drop location.
    208      * Responsibility for the bitmap is transferred to the caller.
    209      */
    210     private Bitmap createGlowingOutline(Canvas canvas, int outlineColor, int glowColor) {
    211         final int padding = mOutlineHelper.mMaxOuterBlurRadius;
    212         final Bitmap b = Bitmap.createBitmap(
    213                 getWidth() + padding, getHeight() + padding, Bitmap.Config.ARGB_8888);
    214 
    215         canvas.setBitmap(b);
    216         drawWithPadding(canvas, padding);
    217         mOutlineHelper.applyExtraThickExpensiveOutlineWithBlur(b, canvas, glowColor, outlineColor);
    218         canvas.setBitmap(null);
    219 
    220         return b;
    221     }
    222 
    223     @Override
    224     public boolean onTouchEvent(MotionEvent event) {
    225         // Call the superclass onTouchEvent first, because sometimes it changes the state to
    226         // isPressed() on an ACTION_UP
    227         boolean result = super.onTouchEvent(event);
    228 
    229         switch (event.getAction()) {
    230             case MotionEvent.ACTION_DOWN:
    231                 // So that the pressed outline is visible immediately when isPressed() is true,
    232                 // we pre-create it on ACTION_DOWN (it takes a small but perceptible amount of time
    233                 // to create it)
    234                 if (mPressedOrFocusedBackground == null) {
    235                     mPressedOrFocusedBackground = createGlowingOutline(
    236                             mTempCanvas, mPressedGlowColor, mPressedOutlineColor);
    237                 }
    238                 // Invalidate so the pressed state is visible, or set a flag so we know that we
    239                 // have to call invalidate as soon as the state is "pressed"
    240                 if (isPressed()) {
    241                     mDidInvalidateForPressedState = true;
    242                     setCellLayoutPressedOrFocusedIcon();
    243                 } else {
    244                     mDidInvalidateForPressedState = false;
    245                 }
    246 
    247                 mLongPressHelper.postCheckForLongPress();
    248                 break;
    249             case MotionEvent.ACTION_CANCEL:
    250             case MotionEvent.ACTION_UP:
    251                 // If we've touched down and up on an item, and it's still not "pressed", then
    252                 // destroy the pressed outline
    253                 if (!isPressed()) {
    254                     mPressedOrFocusedBackground = null;
    255                 }
    256 
    257                 mLongPressHelper.cancelLongPress();
    258                 break;
    259         }
    260         return result;
    261     }
    262 
    263     void setStayPressed(boolean stayPressed) {
    264         mStayPressed = stayPressed;
    265         if (!stayPressed) {
    266             mPressedOrFocusedBackground = null;
    267         }
    268         setCellLayoutPressedOrFocusedIcon();
    269     }
    270 
    271     void setCellLayoutPressedOrFocusedIcon() {
    272         if (getParent() instanceof ShortcutAndWidgetContainer) {
    273             ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) getParent();
    274             if (parent != null) {
    275                 CellLayout layout = (CellLayout) parent.getParent();
    276                 layout.setPressedOrFocusedIcon((mPressedOrFocusedBackground != null) ? this : null);
    277             }
    278         }
    279     }
    280 
    281     void clearPressedOrFocusedBackground() {
    282         mPressedOrFocusedBackground = null;
    283         setCellLayoutPressedOrFocusedIcon();
    284     }
    285 
    286     Bitmap getPressedOrFocusedBackground() {
    287         return mPressedOrFocusedBackground;
    288     }
    289 
    290     int getPressedOrFocusedBackgroundPadding() {
    291         return mOutlineHelper.mMaxOuterBlurRadius / 2;
    292     }
    293 
    294     @Override
    295     public void draw(Canvas canvas) {
    296         if (!mShadowsEnabled) {
    297             super.draw(canvas);
    298             return;
    299         }
    300 
    301         final Drawable background = mBackground;
    302         if (background != null) {
    303             final int scrollX = getScrollX();
    304             final int scrollY = getScrollY();
    305 
    306             if (mBackgroundSizeChanged) {
    307                 background.setBounds(0, 0,  getRight() - getLeft(), getBottom() - getTop());
    308                 mBackgroundSizeChanged = false;
    309             }
    310 
    311             if ((scrollX | scrollY) == 0) {
    312                 background.draw(canvas);
    313             } else {
    314                 canvas.translate(scrollX, scrollY);
    315                 background.draw(canvas);
    316                 canvas.translate(-scrollX, -scrollY);
    317             }
    318         }
    319 
    320         // If text is transparent, don't draw any shadow
    321         if (getCurrentTextColor() == getResources().getColor(android.R.color.transparent)) {
    322             getPaint().clearShadowLayer();
    323             super.draw(canvas);
    324             return;
    325         }
    326 
    327         // We enhance the shadow by drawing the shadow twice
    328         getPaint().setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR);
    329         super.draw(canvas);
    330         canvas.save(Canvas.CLIP_SAVE_FLAG);
    331         canvas.clipRect(getScrollX(), getScrollY() + getExtendedPaddingTop(),
    332                 getScrollX() + getWidth(),
    333                 getScrollY() + getHeight(), Region.Op.INTERSECT);
    334         getPaint().setShadowLayer(SHADOW_SMALL_RADIUS, 0.0f, 0.0f, SHADOW_SMALL_COLOUR);
    335         super.draw(canvas);
    336         canvas.restore();
    337     }
    338 
    339     @Override
    340     protected void onAttachedToWindow() {
    341         super.onAttachedToWindow();
    342         if (mBackground != null) mBackground.setCallback(this);
    343     }
    344 
    345     @Override
    346     protected void onDetachedFromWindow() {
    347         super.onDetachedFromWindow();
    348         if (mBackground != null) mBackground.setCallback(null);
    349     }
    350 
    351     @Override
    352     public void setTextColor(int color) {
    353         mTextColor = color;
    354         super.setTextColor(color);
    355     }
    356 
    357     public void setShadowsEnabled(boolean enabled) {
    358         mShadowsEnabled = enabled;
    359         getPaint().clearShadowLayer();
    360         invalidate();
    361     }
    362 
    363     public void setTextVisibility(boolean visible) {
    364         Resources res = getResources();
    365         if (visible) {
    366             super.setTextColor(mTextColor);
    367         } else {
    368             super.setTextColor(res.getColor(android.R.color.transparent));
    369         }
    370         mIsTextVisible = visible;
    371     }
    372 
    373     public boolean isTextVisible() {
    374         return mIsTextVisible;
    375     }
    376 
    377     @Override
    378     protected boolean onSetAlpha(int alpha) {
    379         if (mPrevAlpha != alpha) {
    380             mPrevAlpha = alpha;
    381             super.onSetAlpha(alpha);
    382         }
    383         return true;
    384     }
    385 
    386     @Override
    387     public void cancelLongPress() {
    388         super.cancelLongPress();
    389 
    390         mLongPressHelper.cancelLongPress();
    391     }
    392 }
    393