1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.tv.settings.widget;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.Canvas;
22import android.graphics.Matrix;
23import android.graphics.Rect;
24import android.graphics.RectF;
25import android.graphics.drawable.Drawable;
26import android.graphics.drawable.ShapeDrawable;
27import android.graphics.drawable.shapes.RectShape;
28import android.util.AttributeSet;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.ViewParent;
32import android.view.ViewDebug.ExportedProperty;
33import android.widget.FrameLayout;
34import android.widget.ImageView;
35
36import com.android.tv.settings.R;
37
38import java.util.ArrayList;
39
40/**
41 * Allows a drawable to be added for shadowing views in this layout. The shadows
42 * will automatically be sized to wrap their corresponding view. The default
43 * drawable to use can be set in xml by defining the namespace and then using
44 * defaultShadow="@drawable/reference"
45 * <p>
46 * In code views can then have Shadows added to them via
47 * {@link #addShadowView(View)} to use the default drawable or with
48 * {@link #addShadowView(View, Drawable)}.
49 */
50public class FrameLayoutWithShadows extends FrameLayout {
51
52    private static final int MAX_RECYCLE = 12;
53
54    static class ShadowView extends View {
55
56        private View shadowedView;
57        private Drawable mDrawableBottom;
58        private float mAlpha = 1f;
59
60        ShadowView(Context context) {
61            super(context);
62            setWillNotDraw(false);
63        }
64
65        void init() {
66            shadowedView = null;
67            mDrawableBottom = null;
68        }
69
70        @Override
71        public void setBackground(Drawable background) {
72            super.setBackground(background);
73            if (background != null) {
74                // framework adds a callback on background to trigger a repaint
75                // when call Drawable.setAlpha(),  this is not desired when we override
76                // setAlpha();  if we call Drawable.setAlpha() in the overriden
77                // setAlpha(),  it will trigger another repaint event thus cause system
78                // never stop rendering.
79                background.setCallback(null);
80                background.setAlpha((int)(255 * mAlpha));
81            }
82        }
83
84        @Override
85        public void setAlpha(float alpha) {
86            if (mAlpha != alpha) {
87                mAlpha = alpha;
88                Drawable d = getBackground();
89                int alphaMulitplied = (int)(alpha * 255);
90                if (d != null) {
91                    d.setAlpha(alphaMulitplied);
92                }
93                if (mDrawableBottom != null) {
94                    mDrawableBottom.setAlpha(alphaMulitplied);
95                }
96                invalidate();
97            }
98        }
99
100        @Override
101        @ExportedProperty(category = "drawing")
102        public float getAlpha() {
103            return mAlpha;
104        }
105
106        @Override
107        protected boolean onSetAlpha(int alpha) {
108            return true;
109        }
110
111        public void setDrawableBottom(Drawable drawable) {
112            mDrawableBottom = drawable;
113            if (mAlpha >= 0) {
114                mDrawableBottom.setAlpha((int)(255 * mAlpha));
115            }
116            invalidate();
117        }
118
119        @Override
120        protected void onDraw(Canvas canvas) {
121            // draw background 9 patch
122            super.onDraw(canvas);
123            // draw bottom
124            if (mDrawableBottom != null) {
125                mDrawableBottom.setBounds(getPaddingLeft(), getHeight() - getPaddingBottom(),
126                        getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()
127                        + mDrawableBottom.getIntrinsicHeight());
128                mDrawableBottom.draw(canvas);
129            }
130        }
131    }
132
133    private Rect rect = new Rect();
134    private RectF rectf = new RectF();
135    private int mShadowResourceId;
136    private int mBottomResourceId;
137    private float mShadowsAlpha = 1f;
138    private ArrayList<ShadowView> mRecycleBin = new ArrayList<ShadowView>(MAX_RECYCLE);
139
140    public FrameLayoutWithShadows(Context context) {
141        this(context, null);
142    }
143
144    public FrameLayoutWithShadows(Context context, AttributeSet attrs) {
145        this(context, attrs, 0);
146    }
147
148    public FrameLayoutWithShadows(Context context, AttributeSet attrs, int defStyle) {
149        super(context, attrs, defStyle);
150        initFromAttributes(context, attrs);
151    }
152
153    @Override
154    protected void onLayout(boolean changed, int l, int t, int r, int b) {
155        super.onLayout(changed, l, t, r, b);
156        layoutShadows();
157    }
158
159    private void initFromAttributes(Context context, AttributeSet attrs) {
160        if (attrs == null) {
161            return;
162        }
163        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FrameLayoutWithShadows);
164
165        setDefaultShadowResourceId(a.getResourceId(
166                R.styleable.FrameLayoutWithShadows_defaultShadow, 0));
167        setDrawableBottomResourceId(a.getResourceId(
168                R.styleable.FrameLayoutWithShadows_drawableBottom, 0));
169
170        a.recycle();
171    }
172
173    public void setDefaultShadowResourceId(int id) {
174        mShadowResourceId = id;
175    }
176
177    public int getDefaultShadowResourceId() {
178        return mShadowResourceId;
179    }
180
181    public void setDrawableBottomResourceId(int id) {
182        mBottomResourceId = id;
183    }
184
185    public int getDrawableBottomResourceId() {
186        return mBottomResourceId;
187    }
188
189    public void setShadowsAlpha(float alpha) {
190        mShadowsAlpha = alpha;
191        for (int i = getChildCount() - 1; i >= 0; i--) {
192            View shadow = getChildAt(i);
193            if (shadow instanceof ShadowView) {
194                shadow.setAlpha(alpha);
195            }
196        }
197    }
198
199    /**
200     * prune shadow views whose related view was detached from FrameLayoutWithShadows
201     */
202    private void prune() {
203        if (getWindowToken() ==null) {
204            return;
205        }
206        for (int i = getChildCount() - 1; i >= 0; i--) {
207            View shadow = getChildAt(i);
208            if (shadow instanceof ShadowView) {
209                ShadowView shadowView = (ShadowView) shadow;
210                View view = shadowView.shadowedView;
211                if (this != findParentShadowsView(view)) {
212                    view.setTag(R.id.ShadowView, null);
213                    shadowView.shadowedView = null;
214                    removeView(shadowView);
215                    addToRecycleBin(shadowView);
216                }
217            }
218        }
219    }
220
221    /**
222     * Perform a layout of the shadow views. This is done as part of the layout
223     * pass for the view but may also be triggered manually if the borders of a
224     * child view has changed.
225     */
226    public void layoutShadows() {
227        prune();
228        for (int i = getChildCount() - 1; i >= 0; i--) {
229            View shadow = getChildAt(i);
230            if (!(shadow instanceof ShadowView)) {
231                continue;
232            }
233            ShadowView shadowView = (ShadowView) shadow;
234            View view = shadowView.shadowedView;
235            if (view != null) {
236                if (this != findParentShadowsView(view)) {
237                    continue;
238                }
239                boolean isImageMatrix = false;
240                if (view instanceof ImageView) {
241                    // For ImageView, we get the draw bounds of the image drawable,
242                    // which could be smaller than the imageView depending on ScaleType.
243                    Matrix matrix = ((ImageView) view).getImageMatrix();
244                    Drawable drawable = ((ImageView) view).getDrawable();
245                    if (drawable != null) {
246                        isImageMatrix = true;
247                        rect.set(drawable.getBounds());
248                        rectf.set(rect);
249                        matrix.mapRect(rectf);
250                        rectf.offset(view.getPaddingLeft(), view.getPaddingTop());
251                        rectf.intersect(view.getPaddingLeft(), view.getPaddingTop(),
252                                view.getWidth() - view.getPaddingLeft() - view.getPaddingRight(),
253                                view.getHeight() - view.getPaddingTop() - view.getPaddingBottom());
254                        rectf.left -= shadow.getPaddingLeft();
255                        rectf.top -= shadow.getPaddingTop();
256                        rectf.right += shadow.getPaddingRight();
257                        rectf.bottom += shadow.getPaddingBottom();
258                        rect.left = (int) (rectf.left + 0.5f);
259                        rect.top = (int) (rectf.top + 0.5f);
260                        rect.right = (int) (rectf.right + 0.5f);
261                        rect.bottom = (int) (rectf.bottom + 0.5f);
262                    }
263                }
264                if (!isImageMatrix){
265                    rect.left = view.getPaddingLeft() - shadow.getPaddingLeft();
266                    rect.top = view.getPaddingTop() - shadow.getPaddingTop();
267                    rect.right = view.getWidth() + view.getPaddingRight()
268                            + shadow.getPaddingRight();
269                    rect.bottom = view.getHeight() + view.getPaddingBottom()
270                            + shadow.getPaddingBottom();
271                }
272                offsetDescendantRectToMyCoords(view, rect);
273                shadow.layout(rect.left, rect.top, rect.right, rect.bottom);
274            }
275        }
276    }
277
278    /**
279     * Add a shadow view to FrameLayoutWithShadows. This will use the drawable
280     * specified for the shadow view and will also handle clean-up of any
281     * previous shadow set for this view.
282     */
283    public View addShadowView(View view, Drawable shadow) {
284        ShadowView shadowView = (ShadowView) view.getTag(R.id.ShadowView);
285        if (shadowView == null) {
286            shadowView = getFromRecycleBin();
287            if (shadowView == null) {
288                shadowView = new ShadowView(getContext());
289                shadowView.setLayoutParams(new LayoutParams(0, 0));
290            }
291            view.setTag(R.id.ShadowView, shadowView);
292            shadowView.shadowedView = view;
293            addView(shadowView, 0);
294        }
295        shadow.mutate();
296        shadowView.setAlpha(mShadowsAlpha);
297        shadowView.setBackground(shadow);
298        if (mBottomResourceId != 0) {
299            Drawable d = getResources().getDrawable(mBottomResourceId);
300            shadowView.setDrawableBottom(d.mutate());
301        }
302        return shadowView;
303    }
304
305    /**
306     * Add a shadow view using the default shadow. This will also handle
307     * clean-up of any previous shadow set for this view.
308     */
309    public View addShadowView(View view) {
310        Drawable shadow = null;
311        if (mShadowResourceId != 0) {
312            shadow = getContext().getResources().getDrawable(mShadowResourceId);
313        } else {
314            return null;
315        }
316        return addShadowView(view, shadow);
317    }
318
319    /**
320     * Get the shadow associated with the given view. Returns null if the view
321     * does not have a shadow.
322     */
323    public static View getShadowView(View view) {
324        View shadowView = (View) view.getTag(R.id.ShadowView);
325        if (shadowView != null) {
326            return shadowView;
327        }
328        return null;
329    }
330
331    public void setShadowViewUnderline(View shadowView, int underlineColor, int heightInPx) {
332        ShapeDrawable drawable = new ShapeDrawable();
333        drawable.setShape(new RectShape());
334        drawable.setIntrinsicHeight(heightInPx);
335        drawable.getPaint().setColor(underlineColor);
336        ((ShadowView) shadowView).setDrawableBottom(drawable);
337    }
338
339    public void setShadowViewUnderline(View shadowView, Drawable drawable) {
340        ((ShadowView) shadowView).setDrawableBottom(drawable);
341    }
342
343    /**
344     * Makes the shadow associated with the given view draw above other views.
345     * Subsequent calls to this or changes to the z-order may move the shadow
346     * back down in the z-order.
347     */
348    public void bringViewShadowToTop(View view) {
349        View shadowView = (View) view.getTag(R.id.ShadowView);
350        if (shadowView == null) {
351            return;
352        }
353        int index = indexOfChild(shadowView);
354        if (index < 0) {
355            // not found
356            return;
357        }
358        int lastIndex = getChildCount() - 1;
359        if (lastIndex == index) {
360            // already last one
361            return;
362        }
363        View lastShadowView = getChildAt(lastIndex);
364        if (!(lastShadowView instanceof ShadowView)) {
365            removeView(shadowView);
366            addView(shadowView);
367        } else {
368            removeView(lastShadowView);
369            removeView(shadowView);
370            addView(lastShadowView, 0);
371            addView(shadowView);
372        }
373    }
374
375    /**
376     * Utility function to remove the shadow associated with the given view.
377     */
378    public static void removeShadowView(View view) {
379        ShadowView shadowView = (ShadowView) view.getTag(R.id.ShadowView);
380        if (shadowView != null) {
381            view.setTag(R.id.ShadowView, null);
382            shadowView.shadowedView = null;
383            if (shadowView.getRootView() != null) {
384                ViewParent parent = shadowView.getParent();
385                if (parent instanceof ViewGroup) {
386                    ((ViewGroup) parent).removeView(shadowView);
387                    if (parent instanceof FrameLayoutWithShadows) {
388                        ((FrameLayoutWithShadows) parent).addToRecycleBin(shadowView);
389                    }
390                }
391            }
392        }
393    }
394
395    private void addToRecycleBin(ShadowView shadowView) {
396        if (mRecycleBin.size() < MAX_RECYCLE) {
397            mRecycleBin.add(shadowView);
398        }
399    }
400
401    public ShadowView getFromRecycleBin() {
402        int size = mRecycleBin.size();
403        if (size > 0) {
404            ShadowView view = mRecycleBin.remove(size - 1);
405            view.init();
406        }
407        return null;
408    }
409
410    /**
411     * Sets the visibility of the shadow associated with the given view. This
412     * should be called when the view's visibility changes to keep the shadow's
413     * visibility in sync.
414     */
415    public void setShadowVisibility(View view, int visibility) {
416        View shadowView = (View) view.getTag(R.id.ShadowView);
417        if (shadowView != null) {
418            shadowView.setVisibility(visibility);
419            return;
420        }
421    }
422
423    /**
424     * Finds the first parent of this view that is a FrameLayoutWithShadows and
425     * returns that or null if there is none.
426     */
427    public static FrameLayoutWithShadows findParentShadowsView(View view) {
428        ViewParent nextView = view.getParent();
429        while (nextView != null && !(nextView instanceof FrameLayoutWithShadows)) {
430            nextView = nextView.getParent();
431        }
432        return (FrameLayoutWithShadows) nextView;
433    }
434}
435