Merge "Distance based animation duration"
diff --git a/api/current.txt b/api/current.txt
index 75a8df9..686a6e8 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -525,6 +525,7 @@
field public static final int dropDownWidth = 16843362; // 0x1010262
field public static final int duplicateParentState = 16842985; // 0x10100e9
field public static final int duration = 16843160; // 0x1010198
+ field public static final int durationScaleHint = 16844014; // 0x10104ee
field public static final int editTextBackground = 16843602; // 0x1010352
field public static final int editTextColor = 16843601; // 0x1010351
field public static final int editTextPreferenceStyle = 16842898; // 0x1010092
@@ -2879,6 +2880,7 @@
method public void cancel();
method public android.animation.Animator clone();
method public void end();
+ method public long getDistanceBasedDuration();
method public abstract long getDuration();
method public android.animation.TimeInterpolator getInterpolator();
method public java.util.ArrayList<android.animation.Animator.AnimatorListener> getListeners();
@@ -2892,12 +2894,16 @@
method public void removePauseListener(android.animation.Animator.AnimatorPauseListener);
method public void resume();
method public abstract android.animation.Animator setDuration(long);
+ method public void setDurationScaleHint(int, android.content.res.Resources);
method public abstract void setInterpolator(android.animation.TimeInterpolator);
method public abstract void setStartDelay(long);
method public void setTarget(java.lang.Object);
method public void setupEndValues();
method public void setupStartValues();
method public void start();
+ field public static final int HINT_DISTANCE_DEFINED_IN_DP = 2; // 0x2
+ field public static final int HINT_DISTANCE_PROPORTIONAL_TO_SCREEN_SIZE = 1; // 0x1
+ field public static final int HINT_NO_SCALE = 0; // 0x0
}
public static abstract interface Animator.AnimatorListener {
diff --git a/api/system-current.txt b/api/system-current.txt
index d272c20..9a637ed 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -598,6 +598,7 @@
field public static final int dropDownWidth = 16843362; // 0x1010262
field public static final int duplicateParentState = 16842985; // 0x10100e9
field public static final int duration = 16843160; // 0x1010198
+ field public static final int durationScaleHint = 16844014; // 0x10104ee
field public static final int editTextBackground = 16843602; // 0x1010352
field public static final int editTextColor = 16843601; // 0x1010351
field public static final int editTextPreferenceStyle = 16842898; // 0x1010092
@@ -2959,6 +2960,7 @@
method public void cancel();
method public android.animation.Animator clone();
method public void end();
+ method public long getDistanceBasedDuration();
method public abstract long getDuration();
method public android.animation.TimeInterpolator getInterpolator();
method public java.util.ArrayList<android.animation.Animator.AnimatorListener> getListeners();
@@ -2972,12 +2974,16 @@
method public void removePauseListener(android.animation.Animator.AnimatorPauseListener);
method public void resume();
method public abstract android.animation.Animator setDuration(long);
+ method public void setDurationScaleHint(int, android.content.res.Resources);
method public abstract void setInterpolator(android.animation.TimeInterpolator);
method public abstract void setStartDelay(long);
method public void setTarget(java.lang.Object);
method public void setupEndValues();
method public void setupStartValues();
method public void start();
+ field public static final int HINT_DISTANCE_DEFINED_IN_DP = 2; // 0x2
+ field public static final int HINT_DISTANCE_PROPORTIONAL_TO_SCREEN_SIZE = 1; // 0x1
+ field public static final int HINT_NO_SCALE = 0; // 0x0
}
public static abstract interface Animator.AnimatorListener {
diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java
index da48709..02a329d 100644
--- a/core/java/android/animation/Animator.java
+++ b/core/java/android/animation/Animator.java
@@ -16,7 +16,12 @@
package android.animation;
+import android.content.res.Configuration;
import android.content.res.ConstantState;
+import android.content.res.Resources;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.animation.AnimationUtils;
import java.util.ArrayList;
@@ -25,6 +30,29 @@
* started, ended, and have <code>AnimatorListeners</code> added to them.
*/
public abstract class Animator implements Cloneable {
+ /**
+ * Set this hint when duration for the animation does not need to be scaled. By default, no
+ * scaling is applied to the duration.
+ */
+ public static final int HINT_NO_SCALE = 0;
+
+ /**
+ * Set this scale hint (using {@link #setDurationScaleHint(int, Resources)} when the animation's
+ * moving distance is proportional to the screen size. (e.g. a view coming in from the bottom of
+ * the screen to top/center). With this scale hint set, the animation duration will be
+ * automatically scaled based on screen size.
+ */
+ public static final int HINT_DISTANCE_PROPORTIONAL_TO_SCREEN_SIZE = 1;
+
+ /**
+ * Set this scale hint (using {@link #setDurationScaleHint(int, Resources)}) if the animation
+ * has pre-defined moving distance in dp that does not vary from device to device. This is
+ * extremely useful when the animation needs to run on both phones/tablets and TV, because TV
+ * has inflated dp and therefore will have a longer visual arc for the same animation than on
+ * the phone. This hint is used to calculate a scaling factor to compensate for different
+ * visual arcs while maintaining the same angular velocity for the animation.
+ */
+ public static final int HINT_DISTANCE_DEFINED_IN_DP = 2;
/**
* The set of listeners to be sent events through the life of an animation.
@@ -55,6 +83,24 @@
private AnimatorConstantState mConstantState;
/**
+ * Scaling factor for an animation that moves across the whole screen.
+ */
+ float mScreenSizeBasedDurationScale = 1.0f;
+
+ /**
+ * Scaling factor for an animation that is defined to move the same amount of dp across all
+ * devices.
+ */
+ float mDpBasedDurationScale = 1.0f;
+
+ /**
+ * By default, the scaling assumes the animation moves across the entire screen.
+ */
+ int mDurationScaleHint = HINT_NO_SCALE;
+
+ private final static boolean ANIM_DEBUG = false;
+
+ /**
* Starts this animation. If the animation has a nonzero startDelay, the animation will start
* running after that delay elapses. A non-delayed animation will have its initial
* value(s) set immediately, followed by calls to
@@ -184,6 +230,78 @@
public abstract long getDuration();
/**
+ * Hints how duration scaling factor should be calculated. The duration will not be scaled when
+ * hint is set to {@link #HINT_NO_SCALE}. Otherwise, the duration will be automatically scaled
+ * per device to achieve the same look and feel across different devices. In order to do
+ * that, the same angular velocity of the animation will be needed on different devices in
+ * users' field of view. Therefore, the duration scale factor is determined by the ratio of the
+ * angular movement on current devices to that on the baseline device (i.e. Nexus 5).
+ *
+ * @param hint an indicator on how the animation is defined. The hint could be
+ * {@link #HINT_NO_SCALE}, {@link #HINT_DISTANCE_PROPORTIONAL_TO_SCREEN_SIZE} or
+ * {@link #HINT_DISTANCE_DEFINED_IN_DP}.
+ * @param res The resources {@see android.content.res.Resources} for getting display metrics
+ */
+ public void setDurationScaleHint(int hint, Resources res) {
+ if (ANIM_DEBUG) {
+ Log.d("ANIM_DEBUG", "distance based duration hint: " + hint);
+ }
+ if (hint == mDurationScaleHint) {
+ return;
+ }
+ mDurationScaleHint = hint;
+ if (hint != HINT_NO_SCALE) {
+ int uiMode = res.getConfiguration().uiMode & Configuration.UI_MODE_TYPE_MASK;
+ DisplayMetrics metrics = res.getDisplayMetrics();
+ float width = metrics.widthPixels / metrics.xdpi;
+ float height = metrics.heightPixels / metrics.ydpi;
+ float viewingDistance = AnimationUtils.getViewingDistance(width, height, uiMode);
+ if (ANIM_DEBUG) {
+ Log.d("ANIM_DEBUG", "width, height, viewing distance, uimode: "
+ + width + ", " + height + ", " + viewingDistance + ", " + uiMode);
+ }
+ mScreenSizeBasedDurationScale = AnimationUtils
+ .getScreenSizeBasedDurationScale(width, height, viewingDistance);
+ mDpBasedDurationScale = AnimationUtils.getDpBasedDurationScale(
+ metrics.density, metrics.xdpi, viewingDistance);
+ if (ANIM_DEBUG) {
+ Log.d("ANIM_DEBUG", "screen based scale, dp based scale: " +
+ mScreenSizeBasedDurationScale + ", " + mDpBasedDurationScale);
+ }
+ }
+ }
+
+ // Copies duration scale hint and scaling factors to the new animation.
+ void copyDurationScaleInfoTo(Animator anim) {
+ anim.mDurationScaleHint = mDurationScaleHint;
+ anim.mScreenSizeBasedDurationScale = mScreenSizeBasedDurationScale;
+ anim.mDpBasedDurationScale = mDpBasedDurationScale;
+ }
+
+ /**
+ * @return The scaled duration calculated based on distance of movement (as defined by the
+ * animation) and perceived velocity (derived from the duration set on the animation for
+ * baseline device)
+ */
+ public long getDistanceBasedDuration() {
+ return (long) (getDuration() * getDistanceBasedDurationScale());
+ }
+
+ /**
+ * @return scaling factor of duration based on the duration scale hint. A scaling factor of 1
+ * means no scaling will be applied to the duration.
+ */
+ float getDistanceBasedDurationScale() {
+ if (mDurationScaleHint == HINT_DISTANCE_PROPORTIONAL_TO_SCREEN_SIZE) {
+ return mScreenSizeBasedDurationScale;
+ } else if (mDurationScaleHint == HINT_DISTANCE_DEFINED_IN_DP) {
+ return mDpBasedDurationScale;
+ } else {
+ return 1f;
+ }
+ }
+
+ /**
* The time interpolator used in calculating the elapsed fraction of the
* animation. The interpolator determines whether the animation runs with
* linear or non-linear motion, such as acceleration and deceleration. The
diff --git a/core/java/android/animation/AnimatorInflater.java b/core/java/android/animation/AnimatorInflater.java
index 4a9ba3b..df5a4cb 100644
--- a/core/java/android/animation/AnimatorInflater.java
+++ b/core/java/android/animation/AnimatorInflater.java
@@ -70,6 +70,13 @@
private static final int VALUE_TYPE_COLOR = 3;
private static final int VALUE_TYPE_UNDEFINED = 4;
+ /**
+ * Enum values used in XML attributes to indicate the duration scale hint.
+ */
+ private static final int HINT_NO_SCALE = 0;
+ private static final int HINT_PROPORTIONAL_TO_SCREEN = 1;
+ private static final int HINT_DEFINED_IN_DP = 2;
+
private static final boolean DBG_ANIMATOR_INFLATER = false;
// used to calculate changing configs for resource references
@@ -691,6 +698,9 @@
int ordering = a.getInt(R.styleable.AnimatorSet_ordering, TOGETHER);
createAnimatorFromXml(res, theme, parser, attrs, (AnimatorSet) anim, ordering,
pixelSize);
+ final int hint = a.getInt(R.styleable.Animator_durationScaleHint,
+ HINT_NO_SCALE);
+ anim.setDurationScaleHint(hint, res);
a.recycle();
} else if (name.equals("propertyValuesHolder")) {
PropertyValuesHolder[] values = loadValues(res, theme, parser,
@@ -1027,6 +1037,9 @@
anim.setInterpolator(interpolator);
}
+ final int hint = arrayAnimator.getInt(R.styleable.Animator_durationScaleHint,
+ HINT_NO_SCALE);
+ anim.setDurationScaleHint(hint, res);
arrayAnimator.recycle();
if (arrayObjectAnimator != null) {
arrayObjectAnimator.recycle();
diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java
index 53d5237..dd5f18e 100644
--- a/core/java/android/animation/AnimatorSet.java
+++ b/core/java/android/animation/AnimatorSet.java
@@ -519,6 +519,7 @@
for (Node node : mNodes) {
node.animation.setAllowRunningAsynchronously(false);
+ copyDurationScaleInfoTo(node.animation);
}
if (mDuration >= 0) {
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index 6ffa5dd..275e78e 100644
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -17,9 +17,12 @@
package android.animation;
import android.annotation.CallSuper;
+import android.content.res.Configuration;
+import android.content.res.Resources;
import android.os.Looper;
import android.os.Trace;
import android.util.AndroidRuntimeException;
+import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Choreographer;
import android.view.animation.AccelerateDecelerateInterpolator;
@@ -561,7 +564,7 @@
}
private void updateScaledDuration() {
- mDuration = (long)(mUnscaledDuration * sDurationScale);
+ mDuration = (long)(mUnscaledDuration * sDurationScale * getDistanceBasedDurationScale());
}
/**
diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java
index 4d1209a..0417921 100644
--- a/core/java/android/view/animation/AnimationUtils.java
+++ b/core/java/android/view/animation/AnimationUtils.java
@@ -16,6 +16,7 @@
package android.view.animation;
+import android.content.res.Configuration;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -44,6 +45,16 @@
private static final int TOGETHER = 0;
private static final int SEQUENTIALLY = 1;
+ private static final float RECOMMENDED_FIELD_OF_VIEW_FOR_TV = 40f;
+ private static final float ESTIMATED_VIEWING_DISTANCE_FOR_WATCH = 11f;
+ private static final float AVERAGE_VIEWING_DISTANCE_FOR_PHONES = 14.2f;
+ private static final float N5_DIAGONAL_VIEW_ANGLE = 19.58f;
+ private static final float N5_DENSITY = 3.0f;
+ private static final float N5_DPI = 443f;
+
+ private static final float COTANGENT_OF_HALF_TV_ANGLE = (float) (1 / Math.tan(Math.toRadians
+ (RECOMMENDED_FIELD_OF_VIEW_FOR_TV / 2)));
+
/**
* Returns the current animation time in milliseconds. This time should be used when invoking
@@ -367,4 +378,78 @@
}
return interpolator;
}
+
+ /**
+ * Derives the viewing distance of a device based on the device size (in inches), and the
+ * device type.
+ * @hide
+ */
+ public static float getViewingDistance(float width, float height, int uiMode) {
+ if (uiMode == Configuration.UI_MODE_TYPE_TELEVISION) {
+ // TV
+ return (width / 2) * COTANGENT_OF_HALF_TV_ANGLE;
+ } else if (uiMode == Configuration.UI_MODE_TYPE_WATCH) {
+ // Watch
+ return ESTIMATED_VIEWING_DISTANCE_FOR_WATCH;
+ } else {
+ // Tablet, phone, etc
+ return AVERAGE_VIEWING_DISTANCE_FOR_PHONES;
+ }
+ }
+
+ /**
+ * Calculates the duration scaling factor of an animation based on the hint that the animation
+ * will move across the entire screen. A scaling factor of 1 means the duration on this given
+ * device will be the same as the duration set through
+ * {@link android.animation.Animator#setDuration(long)}. The calculation uses Nexus 5 as a
+ * baseline device. That is, the duration of the animation on a given device will scale its
+ * duration so that it has the same look and feel as the animation on Nexus 5. In order to
+ * achieve the same perceived effect of the animation across different devices, we maintain
+ * the same angular speed of the same animation in users' field of view. Therefore, the
+ * duration scale factor is determined by the ratio of the angular movement on current
+ * devices to that on the baseline device.
+ *
+ * @param width width of the screen (in inches)
+ * @param height height of the screen (in inches)
+ * @param viewingDistance the viewing distance of the device (i.e. watch, phone, TV, etc) in
+ * inches
+ * @return scaling factor (or multiplier) of the duration set through
+ * {@link android.animation.Animator#setDuration(long)} on current device.
+ * @hide
+ */
+ public static float getScreenSizeBasedDurationScale(float width, float height,
+ float viewingDistance) {
+ // Animation's moving distance is proportional to the screen size.
+ float diagonal = (float) Math.sqrt(width * width + height * height);
+ float diagonalViewAngle = (float) Math.toDegrees(Math.atan((diagonal / 2f)
+ / viewingDistance) * 2);
+ return diagonalViewAngle / N5_DIAGONAL_VIEW_ANGLE;
+ }
+
+ /**
+ * Calculates the duration scaling factor of an animation under the assumption that the
+ * animation is defined to move the same amount of distance (in dp) across all devices. A
+ * scaling factor of 1 means the duration on this given device will be the same as the
+ * duration set through {@link android.animation.Animator#setDuration(long)}. The calculation
+ * uses Nexus 5 as a baseline device. That is, the duration of the animation on a given
+ * device will scale its duration so that it has the same look and feel as the animation on
+ * Nexus 5. In order to achieve the same perceived effect of the animation across different
+ * devices, we maintain the same angular velocity of the same animation in users' field of
+ * view. Therefore, the duration scale factor is determined by the ratio of the angular
+ * movement on current devices to that on the baseline device.
+ *
+ * @param density logical density of the display. {@link android.util.DisplayMetrics#density}
+ * @param dpi pixels per inch
+ * @param viewingDistance viewing distance of the device (in inches)
+ * @return the scaling factor of duration
+ * @hide
+ */
+ public static float getDpBasedDurationScale(float density, float dpi,
+ float viewingDistance) {
+ // Angle in users' field of view per dp:
+ float anglePerDp = (float) Math.atan2((density / dpi) / 2, viewingDistance) * 2;
+ float baselineAnglePerDp = (float) Math.atan2((N5_DENSITY / N5_DPI) / 2,
+ AVERAGE_VIEWING_DISTANCE_FOR_PHONES) * 2;
+ return anglePerDp / baselineAnglePerDp;
+ }
}
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index aefe79d..cbd74cd 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -5981,6 +5981,19 @@
<!-- values are colors, which are integers starting with "#". -->
<enum name="colorType" value="3" />
</attr>
+ <!-- Defines whether the animation should adjust duration in order to achieve the same
+ perceived effects on different devices. -->
+ <attr name="durationScaleHint" >
+ <!-- Default value for scale hint. When set, duration will not be scaled.-->
+ <enum name="noScale" value="0"/>
+ <!-- This should be used when the animation's moving distance is proportional to screen,
+ as the scaling is based on screen size. -->
+ <enum name="screen" value="1"/>
+ <!-- This is for animations that have a distance defined in dp, which will be the same
+ across different devices. In this case, scaling is based on the physical distance
+ per dp on the current device. -->
+ <enum name="dp" value="2"/>
+ </attr>
</declare-styleable>
<declare-styleable name="PropertyValuesHolder">
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 7349d23..c2f2c6d 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2660,4 +2660,7 @@
<public type="attr" name="supportsAssistGesture" />
<public type="attr" name="thumbPosition" />
+
+ <!-- Animation -->
+ <public type="attr" name="durationScaleHint" />
</resources>