1 /*
2  * Copyright (C) 2014 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.tv.settings.widget;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.graphics.Canvas;
22 import android.graphics.Matrix;
23 import android.graphics.Rect;
24 import android.graphics.RectF;
25 import android.graphics.drawable.Drawable;
26 import android.graphics.drawable.ShapeDrawable;
27 import android.graphics.drawable.shapes.RectShape;
28 import android.util.AttributeSet;
29 import android.view.View;
30 import android.view.ViewDebug.ExportedProperty;
31 import android.view.ViewGroup;
32 import android.view.ViewParent;
33 import android.widget.FrameLayout;
34 import android.widget.ImageView;
35 
36 import com.android.tv.settings.R;
37 
38 import java.util.ArrayList;
39 
40 /**
41  * Allows a drawable to be added for shadowing views in this layout. The shadows
42  * will automatically be sized to wrap their corresponding view. The default
43  * drawable to use can be set in xml by defining the namespace and then using
44  * defaultShadow="@drawable/reference"
45  * <p>
46  * In code views can then have Shadows added to them via
47  * {@link #addShadowView(View)} to use the default drawable or with
48  * {@link #addShadowView(View, Drawable)}.
49  */
50 public class FrameLayoutWithShadows extends FrameLayout {
51 
52     private static final int MAX_RECYCLE = 12;
53 
54     static class ShadowView extends View {
55 
56         private View shadowedView;
57         private Drawable mDrawableBottom;
58         private float mAlpha = 1f;
59 
ShadowView(Context context)60         ShadowView(Context context) {
61             super(context);
62             setWillNotDraw(false);
63         }
64 
init()65         void init() {
66             shadowedView = null;
67             mDrawableBottom = null;
68         }
69 
70         @Override
setBackground(Drawable background)71         public void setBackground(Drawable background) {
72             super.setBackground(background);
73             if (background != null) {
74                 // framework adds a callback on background to trigger a repaint
75                 // when call Drawable.setAlpha(),  this is not desired when we override
76                 // setAlpha();  if we call Drawable.setAlpha() in the overriden
77                 // setAlpha(),  it will trigger another repaint event thus cause system
78                 // never stop rendering.
79                 background.setCallback(null);
80                 background.setAlpha((int)(255 * mAlpha));
81             }
82         }
83 
84         @Override
setAlpha(float alpha)85         public void setAlpha(float alpha) {
86             if (mAlpha != alpha) {
87                 mAlpha = alpha;
88                 Drawable d = getBackground();
89                 int alphaMulitplied = (int)(alpha * 255);
90                 if (d != null) {
91                     d.setAlpha(alphaMulitplied);
92                 }
93                 if (mDrawableBottom != null) {
94                     mDrawableBottom.setAlpha(alphaMulitplied);
95                 }
96                 invalidate();
97             }
98         }
99 
100         @Override
101         @ExportedProperty(category = "drawing")
getAlpha()102         public float getAlpha() {
103             return mAlpha;
104         }
105 
106         @Override
onSetAlpha(int alpha)107         protected boolean onSetAlpha(int alpha) {
108             return true;
109         }
110 
setDrawableBottom(Drawable drawable)111         public void setDrawableBottom(Drawable drawable) {
112             mDrawableBottom = drawable;
113             if (mAlpha >= 0) {
114                 mDrawableBottom.setAlpha((int)(255 * mAlpha));
115             }
116             invalidate();
117         }
118 
119         @Override
onDraw(Canvas canvas)120         protected void onDraw(Canvas canvas) {
121             // draw background 9 patch
122             super.onDraw(canvas);
123             // draw bottom
124             if (mDrawableBottom != null) {
125                 mDrawableBottom.setBounds(getPaddingLeft(), getHeight() - getPaddingBottom(),
126                         getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()
127                         + mDrawableBottom.getIntrinsicHeight());
128                 mDrawableBottom.draw(canvas);
129             }
130         }
131     }
132 
133     private final Rect rect = new Rect();
134     private final RectF rectf = new RectF();
135     private int mShadowResourceId;
136     private int mBottomResourceId;
137     private float mShadowsAlpha = 1f;
138     private final ArrayList<ShadowView> mRecycleBin = new ArrayList<>(MAX_RECYCLE);
139 
FrameLayoutWithShadows(Context context)140     public FrameLayoutWithShadows(Context context) {
141         this(context, null);
142     }
143 
FrameLayoutWithShadows(Context context, AttributeSet attrs)144     public FrameLayoutWithShadows(Context context, AttributeSet attrs) {
145         this(context, attrs, 0);
146     }
147 
FrameLayoutWithShadows(Context context, AttributeSet attrs, int defStyle)148     public FrameLayoutWithShadows(Context context, AttributeSet attrs, int defStyle) {
149         super(context, attrs, defStyle);
150         initFromAttributes(context, attrs);
151     }
152 
153     @Override
onLayout(boolean changed, int l, int t, int r, int b)154     protected void onLayout(boolean changed, int l, int t, int r, int b) {
155         super.onLayout(changed, l, t, r, b);
156         layoutShadows();
157     }
158 
initFromAttributes(Context context, AttributeSet attrs)159     private void initFromAttributes(Context context, AttributeSet attrs) {
160         if (attrs == null) {
161             return;
162         }
163         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FrameLayoutWithShadows);
164 
165         setDefaultShadowResourceId(a.getResourceId(
166                 R.styleable.FrameLayoutWithShadows_defaultShadow, 0));
167         setDrawableBottomResourceId(a.getResourceId(
168                 R.styleable.FrameLayoutWithShadows_drawableBottom, 0));
169 
170         a.recycle();
171     }
172 
setDefaultShadowResourceId(int id)173     public void setDefaultShadowResourceId(int id) {
174         mShadowResourceId = id;
175     }
176 
getDefaultShadowResourceId()177     public int getDefaultShadowResourceId() {
178         return mShadowResourceId;
179     }
180 
setDrawableBottomResourceId(int id)181     public void setDrawableBottomResourceId(int id) {
182         mBottomResourceId = id;
183     }
184 
getDrawableBottomResourceId()185     public int getDrawableBottomResourceId() {
186         return mBottomResourceId;
187     }
188 
setShadowsAlpha(float alpha)189     public void setShadowsAlpha(float alpha) {
190         mShadowsAlpha = alpha;
191         for (int i = getChildCount() - 1; i >= 0; i--) {
192             View shadow = getChildAt(i);
193             if (shadow instanceof ShadowView) {
194                 shadow.setAlpha(alpha);
195             }
196         }
197     }
198 
199     /**
200      * prune shadow views whose related view was detached from FrameLayoutWithShadows
201      */
prune()202     private void prune() {
203         if (getWindowToken() ==null) {
204             return;
205         }
206         for (int i = getChildCount() - 1; i >= 0; i--) {
207             View shadow = getChildAt(i);
208             if (shadow instanceof ShadowView) {
209                 ShadowView shadowView = (ShadowView) shadow;
210                 View view = shadowView.shadowedView;
211                 if (this != findParentShadowsView(view)) {
212                     view.setTag(R.id.ShadowView, null);
213                     shadowView.shadowedView = null;
214                     removeView(shadowView);
215                     addToRecycleBin(shadowView);
216                 }
217             }
218         }
219     }
220 
221     /**
222      * Perform a layout of the shadow views. This is done as part of the layout
223      * pass for the view but may also be triggered manually if the borders of a
224      * child view has changed.
225      */
layoutShadows()226     public void layoutShadows() {
227         prune();
228         for (int i = getChildCount() - 1; i >= 0; i--) {
229             View shadow = getChildAt(i);
230             if (!(shadow instanceof ShadowView)) {
231                 continue;
232             }
233             ShadowView shadowView = (ShadowView) shadow;
234             View view = shadowView.shadowedView;
235             if (view != null) {
236                 if (this != findParentShadowsView(view)) {
237                     continue;
238                 }
239                 boolean isImageMatrix = false;
240                 if (view instanceof ImageView) {
241                     // For ImageView, we get the draw bounds of the image drawable,
242                     // which could be smaller than the imageView depending on ScaleType.
243                     Matrix matrix = ((ImageView) view).getImageMatrix();
244                     Drawable drawable = ((ImageView) view).getDrawable();
245                     if (drawable != null) {
246                         isImageMatrix = true;
247                         rect.set(drawable.getBounds());
248                         rectf.set(rect);
249                         matrix.mapRect(rectf);
250                         rectf.offset(view.getPaddingLeft(), view.getPaddingTop());
251                         rectf.intersect(view.getPaddingLeft(), view.getPaddingTop(),
252                                 view.getWidth() - view.getPaddingLeft() - view.getPaddingRight(),
253                                 view.getHeight() - view.getPaddingTop() - view.getPaddingBottom());
254                         rectf.left -= shadow.getPaddingLeft();
255                         rectf.top -= shadow.getPaddingTop();
256                         rectf.right += shadow.getPaddingRight();
257                         rectf.bottom += shadow.getPaddingBottom();
258                         rect.left = (int) (rectf.left + 0.5f);
259                         rect.top = (int) (rectf.top + 0.5f);
260                         rect.right = (int) (rectf.right + 0.5f);
261                         rect.bottom = (int) (rectf.bottom + 0.5f);
262                     }
263                 }
264                 if (!isImageMatrix){
265                     rect.left = view.getPaddingLeft() - shadow.getPaddingLeft();
266                     rect.top = view.getPaddingTop() - shadow.getPaddingTop();
267                     rect.right = view.getWidth() + view.getPaddingRight()
268                             + shadow.getPaddingRight();
269                     rect.bottom = view.getHeight() + view.getPaddingBottom()
270                             + shadow.getPaddingBottom();
271                 }
272                 offsetDescendantRectToMyCoords(view, rect);
273                 shadow.layout(rect.left, rect.top, rect.right, rect.bottom);
274             }
275         }
276     }
277 
278     /**
279      * Add a shadow view to FrameLayoutWithShadows. This will use the drawable
280      * specified for the shadow view and will also handle clean-up of any
281      * previous shadow set for this view.
282      */
addShadowView(View view, Drawable shadow)283     public View addShadowView(View view, Drawable shadow) {
284         ShadowView shadowView = (ShadowView) view.getTag(R.id.ShadowView);
285         if (shadowView == null) {
286             shadowView = getFromRecycleBin();
287             if (shadowView == null) {
288                 shadowView = new ShadowView(getContext());
289                 shadowView.setLayoutParams(new LayoutParams(0, 0));
290             }
291             view.setTag(R.id.ShadowView, shadowView);
292             shadowView.shadowedView = view;
293             addView(shadowView, 0);
294         }
295         shadow.mutate();
296         shadowView.setAlpha(mShadowsAlpha);
297         shadowView.setBackground(shadow);
298         if (mBottomResourceId != 0) {
299             Drawable d = getContext().getDrawable(mBottomResourceId);
300             shadowView.setDrawableBottom(d.mutate());
301         }
302         return shadowView;
303     }
304 
305     /**
306      * Add a shadow view using the default shadow. This will also handle
307      * clean-up of any previous shadow set for this view.
308      */
addShadowView(View view)309     public View addShadowView(View view) {
310         final Drawable shadow;
311         if (mShadowResourceId != 0) {
312             shadow = getContext().getDrawable(mShadowResourceId);
313         } else {
314             return null;
315         }
316         return addShadowView(view, shadow);
317     }
318 
319     /**
320      * Get the shadow associated with the given view. Returns null if the view
321      * does not have a shadow.
322      */
getShadowView(View view)323     public static View getShadowView(View view) {
324         View shadowView = (View) view.getTag(R.id.ShadowView);
325         if (shadowView != null) {
326             return shadowView;
327         }
328         return null;
329     }
330 
setShadowViewUnderline(View shadowView, int underlineColor, int heightInPx)331     public void setShadowViewUnderline(View shadowView, int underlineColor, int heightInPx) {
332         ShapeDrawable drawable = new ShapeDrawable();
333         drawable.setShape(new RectShape());
334         drawable.setIntrinsicHeight(heightInPx);
335         drawable.getPaint().setColor(underlineColor);
336         ((ShadowView) shadowView).setDrawableBottom(drawable);
337     }
338 
setShadowViewUnderline(View shadowView, Drawable drawable)339     public void setShadowViewUnderline(View shadowView, Drawable drawable) {
340         ((ShadowView) shadowView).setDrawableBottom(drawable);
341     }
342 
343     /**
344      * Makes the shadow associated with the given view draw above other views.
345      * Subsequent calls to this or changes to the z-order may move the shadow
346      * back down in the z-order.
347      */
bringViewShadowToTop(View view)348     public void bringViewShadowToTop(View view) {
349         View shadowView = (View) view.getTag(R.id.ShadowView);
350         if (shadowView == null) {
351             return;
352         }
353         int index = indexOfChild(shadowView);
354         if (index < 0) {
355             // not found
356             return;
357         }
358         int lastIndex = getChildCount() - 1;
359         if (lastIndex == index) {
360             // already last one
361             return;
362         }
363         View lastShadowView = getChildAt(lastIndex);
364         if (!(lastShadowView instanceof ShadowView)) {
365             removeView(shadowView);
366             addView(shadowView);
367         } else {
368             removeView(lastShadowView);
369             removeView(shadowView);
370             addView(lastShadowView, 0);
371             addView(shadowView);
372         }
373     }
374 
375     /**
376      * Utility function to remove the shadow associated with the given view.
377      */
removeShadowView(View view)378     public static void removeShadowView(View view) {
379         ShadowView shadowView = (ShadowView) view.getTag(R.id.ShadowView);
380         if (shadowView != null) {
381             view.setTag(R.id.ShadowView, null);
382             shadowView.shadowedView = null;
383             if (shadowView.getRootView() != null) {
384                 ViewParent parent = shadowView.getParent();
385                 if (parent instanceof ViewGroup) {
386                     ((ViewGroup) parent).removeView(shadowView);
387                     if (parent instanceof FrameLayoutWithShadows) {
388                         ((FrameLayoutWithShadows) parent).addToRecycleBin(shadowView);
389                     }
390                 }
391             }
392         }
393     }
394 
addToRecycleBin(ShadowView shadowView)395     private void addToRecycleBin(ShadowView shadowView) {
396         if (mRecycleBin.size() < MAX_RECYCLE) {
397             mRecycleBin.add(shadowView);
398         }
399     }
400 
getFromRecycleBin()401     public ShadowView getFromRecycleBin() {
402         int size = mRecycleBin.size();
403         if (size > 0) {
404             ShadowView view = mRecycleBin.remove(size - 1);
405             view.init();
406         }
407         return null;
408     }
409 
410     /**
411      * Sets the visibility of the shadow associated with the given view. This
412      * should be called when the view's visibility changes to keep the shadow's
413      * visibility in sync.
414      */
setShadowVisibility(View view, int visibility)415     public void setShadowVisibility(View view, int visibility) {
416         View shadowView = (View) view.getTag(R.id.ShadowView);
417         if (shadowView != null) {
418             shadowView.setVisibility(visibility);
419             return;
420         }
421     }
422 
423     /**
424      * Finds the first parent of this view that is a FrameLayoutWithShadows and
425      * returns that or null if there is none.
426      */
findParentShadowsView(View view)427     public static FrameLayoutWithShadows findParentShadowsView(View view) {
428         ViewParent nextView = view.getParent();
429         while (nextView != null && !(nextView instanceof FrameLayoutWithShadows)) {
430             nextView = nextView.getParent();
431         }
432         return (FrameLayoutWithShadows) nextView;
433     }
434 }
435