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