1/*
2 * Copyright (C) 2014 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.graphics.Canvas;
18import android.graphics.Color;
19import android.graphics.Paint;
20import android.graphics.Rect;
21import android.support.annotation.ColorInt;
22import android.support.v17.leanback.R;
23import android.util.AttributeSet;
24import android.view.View;
25import android.view.ViewGroup;
26import android.widget.FrameLayout;
27
28/**
29 * Provides an SDK version-independent wrapper to support shadows, color overlays, and rounded
30 * corners.  It's not always preferred to create a ShadowOverlayContainer, use
31 * {@link ShadowOverlayHelper} instead.
32 * <p>
33 * {@link #prepareParentForShadow(ViewGroup)} must be called on parent of container
34 * before using shadow.  Depending on sdk version, optical bounds might be applied
35 * to parent.
36 * </p>
37 * <p>
38 * If shadows can appear outside the bounds of the parent view, setClipChildren(false) must
39 * be called on the grandparent view.
40 * </p>
41 * <p>
42 * {@link #initialize(boolean, boolean, boolean)} must be first called on the container.
43 * Then call {@link #wrap(View)} to insert the wrapped view into the container.
44 * </p>
45 * <p>
46 * Call {@link #setShadowFocusLevel(float)} to control the strength of the shadow (focused shadows
47 * cast stronger shadows).
48 * </p>
49 * <p>
50 * Call {@link #setOverlayColor(int)} to control overlay color.
51 * </p>
52 */
53public class ShadowOverlayContainer extends FrameLayout {
54
55    /**
56     * No shadow.
57     */
58    public static final int SHADOW_NONE = ShadowOverlayHelper.SHADOW_NONE;
59
60    /**
61     * Shadows are fixed.
62     */
63    public static final int SHADOW_STATIC = ShadowOverlayHelper.SHADOW_STATIC;
64
65    /**
66     * Shadows depend on the size, shape, and position of the view.
67     */
68    public static final int SHADOW_DYNAMIC = ShadowOverlayHelper.SHADOW_DYNAMIC;
69
70    private boolean mInitialized;
71    private Object mShadowImpl;
72    private View mWrappedView;
73    private boolean mRoundedCorners;
74    private int mShadowType = SHADOW_NONE;
75    private float mUnfocusedZ;
76    private float mFocusedZ;
77    private int mRoundedCornerRadius;
78    private static final Rect sTempRect = new Rect();
79    private Paint mOverlayPaint;
80    int mOverlayColor;
81
82    /**
83     * Create ShadowOverlayContainer and auto select shadow type.
84     */
85    public ShadowOverlayContainer(Context context) {
86        this(context, null, 0);
87    }
88
89    /**
90     * Create ShadowOverlayContainer and auto select shadow type.
91     */
92    public ShadowOverlayContainer(Context context, AttributeSet attrs) {
93        this(context, attrs, 0);
94    }
95
96    /**
97     * Create ShadowOverlayContainer and auto select shadow type.
98     */
99    public ShadowOverlayContainer(Context context, AttributeSet attrs, int defStyle) {
100        super(context, attrs, defStyle);
101        useStaticShadow();
102        useDynamicShadow();
103    }
104
105    /**
106     * Create ShadowOverlayContainer with specific shadowType.
107     */
108    ShadowOverlayContainer(Context context,
109            int shadowType, boolean hasColorDimOverlay,
110            float unfocusedZ, float focusedZ, int roundedCornerRadius) {
111        super(context);
112        mUnfocusedZ = unfocusedZ;
113        mFocusedZ = focusedZ;
114        initialize(shadowType, hasColorDimOverlay, roundedCornerRadius);
115    }
116
117    /**
118     * Return true if the platform sdk supports shadow.
119     */
120    public static boolean supportsShadow() {
121        return StaticShadowHelper.getInstance().supportsShadow();
122    }
123
124    /**
125     * Returns true if the platform sdk supports dynamic shadows.
126     */
127    public static boolean supportsDynamicShadow() {
128        return ShadowHelper.getInstance().supportsDynamicShadow();
129    }
130
131    /**
132     * {@link #prepareParentForShadow(ViewGroup)} must be called on parent of container
133     * before using shadow.  Depending on sdk version, optical bounds might be applied
134     * to parent.
135     */
136    public static void prepareParentForShadow(ViewGroup parent) {
137        StaticShadowHelper.getInstance().prepareParent(parent);
138    }
139
140    /**
141     * Sets the shadow type to {@link #SHADOW_DYNAMIC} if supported.
142     */
143    public void useDynamicShadow() {
144        useDynamicShadow(getResources().getDimension(R.dimen.lb_material_shadow_normal_z),
145                getResources().getDimension(R.dimen.lb_material_shadow_focused_z));
146    }
147
148    /**
149     * Sets the shadow type to {@link #SHADOW_DYNAMIC} if supported and sets the elevation/Z
150     * values to the given parameters.
151     */
152    public void useDynamicShadow(float unfocusedZ, float focusedZ) {
153        if (mInitialized) {
154            throw new IllegalStateException("Already initialized");
155        }
156        if (supportsDynamicShadow()) {
157            mShadowType = SHADOW_DYNAMIC;
158            mUnfocusedZ = unfocusedZ;
159            mFocusedZ = focusedZ;
160        }
161    }
162
163    /**
164     * Sets the shadow type to {@link #SHADOW_STATIC} if supported.
165     */
166    public void useStaticShadow() {
167        if (mInitialized) {
168            throw new IllegalStateException("Already initialized");
169        }
170        if (supportsShadow()) {
171            mShadowType = SHADOW_STATIC;
172        }
173    }
174
175    /**
176     * Returns the shadow type, one of {@link #SHADOW_NONE}, {@link #SHADOW_STATIC}, or
177     * {@link #SHADOW_DYNAMIC}.
178     */
179    public int getShadowType() {
180        return mShadowType;
181    }
182
183    /**
184     * Initialize shadows, color overlay.
185     * @deprecated use {@link ShadowOverlayHelper#createShadowOverlayContainer(Context)} instead.
186     */
187    @Deprecated
188    public void initialize(boolean hasShadow, boolean hasColorDimOverlay) {
189        initialize(hasShadow, hasColorDimOverlay, true);
190    }
191
192    /**
193     * Initialize shadows, color overlay, and rounded corners.  All are optional.
194     * Shadow type are auto-selected based on {@link #useStaticShadow()} and
195     * {@link #useDynamicShadow()} call.
196     * @deprecated use {@link ShadowOverlayHelper#createShadowOverlayContainer(Context)} instead.
197     */
198    @Deprecated
199    public void initialize(boolean hasShadow, boolean hasColorDimOverlay, boolean roundedCorners) {
200        int shadowType;
201        if (!hasShadow) {
202            shadowType = SHADOW_NONE;
203        } else {
204            shadowType = mShadowType;
205        }
206        int roundedCornerRadius = roundedCorners ? getContext().getResources().getDimensionPixelSize(
207                R.dimen.lb_rounded_rect_corner_radius) : 0;
208        initialize(shadowType, hasColorDimOverlay, roundedCornerRadius);
209    }
210
211    /**
212     * Initialize shadows, color overlay, and rounded corners.  All are optional.
213     */
214    void initialize(int shadowType, boolean hasColorDimOverlay, int roundedCornerRadius) {
215        if (mInitialized) {
216            throw new IllegalStateException();
217        }
218        mInitialized = true;
219        mRoundedCornerRadius = roundedCornerRadius;
220        mRoundedCorners = roundedCornerRadius > 0;
221        mShadowType = shadowType;
222        switch (mShadowType) {
223            case SHADOW_DYNAMIC:
224                mShadowImpl = ShadowHelper.getInstance().addDynamicShadow(
225                        this, mUnfocusedZ, mFocusedZ, mRoundedCornerRadius);
226                break;
227            case SHADOW_STATIC:
228                mShadowImpl = StaticShadowHelper.getInstance().addStaticShadow(this);
229                break;
230        }
231        if (hasColorDimOverlay) {
232            setWillNotDraw(false);
233            mOverlayColor = Color.TRANSPARENT;
234            mOverlayPaint = new Paint();
235            mOverlayPaint.setColor(mOverlayColor);
236            mOverlayPaint.setStyle(Paint.Style.FILL);
237        } else {
238            setWillNotDraw(true);
239            mOverlayPaint = null;
240        }
241    }
242
243    @Override
244    public void draw(Canvas canvas) {
245        super.draw(canvas);
246        if (mOverlayPaint != null && mOverlayColor != Color.TRANSPARENT) {
247            canvas.drawRect(mWrappedView.getLeft(), mWrappedView.getTop(),
248                    mWrappedView.getRight(), mWrappedView.getBottom(),
249                    mOverlayPaint);
250        }
251    }
252
253    /**
254     * Set shadow focus level (0 to 1). 0 for unfocused, 1f for fully focused.
255     */
256    public void setShadowFocusLevel(float level) {
257        if (mShadowImpl != null) {
258            ShadowOverlayHelper.setShadowFocusLevel(mShadowImpl, mShadowType, level);
259        }
260    }
261
262    /**
263     * Set color (with alpha) of the overlay.
264     */
265    public void setOverlayColor(@ColorInt int overlayColor) {
266        if (mOverlayPaint != null) {
267            if (overlayColor != mOverlayColor) {
268                mOverlayColor = overlayColor;
269                mOverlayPaint.setColor(overlayColor);
270                invalidate();
271            }
272        }
273    }
274
275    /**
276     * Inserts view into the wrapper.
277     */
278    public void wrap(View view) {
279        if (!mInitialized || mWrappedView != null) {
280            throw new IllegalStateException();
281        }
282        ViewGroup.LayoutParams lp = view.getLayoutParams();
283        if (lp != null) {
284            // if wrapped view has layout params, inherit everything but width/height.
285            // Wrapped view is assigned a FrameLayout.LayoutParams with width and height only.
286            // Margins, etc are assigned to the wrapper and take effect in parent container.
287            ViewGroup.LayoutParams wrapped_lp = new FrameLayout.LayoutParams(lp.width, lp.height);
288            // Uses MATCH_PARENT for MATCH_PARENT, WRAP_CONTENT for WRAP_CONTENT and fixed size,
289            // App can still change wrapped view fixed width/height afterwards.
290            lp.width = lp.width == LayoutParams.MATCH_PARENT
291                    ? LayoutParams.MATCH_PARENT : LayoutParams.WRAP_CONTENT;
292            lp.height = lp.height == LayoutParams.MATCH_PARENT
293                    ? LayoutParams.MATCH_PARENT : LayoutParams.WRAP_CONTENT;
294            this.setLayoutParams(lp);
295            addView(view, wrapped_lp);
296        } else {
297            addView(view);
298        }
299        if (mRoundedCorners && mShadowType == SHADOW_STATIC) {
300            RoundedRectHelper.getInstance().setClipToRoundedOutline(view, true);
301        }
302        mWrappedView = view;
303    }
304
305    /**
306     * Returns the wrapper view.
307     */
308    public View getWrappedView() {
309        return mWrappedView;
310    }
311
312    @Override
313    protected void onLayout(boolean changed, int l, int t, int r, int b) {
314        super.onLayout(changed, l, t, r, b);
315        if (changed && mWrappedView != null) {
316            sTempRect.left = (int) mWrappedView.getPivotX();
317            sTempRect.top = (int) mWrappedView.getPivotY();
318            offsetDescendantRectToMyCoords(mWrappedView, sTempRect);
319            setPivotX(sTempRect.left);
320            setPivotY(sTempRect.top);
321        }
322    }
323
324    @Override
325    public boolean hasOverlappingRendering() {
326        return false;
327    }
328}
329