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