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