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 android.animation;
18
19import android.content.pm.ActivityInfo.Config;
20import android.content.res.ConstantState;
21import android.util.StateSet;
22import android.view.View;
23
24import java.lang.ref.WeakReference;
25import java.util.ArrayList;
26
27/**
28 * Lets you define a number of Animators that will run on the attached View depending on the View's
29 * drawable state.
30 * <p>
31 * It can be defined in an XML file with the <code>&lt;selector></code> element.
32 * Each State Animator is defined in a nested <code>&lt;item></code> element.
33 *
34 * @attr ref android.R.styleable#DrawableStates_state_focused
35 * @attr ref android.R.styleable#DrawableStates_state_window_focused
36 * @attr ref android.R.styleable#DrawableStates_state_enabled
37 * @attr ref android.R.styleable#DrawableStates_state_checkable
38 * @attr ref android.R.styleable#DrawableStates_state_checked
39 * @attr ref android.R.styleable#DrawableStates_state_selected
40 * @attr ref android.R.styleable#DrawableStates_state_activated
41 * @attr ref android.R.styleable#DrawableStates_state_active
42 * @attr ref android.R.styleable#DrawableStates_state_single
43 * @attr ref android.R.styleable#DrawableStates_state_first
44 * @attr ref android.R.styleable#DrawableStates_state_middle
45 * @attr ref android.R.styleable#DrawableStates_state_last
46 * @attr ref android.R.styleable#DrawableStates_state_pressed
47 * @attr ref android.R.styleable#StateListAnimatorItem_animation
48 */
49public class StateListAnimator implements Cloneable {
50
51    private ArrayList<Tuple> mTuples = new ArrayList<Tuple>();
52    private Tuple mLastMatch = null;
53    private Animator mRunningAnimator = null;
54    private WeakReference<View> mViewRef;
55    private StateListAnimatorConstantState mConstantState;
56    private AnimatorListenerAdapter mAnimatorListener;
57    private @Config int mChangingConfigurations;
58
59    public StateListAnimator() {
60        initAnimatorListener();
61    }
62
63    private void initAnimatorListener() {
64        mAnimatorListener = new AnimatorListenerAdapter() {
65            @Override
66            public void onAnimationEnd(Animator animation) {
67                animation.setTarget(null);
68                if (mRunningAnimator == animation) {
69                    mRunningAnimator = null;
70                }
71            }
72        };
73    }
74
75    /**
76     * Associates the given animator with the provided drawable state specs so that it will be run
77     * when the View's drawable state matches the specs.
78     *
79     * @param specs The drawable state specs to match against
80     * @param animator The animator to run when the specs match
81     */
82    public void addState(int[] specs, Animator animator) {
83        Tuple tuple = new Tuple(specs, animator);
84        tuple.mAnimator.addListener(mAnimatorListener);
85        mTuples.add(tuple);
86        mChangingConfigurations |= animator.getChangingConfigurations();
87    }
88
89    /**
90     * Returns the current {@link android.animation.Animator} which is started because of a state
91     * change.
92     *
93     * @return The currently running Animator or null if no Animator is running
94     * @hide
95     */
96    public Animator getRunningAnimator() {
97        return mRunningAnimator;
98    }
99
100    /**
101     * @hide
102     */
103    public View getTarget() {
104        return mViewRef == null ? null : mViewRef.get();
105    }
106
107    /**
108     * Called by View
109     * @hide
110     */
111    public void setTarget(View view) {
112        final View current = getTarget();
113        if (current == view) {
114            return;
115        }
116        if (current != null) {
117            clearTarget();
118        }
119        if (view != null) {
120            mViewRef = new WeakReference<View>(view);
121        }
122
123    }
124
125    private void clearTarget() {
126        final int size = mTuples.size();
127        for (int i = 0; i < size; i++) {
128            mTuples.get(i).mAnimator.setTarget(null);
129        }
130        mViewRef = null;
131        mLastMatch = null;
132        mRunningAnimator = null;
133    }
134
135    @Override
136    public StateListAnimator clone() {
137        try {
138            StateListAnimator clone = (StateListAnimator) super.clone();
139            clone.mTuples = new ArrayList<Tuple>(mTuples.size());
140            clone.mLastMatch = null;
141            clone.mRunningAnimator = null;
142            clone.mViewRef = null;
143            clone.mAnimatorListener = null;
144            clone.initAnimatorListener();
145            final int tupleSize = mTuples.size();
146            for (int i = 0; i < tupleSize; i++) {
147                final Tuple tuple = mTuples.get(i);
148                final Animator animatorClone = tuple.mAnimator.clone();
149                animatorClone.removeListener(mAnimatorListener);
150                clone.addState(tuple.mSpecs, animatorClone);
151            }
152            clone.setChangingConfigurations(getChangingConfigurations());
153            return clone;
154        } catch (CloneNotSupportedException e) {
155            throw new AssertionError("cannot clone state list animator", e);
156        }
157    }
158
159    /**
160     * Called by View
161     * @hide
162     */
163    public void setState(int[] state) {
164        Tuple match = null;
165        final int count = mTuples.size();
166        for (int i = 0; i < count; i++) {
167            final Tuple tuple = mTuples.get(i);
168            if (StateSet.stateSetMatches(tuple.mSpecs, state)) {
169                match = tuple;
170                break;
171            }
172        }
173        if (match == mLastMatch) {
174            return;
175        }
176        if (mLastMatch != null) {
177            cancel();
178        }
179        mLastMatch = match;
180        if (match != null) {
181            start(match);
182        }
183    }
184
185    private void start(Tuple match) {
186        match.mAnimator.setTarget(getTarget());
187        mRunningAnimator = match.mAnimator;
188        mRunningAnimator.start();
189    }
190
191    private void cancel() {
192        if (mRunningAnimator != null) {
193            mRunningAnimator.cancel();
194            mRunningAnimator = null;
195        }
196    }
197
198    /**
199     * @hide
200     */
201    public ArrayList<Tuple> getTuples() {
202        return mTuples;
203    }
204
205    /**
206     * If there is an animation running for a recent state change, ends it.
207     * <p>
208     * This causes the animation to assign the end value(s) to the View.
209     */
210    public void jumpToCurrentState() {
211        if (mRunningAnimator != null) {
212            mRunningAnimator.end();
213        }
214    }
215
216    /**
217     * Return a mask of the configuration parameters for which this animator may change, requiring
218     * that it be re-created.  The default implementation returns whatever was provided through
219     * {@link #setChangingConfigurations(int)} or 0 by default.
220     *
221     * @return Returns a mask of the changing configuration parameters, as defined by
222     * {@link android.content.pm.ActivityInfo}.
223     *
224     * @see android.content.pm.ActivityInfo
225     * @hide
226     */
227    public @Config int getChangingConfigurations() {
228        return mChangingConfigurations;
229    }
230
231    /**
232     * Set a mask of the configuration parameters for which this animator may change, requiring
233     * that it should be recreated from resources instead of being cloned.
234     *
235     * @param configs A mask of the changing configuration parameters, as
236     * defined by {@link android.content.pm.ActivityInfo}.
237     *
238     * @see android.content.pm.ActivityInfo
239     * @hide
240     */
241    public void setChangingConfigurations(@Config int configs) {
242        mChangingConfigurations = configs;
243    }
244
245    /**
246     * Sets the changing configurations value to the union of the current changing configurations
247     * and the provided configs.
248     * This method is called while loading the animator.
249     * @hide
250     */
251    public void appendChangingConfigurations(@Config int configs) {
252        mChangingConfigurations |= configs;
253    }
254
255    /**
256     * Return a {@link android.content.res.ConstantState} instance that holds the shared state of
257     * this Animator.
258     * <p>
259     * This constant state is used to create new instances of this animator when needed. Default
260     * implementation creates a new {@link StateListAnimatorConstantState}. You can override this
261     * method to provide your custom logic or return null if you don't want this animator to be
262     * cached.
263     *
264     * @return The {@link android.content.res.ConstantState} associated to this Animator.
265     * @see android.content.res.ConstantState
266     * @see #clone()
267     * @hide
268     */
269    public ConstantState<StateListAnimator> createConstantState() {
270        return new StateListAnimatorConstantState(this);
271    }
272
273    /**
274     * @hide
275     */
276    public static class Tuple {
277
278        final int[] mSpecs;
279
280        final Animator mAnimator;
281
282        private Tuple(int[] specs, Animator animator) {
283            mSpecs = specs;
284            mAnimator = animator;
285        }
286
287        /**
288         * @hide
289         */
290        public int[] getSpecs() {
291            return mSpecs;
292        }
293
294        /**
295         * @hide
296         */
297        public Animator getAnimator() {
298            return mAnimator;
299        }
300    }
301
302    /**
303     * Creates a constant state which holds changing configurations information associated with the
304     * given Animator.
305     * <p>
306     * When new instance is called, default implementation clones the Animator.
307     */
308    private static class StateListAnimatorConstantState
309            extends ConstantState<StateListAnimator> {
310
311        final StateListAnimator mAnimator;
312
313        @Config int mChangingConf;
314
315        public StateListAnimatorConstantState(StateListAnimator animator) {
316            mAnimator = animator;
317            mAnimator.mConstantState = this;
318            mChangingConf = mAnimator.getChangingConfigurations();
319        }
320
321        @Override
322        public @Config int getChangingConfigurations() {
323            return mChangingConf;
324        }
325
326        @Override
327        public StateListAnimator newInstance() {
328            final StateListAnimator clone = mAnimator.clone();
329            clone.mConstantState = this;
330            return clone;
331        }
332    }
333}
334