| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
| * in compliance with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software distributed under the License |
| * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
| * or implied. See the License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| |
| package android.graphics.drawable; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorInflater; |
| import android.animation.ValueAnimator; |
| import android.annotation.NonNull; |
| import android.content.res.ColorStateList; |
| import android.content.res.Resources; |
| import android.content.res.Resources.Theme; |
| import android.content.res.TypedArray; |
| import android.graphics.Canvas; |
| import android.graphics.ColorFilter; |
| import android.graphics.Outline; |
| import android.graphics.PorterDuff; |
| import android.graphics.Rect; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| |
| import com.android.internal.R; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| |
| /** |
| * This class uses {@link android.animation.ObjectAnimator} and |
| * {@link android.animation.AnimatorSet} to animate the properties of a |
| * {@link android.graphics.drawable.VectorDrawable} to create an animated drawable. |
| * <p> |
| * AnimatedVectorDrawable are normally defined as 3 separate XML files. |
| * </p> |
| * <p> |
| * First is the XML file for {@link android.graphics.drawable.VectorDrawable}. |
| * Note that we allow the animation happen on the group's attributes and path's |
| * attributes, which requires they are uniquely named in this xml file. Groups |
| * and paths without animations do not need names. |
| * </p> |
| * <li>Here is a simple VectorDrawable in this vectordrawable.xml file. |
| * <pre> |
| * <vector xmlns:android="http://schemas.android.com/apk/res/android" |
| * android:height="64dp" |
| * android:width="64dp" |
| * android:viewportHeight="600" |
| * android:viewportWidth="600" > |
| * <group |
| * android:name="rotationGroup" |
| * android:pivotX="300.0" |
| * android:pivotY="300.0" |
| * android:rotation="45.0" > |
| * <path |
| * android:name="v" |
| * android:fillColor="#000000" |
| * android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" /> |
| * </group> |
| * </vector> |
| * </pre></li> |
| * <p> |
| * Second is the AnimatedVectorDrawable's xml file, which defines the target |
| * VectorDrawable, the target paths and groups to animate, the properties of the |
| * path and group to animate and the animations defined as the ObjectAnimators |
| * or AnimatorSets. |
| * </p> |
| * <li>Here is a simple AnimatedVectorDrawable defined in this avd.xml file. |
| * Note how we use the names to refer to the groups and paths in the vectordrawable.xml. |
| * <pre> |
| * <animated-vector xmlns:android="http://schemas.android.com/apk/res/android" |
| * android:drawable="@drawable/vectordrawable" > |
| * <target |
| * android:name="rotationGroup" |
| * android:animation="@anim/rotation" /> |
| * <target |
| * android:name="v" |
| * android:animation="@anim/path_morph" /> |
| * </animated-vector> |
| * </pre></li> |
| * <p> |
| * Last is the Animator xml file, which is the same as a normal ObjectAnimator |
| * or AnimatorSet. |
| * To complete this example, here are the 2 animator files used in avd.xml: |
| * rotation.xml and path_morph.xml. |
| * </p> |
| * <li>Here is the rotation.xml, which will rotate the target group for 360 degrees. |
| * <pre> |
| * <objectAnimator |
| * android:duration="6000" |
| * android:propertyName="rotation" |
| * android:valueFrom="0" |
| * android:valueTo="360" /> |
| * </pre></li> |
| * <li>Here is the path_morph.xml, which will morph the path from one shape to |
| * the other. Note that the paths must be compatible for morphing. |
| * In more details, the paths should have exact same length of commands , and |
| * exact same length of parameters for each commands. |
| * Note that the path string are better stored in strings.xml for reusing. |
| * <pre> |
| * <set xmlns:android="http://schemas.android.com/apk/res/android"> |
| * <objectAnimator |
| * android:duration="3000" |
| * android:propertyName="pathData" |
| * android:valueFrom="M300,70 l 0,-70 70,70 0,0 -70,70z" |
| * android:valueTo="M300,70 l 0,-70 70,0 0,140 -70,0 z" |
| * android:valueType="pathType"/> |
| * </set> |
| * </pre></li> |
| * |
| * @attr ref android.R.styleable#AnimatedVectorDrawable_drawable |
| * @attr ref android.R.styleable#AnimatedVectorDrawableTarget_name |
| * @attr ref android.R.styleable#AnimatedVectorDrawableTarget_animation |
| */ |
| public class AnimatedVectorDrawable extends Drawable implements Animatable { |
| private static final String LOGTAG = AnimatedVectorDrawable.class.getSimpleName(); |
| |
| private static final String ANIMATED_VECTOR = "animated-vector"; |
| private static final String TARGET = "target"; |
| |
| private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false; |
| |
| private final AnimatedVectorDrawableState mAnimatedVectorState; |
| |
| public AnimatedVectorDrawable() { |
| mAnimatedVectorState = new AnimatedVectorDrawableState( |
| new AnimatedVectorDrawableState(null)); |
| } |
| |
| private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res, |
| Theme theme) { |
| // TODO: Correctly handle the constant state for AVD. |
| mAnimatedVectorState = new AnimatedVectorDrawableState(state); |
| if (theme != null && canApplyTheme()) { |
| applyTheme(theme); |
| } |
| } |
| |
| @Override |
| public ConstantState getConstantState() { |
| return null; |
| } |
| |
| @Override |
| public void draw(Canvas canvas) { |
| mAnimatedVectorState.mVectorDrawable.draw(canvas); |
| if (isStarted()) { |
| invalidateSelf(); |
| } |
| } |
| |
| @Override |
| protected void onBoundsChange(Rect bounds) { |
| mAnimatedVectorState.mVectorDrawable.setBounds(bounds); |
| } |
| |
| @Override |
| protected boolean onStateChange(int[] state) { |
| return mAnimatedVectorState.mVectorDrawable.setState(state); |
| } |
| |
| @Override |
| protected boolean onLevelChange(int level) { |
| return mAnimatedVectorState.mVectorDrawable.setLevel(level); |
| } |
| |
| @Override |
| public int getAlpha() { |
| return mAnimatedVectorState.mVectorDrawable.getAlpha(); |
| } |
| |
| @Override |
| public void setAlpha(int alpha) { |
| mAnimatedVectorState.mVectorDrawable.setAlpha(alpha); |
| } |
| |
| @Override |
| public void setColorFilter(ColorFilter colorFilter) { |
| mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter); |
| } |
| |
| @Override |
| public void setTintList(ColorStateList tint) { |
| mAnimatedVectorState.mVectorDrawable.setTintList(tint); |
| } |
| |
| @Override |
| public void setHotspot(float x, float y) { |
| mAnimatedVectorState.mVectorDrawable.setHotspot(x, y); |
| } |
| |
| @Override |
| public void setHotspotBounds(int left, int top, int right, int bottom) { |
| mAnimatedVectorState.mVectorDrawable.setHotspotBounds(left, top, right, bottom); |
| } |
| |
| @Override |
| public void setTintMode(PorterDuff.Mode tintMode) { |
| mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode); |
| } |
| |
| @Override |
| public boolean setVisible(boolean visible, boolean restart) { |
| mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart); |
| return super.setVisible(visible, restart); |
| } |
| |
| /** {@hide} */ |
| @Override |
| public void setLayoutDirection(int layoutDirection) { |
| mAnimatedVectorState.mVectorDrawable.setLayoutDirection(layoutDirection); |
| } |
| |
| @Override |
| public boolean isStateful() { |
| return mAnimatedVectorState.mVectorDrawable.isStateful(); |
| } |
| |
| @Override |
| public int getOpacity() { |
| return mAnimatedVectorState.mVectorDrawable.getOpacity(); |
| } |
| |
| @Override |
| public int getIntrinsicWidth() { |
| return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth(); |
| } |
| |
| @Override |
| public int getIntrinsicHeight() { |
| return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight(); |
| } |
| |
| @Override |
| public void getOutline(@NonNull Outline outline) { |
| mAnimatedVectorState.mVectorDrawable.getOutline(outline); |
| } |
| |
| @Override |
| public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme) |
| throws XmlPullParserException, IOException { |
| |
| int eventType = parser.getEventType(); |
| while (eventType != XmlPullParser.END_DOCUMENT) { |
| if (eventType == XmlPullParser.START_TAG) { |
| final String tagName = parser.getName(); |
| if (ANIMATED_VECTOR.equals(tagName)) { |
| final TypedArray a = obtainAttributes(res, theme, attrs, |
| R.styleable.AnimatedVectorDrawable); |
| int drawableRes = a.getResourceId( |
| R.styleable.AnimatedVectorDrawable_drawable, 0); |
| if (drawableRes != 0) { |
| mAnimatedVectorState.mVectorDrawable = (VectorDrawable) res.getDrawable( |
| drawableRes, theme).mutate(); |
| mAnimatedVectorState.mVectorDrawable.setAllowCaching(false); |
| } |
| a.recycle(); |
| } else if (TARGET.equals(tagName)) { |
| final TypedArray a = obtainAttributes(res, theme, attrs, |
| R.styleable.AnimatedVectorDrawableTarget); |
| final String target = a.getString( |
| R.styleable.AnimatedVectorDrawableTarget_name); |
| |
| int id = a.getResourceId( |
| R.styleable.AnimatedVectorDrawableTarget_animation, 0); |
| if (id != 0) { |
| Animator objectAnimator = AnimatorInflater.loadAnimator(res, theme, id); |
| setupAnimatorsForTarget(target, objectAnimator); |
| } |
| a.recycle(); |
| } |
| } |
| |
| eventType = parser.next(); |
| } |
| } |
| |
| @Override |
| public boolean canApplyTheme() { |
| return super.canApplyTheme() || mAnimatedVectorState != null |
| && mAnimatedVectorState.mVectorDrawable != null |
| && mAnimatedVectorState.mVectorDrawable.canApplyTheme(); |
| } |
| |
| @Override |
| public void applyTheme(Theme t) { |
| super.applyTheme(t); |
| |
| final VectorDrawable vectorDrawable = mAnimatedVectorState.mVectorDrawable; |
| if (vectorDrawable != null && vectorDrawable.canApplyTheme()) { |
| vectorDrawable.applyTheme(t); |
| } |
| } |
| |
| private static class AnimatedVectorDrawableState extends ConstantState { |
| int mChangingConfigurations; |
| VectorDrawable mVectorDrawable; |
| ArrayList<Animator> mAnimators; |
| |
| public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy) { |
| if (copy != null) { |
| mChangingConfigurations = copy.mChangingConfigurations; |
| // TODO: Make sure the constant state are handled correctly. |
| mVectorDrawable = new VectorDrawable(); |
| mVectorDrawable.setAllowCaching(false); |
| mAnimators = new ArrayList<Animator>(); |
| } |
| } |
| |
| @Override |
| public Drawable newDrawable() { |
| return new AnimatedVectorDrawable(this, null, null); |
| } |
| |
| @Override |
| public Drawable newDrawable(Resources res) { |
| return new AnimatedVectorDrawable(this, res, null); |
| } |
| |
| @Override |
| public Drawable newDrawable(Resources res, Theme theme) { |
| return new AnimatedVectorDrawable(this, res, theme); |
| } |
| |
| @Override |
| public int getChangingConfigurations() { |
| return mChangingConfigurations; |
| } |
| } |
| |
| private void setupAnimatorsForTarget(String name, Animator animator) { |
| Object target = mAnimatedVectorState.mVectorDrawable.getTargetByName(name); |
| animator.setTarget(target); |
| mAnimatedVectorState.mAnimators.add(animator); |
| if (DBG_ANIMATION_VECTOR_DRAWABLE) { |
| Log.v(LOGTAG, "add animator for target " + name + " " + animator); |
| } |
| } |
| |
| @Override |
| public boolean isRunning() { |
| final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators; |
| final int size = animators.size(); |
| for (int i = 0; i < size; i++) { |
| final Animator animator = animators.get(i); |
| if (animator.isRunning()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean isStarted() { |
| final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators; |
| final int size = animators.size(); |
| for (int i = 0; i < size; i++) { |
| final Animator animator = animators.get(i); |
| if (animator.isStarted()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public void start() { |
| final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators; |
| final int size = animators.size(); |
| for (int i = 0; i < size; i++) { |
| final Animator animator = animators.get(i); |
| if (!animator.isStarted()) { |
| animator.start(); |
| } |
| } |
| invalidateSelf(); |
| } |
| |
| @Override |
| public void stop() { |
| final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators; |
| final int size = animators.size(); |
| for (int i = 0; i < size; i++) { |
| final Animator animator = animators.get(i); |
| animator.end(); |
| } |
| } |
| |
| /** |
| * Reverses ongoing animations or starts pending animations in reverse. |
| * <p> |
| * NOTE: Only works of all animations are ValueAnimators. |
| * @hide |
| */ |
| public void reverse() { |
| final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators; |
| final int size = animators.size(); |
| for (int i = 0; i < size; i++) { |
| final Animator animator = animators.get(i); |
| if (animator.canReverse()) { |
| animator.reverse(); |
| } else { |
| Log.w(LOGTAG, "AnimatedVectorDrawable can't reverse()"); |
| } |
| } |
| } |
| |
| /** |
| * @hide |
| */ |
| public boolean canReverse() { |
| final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators; |
| final int size = animators.size(); |
| for (int i = 0; i < size; i++) { |
| final Animator animator = animators.get(i); |
| if (!animator.canReverse()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |