1/*
2 * Copyright (C) 2006 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 android.graphics.drawable;
18
19import android.content.res.Resources;
20import android.graphics.Canvas;
21import android.graphics.ColorFilter;
22import android.graphics.Insets;
23import android.graphics.PixelFormat;
24import android.graphics.Rect;
25import android.os.SystemClock;
26
27/**
28 * A helper class that contains several {@link Drawable}s and selects which one to use.
29 *
30 * You can subclass it to create your own DrawableContainers or directly use one its child classes.
31 */
32public class DrawableContainer extends Drawable implements Drawable.Callback {
33    private static final boolean DEBUG = false;
34    private static final String TAG = "DrawableContainer";
35
36    /**
37     * To be proper, we should have a getter for dither (and alpha, etc.)
38     * so that proxy classes like this can save/restore their delegates'
39     * values, but we don't have getters. Since we do have setters
40     * (e.g. setDither), which this proxy forwards on, we have to have some
41     * default/initial setting.
42     *
43     * The initial setting for dither is now true, since it almost always seems
44     * to improve the quality at negligible cost.
45     */
46    private static final boolean DEFAULT_DITHER = true;
47    private DrawableContainerState mDrawableContainerState;
48    private Drawable mCurrDrawable;
49    private int mAlpha = 0xFF;
50    private ColorFilter mColorFilter;
51
52    private int mCurIndex = -1;
53    private boolean mMutated;
54
55    // Animations.
56    private Runnable mAnimationRunnable;
57    private long mEnterAnimationEnd;
58    private long mExitAnimationEnd;
59    private Drawable mLastDrawable;
60
61    // overrides from Drawable
62
63    @Override
64    public void draw(Canvas canvas) {
65        if (mCurrDrawable != null) {
66            mCurrDrawable.draw(canvas);
67        }
68        if (mLastDrawable != null) {
69            mLastDrawable.draw(canvas);
70        }
71    }
72
73    @Override
74    public int getChangingConfigurations() {
75        return super.getChangingConfigurations()
76                | mDrawableContainerState.mChangingConfigurations
77                | mDrawableContainerState.mChildrenChangingConfigurations;
78    }
79
80    @Override
81    public boolean getPadding(Rect padding) {
82        final Rect r = mDrawableContainerState.getConstantPadding();
83        if (r != null) {
84            padding.set(r);
85            return true;
86        }
87        if (mCurrDrawable != null) {
88            return mCurrDrawable.getPadding(padding);
89        } else {
90            return super.getPadding(padding);
91        }
92    }
93
94    /**
95     * @hide
96     */
97    @Override
98    public Insets getLayoutInsets() {
99        return (mCurrDrawable == null) ? Insets.NONE : mCurrDrawable.getLayoutInsets();
100    }
101
102    @Override
103    public void setAlpha(int alpha) {
104        if (mAlpha != alpha) {
105            mAlpha = alpha;
106            if (mCurrDrawable != null) {
107                if (mEnterAnimationEnd == 0) {
108                    mCurrDrawable.mutate().setAlpha(alpha);
109                } else {
110                    animate(false);
111                }
112            }
113        }
114    }
115
116    @Override
117    public void setDither(boolean dither) {
118        if (mDrawableContainerState.mDither != dither) {
119            mDrawableContainerState.mDither = dither;
120            if (mCurrDrawable != null) {
121                mCurrDrawable.mutate().setDither(mDrawableContainerState.mDither);
122            }
123        }
124    }
125
126    @Override
127    public void setColorFilter(ColorFilter cf) {
128        if (mColorFilter != cf) {
129            mColorFilter = cf;
130            if (mCurrDrawable != null) {
131                mCurrDrawable.mutate().setColorFilter(cf);
132            }
133        }
134    }
135
136    /**
137     * Change the global fade duration when a new drawable is entering
138     * the scene.
139     * @param ms The amount of time to fade in milliseconds.
140     */
141    public void setEnterFadeDuration(int ms) {
142        mDrawableContainerState.mEnterFadeDuration = ms;
143    }
144
145    /**
146     * Change the global fade duration when a new drawable is leaving
147     * the scene.
148     * @param ms The amount of time to fade in milliseconds.
149     */
150    public void setExitFadeDuration(int ms) {
151        mDrawableContainerState.mExitFadeDuration = ms;
152    }
153
154    @Override
155    protected void onBoundsChange(Rect bounds) {
156        if (mLastDrawable != null) {
157            mLastDrawable.setBounds(bounds);
158        }
159        if (mCurrDrawable != null) {
160            mCurrDrawable.setBounds(bounds);
161        }
162    }
163
164    @Override
165    public boolean isStateful() {
166        return mDrawableContainerState.isStateful();
167    }
168
169    @Override
170    public void jumpToCurrentState() {
171        boolean changed = false;
172        if (mLastDrawable != null) {
173            mLastDrawable.jumpToCurrentState();
174            mLastDrawable = null;
175            changed = true;
176        }
177        if (mCurrDrawable != null) {
178            mCurrDrawable.jumpToCurrentState();
179            mCurrDrawable.mutate().setAlpha(mAlpha);
180        }
181        if (mExitAnimationEnd != 0) {
182            mExitAnimationEnd = 0;
183            changed = true;
184        }
185        if (mEnterAnimationEnd != 0) {
186            mEnterAnimationEnd = 0;
187            changed = true;
188        }
189        if (changed) {
190            invalidateSelf();
191        }
192    }
193
194    @Override
195    protected boolean onStateChange(int[] state) {
196        if (mLastDrawable != null) {
197            return mLastDrawable.setState(state);
198        }
199        if (mCurrDrawable != null) {
200            return mCurrDrawable.setState(state);
201        }
202        return false;
203    }
204
205    @Override
206    protected boolean onLevelChange(int level) {
207        if (mLastDrawable != null) {
208            return mLastDrawable.setLevel(level);
209        }
210        if (mCurrDrawable != null) {
211            return mCurrDrawable.setLevel(level);
212        }
213        return false;
214    }
215
216    @Override
217    public int getIntrinsicWidth() {
218        if (mDrawableContainerState.isConstantSize()) {
219            return mDrawableContainerState.getConstantWidth();
220        }
221        return mCurrDrawable != null ? mCurrDrawable.getIntrinsicWidth() : -1;
222    }
223
224    @Override
225    public int getIntrinsicHeight() {
226        if (mDrawableContainerState.isConstantSize()) {
227            return mDrawableContainerState.getConstantHeight();
228        }
229        return mCurrDrawable != null ? mCurrDrawable.getIntrinsicHeight() : -1;
230    }
231
232    @Override
233    public int getMinimumWidth() {
234        if (mDrawableContainerState.isConstantSize()) {
235            return mDrawableContainerState.getConstantMinimumWidth();
236        }
237        return mCurrDrawable != null ? mCurrDrawable.getMinimumWidth() : 0;
238    }
239
240    @Override
241    public int getMinimumHeight() {
242        if (mDrawableContainerState.isConstantSize()) {
243            return mDrawableContainerState.getConstantMinimumHeight();
244        }
245        return mCurrDrawable != null ? mCurrDrawable.getMinimumHeight() : 0;
246    }
247
248    public void invalidateDrawable(Drawable who) {
249        if (who == mCurrDrawable && getCallback() != null) {
250            getCallback().invalidateDrawable(this);
251        }
252    }
253
254    public void scheduleDrawable(Drawable who, Runnable what, long when) {
255        if (who == mCurrDrawable && getCallback() != null) {
256            getCallback().scheduleDrawable(this, what, when);
257        }
258    }
259
260    public void unscheduleDrawable(Drawable who, Runnable what) {
261        if (who == mCurrDrawable && getCallback() != null) {
262            getCallback().unscheduleDrawable(this, what);
263        }
264    }
265
266    @Override
267    public boolean setVisible(boolean visible, boolean restart) {
268        boolean changed = super.setVisible(visible, restart);
269        if (mLastDrawable != null) {
270            mLastDrawable.setVisible(visible, restart);
271        }
272        if (mCurrDrawable != null) {
273            mCurrDrawable.setVisible(visible, restart);
274        }
275        return changed;
276    }
277
278    @Override
279    public int getOpacity() {
280        return mCurrDrawable == null || !mCurrDrawable.isVisible() ? PixelFormat.TRANSPARENT :
281                mDrawableContainerState.getOpacity();
282    }
283
284    public boolean selectDrawable(int idx) {
285        if (idx == mCurIndex) {
286            return false;
287        }
288
289        final long now = SystemClock.uptimeMillis();
290
291        if (DEBUG) android.util.Log.i(TAG, toString() + " from " + mCurIndex + " to " + idx
292                + ": exit=" + mDrawableContainerState.mExitFadeDuration
293                + " enter=" + mDrawableContainerState.mEnterFadeDuration);
294
295        if (mDrawableContainerState.mExitFadeDuration > 0) {
296            if (mLastDrawable != null) {
297                mLastDrawable.setVisible(false, false);
298            }
299            if (mCurrDrawable != null) {
300                mLastDrawable = mCurrDrawable;
301                mExitAnimationEnd = now + mDrawableContainerState.mExitFadeDuration;
302            } else {
303                mLastDrawable = null;
304                mExitAnimationEnd = 0;
305            }
306        } else if (mCurrDrawable != null) {
307            mCurrDrawable.setVisible(false, false);
308        }
309
310        if (idx >= 0 && idx < mDrawableContainerState.mNumChildren) {
311            Drawable d = mDrawableContainerState.mDrawables[idx];
312            mCurrDrawable = d;
313            mCurIndex = idx;
314            if (d != null) {
315                d.mutate();
316                if (mDrawableContainerState.mEnterFadeDuration > 0) {
317                    mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration;
318                } else {
319                    d.setAlpha(mAlpha);
320                }
321                d.setVisible(isVisible(), true);
322                d.setDither(mDrawableContainerState.mDither);
323                d.setColorFilter(mColorFilter);
324                d.setState(getState());
325                d.setLevel(getLevel());
326                d.setBounds(getBounds());
327            }
328        } else {
329            mCurrDrawable = null;
330            mCurIndex = -1;
331        }
332
333        if (mEnterAnimationEnd != 0 || mExitAnimationEnd != 0) {
334            if (mAnimationRunnable == null) {
335                mAnimationRunnable = new Runnable() {
336                    @Override public void run() {
337                        animate(true);
338                        invalidateSelf();
339                    }
340                };
341            } else {
342                unscheduleSelf(mAnimationRunnable);
343            }
344            // Compute first frame and schedule next animation.
345            animate(true);
346        }
347
348        invalidateSelf();
349
350        return true;
351    }
352
353    void animate(boolean schedule) {
354        final long now = SystemClock.uptimeMillis();
355        boolean animating = false;
356        if (mCurrDrawable != null) {
357            if (mEnterAnimationEnd != 0) {
358                if (mEnterAnimationEnd <= now) {
359                    mCurrDrawable.mutate().setAlpha(mAlpha);
360                    mEnterAnimationEnd = 0;
361                } else {
362                    int animAlpha = (int)((mEnterAnimationEnd-now)*255)
363                            / mDrawableContainerState.mEnterFadeDuration;
364                    if (DEBUG) android.util.Log.i(TAG, toString() + " cur alpha " + animAlpha);
365                    mCurrDrawable.mutate().setAlpha(((255-animAlpha)*mAlpha)/255);
366                    animating = true;
367                }
368            }
369        } else {
370            mEnterAnimationEnd = 0;
371        }
372        if (mLastDrawable != null) {
373            if (mExitAnimationEnd != 0) {
374                if (mExitAnimationEnd <= now) {
375                    mLastDrawable.setVisible(false, false);
376                    mLastDrawable = null;
377                    mExitAnimationEnd = 0;
378                } else {
379                    int animAlpha = (int)((mExitAnimationEnd-now)*255)
380                            / mDrawableContainerState.mExitFadeDuration;
381                    if (DEBUG) android.util.Log.i(TAG, toString() + " last alpha " + animAlpha);
382                    mLastDrawable.mutate().setAlpha((animAlpha*mAlpha)/255);
383                    animating = true;
384                }
385            }
386        } else {
387            mExitAnimationEnd = 0;
388        }
389
390        if (schedule && animating) {
391            scheduleSelf(mAnimationRunnable, now + 1000/60);
392        }
393    }
394
395    @Override
396    public Drawable getCurrent() {
397        return mCurrDrawable;
398    }
399
400    @Override
401    public ConstantState getConstantState() {
402        if (mDrawableContainerState.canConstantState()) {
403            mDrawableContainerState.mChangingConfigurations = getChangingConfigurations();
404            return mDrawableContainerState;
405        }
406        return null;
407    }
408
409    @Override
410    public Drawable mutate() {
411        if (!mMutated && super.mutate() == this) {
412            final int N = mDrawableContainerState.getChildCount();
413            final Drawable[] drawables = mDrawableContainerState.getChildren();
414            for (int i = 0; i < N; i++) {
415                if (drawables[i] != null) drawables[i].mutate();
416            }
417            mMutated = true;
418        }
419        return this;
420    }
421
422    /**
423     * A ConstantState that can contain several {@link Drawable}s.
424     *
425     * This class was made public to enable testing, and its visibility may change in a future
426     * release.
427     */
428    public abstract static class DrawableContainerState extends ConstantState {
429        final DrawableContainer mOwner;
430
431        int         mChangingConfigurations;
432        int         mChildrenChangingConfigurations;
433
434        Drawable[]  mDrawables;
435        int         mNumChildren;
436
437        boolean     mVariablePadding = false;
438        Rect        mConstantPadding = null;
439
440        boolean     mConstantSize = false;
441        boolean     mComputedConstantSize = false;
442        int         mConstantWidth;
443        int         mConstantHeight;
444        int         mConstantMinimumWidth;
445        int         mConstantMinimumHeight;
446
447        int         mOpacity;
448
449        boolean     mHaveStateful = false;
450        boolean     mStateful;
451
452        boolean     mCheckedConstantState;
453        boolean     mCanConstantState;
454
455        boolean     mPaddingChecked = false;
456
457        boolean     mDither = DEFAULT_DITHER;
458
459        int         mEnterFadeDuration;
460        int         mExitFadeDuration;
461
462        DrawableContainerState(DrawableContainerState orig, DrawableContainer owner,
463                Resources res) {
464            mOwner = owner;
465
466            if (orig != null) {
467                mChangingConfigurations = orig.mChangingConfigurations;
468                mChildrenChangingConfigurations = orig.mChildrenChangingConfigurations;
469
470                final Drawable[] origDr = orig.mDrawables;
471
472                mDrawables = new Drawable[origDr.length];
473                mNumChildren = orig.mNumChildren;
474
475                final int N = mNumChildren;
476                for (int i=0; i<N; i++) {
477                    if (res != null) {
478                        mDrawables[i] = origDr[i].getConstantState().newDrawable(res);
479                    } else {
480                        mDrawables[i] = origDr[i].getConstantState().newDrawable();
481                    }
482                    mDrawables[i].setCallback(owner);
483                }
484
485                mCheckedConstantState = mCanConstantState = true;
486                mVariablePadding = orig.mVariablePadding;
487                if (orig.mConstantPadding != null) {
488                    mConstantPadding = new Rect(orig.mConstantPadding);
489                }
490                mConstantSize = orig.mConstantSize;
491                mComputedConstantSize = orig.mComputedConstantSize;
492                mConstantWidth = orig.mConstantWidth;
493                mConstantHeight = orig.mConstantHeight;
494
495                mOpacity = orig.mOpacity;
496                mHaveStateful = orig.mHaveStateful;
497                mStateful = orig.mStateful;
498
499                mDither = orig.mDither;
500
501                mEnterFadeDuration = orig.mEnterFadeDuration;
502                mExitFadeDuration = orig.mExitFadeDuration;
503
504            } else {
505                mDrawables = new Drawable[10];
506                mNumChildren = 0;
507                mCheckedConstantState = mCanConstantState = false;
508            }
509        }
510
511        @Override
512        public int getChangingConfigurations() {
513            return mChangingConfigurations;
514        }
515
516        public final int addChild(Drawable dr) {
517            final int pos = mNumChildren;
518
519            if (pos >= mDrawables.length) {
520                growArray(pos, pos+10);
521            }
522
523            dr.setVisible(false, true);
524            dr.setCallback(mOwner);
525
526            mDrawables[pos] = dr;
527            mNumChildren++;
528            mChildrenChangingConfigurations |= dr.getChangingConfigurations();
529            mHaveStateful = false;
530
531            mConstantPadding = null;
532            mPaddingChecked = false;
533            mComputedConstantSize = false;
534
535            return pos;
536        }
537
538        public final int getChildCount() {
539            return mNumChildren;
540        }
541
542        public final Drawable[] getChildren() {
543            return mDrawables;
544        }
545
546        /** A boolean value indicating whether to use the maximum padding value of
547          * all frames in the set (false), or to use the padding value of the frame
548          * being shown (true). Default value is false.
549          */
550        public final void setVariablePadding(boolean variable) {
551            mVariablePadding = variable;
552        }
553
554        public final Rect getConstantPadding() {
555            if (mVariablePadding) {
556                return null;
557            }
558            if (mConstantPadding != null || mPaddingChecked) {
559                return mConstantPadding;
560            }
561
562            Rect r = null;
563            final Rect t = new Rect();
564            final int N = getChildCount();
565            final Drawable[] drawables = mDrawables;
566            for (int i = 0; i < N; i++) {
567                if (drawables[i].getPadding(t)) {
568                    if (r == null) r = new Rect(0, 0, 0, 0);
569                    if (t.left > r.left) r.left = t.left;
570                    if (t.top > r.top) r.top = t.top;
571                    if (t.right > r.right) r.right = t.right;
572                    if (t.bottom > r.bottom) r.bottom = t.bottom;
573                }
574            }
575            mPaddingChecked = true;
576            return (mConstantPadding = r);
577        }
578
579        public final void setConstantSize(boolean constant) {
580            mConstantSize = constant;
581        }
582
583        public final boolean isConstantSize() {
584            return mConstantSize;
585        }
586
587        public final int getConstantWidth() {
588            if (!mComputedConstantSize) {
589                computeConstantSize();
590            }
591
592            return mConstantWidth;
593        }
594
595        public final int getConstantHeight() {
596            if (!mComputedConstantSize) {
597                computeConstantSize();
598            }
599
600            return mConstantHeight;
601        }
602
603        public final int getConstantMinimumWidth() {
604            if (!mComputedConstantSize) {
605                computeConstantSize();
606            }
607
608            return mConstantMinimumWidth;
609        }
610
611        public final int getConstantMinimumHeight() {
612            if (!mComputedConstantSize) {
613                computeConstantSize();
614            }
615
616            return mConstantMinimumHeight;
617        }
618
619        protected void computeConstantSize() {
620            mComputedConstantSize = true;
621
622            final int N = getChildCount();
623            final Drawable[] drawables = mDrawables;
624            mConstantWidth = mConstantHeight = -1;
625            mConstantMinimumWidth = mConstantMinimumHeight = 0;
626            for (int i = 0; i < N; i++) {
627                Drawable dr = drawables[i];
628                int s = dr.getIntrinsicWidth();
629                if (s > mConstantWidth) mConstantWidth = s;
630                s = dr.getIntrinsicHeight();
631                if (s > mConstantHeight) mConstantHeight = s;
632                s = dr.getMinimumWidth();
633                if (s > mConstantMinimumWidth) mConstantMinimumWidth = s;
634                s = dr.getMinimumHeight();
635                if (s > mConstantMinimumHeight) mConstantMinimumHeight = s;
636            }
637        }
638
639        public final void setEnterFadeDuration(int duration) {
640            mEnterFadeDuration = duration;
641        }
642
643        public final int getEnterFadeDuration() {
644            return mEnterFadeDuration;
645        }
646
647        public final void setExitFadeDuration(int duration) {
648            mExitFadeDuration = duration;
649        }
650
651        public final int getExitFadeDuration() {
652            return mExitFadeDuration;
653        }
654
655        public final int getOpacity() {
656            final int N = getChildCount();
657            final Drawable[] drawables = mDrawables;
658            int op = N > 0 ? drawables[0].getOpacity() : PixelFormat.TRANSPARENT;
659            for (int i = 1; i < N; i++) {
660                op = Drawable.resolveOpacity(op, drawables[i].getOpacity());
661            }
662            mOpacity = op;
663            return op;
664        }
665
666        public final boolean isStateful() {
667            if (mHaveStateful) {
668                return mStateful;
669            }
670
671            boolean stateful = false;
672            final int N = getChildCount();
673            for (int i = 0; i < N; i++) {
674                if (mDrawables[i].isStateful()) {
675                    stateful = true;
676                    break;
677                }
678            }
679
680            mStateful = stateful;
681            mHaveStateful = true;
682            return stateful;
683        }
684
685        public void growArray(int oldSize, int newSize) {
686            Drawable[] newDrawables = new Drawable[newSize];
687            System.arraycopy(mDrawables, 0, newDrawables, 0, oldSize);
688            mDrawables = newDrawables;
689        }
690
691        public synchronized boolean canConstantState() {
692            if (!mCheckedConstantState) {
693                mCanConstantState = true;
694                final int N = mNumChildren;
695                for (int i=0; i<N; i++) {
696                    if (mDrawables[i].getConstantState() == null) {
697                        mCanConstantState = false;
698                        break;
699                    }
700                }
701                mCheckedConstantState = true;
702            }
703
704            return mCanConstantState;
705        }
706    }
707
708    protected void setConstantState(DrawableContainerState state)
709    {
710        mDrawableContainerState = state;
711    }
712}
713