Java tutorial
/* * 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 com.hippo.vector; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; 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.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.NonNull; import android.support.v4.util.ArrayMap; import android.util.AttributeSet; import android.util.Log; import android.util.Xml; import com.hippo.anani.Animatable2; import com.hippo.anani.AnimatorInflater; 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 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 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 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#AnimatedVectorDrawable_drawable * @attr ref android.R.styleable#AnimatedVectorDrawableTarget_name * @attr ref android.R.styleable#AnimatedVectorDrawableTarget_animation */ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { private static final String LOGTAG = "AnimatedVectorDrawable"; private static final String ANIMATED_VECTOR = "animated-vector"; private static final String TARGET = "target"; private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false; /** Local, mutable animator set. */ private final AnimatorSet mAnimatorSet = new AnimatorSet(); private AnimatedVectorDrawableState mAnimatedVectorState; /** Whether the animator set has been prepared. */ private boolean mHasAnimatorSet; private boolean mMutated; /** Use a internal AnimatorListener to support callbacks during animation events. */ private ArrayList<Animatable2.AnimationCallback> mAnimationCallbacks = null; private AnimatorListener mAnimatorListener = null; public AnimatedVectorDrawable() { this(null); } private AnimatedVectorDrawable(AnimatedVectorDrawableState state) { mAnimatedVectorState = new AnimatedVectorDrawableState(state, mCallback); } @Override public Drawable mutate() { if (!mMutated && super.mutate() == this) { mAnimatedVectorState = new AnimatedVectorDrawableState(mAnimatedVectorState, mCallback); mMutated = true; } return this; } public void clearMutated() { if (mAnimatedVectorState.mVectorDrawable != null) { mAnimatedVectorState.mVectorDrawable.clearMutated(); } mMutated = false; } @Override public ConstantState getConstantState() { mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations(); return mAnimatedVectorState; } @Override public int getChangingConfigurations() { return super.getChangingConfigurations() | mAnimatedVectorState.getChangingConfigurations(); } @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 boolean onLayoutDirectionChanged(int layoutDirection) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return mAnimatedVectorState.mVectorDrawable.setLayoutDirection(layoutDirection); } else { return false; } } @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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mAnimatedVectorState.mVectorDrawable.setHotspot(x, y); } } @Override public void setHotspotBounds(int left, int top, int right, int bottom) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mAnimatedVectorState.mVectorDrawable.setHotspotBounds(left, top, right, bottom); } } @Override public void setTintMode(@NonNull 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); } @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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mAnimatedVectorState.mVectorDrawable.getOutline(outline); } } public Insets getOpticalInsets() { return mAnimatedVectorState.mVectorDrawable.getOpticalInsets(); } public static AnimatedVectorDrawable create(Context context, int rid) { try { final XmlPullParser parser = context.getResources().getXml(rid); 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 String name = parser.getName(); if (!ANIMATED_VECTOR.equals(name)) { throw new IllegalStateException("It is not animated-vector"); } final AnimatedVectorDrawable drawable = new AnimatedVectorDrawable(); drawable.inflate(context, parser, attrs); return drawable; } catch (XmlPullParserException e) { Log.e(LOGTAG, "parser error", e); } catch (IOException e) { Log.e(LOGTAG, "parser error", e); } return null; } public void inflate(Context context, XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException { final AnimatedVectorDrawableState state = mAnimatedVectorState; int eventType = parser.getEventType(); float pathErrorScale = 1; while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG) { final String tagName = parser.getName(); if (ANIMATED_VECTOR.equals(tagName)) { final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AnimatedVectorDrawable); int drawableRes = a.getResourceId(R.styleable.AnimatedVectorDrawable_drawable, 0); if (drawableRes != 0) { VectorDrawable vectorDrawable = VectorDrawable.create(context, drawableRes); if (vectorDrawable != null) { vectorDrawable.setAllowCaching(false); vectorDrawable.setCallback(mCallback); pathErrorScale = vectorDrawable.getPixelSize(); if (state.mVectorDrawable != null) { state.mVectorDrawable.setCallback(null); } state.mVectorDrawable = vectorDrawable; } } a.recycle(); } else if (TARGET.equals(tagName)) { final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AnimatedVectorDrawableTarget); final String target = a.getString(R.styleable.AnimatedVectorDrawableTarget_name); final int animResId = a.getResourceId(R.styleable.AnimatedVectorDrawableTarget_animation, 0); if (animResId != 0) { state.addTargetAnimator(target, AnimatorInflater.loadAnimator(context, animResId, pathErrorScale)); } a.recycle(); } } eventType = parser.next(); } } private static class AnimatedVectorDrawableState extends ConstantState { int mChangingConfigurations; VectorDrawable mVectorDrawable; /** Fully inflated animators awaiting cloning into an AnimatorSet. */ ArrayList<Animator> mAnimators; /** Map of animators to their target object names */ ArrayMap<Animator, String> mTargetNameMap; public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy, Callback owner) { if (copy != null) { mChangingConfigurations = copy.mChangingConfigurations; if (copy.mVectorDrawable != null) { final ConstantState cs = copy.mVectorDrawable.getConstantState(); mVectorDrawable = (VectorDrawable) cs.newDrawable(); mVectorDrawable = (VectorDrawable) mVectorDrawable.mutate(); mVectorDrawable.setCallback(owner); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { //noinspection WrongConstant mVectorDrawable.setLayoutDirection(copy.mVectorDrawable.getLayoutDirection()); } mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds()); mVectorDrawable.setAllowCaching(false); } if (copy.mAnimators != null) { mAnimators = new ArrayList<>(copy.mAnimators); } if (copy.mTargetNameMap != null) { mTargetNameMap = new ArrayMap<>(copy.mTargetNameMap); } } else { mVectorDrawable = new VectorDrawable(); } } @Override public Drawable newDrawable() { return new AnimatedVectorDrawable(this); } @Override public Drawable newDrawable(Resources res) { return new AnimatedVectorDrawable(this); } @Override public int getChangingConfigurations() { return mChangingConfigurations; } public void addTargetAnimator(String targetName, Animator animator) { if (mAnimators == null) { mAnimators = new ArrayList<>(1); mTargetNameMap = new ArrayMap<>(1); } mAnimators.add(animator); mTargetNameMap.put(animator, targetName); if (DBG_ANIMATION_VECTOR_DRAWABLE) { Log.v(LOGTAG, "add animator for target " + targetName + " " + animator); } } /** * Prepares a local set of mutable animators based on the constant * state. * <p> * If there are any pending uninflated animators, attempts to inflate * them immediately against the provided resources object. * * @param animatorSet the animator set to which the animators should * be added */ public void prepareLocalAnimators(@NonNull AnimatorSet animatorSet) { // Perform a deep copy of the constant state's animators. final int count = mAnimators == null ? 0 : mAnimators.size(); if (count > 0) { final Animator firstAnim = prepareLocalAnimator(0); final AnimatorSet.Builder builder = animatorSet.play(firstAnim); for (int i = 1; i < count; ++i) { final Animator nextAnim = prepareLocalAnimator(i); builder.with(nextAnim); } } } /** * Prepares a local animator for the given index within the constant * state's list of animators. * * @param index the index of the animator within the constant state */ private Animator prepareLocalAnimator(int index) { final Animator animator = mAnimators.get(index); final Animator localAnimator = animator.clone(); final String targetName = mTargetNameMap.get(animator); final Object target = mVectorDrawable.getTargetByName(targetName); localAnimator.setTarget(target); return localAnimator; } } @Override public boolean isRunning() { return mAnimatorSet.isRunning(); } private boolean isStarted() { return mAnimatorSet.isStarted(); } /** * Resets the AnimatedVectorDrawable to the start state as specified in the animators. */ public void reset() { // TODO: Use reverse or seek to implement reset, when AnimatorSet supports them. start(); mAnimatorSet.cancel(); } @Override public void start() { ensureAnimatorSet(); // If any one of the animator has not ended, do nothing. if (isStarted()) { return; } mAnimatorSet.start(); invalidateSelf(); } private void ensureAnimatorSet() { if (!mHasAnimatorSet) { mAnimatedVectorState.prepareLocalAnimators(mAnimatorSet); mHasAnimatorSet = true; } } @Override public void stop() { mAnimatorSet.end(); } /* /** * Reverses ongoing animations or starts pending animations in reverse. * <p> * NOTE: Only works if all animations support reverse. Otherwise, this will * do nothing. * @hide */ /* public void reverse() { ensureAnimatorSet(); // Only reverse when all the animators can be reversed. if (!canReverse()) { Log.w(LOGTAG, "AnimatedVectorDrawable can't reverse()"); return; } mAnimatorSet.reverse(); invalidateSelf(); } */ /* /** * @hide */ /* public boolean canReverse() { return mAnimatorSet.canReverse(); } */ 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); } }; @Override public void registerAnimationCallback(@NonNull AnimationCallback callback) { if (callback == null) { return; } // Add listener accordingly. if (mAnimationCallbacks == null) { mAnimationCallbacks = new ArrayList<>(); } mAnimationCallbacks.add(callback); if (mAnimatorListener == null) { // Create a animator listener and trigger the callback events when listener is // triggered. mAnimatorListener = new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { ArrayList<AnimationCallback> tmpCallbacks = new ArrayList<>(mAnimationCallbacks); int size = tmpCallbacks.size(); for (int i = 0; i < size; i++) { tmpCallbacks.get(i).onAnimationStart(AnimatedVectorDrawable.this); } } @Override public void onAnimationEnd(Animator animation) { ArrayList<AnimationCallback> tmpCallbacks = new ArrayList<>(mAnimationCallbacks); int size = tmpCallbacks.size(); for (int i = 0; i < size; i++) { tmpCallbacks.get(i).onAnimationEnd(AnimatedVectorDrawable.this); } } }; } mAnimatorSet.addListener(mAnimatorListener); } // A helper function to clean up the animator listener in the mAnimatorSet. private void removeAnimatorSetListener() { if (mAnimatorListener != null) { mAnimatorSet.removeListener(mAnimatorListener); mAnimatorListener = null; } } @Override public boolean unregisterAnimationCallback(@NonNull AnimationCallback callback) { if (mAnimationCallbacks == null || callback == null) { // Nothing to be removed. return false; } boolean removed = mAnimationCallbacks.remove(callback); // When the last call back unregistered, remove the listener accordingly. if (mAnimationCallbacks.size() == 0) { removeAnimatorSetListener(); } return removed; } @Override public void clearAnimationCallbacks() { removeAnimatorSetListener(); if (mAnimationCallbacks == null) { return; } mAnimationCallbacks.clear(); } }