am 91c091cf: Merge "State based animators for Views"

* commit '91c091cf7b732796d26971de6a508966e24d40a0':
  State based animators for Views
This commit is contained in:
Alan Viverette
2014-05-08 00:01:07 +00:00
committed by Android Git Automerger
13 changed files with 535 additions and 5 deletions

View File

@@ -1061,6 +1061,7 @@ package android {
field public static final int startDelay = 16843746; // 0x10103e2
field public static final int startOffset = 16843198; // 0x10101be
field public static final deprecated int startYear = 16843132; // 0x101017c
field public static final int stateListAnimator = 16843860; // 0x1010454
field public static final int stateNotNeeded = 16842774; // 0x1010016
field public static final int state_above_anchor = 16842922; // 0x10100aa
field public static final int state_accelerated = 16843547; // 0x101031b
@@ -2788,6 +2789,7 @@ package android.animation {
public class AnimatorInflater {
ctor public AnimatorInflater();
method public static android.animation.Animator loadAnimator(android.content.Context, int) throws android.content.res.Resources.NotFoundException;
method public static android.animation.StateListAnimator loadStateListAnimator(android.content.Context, int) throws android.content.res.Resources.NotFoundException;
}
public abstract class AnimatorListenerAdapter implements android.animation.Animator.AnimatorListener android.animation.Animator.AnimatorPauseListener {
@@ -2984,6 +2986,12 @@ package android.animation {
method public android.graphics.Rect evaluate(float, android.graphics.Rect, android.graphics.Rect);
}
public class StateListAnimator {
ctor public StateListAnimator();
method public void addState(int[], android.animation.Animator);
method public void jumpToCurrentState();
}
public class TimeAnimator extends android.animation.ValueAnimator {
ctor public TimeAnimator();
method public void setTimeListener(android.animation.TimeAnimator.TimeListener);
@@ -30834,6 +30842,7 @@ package android.view {
method public final int getScrollY();
method public java.lang.String getSharedElementName();
method public int getSolidColor();
method public android.animation.StateListAnimator getStateListAnimator();
method protected int getSuggestedMinimumHeight();
method protected int getSuggestedMinimumWidth();
method public int getSystemUiVisibility();
@@ -31098,6 +31107,7 @@ package android.view {
method public void setSelected(boolean);
method public void setSharedElementName(java.lang.String);
method public void setSoundEffectsEnabled(boolean);
method public void setStateListAnimator(android.animation.StateListAnimator);
method public void setSystemUiVisibility(int);
method public void setTag(java.lang.Object);
method public void setTag(int, java.lang.Object);

View File

@@ -21,6 +21,7 @@ import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.content.res.Resources.NotFoundException;
import android.util.AttributeSet;
import android.util.StateSet;
import android.util.TypedValue;
import android.util.Xml;
import android.view.animation.AnimationUtils;
@@ -87,9 +88,86 @@ public class AnimatorInflater {
}
}
public static StateListAnimator loadStateListAnimator(Context context, int id)
throws NotFoundException {
XmlResourceParser parser = null;
try {
parser = context.getResources().getAnimation(id);
return createStateListAnimatorFromXml(context, parser, Xml.asAttributeSet(parser));
} catch (XmlPullParserException ex) {
Resources.NotFoundException rnf =
new Resources.NotFoundException(
"Can't load state list animator resource ID #0x" +
Integer.toHexString(id)
);
rnf.initCause(ex);
throw rnf;
} catch (IOException ex) {
Resources.NotFoundException rnf =
new Resources.NotFoundException(
"Can't load state list animator resource ID #0x" +
Integer.toHexString(id)
);
rnf.initCause(ex);
throw rnf;
} finally {
if (parser != null) {
parser.close();
}
}
}
private static StateListAnimator createStateListAnimatorFromXml(Context context,
XmlPullParser parser, AttributeSet attributeSet)
throws IOException, XmlPullParserException {
int type;
StateListAnimator stateListAnimator = new StateListAnimator();
while (true) {
type = parser.next();
switch (type) {
case XmlPullParser.END_DOCUMENT:
case XmlPullParser.END_TAG:
return stateListAnimator;
case XmlPullParser.START_TAG:
// parse item
Animator animator = null;
if ("item".equals(parser.getName())) {
int attributeCount = parser.getAttributeCount();
int[] states = new int[attributeCount];
int stateIndex = 0;
for (int i = 0; i < attributeCount; i++) {
int attrName = attributeSet.getAttributeNameResource(i);
if (attrName == com.android.internal.R.attr.animation) {
animator = loadAnimator(context,
attributeSet.getAttributeResourceValue(i, 0));
} else {
states[stateIndex++] =
attributeSet.getAttributeBooleanValue(i, false) ?
attrName : -attrName;
}
}
if (animator == null) {
animator = createAnimatorFromXml(context, parser);
}
if (animator == null) {
throw new Resources.NotFoundException(
"animation state item must have a valid animation");
}
stateListAnimator
.addState(StateSet.trimStateSet(states, stateIndex), animator);
}
break;
}
}
}
private static Animator createAnimatorFromXml(Context c, XmlPullParser parser)
throws XmlPullParserException, IOException {
return createAnimatorFromXml(c, parser, Xml.asAttributeSet(parser), null, 0);
}

View File

@@ -0,0 +1,209 @@
/*
* 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.animation;
import android.util.StateSet;
import android.view.View;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
/**
* Lets you define a number of Animators that will run on the attached View depending on the View's
* drawable state.
* <p>
* It can be defined in an XML file with the <code>&lt;selector></code> element.
* Each State Animator is defined in a nested <code>&lt;item></code> element.
*
* @attr ref android.R.styleable#DrawableStates_state_focused
* @attr ref android.R.styleable#DrawableStates_state_window_focused
* @attr ref android.R.styleable#DrawableStates_state_enabled
* @attr ref android.R.styleable#DrawableStates_state_checkable
* @attr ref android.R.styleable#DrawableStates_state_checked
* @attr ref android.R.styleable#DrawableStates_state_selected
* @attr ref android.R.styleable#DrawableStates_state_activated
* @attr ref android.R.styleable#DrawableStates_state_active
* @attr ref android.R.styleable#DrawableStates_state_single
* @attr ref android.R.styleable#DrawableStates_state_first
* @attr ref android.R.styleable#DrawableStates_state_middle
* @attr ref android.R.styleable#DrawableStates_state_last
* @attr ref android.R.styleable#DrawableStates_state_pressed
* @attr ref android.R.styleable#StateListAnimatorItem_animation
*/
public class StateListAnimator {
private final ArrayList<Tuple> mTuples = new ArrayList<Tuple>();
private Tuple mLastMatch = null;
private Animator mRunningAnimator = null;
private WeakReference<View> mViewRef;
private AnimatorListenerAdapter mAnimatorListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (mRunningAnimator == animation) {
mRunningAnimator = null;
}
}
};
/**
* Associates the given animator with the provided drawable state specs so that it will be run
* when the View's drawable state matches the specs.
*
* @param specs The drawable state specs to match against
* @param animator The animator to run when the specs match
*/
public void addState(int[] specs, Animator animator) {
Tuple tuple = new Tuple(specs, animator);
tuple.mAnimator.addListener(mAnimatorListener);
mTuples.add(tuple);
}
/**
* Returns the current {@link android.animation.Animator} which is started because of a state
* change.
*
* @return The currently running Animator or null if no Animator is running
* @hide
*/
public Animator getRunningAnimator() {
return mRunningAnimator;
}
/**
* @hide
*/
public View getTarget() {
return mViewRef == null ? null : mViewRef.get();
}
/**
* Called by View
* @hide
*/
public void setTarget(View view) {
final View current = getTarget();
if (current == view) {
return;
}
if (current != null) {
clearTarget();
}
if (view != null) {
mViewRef = new WeakReference<View>(view);
}
}
private void clearTarget() {
final int size = mTuples.size();
for (int i = 0; i < size; i++) {
mTuples.get(i).mAnimator.setTarget(null);
}
mViewRef = null;
mLastMatch = null;
mRunningAnimator = null;
}
/**
* Called by View
* @hide
*/
public void setState(int[] state) {
Tuple match = null;
final int count = mTuples.size();
for (int i = 0; i < count; i++) {
final Tuple tuple = mTuples.get(i);
if (StateSet.stateSetMatches(tuple.mSpecs, state)) {
match = tuple;
break;
}
}
if (match == mLastMatch) {
return;
}
if (mLastMatch != null) {
cancel(mLastMatch);
}
mLastMatch = match;
if (match != null) {
start(match);
}
}
private void start(Tuple match) {
match.mAnimator.setTarget(getTarget());
mRunningAnimator = match.mAnimator;
match.mAnimator.start();
}
private void cancel(Tuple lastMatch) {
lastMatch.mAnimator.cancel();
lastMatch.mAnimator.setTarget(null);
}
/**
* @hide
*/
public ArrayList<Tuple> getTuples() {
return mTuples;
}
/**
* If there is an animation running for a recent state change, ends it.
* <p>
* This causes the animation to assign the end value(s) to the View.
*/
public void jumpToCurrentState() {
if (mRunningAnimator != null) {
mRunningAnimator.end();
}
}
/**
* @hide
*/
public static class Tuple {
final int[] mSpecs;
final Animator mAnimator;
private Tuple(int[] specs, Animator animator) {
mSpecs = specs;
mAnimator = animator;
}
/**
* @hide
*/
public int[] getSpecs() {
return mSpecs;
}
/**
* @hide
*/
public Animator getAnimator() {
return mAnimator;
}
}
}

View File

@@ -16,7 +16,9 @@
package android.view;
import android.animation.AnimatorInflater;
import android.animation.RevealAnimator;
import android.animation.StateListAnimator;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -667,6 +669,7 @@ import java.util.concurrent.atomic.AtomicInteger;
* @attr ref android.R.styleable#View_scrollbarTrackVertical
* @attr ref android.R.styleable#View_scrollbarAlwaysDrawHorizontalTrack
* @attr ref android.R.styleable#View_scrollbarAlwaysDrawVerticalTrack
* @attr ref android.R.styleable#View_stateListAnimator
* @attr ref android.R.styleable#View_sharedElementName
* @attr ref android.R.styleable#View_soundEffectsEnabled
* @attr ref android.R.styleable#View_tag
@@ -3257,6 +3260,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
private Outline mOutline;
/**
* Animator that automatically runs based on state changes.
*/
private StateListAnimator mStateListAnimator;
/**
* When this view has focus and the next focus is {@link #FOCUS_LEFT},
* the user may specify which view to go to next.
@@ -3995,6 +4003,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
case R.styleable.View_nestedScrollingEnabled:
setNestedScrollingEnabled(a.getBoolean(attr, false));
break;
case R.styleable.View_stateListAnimator:
setStateListAnimator(AnimatorInflater.loadStateListAnimator(context,
a.getResourceId(attr, 0)));
break;
}
}
@@ -10619,6 +10631,40 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
startRadius, endRadius, true);
}
/**
* Returns the current StateListAnimator if exists.
*
* @return StateListAnimator or null if it does not exists
* @see #setStateListAnimator(android.animation.StateListAnimator)
*/
public StateListAnimator getStateListAnimator() {
return mStateListAnimator;
}
/**
* Attaches the provided StateListAnimator to this View.
* <p>
* Any previously attached StateListAnimator will be detached.
*
* @param stateListAnimator The StateListAnimator to update the view
* @see {@link android.animation.StateListAnimator}
*/
public void setStateListAnimator(StateListAnimator stateListAnimator) {
if (mStateListAnimator == stateListAnimator) {
return;
}
if (mStateListAnimator != null) {
mStateListAnimator.setTarget(null);
}
mStateListAnimator = stateListAnimator;
if (stateListAnimator != null) {
stateListAnimator.setTarget(this);
if (isAttachedToWindow()) {
stateListAnimator.setState(getDrawableState());
}
}
}
/**
* Sets the outline of the view, which defines the shape of the shadow it
* casts.
@@ -12835,7 +12881,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
destroyLayer(false);
cleanupDraw();
mCurrentAnimation = null;
}
@@ -15489,9 +15534,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
/**
* This function is called whenever the state of the view changes in such
* a way that it impacts the state of drawables being shown.
*
* <p>Be sure to call through to the superclass when overriding this
* function.
* <p>
* If the View has a StateListAnimator, it will also be called to run necessary state
* change animations.
* <p>
* Be sure to call through to the superclass when overriding this function.
*
* @see Drawable#setState(int[])
*/
@@ -15500,6 +15547,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
if (d != null && d.isStateful()) {
d.setState(getDrawableState());
}
if (mStateListAnimator != null) {
mStateListAnimator.setState(getDrawableState());
}
}
/**
@@ -15644,11 +15695,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
/**
* Call {@link Drawable#jumpToCurrentState() Drawable.jumpToCurrentState()}
* on all Drawable objects associated with this view.
* <p>
* Also calls {@link StateListAnimator#jumpToCurrentState()} if there is a StateListAnimator
* attached to this view.
*/
public void jumpDrawablesToCurrentState() {
if (mBackground != null) {
mBackground.jumpToCurrentState();
}
if (mStateListAnimator != null) {
mStateListAnimator.jumpToCurrentState();
}
}
/**

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:state_enabled="true">
<set>
<objectAnimator android:propertyName="translationZ"
android:duration="@integer/button_pressed_animation_duration"
android:valueTo="@dimen/button_pressed_z"
android:valueType="floatType"/>
</set>
</item>
<!-- base state -->
<item>
<set>
<objectAnimator android:propertyName="translationZ"
android:duration="@integer/button_pressed_animation_duration"
android:valueTo="0"
android:valueType="floatType"/>
</set>
</item>
</selector>

View File

@@ -2364,6 +2364,9 @@
<!-- Specifies that this view should permit nested scrolling within a compatible
ancestor view. -->
<attr name="nestedScrollingEnabled" format="boolean" />
<!-- Sets the state-based animator for the View. -->
<attr name="stateListAnimator" format="reference"/>
</declare-styleable>
<!-- Attributes that can be assigned to a tag for a particular View. -->
@@ -4255,6 +4258,11 @@
<attr name="drawable" format="reference" />
</declare-styleable>
<!-- Attributes that can be assigned to a StateListAnimator item. -->
<declare-styleable name="StateListAnimatorItem">
<attr name="animation"/>
</declare-styleable>
<!-- Drawable used to render a geometric shape, with a gradient or a solid color. -->
<declare-styleable name="GradientDrawable">
<!-- Indicates whether the drawable should intially be visible. -->

View File

@@ -49,4 +49,7 @@
<dimen name="floating_window_z">16dp</dimen>
<dimen name="floating_window_margin">32dp</dimen>
<!-- the amount of elevation for pressed button state-->
<dimen name="button_pressed_z">2dp</dimen>
</resources>

View File

@@ -19,4 +19,5 @@
<resources>
<integer name="kg_carousel_angle">75</integer>
<integer name="kg_glowpad_rotation_offset">0</integer>
<integer name="button_pressed_animation_duration">100</integer>
</resources>

View File

@@ -2171,6 +2171,7 @@
<public type="attr" name="actionOverflowMenuStyle" />
<public type="attr" name="documentLaunchMode" />
<public type="attr" name="autoRemoveFromRecents" />
<public type="attr" name="stateListAnimator" />
<public-padding type="dimen" name="l_resource_pad" end="0x01050010" />

View File

@@ -364,6 +364,7 @@ please see styles_device_defaults.xml.
<item name="textColor">?attr/textColorPrimary</item>
<item name="minHeight">48dip</item>
<item name="minWidth">96dip</item>
<item name="stateListAnimator">@anim/button_state_list_anim_quantum</item>
</style>
<!-- Small bordered ink button -->
@@ -375,6 +376,7 @@ please see styles_device_defaults.xml.
<!-- Borderless ink button -->
<style name="Widget.Quantum.Button.Borderless">
<item name="background">@drawable/btn_borderless_quantum</item>
<item name="stateListAnimator">@null</item>
</style>
<!-- Small borderless ink button -->

View File

@@ -0,0 +1,6 @@
<?xml version="1.0"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator android:propertyName="x" android:duration="100" android:valueTo="0" android:valueType="floatType"/>
<objectAnimator android:propertyName="y" android:duration="100" android:valueTo="0" android:valueType="floatType"/>
<objectAnimator android:propertyName="z" android:duration="100" android:valueTo="0" android:valueType="floatType"/>
</set>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<set>
<objectAnimator android:propertyName="x" android:duration="100" android:valueTo="10" android:valueType="floatType"/>
<objectAnimator android:propertyName="y" android:duration="100" android:valueTo="20" android:valueType="floatType"/>
<objectAnimator android:propertyName="z" android:duration="100" android:valueTo="20" android:valueType="floatType"/>
</set>
</item>
<item android:state_enabled="true" android:state_pressed="false">
<set>
<objectAnimator android:propertyName="x" android:duration="100" android:valueTo="0" android:valueType="floatType"/>
<objectAnimator android:propertyName="y" android:duration="100" android:valueTo="0" android:valueType="floatType"/>
<objectAnimator android:propertyName="z" android:duration="100" android:valueTo="0" android:valueType="floatType"/>
</set>
</item>
<!-- base state-->
<item android:animation="@anim/reset_state_anim"/>
</selector>

View File

@@ -0,0 +1,102 @@
/*
* 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.animation;
import android.test.ActivityInstrumentationTestCase2;
import android.test.UiThreadTest;
import android.util.StateSet;
import android.view.View;
import android.view.ViewGroup;
import com.android.frameworks.coretests.R;
import java.util.concurrent.atomic.AtomicInteger;
public class StateListAnimatorTest extends ActivityInstrumentationTestCase2<BasicAnimatorActivity> {
public StateListAnimatorTest() {
super(BasicAnimatorActivity.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
}
public void testInflateFromAnimator() throws Exception {
StateListAnimator stateListAnimator = AnimatorInflater
.loadStateListAnimator(getActivity(), R.anim.test_state_anim);
assertNotNull("A state list animator should be returned", stateListAnimator);
assertEquals("State list animator should have three items", 3,
stateListAnimator.getTuples().size());
}
@UiThreadTest
public void testAttachDetach() throws Exception {
View view = new View(getActivity());
final AtomicInteger setStateCount = new AtomicInteger(0);
StateListAnimator stateListAnimator = new StateListAnimator() {
@Override
public void setState(int[] state) {
setStateCount.incrementAndGet();
super.setState(state);
}
};
view.setStateListAnimator(stateListAnimator);
assertNotNull("State list animator should have a reference to view even if it is detached",
stateListAnimator.getTarget());
ViewGroup viewGroup = (ViewGroup) getActivity().findViewById(android.R.id.content);
int preSetStateCount = setStateCount.get();
viewGroup.addView(view);
assertTrue("When view is attached, state list drawable's setState should be called",
preSetStateCount < setStateCount.get());
StateListAnimator stateListAnimator2 = new StateListAnimator();
view.setStateListAnimator(stateListAnimator2);
assertNull("When a new state list animator is assigned, previous one should be detached",
stateListAnimator.getTarget());
assertNull("Any running animator should be removed on detach",
stateListAnimator.getRunningAnimator());
assertEquals("The new state list animator should be attached to the view",
view, stateListAnimator2.getTarget());
viewGroup.removeView(view);
assertNotNull("When view is detached from window, state list animator should still keep the"
+ " reference",
stateListAnimator2.getTarget());
}
public void testStateListLoading() throws InterruptedException {
StateListAnimator stateListAnimator = AnimatorInflater
.loadStateListAnimator(getActivity(), R.anim.test_state_anim);
assertNotNull("A state list animator should be returned", stateListAnimator);
assertEquals("Steate list animator should have two items", 3,
stateListAnimator.getTuples().size());
StateListAnimator.Tuple tuple1 = stateListAnimator.getTuples().get(0);
assertEquals("first tuple should have one state", 1, tuple1.getSpecs().length);
assertEquals("first spec in tuple 1 should be pressed",
com.android.internal.R.attr.state_pressed, tuple1.getSpecs()[0]);
StateListAnimator.Tuple tuple2 = stateListAnimator.getTuples().get(1);
assertEquals("Second tuple should have two specs", 2, tuple2.getSpecs().length);
assertTrue("Tuple two should match the expected state",
StateSet.stateSetMatches(tuple2.getSpecs(),
new int[]{-com.android.internal.R.attr.state_pressed,
com.android.internal.R.attr.state_enabled}));
}
}