| /* |
| * Copyright (C) 2015 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.support.graphics.drawable; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorInflater; |
| import android.content.Context; |
| 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.PorterDuff.Mode; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Animatable; |
| import android.graphics.drawable.Drawable; |
| import android.support.annotation.NonNull; |
| import android.support.annotation.Nullable; |
| import android.support.annotation.DrawableRes; |
| import android.support.v4.util.ArrayMap; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.util.Xml; |
| |
| 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.support.graphics.drawable.VectorDrawableCompat} to create an animated drawable. |
| * <p> |
| * AnimatedVectorDrawableCompat are normally defined as 3 separate XML files. |
| * </p> |
| * <p> |
| * First is the XML file for {@link android.support.graphics.drawable.VectorDrawableCompat}. Note that we |
| * allow the animation to 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 AnimatedVectorDrawableCompat's XML file, which defines the target |
| * VectorDrawableCompat, 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 strings 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#AnimatedVectorDrawableCompat_drawable |
| * @attr ref android.R.styleable#AnimatedVectorDrawableCompatTarget_name |
| * @attr ref android.R.styleable#AnimatedVectorDrawableCompatTarget_animation |
| */ |
| public class AnimatedVectorDrawableCompat extends Drawable implements Animatable { |
| private static final String LOGTAG = "AnimatedVectorDrawableCompat"; |
| |
| private static final String ANIMATED_VECTOR = "animated-vector"; |
| private static final String TARGET = "target"; |
| |
| private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false; |
| |
| private AnimatedVectorDrawableCompatState mAnimatedVectorState; |
| |
| private boolean mMutated; |
| |
| private Context mContext; |
| |
| // Currently the only useful ctor. |
| public AnimatedVectorDrawableCompat(Context context) { |
| this(context, null, null); |
| } |
| |
| private AnimatedVectorDrawableCompat(Context context, AnimatedVectorDrawableCompatState state, |
| Resources res) { |
| mContext = context; |
| if (state != null) { |
| mAnimatedVectorState = state; |
| } else { |
| mAnimatedVectorState = new AnimatedVectorDrawableCompatState(context, state, mCallback, |
| res); |
| } |
| } |
| |
| @Override |
| public Drawable mutate() { |
| if (!mMutated && super.mutate() == this) { |
| mAnimatedVectorState = |
| new AnimatedVectorDrawableCompatState(null, mAnimatedVectorState, mCallback, |
| null); |
| mMutated = true; |
| } |
| return this; |
| } |
| |
| |
| /** |
| * Create a AnimatedVectorDrawableCompat object. |
| * |
| * @param context the context for creating the animators. |
| * @param resId the resource ID for AnimatedVectorDrawableCompat object. |
| * @return a new AnimatedVectorDrawableCompat or null if parsing error is found. |
| */ |
| @Nullable |
| public static AnimatedVectorDrawableCompat create(@NonNull Context context, |
| @DrawableRes int resId) { |
| Resources resources = context.getResources(); |
| try { |
| final XmlPullParser parser = resources.getXml(resId); |
| final AttributeSet attrs = Xml.asAttributeSet(parser); |
| int type; |
| while ((type = parser.next()) != XmlPullParser.START_TAG |
| && type != XmlPullParser.END_DOCUMENT) { |
| // Empty loop |
| } |
| if (type != XmlPullParser.START_TAG) { |
| throw new XmlPullParserException("No start tag found"); |
| } |
| |
| final AnimatedVectorDrawableCompat drawable = new AnimatedVectorDrawableCompat(context); |
| drawable.inflate(resources, parser, attrs, context.getTheme()); |
| |
| return drawable; |
| } catch (XmlPullParserException e) { |
| Log.e(LOGTAG, "parser error", e); |
| } catch (IOException e) { |
| Log.e(LOGTAG, "parser error", e); |
| } |
| return null; |
| } |
| |
| @Override |
| public ConstantState getConstantState() { |
| mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations(); |
| return mAnimatedVectorState; |
| } |
| |
| @Override |
| public int getChangingConfigurations() { |
| return super.getChangingConfigurations() | mAnimatedVectorState.mChangingConfigurations; |
| } |
| |
| @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); |
| } |
| |
| public void setTintList(ColorStateList tint) { |
| mAnimatedVectorState.mVectorDrawable.setTintList(tint); |
| } |
| |
| public void setTintMode(Mode tintMode) { |
| mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode); |
| } |
| |
| @Override |
| public boolean setVisible(boolean visible, boolean restart) { |
| mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart); |
| return super.setVisible(visible, restart); |
| } |
| |
| @Override |
| public boolean isStateful() { |
| return mAnimatedVectorState.mVectorDrawable.isStateful(); |
| } |
| |
| @Override |
| public int getOpacity() { |
| return mAnimatedVectorState.mVectorDrawable.getOpacity(); |
| } |
| |
| public int getIntrinsicWidth() { |
| return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth(); |
| } |
| |
| public int getIntrinsicHeight() { |
| return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight(); |
| } |
| |
| /** |
| * Obtains styled attributes from the theme, if available, or unstyled |
| * resources if the theme is null. |
| */ |
| static TypedArray obtainAttributes( |
| Resources res, Theme theme, AttributeSet set, int[] attrs) { |
| if (theme == null) { |
| return res.obtainAttributes(set, attrs); |
| } |
| return theme.obtainStyledAttributes(set, attrs, 0, 0); |
| } |
| |
| public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme) |
| throws XmlPullParserException, IOException { |
| int eventType = parser.getEventType(); |
| float pathErrorScale = 1; |
| while (eventType != XmlPullParser.END_DOCUMENT) { |
| if (eventType == XmlPullParser.START_TAG) { |
| final String tagName = parser.getName(); |
| if (DBG_ANIMATION_VECTOR_DRAWABLE) { |
| Log.v(LOGTAG, "tagName is " + tagName); |
| } |
| if (ANIMATED_VECTOR.equals(tagName)) { |
| final TypedArray a = |
| obtainAttributes(res, theme, attrs, AndroidResources.styleable_AnimatedVectorDrawable); |
| |
| int drawableRes = a.getResourceId(AndroidResources.styleable_AnimatedVectorDrawable_drawable, |
| 0); |
| if (DBG_ANIMATION_VECTOR_DRAWABLE) { |
| Log.v(LOGTAG, "drawableRes is " + drawableRes); |
| } |
| if (drawableRes != 0) { |
| VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create(res, |
| drawableRes, theme); |
| vectorDrawable.setAllowCaching(false); |
| vectorDrawable.setCallback(mCallback); |
| pathErrorScale = vectorDrawable.getPixelSize(); |
| if (mAnimatedVectorState.mVectorDrawable != null) { |
| mAnimatedVectorState.mVectorDrawable.setCallback(null); |
| } |
| mAnimatedVectorState.mVectorDrawable = vectorDrawable; |
| } |
| a.recycle(); |
| } else if (TARGET.equals(tagName)) { |
| final TypedArray a = |
| res.obtainAttributes(attrs, AndroidResources.styleable_AnimatedVectorDrawableTarget); |
| final String target = a.getString( |
| AndroidResources.styleable_AnimatedVectorDrawableTarget_name); |
| |
| int id = a.getResourceId(AndroidResources.styleable_AnimatedVectorDrawableTarget_animation, 0); |
| if (id != 0) { |
| Animator objectAnimator = AnimatorInflater.loadAnimator(mContext, id); |
| setupAnimatorsForTarget(target, objectAnimator); |
| } |
| a.recycle(); |
| } |
| } |
| |
| eventType = parser.next(); |
| } |
| } |
| |
| @Override |
| public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs) |
| throws XmlPullParserException, IOException { |
| inflate(res, parser, attrs, null); |
| } |
| |
| public boolean canApplyTheme() { |
| return false; |
| } |
| |
| private static class AnimatedVectorDrawableCompatState extends ConstantState { |
| int mChangingConfigurations; |
| VectorDrawableCompat mVectorDrawable; |
| ArrayList<Animator> mAnimators; |
| ArrayMap<Animator, String> mTargetNameMap; |
| Context mContext; |
| |
| public AnimatedVectorDrawableCompatState(Context context, |
| AnimatedVectorDrawableCompatState copy, Callback owner, Resources res) { |
| if (copy != null) { |
| mChangingConfigurations = copy.mChangingConfigurations; |
| if (copy.mVectorDrawable != null) { |
| final ConstantState cs = copy.mVectorDrawable.getConstantState(); |
| if (res != null) { |
| mVectorDrawable = (VectorDrawableCompat) cs.newDrawable(res); |
| } else { |
| mVectorDrawable = (VectorDrawableCompat) cs.newDrawable(); |
| } |
| mVectorDrawable = (VectorDrawableCompat) mVectorDrawable.mutate(); |
| mVectorDrawable.setCallback(owner); |
| mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds()); |
| mVectorDrawable.setAllowCaching(false); |
| } |
| if (copy.mAnimators != null) { |
| final int numAnimators = copy.mAnimators.size(); |
| mAnimators = new ArrayList<Animator>(numAnimators); |
| mTargetNameMap = new ArrayMap<Animator, String>(numAnimators); |
| for (int i = 0; i < numAnimators; ++i) { |
| Animator anim = copy.mAnimators.get(i); |
| Animator animClone = anim.clone(); |
| String targetName = copy.mTargetNameMap.get(anim); |
| Object targetObject = mVectorDrawable.getTargetByName(targetName); |
| animClone.setTarget(targetObject); |
| mAnimators.add(animClone); |
| mTargetNameMap.put(animClone, targetName); |
| } |
| } |
| } |
| |
| if (context != null) { |
| mContext = context; |
| } else { |
| mContext = copy.mContext; |
| } |
| |
| } |
| |
| @Override |
| public Drawable newDrawable() { |
| return new AnimatedVectorDrawableCompat(mContext, this, null); |
| } |
| |
| @Override |
| public Drawable newDrawable(Resources res) { |
| return new AnimatedVectorDrawableCompat(mContext, this, res); |
| } |
| |
| @Override |
| public int getChangingConfigurations() { |
| return mChangingConfigurations; |
| } |
| } |
| |
| private void setupAnimatorsForTarget(String name, Animator animator) { |
| Object target = mAnimatedVectorState.mVectorDrawable.getTargetByName(name); |
| animator.setTarget(target); |
| if (mAnimatedVectorState.mAnimators == null) { |
| mAnimatedVectorState.mAnimators = new ArrayList<Animator>(); |
| mAnimatedVectorState.mTargetNameMap = new ArrayMap<Animator, String>(); |
| } |
| mAnimatedVectorState.mAnimators.add(animator); |
| mAnimatedVectorState.mTargetNameMap.put(animator, name); |
| 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; |
| if (animators == null) { |
| return false; |
| } |
| 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; |
| } |
| |
| @Override |
| public void start() { |
| // If any one of the animator has not ended, do nothing. |
| if (isStarted()) { |
| return; |
| } |
| // Otherwise, kick off every animator. |
| 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.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(); |
| } |
| } |
| |
| private final Callback mCallback = new Callback() { |
| @Override |
| public void invalidateDrawable(Drawable who) { |
| invalidateSelf(); |
| } |
| |
| @Override |
| public void scheduleDrawable(Drawable who, Runnable what, long when) { |
| scheduleSelf(what, when); |
| } |
| |
| @Override |
| public void unscheduleDrawable(Drawable who, Runnable what) { |
| unscheduleSelf(what); |
| } |
| }; |
| } |