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