1/*
2 * Copyright (C) 2015 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 */
16package android.support.v17.leanback.graphics;
17
18import android.content.res.Resources;
19import android.graphics.Canvas;
20import android.graphics.ColorFilter;
21import android.graphics.PixelFormat;
22import android.graphics.Rect;
23import android.graphics.drawable.Drawable;
24import android.support.annotation.NonNull;
25import android.support.v17.leanback.graphics.BoundsRule.ValueRule;
26import android.support.v4.graphics.drawable.DrawableCompat;
27import android.util.Property;
28
29import java.util.ArrayList;
30
31/**
32 * Generic drawable class that can be composed of multiple children. Whenever the bounds changes
33 * for this class, it updates those of its children.
34 */
35public class CompositeDrawable extends Drawable implements Drawable.Callback {
36
37    static class CompositeState extends Drawable.ConstantState {
38
39        final ArrayList<ChildDrawable> mChildren;
40
41        CompositeState() {
42            mChildren = new ArrayList<ChildDrawable>();
43        }
44
45        CompositeState(CompositeState other, CompositeDrawable parent, Resources res) {
46            final int n = other.mChildren.size();
47            mChildren = new ArrayList<ChildDrawable>(n);
48            for (int k = 0; k < n; k++) {
49                mChildren.add(new ChildDrawable(other.mChildren.get(k), parent, res));
50            }
51        }
52
53        @NonNull
54        @Override
55        public Drawable newDrawable() {
56            return new CompositeDrawable(this);
57        }
58
59        @Override
60        public int getChangingConfigurations() {
61            return 0;
62        }
63
64    }
65
66    CompositeState mState;
67    boolean mMutated = false;
68
69    public CompositeDrawable() {
70        mState = new CompositeState();
71    }
72
73    CompositeDrawable(CompositeState state) {
74        mState = state;
75    }
76
77    @Override
78    public ConstantState getConstantState() {
79        return mState;
80    }
81
82    @Override
83    public Drawable mutate() {
84        if (!mMutated && super.mutate() == this) {
85            mState = new CompositeState(mState, this, null);
86            final ArrayList<ChildDrawable> children = mState.mChildren;
87            for (int i = 0, n = children.size(); i < n; i++) {
88                final Drawable dr = children.get(i).mDrawable;
89                if (dr != null) {
90                    dr.mutate();
91                }
92            }
93            mMutated = true;
94        }
95        return this;
96    }
97
98    /**
99     * Adds the supplied region.
100     */
101    public void addChildDrawable(Drawable drawable) {
102        mState.mChildren.add(new ChildDrawable(drawable, this));
103    }
104
105    /**
106     * Sets the supplied region at given index.
107     */
108    public void setChildDrawableAt(int index, Drawable drawable) {
109        mState.mChildren.set(index, new ChildDrawable(drawable, this));
110    }
111
112    /**
113     * Returns the {@link Drawable} for the given index.
114     */
115    public Drawable getDrawable(int index) {
116        return mState.mChildren.get(index).mDrawable;
117    }
118
119    /**
120     * Returns the {@link ChildDrawable} at the given index.
121     */
122    public ChildDrawable getChildAt(int index) {
123        return mState.mChildren.get(index);
124    }
125
126    /**
127     * Removes the child corresponding to the given index.
128     */
129    public void removeChild(int index) {
130        mState.mChildren.remove(index);
131    }
132
133    /**
134     * Removes the given region.
135     */
136    public void removeDrawable(Drawable drawable) {
137        final ArrayList<ChildDrawable> children = mState.mChildren;
138        for (int i = 0; i < children.size(); i++) {
139            if (drawable == children.get(i).mDrawable) {
140                children.get(i).mDrawable.setCallback(null);
141                children.remove(i);
142                return;
143            }
144        }
145    }
146
147    /**
148     * Returns the total number of children.
149     */
150    public int getChildCount() {
151        return mState.mChildren.size();
152    }
153
154    @Override
155    public void draw(Canvas canvas) {
156        final ArrayList<ChildDrawable> children = mState.mChildren;
157        for (int i = 0; i < children.size(); i++) {
158            children.get(i).mDrawable.draw(canvas);
159        }
160    }
161
162    @Override
163    protected void onBoundsChange(Rect bounds) {
164        super.onBoundsChange(bounds);
165        updateBounds(bounds);
166    }
167
168    @Override
169    public void setColorFilter(ColorFilter colorFilter) {
170        final ArrayList<ChildDrawable> children = mState.mChildren;
171        for (int i = 0; i < children.size(); i++) {
172            children.get(i).mDrawable.setColorFilter(colorFilter);
173        }
174    }
175
176    @Override
177    public int getOpacity() {
178        return PixelFormat.UNKNOWN;
179    }
180
181    @Override
182    public void setAlpha(int alpha) {
183        final ArrayList<ChildDrawable> children = mState.mChildren;
184        for (int i = 0; i < children.size(); i++) {
185            children.get(i).mDrawable.setAlpha(alpha);
186        }
187    }
188
189    /**
190     * @return Alpha value between 0(inclusive) and 255(inclusive)
191     */
192    @Override
193    public int getAlpha() {
194        final Drawable dr = getFirstNonNullDrawable();
195        if (dr != null) {
196            return DrawableCompat.getAlpha(dr);
197        } else {
198            return 0xFF;
199        }
200    }
201
202    final Drawable getFirstNonNullDrawable() {
203        final ArrayList<ChildDrawable> children = mState.mChildren;
204        for (int i = 0, n = children.size(); i < n; i++) {
205            final Drawable dr = children.get(i).mDrawable;
206            if (dr != null) {
207                return dr;
208            }
209        }
210        return null;
211    }
212
213    @Override
214    public void invalidateDrawable(Drawable who) {
215        invalidateSelf();
216    }
217
218    @Override
219    public void scheduleDrawable(Drawable who, Runnable what, long when) {
220        scheduleSelf(what, when);
221    }
222
223    @Override
224    public void unscheduleDrawable(Drawable who, Runnable what) {
225        unscheduleSelf(what);
226    }
227
228    /**
229     * Updates the bounds based on the {@link BoundsRule}.
230     */
231    void updateBounds(Rect bounds) {
232        final ArrayList<ChildDrawable> children = mState.mChildren;
233        for (int i = 0; i < children.size(); i++) {
234            ChildDrawable childDrawable = children.get(i);
235            childDrawable.updateBounds(bounds);
236        }
237    }
238
239    /**
240     * Wrapper class holding a drawable object and {@link BoundsRule} to update drawable bounds
241     * when parent bound changes.
242     */
243    public static final class ChildDrawable {
244        private final BoundsRule mBoundsRule;
245        private final Drawable mDrawable;
246        private final Rect adjustedBounds = new Rect();
247        final CompositeDrawable mParent;
248
249        public ChildDrawable(Drawable drawable, CompositeDrawable parent) {
250            this.mDrawable = drawable;
251            this.mParent = parent;
252            this.mBoundsRule = new BoundsRule();
253            drawable.setCallback(parent);
254        }
255
256        ChildDrawable(ChildDrawable orig, CompositeDrawable parent, Resources res) {
257            final Drawable dr = orig.mDrawable;
258            final Drawable clone;
259            if (dr != null) {
260                final ConstantState cs = dr.getConstantState();
261                if (res != null) {
262                    clone = cs.newDrawable(res);
263                } else {
264                    clone = cs.newDrawable();
265                }
266                clone.setCallback(parent);
267                DrawableCompat.setLayoutDirection(clone, DrawableCompat.getLayoutDirection(dr));
268                clone.setBounds(dr.getBounds());
269                clone.setLevel(dr.getLevel());
270            } else {
271                clone = null;
272            }
273            if (orig.mBoundsRule != null) {
274                this.mBoundsRule = new BoundsRule(orig.mBoundsRule);
275            } else {
276                this.mBoundsRule = new BoundsRule();
277            }
278            mDrawable = clone;
279            mParent = parent;
280        }
281
282        /**
283         * Returns the instance of {@link BoundsRule}.
284         */
285        public BoundsRule getBoundsRule() {
286            return this.mBoundsRule;
287        }
288
289        /**
290         * Returns the {@link Drawable}.
291         */
292        public Drawable getDrawable() {
293            return mDrawable;
294        }
295
296        /**
297         * Updates the bounds based on the {@link BoundsRule}.
298         */
299        void updateBounds(Rect bounds) {
300            mBoundsRule.calculateBounds(bounds, adjustedBounds);
301            mDrawable.setBounds(adjustedBounds);
302        }
303
304        /**
305         * After changing the {@link BoundsRule}, user should call this function
306         * for the drawable to recalculate its bounds.
307         */
308        public void recomputeBounds() {
309            updateBounds(mParent.getBounds());
310        }
311
312        /**
313         * Implementation of {@link Property} for overrideTop attribute.
314         */
315        public static final Property<CompositeDrawable.ChildDrawable, Integer> TOP_ABSOLUTE =
316                new Property<CompositeDrawable.ChildDrawable, Integer>(
317                        Integer.class, "absoluteTop") {
318            @Override
319            public void set(CompositeDrawable.ChildDrawable obj, Integer value) {
320                if (obj.getBoundsRule().top == null) {
321                    obj.getBoundsRule().top = ValueRule.absoluteValue(value);
322                } else {
323                    obj.getBoundsRule().top.setAbsoluteValue(value);
324                }
325
326                obj.recomputeBounds();
327            }
328
329            @Override
330            public Integer get(CompositeDrawable.ChildDrawable obj) {
331                if (obj.getBoundsRule().top == null) {
332                    return obj.mParent.getBounds().top;
333                }
334                return obj.getBoundsRule().top.getAbsoluteValue();
335            }
336        };
337
338        /**
339         * Implementation of {@link Property} for overrideBottom attribute.
340         */
341        public static final Property<CompositeDrawable.ChildDrawable, Integer> BOTTOM_ABSOLUTE =
342                new Property<CompositeDrawable.ChildDrawable, Integer>(
343                        Integer.class, "absoluteBottom") {
344            @Override
345            public void set(CompositeDrawable.ChildDrawable obj, Integer value) {
346                if (obj.getBoundsRule().bottom == null) {
347                    obj.getBoundsRule().bottom = ValueRule.absoluteValue(value);
348                } else {
349                    obj.getBoundsRule().bottom.setAbsoluteValue(value);
350                }
351
352                obj.recomputeBounds();
353            }
354
355            @Override
356            public Integer get(CompositeDrawable.ChildDrawable obj) {
357                if (obj.getBoundsRule().bottom == null) {
358                    return obj.mParent.getBounds().bottom;
359                }
360                return obj.getBoundsRule().bottom.getAbsoluteValue();
361            }
362        };
363
364
365        /**
366         * Implementation of {@link Property} for overrideLeft attribute.
367         */
368        public static final Property<CompositeDrawable.ChildDrawable, Integer> LEFT_ABSOLUTE =
369                new Property<CompositeDrawable.ChildDrawable, Integer>(
370                        Integer.class, "absoluteLeft") {
371            @Override
372            public void set(CompositeDrawable.ChildDrawable obj, Integer value) {
373                if (obj.getBoundsRule().left == null) {
374                    obj.getBoundsRule().left = ValueRule.absoluteValue(value);
375                } else {
376                    obj.getBoundsRule().left.setAbsoluteValue(value);
377                }
378
379                obj.recomputeBounds();
380            }
381
382            @Override
383            public Integer get(CompositeDrawable.ChildDrawable obj) {
384                if (obj.getBoundsRule().left == null) {
385                    return obj.mParent.getBounds().left;
386                }
387                return obj.getBoundsRule().left.getAbsoluteValue();
388            }
389        };
390
391        /**
392         * Implementation of {@link Property} for overrideRight attribute.
393         */
394        public static final Property<CompositeDrawable.ChildDrawable, Integer> RIGHT_ABSOLUTE =
395                new Property<CompositeDrawable.ChildDrawable, Integer>(
396                        Integer.class, "absoluteRight") {
397            @Override
398            public void set(CompositeDrawable.ChildDrawable obj, Integer value) {
399                if (obj.getBoundsRule().right == null) {
400                    obj.getBoundsRule().right = ValueRule.absoluteValue(value);
401                } else {
402                    obj.getBoundsRule().right.setAbsoluteValue(value);
403                }
404
405                obj.recomputeBounds();
406            }
407
408            @Override
409            public Integer get(CompositeDrawable.ChildDrawable obj) {
410                if (obj.getBoundsRule().right == null) {
411                    return obj.mParent.getBounds().right;
412                }
413                return obj.getBoundsRule().right.getAbsoluteValue();
414            }
415        };
416
417        /**
418         * Implementation of {@link Property} for overwriting the bottom attribute of
419         * {@link BoundsRule} associated with this {@link ChildDrawable}. This allows users to
420         * change the bounds rules as a percentage of parent size. This is preferable over
421         * {@see PROPERTY_TOP_ABSOLUTE} when the exact start/end position of scroll movement
422         * isn't available at compile time.
423         */
424        public static final Property<CompositeDrawable.ChildDrawable, Float> TOP_FRACTION =
425                new Property<CompositeDrawable.ChildDrawable, Float>(Float.class, "fractionTop") {
426            @Override
427            public void set(CompositeDrawable.ChildDrawable obj, Float value) {
428                if (obj.getBoundsRule().top == null) {
429                    obj.getBoundsRule().top = ValueRule.inheritFromParent(value);
430                } else {
431                    obj.getBoundsRule().top.setFraction(value);
432                }
433
434                obj.recomputeBounds();
435            }
436
437            @Override
438            public Float get(CompositeDrawable.ChildDrawable obj) {
439                if (obj.getBoundsRule().top == null) {
440                    return 0f;
441                }
442                return obj.getBoundsRule().top.getFraction();
443            }
444        };
445
446        /**
447         * Implementation of {@link Property} for overwriting the bottom attribute of
448         * {@link BoundsRule} associated with this {@link ChildDrawable}. This allows users to
449         * change the bounds rules as a percentage of parent size. This is preferable over
450         * {@see PROPERTY_BOTTOM_ABSOLUTE} when the exact start/end position of scroll movement
451         * isn't available at compile time.
452         */
453        public static final Property<CompositeDrawable.ChildDrawable, Float> BOTTOM_FRACTION =
454                new Property<CompositeDrawable.ChildDrawable, Float>(
455                        Float.class, "fractionBottom") {
456            @Override
457            public void set(CompositeDrawable.ChildDrawable obj, Float value) {
458                if (obj.getBoundsRule().bottom == null) {
459                    obj.getBoundsRule().bottom = ValueRule.inheritFromParent(value);
460                } else {
461                    obj.getBoundsRule().bottom.setFraction(value);
462                }
463
464                obj.recomputeBounds();
465            }
466
467            @Override
468            public Float get(CompositeDrawable.ChildDrawable obj) {
469                if (obj.getBoundsRule().bottom == null) {
470                    return 1f;
471                }
472                return obj.getBoundsRule().bottom.getFraction();
473            }
474        };
475
476        /**
477         * Implementation of {@link Property} for overwriting the bottom attribute of
478         * {@link BoundsRule} associated with this {@link ChildDrawable}. This allows users to
479         * change the bounds rules as a percentage of parent size. This is preferable over
480         * {@see PROPERTY_LEFT_ABSOLUTE} when the exact start/end position of scroll movement
481         * isn't available at compile time.
482         */
483        public static final Property<CompositeDrawable.ChildDrawable, Float> LEFT_FRACTION =
484                new Property<CompositeDrawable.ChildDrawable, Float>(Float.class, "fractionLeft") {
485            @Override
486            public void set(CompositeDrawable.ChildDrawable obj, Float value) {
487                if (obj.getBoundsRule().left == null) {
488                    obj.getBoundsRule().left = ValueRule.inheritFromParent(value);
489                } else {
490                    obj.getBoundsRule().left.setFraction(value);
491                }
492
493                obj.recomputeBounds();
494            }
495
496            @Override
497            public Float get(CompositeDrawable.ChildDrawable obj) {
498                if (obj.getBoundsRule().left == null) {
499                    return 0f;
500                }
501                return obj.getBoundsRule().left.getFraction();
502            }
503        };
504
505        /**
506         * Implementation of {@link Property} for overwriting the bottom attribute of
507         * {@link BoundsRule} associated with this {@link ChildDrawable}. This allows users to
508         * change the bounds rules as a percentage of parent size. This is preferable over
509         * {@see PROPERTY_RIGHT_ABSOLUTE} when the exact start/end position of scroll movement
510         * isn't available at compile time.
511         */
512        public static final Property<CompositeDrawable.ChildDrawable, Float> RIGHT_FRACTION =
513                new Property<CompositeDrawable.ChildDrawable, Float>(Float.class, "fractionRight") {
514            @Override
515            public void set(CompositeDrawable.ChildDrawable obj, Float value) {
516                if (obj.getBoundsRule().right == null) {
517                    obj.getBoundsRule().right = ValueRule.inheritFromParent(value);
518                } else {
519                    obj.getBoundsRule().right.setFraction(value);
520                }
521
522                obj.recomputeBounds();
523            }
524
525            @Override
526            public Float get(CompositeDrawable.ChildDrawable obj) {
527                if (obj.getBoundsRule().right == null) {
528                    return 1f;
529                }
530                return obj.getBoundsRule().right.getFraction();
531            }
532        };
533    }
534}
535