1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 package androidx.leanback.widget;
15 
16 import android.content.Context;
17 import android.content.res.Resources;
18 import android.graphics.drawable.ColorDrawable;
19 import android.graphics.drawable.Drawable;
20 import android.view.View;
21 import android.view.ViewGroup;
22 
23 import androidx.leanback.R;
24 import androidx.leanback.system.Settings;
25 
26 
27 /**
28  * ShadowOverlayHelper is a helper class for shadow, overlay color and rounded corner.
29  * There are many choices to implement Shadow, overlay color.
30  * Initialize it with ShadowOverlayHelper.Builder and it decides the best strategy based
31  * on options user choose and current platform version.
32  *
33  * <li> For shadow:  it may use 9-patch with opticalBounds or Z-value based shadow for
34  *                   API >= 21.  When 9-patch is used, it requires a ShadowOverlayContainer
35  *                   to include 9-patch views.
36  * <li> For overlay: it may use ShadowOverlayContainer which overrides draw() or it may
37  *                   use setForeground(new ColorDrawable()) for API>=23.  The foreground support
38  *                   might be disabled if rounded corner is applied due to performance reason.
39  * <li> For rounded-corner:  it uses a ViewOutlineProvider for API>=21.
40  *
41  * There are two different strategies: use Wrapper with a ShadowOverlayContainer;
42  * or apply rounded corner, overlay and rounded-corner to the view itself.  Below is an example
43  * of how helper is used.
44  *
45  * <code>
46  * ShadowOverlayHelper mHelper = new ShadowOverlayHelper.Builder().
47  *         .needsOverlay(true).needsRoundedCorner(true).needsShadow(true)
48  *         .build();
49  * mHelper.prepareParentForShadow(parentView); // apply optical-bounds for 9-patch shadow.
50  * mHelper.setOverlayColor(view, Color.argb(0x80, 0x80, 0x80, 0x80));
51  * mHelper.setShadowFocusLevel(view, 1.0f);
52  * ...
53  * View initializeView(View view) {
54  *     if (mHelper.needsWrapper()) {
55  *         ShadowOverlayContainer wrapper = mHelper.createShadowOverlayContainer(context);
56  *         wrapper.wrap(view);
57  *         return wrapper;
58  *     } else {
59  *         mHelper.onViewCreated(view);
60  *         return view;
61  *     }
62  * }
63  * ...
64  *
65  * </code>
66  */
67 public final class ShadowOverlayHelper {
68 
69     /**
70      * Builder for creating ShadowOverlayHelper.
71      */
72     public static final class Builder {
73 
74         private boolean needsOverlay;
75         private boolean needsRoundedCorner;
76         private boolean needsShadow;
77         private boolean preferZOrder = true;
78         private boolean keepForegroundDrawable;
79         private Options options = Options.DEFAULT;
80 
81         /**
82          * Set if needs overlay color.
83          * @param needsOverlay   True if needs overlay.
84          * @return  The Builder object itself.
85          */
needsOverlay(boolean needsOverlay)86         public Builder needsOverlay(boolean needsOverlay) {
87             this.needsOverlay = needsOverlay;
88             return this;
89         }
90 
91         /**
92          * Set if needs shadow.
93          * @param needsShadow   True if needs shadow.
94          * @return  The Builder object itself.
95          */
needsShadow(boolean needsShadow)96         public Builder needsShadow(boolean needsShadow) {
97             this.needsShadow = needsShadow;
98             return this;
99         }
100 
101         /**
102          * Set if needs rounded corner.
103          * @param needsRoundedCorner   True if needs rounded corner.
104          * @return  The Builder object itself.
105          */
needsRoundedCorner(boolean needsRoundedCorner)106         public Builder needsRoundedCorner(boolean needsRoundedCorner) {
107             this.needsRoundedCorner = needsRoundedCorner;
108             return this;
109         }
110 
111         /**
112          * Set if prefer z-order shadow.  On old devices,  z-order shadow might be slow,
113          * set to false to fall back to static 9-patch shadow.  Recommend to read
114          * from system wide Setting value: see {@link Settings}.
115          *
116          * @param preferZOrder   True if prefer Z shadow.  Default is true.
117          * @return The Builder object itself.
118          */
preferZOrder(boolean preferZOrder)119         public Builder preferZOrder(boolean preferZOrder) {
120             this.preferZOrder = preferZOrder;
121             return this;
122         }
123 
124         /**
125          * Set if not using foreground drawable for overlay color.  For example if
126          * the view has already assigned a foreground drawable for other purposes.
127          * When it's true, helper will use a ShadowOverlayContainer for overlay color.
128          *
129          * @param keepForegroundDrawable   True to keep the original foreground drawable.
130          * @return The Builder object itself.
131          */
keepForegroundDrawable(boolean keepForegroundDrawable)132         public Builder keepForegroundDrawable(boolean keepForegroundDrawable) {
133             this.keepForegroundDrawable = keepForegroundDrawable;
134             return this;
135         }
136 
137         /**
138          * Set option values e.g. Shadow Z value, rounded corner radius.
139          *
140          * @param options   The Options object to create ShadowOverlayHelper.
141          */
options(Options options)142         public Builder options(Options options) {
143             this.options = options;
144             return this;
145         }
146 
147         /**
148          * Create ShadowOverlayHelper object
149          * @param context    The context uses to read Resources settings.
150          * @return           The ShadowOverlayHelper object.
151          */
build(Context context)152         public ShadowOverlayHelper build(Context context) {
153             final ShadowOverlayHelper helper = new ShadowOverlayHelper();
154             helper.mNeedsOverlay = needsOverlay;
155             helper.mNeedsRoundedCorner = needsRoundedCorner && supportsRoundedCorner();
156             helper.mNeedsShadow = needsShadow && supportsShadow();
157 
158             if (helper.mNeedsRoundedCorner) {
159                 helper.setupRoundedCornerRadius(options, context);
160             }
161 
162             // figure out shadow type and if we need use wrapper:
163             if (helper.mNeedsShadow) {
164                 // if static shadow is preferred or dynamic shadow is not supported,
165                 // use static shadow,  otherwise use dynamic shadow.
166                 if (!preferZOrder || !supportsDynamicShadow()) {
167                     helper.mShadowType = SHADOW_STATIC;
168                     // static shadow requires ShadowOverlayContainer to support crossfading
169                     // of two shadow views.
170                     helper.mNeedsWrapper = true;
171                 } else {
172                     helper.mShadowType = SHADOW_DYNAMIC;
173                     helper.setupDynamicShadowZ(options, context);
174                     helper.mNeedsWrapper = ((!supportsForeground() || keepForegroundDrawable)
175                             && helper.mNeedsOverlay);
176                 }
177             } else {
178                 helper.mShadowType = SHADOW_NONE;
179                 helper.mNeedsWrapper = ((!supportsForeground() || keepForegroundDrawable)
180                         && helper.mNeedsOverlay);
181             }
182 
183             return helper;
184         }
185 
186     }
187 
188     /**
189      * Option values for ShadowOverlayContainer.
190      */
191     public static final class Options {
192 
193         /**
194          * Default Options for values.
195          */
196         public static final Options DEFAULT = new Options();
197 
198         private int roundedCornerRadius = 0; // 0 for default value
199         private float dynamicShadowUnfocusedZ = -1; // < 0 for default value
200         private float dynamicShadowFocusedZ = -1;   // < 0 for default value
201         /**
202          * Set value of rounded corner radius.
203          *
204          * @param roundedCornerRadius   Number of pixels of rounded corner radius.
205          *                              Set to 0 to use default settings.
206          * @return  The Options object itself.
207          */
roundedCornerRadius(int roundedCornerRadius)208         public Options roundedCornerRadius(int roundedCornerRadius){
209             this.roundedCornerRadius = roundedCornerRadius;
210             return this;
211         }
212 
213         /**
214          * Set value of focused and unfocused Z value for shadow.
215          *
216          * @param unfocusedZ   Number of pixels for unfocused Z value.
217          * @param focusedZ     Number of pixels for focused Z value.
218          * @return  The Options object itself.
219          */
dynamicShadowZ(float unfocusedZ, float focusedZ)220         public Options dynamicShadowZ(float unfocusedZ, float focusedZ){
221             this.dynamicShadowUnfocusedZ = unfocusedZ;
222             this.dynamicShadowFocusedZ = focusedZ;
223             return this;
224         }
225 
226         /**
227          * Get radius of rounded corner in pixels.
228          *
229          * @return Radius of rounded corner in pixels.
230          */
getRoundedCornerRadius()231         public final int getRoundedCornerRadius() {
232             return roundedCornerRadius;
233         }
234 
235         /**
236          * Get z value of shadow when a view is not focused.
237          *
238          * @return Z value of shadow when a view is not focused.
239          */
getDynamicShadowUnfocusedZ()240         public final float getDynamicShadowUnfocusedZ() {
241             return dynamicShadowUnfocusedZ;
242         }
243 
244         /**
245          * Get z value of shadow when a view is focused.
246          *
247          * @return Z value of shadow when a view is focused.
248          */
getDynamicShadowFocusedZ()249         public final float getDynamicShadowFocusedZ() {
250             return dynamicShadowFocusedZ;
251         }
252     }
253 
254     /**
255      * No shadow.
256      */
257     public static final int SHADOW_NONE = 1;
258 
259     /**
260      * Shadows are fixed.
261      */
262     public static final int SHADOW_STATIC = 2;
263 
264     /**
265      * Shadows depend on the size, shape, and position of the view.
266      */
267     public static final int SHADOW_DYNAMIC = 3;
268 
269     int mShadowType = SHADOW_NONE;
270     boolean mNeedsOverlay;
271     boolean mNeedsRoundedCorner;
272     boolean mNeedsShadow;
273     boolean mNeedsWrapper;
274 
275     int mRoundedCornerRadius;
276     float mUnfocusedZ;
277     float mFocusedZ;
278 
279     /**
280      * Return true if the platform sdk supports shadow.
281      */
supportsShadow()282     public static boolean supportsShadow() {
283         return StaticShadowHelper.supportsShadow();
284     }
285 
286     /**
287      * Returns true if the platform sdk supports dynamic shadows.
288      */
supportsDynamicShadow()289     public static boolean supportsDynamicShadow() {
290         return ShadowHelper.supportsDynamicShadow();
291     }
292 
293     /**
294      * Returns true if the platform sdk supports rounded corner through outline.
295      */
supportsRoundedCorner()296     public static boolean supportsRoundedCorner() {
297         return RoundedRectHelper.supportsRoundedCorner();
298     }
299 
300     /**
301      * Returns true if view.setForeground() is supported.
302      */
supportsForeground()303     public static boolean supportsForeground() {
304         return ForegroundHelper.supportsForeground();
305     }
306 
307     /*
308      * hide from external, should be only created by ShadowOverlayHelper.Options.
309      */
ShadowOverlayHelper()310     ShadowOverlayHelper() {
311     }
312 
313     /**
314      * {@link #prepareParentForShadow(ViewGroup)} must be called on parent of container
315      * before using shadow.  Depending on Shadow type, optical bounds might be applied.
316      */
prepareParentForShadow(ViewGroup parent)317     public void prepareParentForShadow(ViewGroup parent) {
318         if (mShadowType == SHADOW_STATIC) {
319             StaticShadowHelper.prepareParent(parent);
320         }
321     }
322 
getShadowType()323     public int getShadowType() {
324         return mShadowType;
325     }
326 
needsOverlay()327     public boolean needsOverlay() {
328         return mNeedsOverlay;
329     }
330 
needsRoundedCorner()331     public boolean needsRoundedCorner() {
332         return mNeedsRoundedCorner;
333     }
334 
335     /**
336      * Returns true if a "wrapper" ShadowOverlayContainer is needed.
337      * When needsWrapper() is true,  call {@link #createShadowOverlayContainer(Context)}
338      * to create the wrapper.
339      */
needsWrapper()340     public boolean needsWrapper() {
341         return mNeedsWrapper;
342     }
343 
344     /**
345      * Create ShadowOverlayContainer for this helper.
346      * @param context   Context to create view.
347      * @return          ShadowOverlayContainer.
348      */
createShadowOverlayContainer(Context context)349     public ShadowOverlayContainer createShadowOverlayContainer(Context context) {
350         if (!needsWrapper()) {
351             throw new IllegalArgumentException();
352         }
353         return new ShadowOverlayContainer(context, mShadowType, mNeedsOverlay,
354                 mUnfocusedZ, mFocusedZ, mRoundedCornerRadius);
355     }
356 
357     /**
358      * Set overlay color for view other than ShadowOverlayContainer.
359      * See also {@link ShadowOverlayContainer#setOverlayColor(int)}.
360      */
setNoneWrapperOverlayColor(View view, int color)361     public static void setNoneWrapperOverlayColor(View view, int color) {
362         Drawable d = ForegroundHelper.getForeground(view);
363         if (d instanceof ColorDrawable) {
364             ((ColorDrawable) d).setColor(color);
365         } else {
366             ForegroundHelper.setForeground(view, new ColorDrawable(color));
367         }
368     }
369 
370     /**
371      * Set overlay color for view, it can be a ShadowOverlayContainer if needsWrapper() is true,
372      * or other view type.
373      */
setOverlayColor(View view, int color)374     public void setOverlayColor(View view, int color) {
375         if (needsWrapper()) {
376             ((ShadowOverlayContainer) view).setOverlayColor(color);
377         } else {
378             setNoneWrapperOverlayColor(view, color);
379         }
380     }
381 
382     /**
383      * Must be called when view is created for cases {@link #needsWrapper()} is false.
384      * @param view
385      */
onViewCreated(View view)386     public void onViewCreated(View view) {
387         if (!needsWrapper()) {
388             if (!mNeedsShadow) {
389                 if (mNeedsRoundedCorner) {
390                     RoundedRectHelper.setClipToRoundedOutline(view, true, mRoundedCornerRadius);
391                 }
392             } else {
393                 if (mShadowType == SHADOW_DYNAMIC) {
394                     Object tag = ShadowHelper.addDynamicShadow(
395                             view, mUnfocusedZ, mFocusedZ, mRoundedCornerRadius);
396                     view.setTag(R.id.lb_shadow_impl, tag);
397                 } else if (mNeedsRoundedCorner) {
398                     RoundedRectHelper.setClipToRoundedOutline(view, true, mRoundedCornerRadius);
399                 }
400             }
401         }
402     }
403 
404     /**
405      * Set shadow focus level (0 to 1). 0 for unfocused, 1 for fully focused.
406      * This is for view other than ShadowOverlayContainer.
407      * See also {@link ShadowOverlayContainer#setShadowFocusLevel(float)}.
408      */
setNoneWrapperShadowFocusLevel(View view, float level)409     public static void setNoneWrapperShadowFocusLevel(View view, float level) {
410         setShadowFocusLevel(getNoneWrapperDynamicShadowImpl(view), SHADOW_DYNAMIC, level);
411     }
412 
413     /**
414      * Set shadow focus level (0 to 1). 0 for unfocused, 1 for fully focused.
415      */
setShadowFocusLevel(View view, float level)416     public void setShadowFocusLevel(View view, float level) {
417         if (needsWrapper()) {
418             ((ShadowOverlayContainer) view).setShadowFocusLevel(level);
419         } else {
420             setShadowFocusLevel(getNoneWrapperDynamicShadowImpl(view), SHADOW_DYNAMIC, level);
421         }
422     }
423 
setupDynamicShadowZ(Options options, Context context)424     void setupDynamicShadowZ(Options options, Context context) {
425         if (options.getDynamicShadowUnfocusedZ() < 0f) {
426             Resources res = context.getResources();
427             mFocusedZ = res.getDimension(R.dimen.lb_material_shadow_focused_z);
428             mUnfocusedZ = res.getDimension(R.dimen.lb_material_shadow_normal_z);
429         } else {
430             mFocusedZ = options.getDynamicShadowFocusedZ();
431             mUnfocusedZ = options.getDynamicShadowUnfocusedZ();
432         }
433     }
434 
setupRoundedCornerRadius(Options options, Context context)435     void setupRoundedCornerRadius(Options options, Context context) {
436         if (options.getRoundedCornerRadius() == 0) {
437             Resources res = context.getResources();
438             mRoundedCornerRadius = res.getDimensionPixelSize(
439                         R.dimen.lb_rounded_rect_corner_radius);
440         } else {
441             mRoundedCornerRadius = options.getRoundedCornerRadius();
442         }
443     }
444 
getNoneWrapperDynamicShadowImpl(View view)445     static Object getNoneWrapperDynamicShadowImpl(View view) {
446         return view.getTag(R.id.lb_shadow_impl);
447     }
448 
setShadowFocusLevel(Object impl, int shadowType, float level)449     static void setShadowFocusLevel(Object impl, int shadowType, float level) {
450         if (impl != null) {
451             if (level < 0f) {
452                 level = 0f;
453             } else if (level > 1f) {
454                 level = 1f;
455             }
456             switch (shadowType) {
457                 case SHADOW_DYNAMIC:
458                     ShadowHelper.setShadowFocusLevel(impl, level);
459                     break;
460                 case SHADOW_STATIC:
461                     StaticShadowHelper.setShadowFocusLevel(impl, level);
462                     break;
463             }
464         }
465     }
466 }
467