ShadowOverlayHelper.java revision ce4c2014042fe6e4723bab30741039848adcf4be
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.os.Build;
21import android.support.v17.leanback.R;
22import android.support.v17.leanback.system.Settings;
23import android.util.AttributeSet;
24import android.view.ViewGroup;
25import android.view.ViewGroup.LayoutParams;
26import 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 */
69public 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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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     */
284    public static boolean supportsShadow() {
285        return StaticShadowHelper.getInstance().supportsShadow();
286    }
287
288    /**
289     * Returns true if the platform sdk supports dynamic shadows.
290     */
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     */
298    public static boolean supportsRoundedCorner() {
299        return RoundedRectHelper.supportsRoundedCorner();
300    }
301
302    /**
303     * Returns true if view.setForeground() is supported.
304     */
305    public static boolean supportsForeground() {
306        return ForegroundHelper.supportsForeground();
307    }
308
309    /*
310     * hide from external, should be only created by ShadowOverlayHelper.Options.
311     */
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     */
319    public void prepareParentForShadow(ViewGroup parent) {
320        if (mShadowType == SHADOW_STATIC) {
321            StaticShadowHelper.getInstance().prepareParent(parent);
322        }
323    }
324
325    public int getShadowType() {
326        return mShadowType;
327    }
328
329    public boolean needsOverlay() {
330        return mNeedsOverlay;
331    }
332
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
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
446    static Object getNoneWrapperDyamicShadowImpl(View view) {
447        return view.getTag(R.id.lb_shadow_impl);
448    }
449
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