Bug 16984007 Animated Vector Drawables were using the viewport dimensions for calculating the allowable animation error. Instead of using viewport dimensions, it is better to use the intrinsic dimensions. Using the viewport dimensions meant that a small viewport (e.g. 1x1) would mean that animation paths within would only have an accuracy of 50% of the dimensions of the drawable. Change-Id: Id0152eabb4effd1e50c644eea7a371b38baeb7c1
437 lines
15 KiB
Java
437 lines
15 KiB
Java
/*
|
|
* 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();
|
|
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 = obtainAttributes(res, theme, attrs,
|
|
R.styleable.AnimatedVectorDrawable);
|
|
int drawableRes = a.getResourceId(
|
|
R.styleable.AnimatedVectorDrawable_drawable, 0);
|
|
if (drawableRes != 0) {
|
|
VectorDrawable vectorDrawable = (VectorDrawable) res.getDrawable(
|
|
drawableRes, theme).mutate();
|
|
vectorDrawable.setAllowCaching(false);
|
|
pathErrorScale = vectorDrawable.getPixelSize();
|
|
mAnimatedVectorState.mVectorDrawable = vectorDrawable;
|
|
}
|
|
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,
|
|
pathErrorScale);
|
|
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;
|
|
}
|
|
}
|