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>