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