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 */
14package android.support.v17.leanback.widget;
15
16import android.content.Context;
17import android.content.res.Resources;
18import android.graphics.drawable.ColorDrawable;
19import android.graphics.drawable.Drawable;
20import android.support.v17.leanback.R;
21import android.support.v17.leanback.system.Settings;
22import android.view.ViewGroup;
23import 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 */
66public 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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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     */
281    public static boolean supportsShadow() {
282        return StaticShadowHelper.getInstance().supportsShadow();
283    }
284
285    /**
286     * Returns true if the platform sdk supports dynamic shadows.
287     */
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     */
295    public static boolean supportsRoundedCorner() {
296        return RoundedRectHelper.supportsRoundedCorner();
297    }
298
299    /**
300     * Returns true if view.setForeground() is supported.
301     */
302    public static boolean supportsForeground() {
303        return ForegroundHelper.supportsForeground();
304    }
305
306    /*
307     * hide from external, should be only created by ShadowOverlayHelper.Options.
308     */
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     */
316    public void prepareParentForShadow(ViewGroup parent) {
317        if (mShadowType == SHADOW_STATIC) {
318            StaticShadowHelper.getInstance().prepareParent(parent);
319        }
320    }
321
322    public int getShadowType() {
323        return mShadowType;
324    }
325
326    public boolean needsOverlay() {
327        return mNeedsOverlay;
328    }
329
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
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
443    static Object getNoneWrapperDynamicShadowImpl(View view) {
444        return view.getTag(R.id.lb_shadow_impl);
445    }
446
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