Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ecd857be authored by George Mount's avatar George Mount
Browse files

Add curved motion to Transitions.

Bug 15197527

Added two public PathMotions: PatternMotion and ArcMotion.
ArcMotion is the algorithm provided by UX. PatternMotion
provides a mechanism for using a Path as a pattern.

Change-Id: Ie57fd5f4e62269acc1164eced39853a12c52bd77
parent 32292eb8
Loading
Loading
Loading
Loading
+45 −0
Original line number Diff line number Diff line
@@ -825,6 +825,7 @@ package android {
    field public static final int maxRows = 16843059; // 0x1010133
    field public static final int maxSdkVersion = 16843377; // 0x1010271
    field public static final int maxWidth = 16843039; // 0x101011f
    field public static final int maximumAngle = 16843905; // 0x1010481
    field public static final int measureAllChildren = 16843018; // 0x101010a
    field public static final int measureWithLargestChild = 16843476; // 0x10102d4
    field public static final int mediaRouteButtonStyle = 16843693; // 0x10103ad
@@ -840,6 +841,8 @@ package android {
    field public static final int minResizeWidth = 16843669; // 0x1010395
    field public static final int minSdkVersion = 16843276; // 0x101020c
    field public static final int minWidth = 16843071; // 0x101013f
    field public static final int minimumHorizontalAngle = 16843903; // 0x101047f
    field public static final int minimumVerticalAngle = 16843904; // 0x1010480
    field public static final int mipMap = 16843725; // 0x10103cd
    field public static final int mirrorForRtl = 16843726; // 0x10103ce
    field public static final int mode = 16843134; // 0x101017e
@@ -30450,12 +30453,26 @@ package android.text.util {
package android.transition {
  public class ArcMotion extends android.transition.PathMotion {
    ctor public ArcMotion();
    ctor public ArcMotion(android.content.Context, android.util.AttributeSet);
    method public float getMaximumAngle();
    method public float getMinimumHorizontalAngle();
    method public float getMinimumVerticalAngle();
    method public android.graphics.Path getPath(float, float, float, float);
    method public void setMaximumAngle(float);
    method public void setMinimumHorizontalAngle(float);
    method public void setMinimumVerticalAngle(float);
  }
  public class AutoTransition extends android.transition.TransitionSet {
    ctor public AutoTransition();
    ctor public AutoTransition(android.content.Context, android.util.AttributeSet);
  }
  public class ChangeBounds extends android.transition.Transition {
    ctor public ChangeBounds();
    ctor public ChangeBounds(android.content.Context, android.util.AttributeSet);
    method public void captureEndValues(android.transition.TransitionValues);
    method public void captureStartValues(android.transition.TransitionValues);
    method public void setReparent(boolean);
@@ -30464,18 +30481,21 @@ package android.transition {
  public class ChangeClipBounds extends android.transition.Transition {
    ctor public ChangeClipBounds();
    ctor public ChangeClipBounds(android.content.Context, android.util.AttributeSet);
    method public void captureEndValues(android.transition.TransitionValues);
    method public void captureStartValues(android.transition.TransitionValues);
  }
  public class ChangeImageTransform extends android.transition.Transition {
    ctor public ChangeImageTransform();
    ctor public ChangeImageTransform(android.content.Context, android.util.AttributeSet);
    method public void captureEndValues(android.transition.TransitionValues);
    method public void captureStartValues(android.transition.TransitionValues);
  }
  public class ChangeTransform extends android.transition.Transition {
    ctor public ChangeTransform();
    ctor public ChangeTransform(android.content.Context, android.util.AttributeSet);
    method public void captureEndValues(android.transition.TransitionValues);
    method public void captureStartValues(android.transition.TransitionValues);
  }
@@ -30488,15 +30508,32 @@ package android.transition {
  public class Explode extends android.transition.Visibility {
    ctor public Explode();
    ctor public Explode(android.content.Context, android.util.AttributeSet);
  }
  public class Fade extends android.transition.Visibility {
    ctor public Fade();
    ctor public Fade(int);
    ctor public Fade(android.content.Context, android.util.AttributeSet);
    field public static final int IN = 1; // 0x1
    field public static final int OUT = 2; // 0x2
  }
  public abstract class PathMotion {
    ctor public PathMotion();
    ctor public PathMotion(android.content.Context, android.util.AttributeSet);
    method public abstract android.graphics.Path getPath(float, float, float, float);
  }
  public class PatternMotion extends android.transition.PathMotion {
    ctor public PatternMotion();
    ctor public PatternMotion(android.content.Context, android.util.AttributeSet);
    ctor public PatternMotion(android.graphics.Path);
    method public android.graphics.Path getPath(float, float, float, float);
    method public android.graphics.Path getPattern();
    method public void setPattern(android.graphics.Path);
  }
  public final class Scene {
    ctor public Scene(android.view.ViewGroup);
    ctor public Scene(android.view.ViewGroup, android.view.View);
@@ -30519,11 +30556,14 @@ package android.transition {
  public class Slide extends android.transition.Visibility {
    ctor public Slide();
    ctor public Slide(int);
    ctor public Slide(android.content.Context, android.util.AttributeSet);
    method public int getSlideEdge();
    method public void setSlideEdge(int);
  }
  public abstract class Transition implements java.lang.Cloneable {
    ctor public Transition();
    ctor public Transition(android.content.Context, android.util.AttributeSet);
    method public android.transition.Transition addListener(android.transition.Transition.TransitionListener);
    method public android.transition.Transition addTarget(int);
    method public android.transition.Transition addTarget(java.lang.String);
@@ -30546,6 +30586,7 @@ package android.transition {
    method public android.transition.Transition.EpicenterCallback getEpicenterCallback();
    method public android.animation.TimeInterpolator getInterpolator();
    method public java.lang.String getName();
    method public android.transition.PathMotion getPathMotion();
    method public android.transition.TransitionPropagation getPropagation();
    method public long getStartDelay();
    method public java.util.List<java.lang.Integer> getTargetIds();
@@ -30563,6 +30604,7 @@ package android.transition {
    method public void setEpicenterCallback(android.transition.Transition.EpicenterCallback);
    method public android.transition.Transition setInterpolator(android.animation.TimeInterpolator);
    method public void setMatchOrder(int...);
    method public void setPathMotion(android.transition.PathMotion);
    method public void setPropagation(android.transition.TransitionPropagation);
    method public android.transition.Transition setStartDelay(long);
    field public static final int MATCH_ID = 3; // 0x3
@@ -30610,6 +30652,7 @@ package android.transition {
  public class TransitionSet extends android.transition.Transition {
    ctor public TransitionSet();
    ctor public TransitionSet(android.content.Context, android.util.AttributeSet);
    method public android.transition.TransitionSet addTransition(android.transition.Transition);
    method public void captureEndValues(android.transition.TransitionValues);
    method public void captureStartValues(android.transition.TransitionValues);
@@ -30628,8 +30671,10 @@ package android.transition {
  public abstract class Visibility extends android.transition.Transition {
    ctor public Visibility();
    ctor public Visibility(android.content.Context, android.util.AttributeSet);
    method public void captureEndValues(android.transition.TransitionValues);
    method public void captureStartValues(android.transition.TransitionValues);
    method public int getMode();
    method public boolean isVisible(android.transition.TransitionValues);
    method public android.animation.Animator onAppear(android.view.ViewGroup, android.transition.TransitionValues, int, android.transition.TransitionValues, int);
    method public android.animation.Animator onAppear(android.view.ViewGroup, android.view.View, android.transition.TransitionValues, android.transition.TransitionValues);
+272 −0
Original line number Diff line number Diff line
/*
 * 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.transition;

import com.android.internal.R;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Path;
import android.util.AttributeSet;
import android.util.FloatMath;

/**
 * A PathMotion that generates a curved path along an arc on an imaginary circle containing
 * the two points. If the horizontal distance between the points is less than the vertical
 * distance, then the circle's center point will be horizontally aligned with the end point. If the
 * vertical distance is less than the horizontal distance then the circle's center point
 * will be vertically aligned with the end point.
 * <p>
 * When the two points are near horizontal or vertical, the curve of the motion will be
 * small as the center of the circle will be far from both points. To force curvature of
 * the path, {@link #setMinimumHorizontalAngle(float)} and
 * {@link #setMinimumVerticalAngle(float)} may be used to set the minimum angle of the
 * arc between two points.
 * </p>
 * <p>This may be used in XML as an element inside a transition.</p>
 * <pre>
 * {@code
 * &lt;changeBounds>
 *   &lt;arcMotion android:minimumHorizontalAngle="15"
 *     android:minimumVerticalAngle="0" android:maximumAngle="90"/>
 * &lt;/changeBounds>
 * }
 * </pre>
 */
public class ArcMotion extends PathMotion {

    private static final float DEFAULT_MIN_ANGLE_DEGREES = 0;
    private static final float DEFAULT_MAX_ANGLE_DEGREES = 70;
    private static final float DEFAULT_MAX_TANGENT = (float)
            Math.tan(Math.toRadians(DEFAULT_MAX_ANGLE_DEGREES/2));

    private float mMinimumHorizontalAngle = 0;
    private float mMinimumVerticalAngle = 0;
    private float mMaximumAngle = DEFAULT_MAX_ANGLE_DEGREES;
    private float mMinimumHorizontalTangent = 0;
    private float mMinimumVerticalTangent = 0;
    private float mMaximumTangent = DEFAULT_MAX_TANGENT;

    public ArcMotion() {}

    public ArcMotion(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ArcMotion);
        float minimumVerticalAngle = a.getFloat(R.styleable.ArcMotion_minimumVerticalAngle,
                DEFAULT_MIN_ANGLE_DEGREES);
        setMinimumVerticalAngle(minimumVerticalAngle);
        float minimumHorizontalAngle = a.getFloat(R.styleable.ArcMotion_minimumHorizontalAngle,
                DEFAULT_MIN_ANGLE_DEGREES);
        setMinimumHorizontalAngle(minimumHorizontalAngle);
        float maximumAngle = a.getFloat(R.styleable.ArcMotion_maximumAngle,
                DEFAULT_MAX_ANGLE_DEGREES);
        setMaximumAngle(maximumAngle);
        a.recycle();
    }

    /**
     * Sets the minimum arc along the circle between two points aligned near horizontally.
     * When start and end points are close to horizontal, the calculated center point of the
     * circle will be far from both points, giving a near straight path between the points.
     * By setting a minimum angle, this forces the center point to be closer and give an
     * exaggerated curve to the path.
     * <p>The default value is 0.</p>
     *
     * @param angleInDegrees The minimum angle of the arc on a circle describing the Path
     *                       between two nearly horizontally-separated points.
     * @attr ref android.R.styleable#ArcMotion_minimumHorizontalAngle
     */
    public void setMinimumHorizontalAngle(float angleInDegrees) {
        mMinimumHorizontalAngle = angleInDegrees;
        mMinimumHorizontalTangent = toTangent(angleInDegrees);
    }

    /**
     * Returns the minimum arc along the circle between two points aligned near horizontally.
     * When start and end points are close to horizontal, the calculated center point of the
     * circle will be far from both points, giving a near straight path between the points.
     * By setting a minimum angle, this forces the center point to be closer and give an
     * exaggerated curve to the path.
     * <p>The default value is 0.</p>
     *
     * @return  The minimum arc along the circle between two points aligned near horizontally.
     * @attr ref android.R.styleable#ArcMotion_minimumHorizontalAngle
     */
    public float getMinimumHorizontalAngle() {
        return mMinimumHorizontalAngle;
    }

    /**
     * Sets the minimum arc along the circle between two points aligned near vertically.
     * When start and end points are close to vertical, the calculated center point of the
     * circle will be far from both points, giving a near straight path between the points.
     * By setting a minimum angle, this forces the center point to be closer and give an
     * exaggerated curve to the path.
     * <p>The default value is 0.</p>
     *
     * @param angleInDegrees The minimum angle of the arc on a circle describing the Path
     *                       between two nearly vertically-separated points.
     * @attr ref android.R.styleable#ArcMotion_minimumVerticalAngle
     */
    public void setMinimumVerticalAngle(float angleInDegrees) {
        mMinimumVerticalAngle = angleInDegrees;
        mMinimumVerticalTangent = toTangent(angleInDegrees);
    }

    /**
     * Returns the minimum arc along the circle between two points aligned near vertically.
     * When start and end points are close to vertical, the calculated center point of the
     * circle will be far from both points, giving a near straight path between the points.
     * By setting a minimum angle, this forces the center point to be closer and give an
     * exaggerated curve to the path.
     * <p>The default value is 0.</p>
     *
     * @return The minimum angle of the arc on a circle describing the Path
     *         between two nearly vertically-separated points.
     * @attr ref android.R.styleable#ArcMotion_minimumVerticalAngle
     */
    public float getMinimumVerticalAngle() {
        return mMinimumVerticalAngle;
    }

    /**
     * Sets the maximum arc along the circle between two points. When start and end points
     * have close to equal x and y differences, the curve between them is large. This forces
     * the curved path to have an arc of at most the given angle.
     * <p>The default value is 70 degrees.</p>
     *
     * @param angleInDegrees The maximum angle of the arc on a circle describing the Path
     *                       between the start and end points.
     * @attr ref android.R.styleable#ArcMotion_maximumAngle
     */
    public void setMaximumAngle(float angleInDegrees) {
        mMaximumAngle = angleInDegrees;
        mMaximumTangent = toTangent(angleInDegrees);
    }

    /**
     * Returns the maximum arc along the circle between two points. When start and end points
     * have close to equal x and y differences, the curve between them is large. This forces
     * the curved path to have an arc of at most the given angle.
     * <p>The default value is 70 degrees.</p>
     *
     * @return The maximum angle of the arc on a circle describing the Path
     *         between the start and end points.
     * @attr ref android.R.styleable#ArcMotion_maximumAngle
     */
    public float getMaximumAngle() {
        return mMaximumAngle;
    }

    private static float toTangent(float arcInDegrees) {
        if (arcInDegrees < 0 || arcInDegrees > 90) {
            throw new IllegalArgumentException("Arc must be between 0 and 90 degrees");
        }
        return (float) Math.tan(Math.toRadians(arcInDegrees / 2));
    }

    @Override
    public Path getPath(float startX, float startY, float endX, float endY) {
        // Here's a little ascii art to show how this is calculated:
        // c---------- b
        //  \        / |
        //    \     d  |
        //      \  /   e
        //        a----f
        // This diagram assumes that the horizontal distance is less than the vertical
        // distance between The start point (a) and end point (b).
        // d is the midpoint between a and b. c is the center point of the circle with
        // This path is formed by assuming that start and end points are in
        // an arc on a circle. The end point is centered in the circle vertically
        // and start is a point on the circle.

        // Triangles bfa and bde form similar right triangles. The control points
        // for the cubic Bezier arc path are the midpoints between a and e and e and b.

        Path path = new Path();
        path.moveTo(startX, startY);

        float ex;
        float ey;
        if (startY == endY) {
            ex = (startX + endX) / 2;
            ey = startY + mMinimumHorizontalTangent * Math.abs(endX - startX) / 2;
        } else if (startX == endX) {
            ex = startX + mMinimumVerticalTangent * Math.abs(endY - startY) / 2;
            ey = (startY + endY) / 2;
        } else {
            float deltaX = endX - startX;
            float deltaY = startY - endY; // Y is inverted compared to diagram above.
            // hypotenuse squared.
            float h2 = deltaX * deltaX + deltaY * deltaY;

            // Midpoint between start and end
            float dx = (startX + endX) / 2;
            float dy = (startY + endY) / 2;

            // Distance squared between end point and mid point is (1/2 hypotenuse)^2
            float midDist2 = h2 * 0.25f;

            float minimumArcDist2 = 0;

            if (Math.abs(deltaX) < Math.abs(deltaY)) {
                // Similar triangles bfa and bde mean that (ab/fb = eb/bd)
                // Therefore, eb = ab * bd / fb
                // ab = hypotenuse
                // bd = hypotenuse/2
                // fb = deltaY
                float eDistY = h2 / (2 * deltaY);
                ey = endY + eDistY;
                ex = endX;

                minimumArcDist2 = midDist2 * mMinimumVerticalTangent
                        * mMinimumVerticalTangent;
            } else {
                // Same as above, but flip X & Y
                float eDistX = h2 / (2 * deltaX);
                ex = endX + eDistX;
                ey = endY;

                minimumArcDist2 = midDist2 * mMinimumHorizontalTangent
                        * mMinimumHorizontalTangent;
            }
            float arcDistX = dx - ex;
            float arcDistY = dy - ey;
            float arcDist2 = arcDistX * arcDistX + arcDistY * arcDistY;

            float maximumArcDist2 = midDist2 * mMaximumTangent * mMaximumTangent;

            float newArcDistance2 = 0;
            if (arcDist2 < minimumArcDist2) {
                newArcDistance2 = minimumArcDist2;
            } else if (arcDist2 > maximumArcDist2) {
                newArcDistance2 = maximumArcDist2;
            }
            if (newArcDistance2 != 0) {
                float ratio2 = newArcDistance2 / arcDist2;
                float ratio = FloatMath.sqrt(ratio2);
                ex = dx + (ratio * (ex - dx));
                ey = dy + (ratio * (ey - dy));
            }
        }
        float controlX1 = (startX + ex) / 2;
        float controlY1 = (startY + ey) / 2;
        float controlX2 = (ex + endX) / 2;
        float controlY2 = (ey + endY) / 2;
        path.cubicTo(controlX1, controlY1, controlX2, controlY2, endX, endY);
        return path;
    }
}
+12 −0
Original line number Diff line number Diff line
@@ -16,6 +16,9 @@

package android.transition;

import android.content.Context;
import android.util.AttributeSet;

/**
 * Utility class for creating a default transition that automatically fades,
 * moves, and resizes views during a scene change.
@@ -33,6 +36,15 @@ public class AutoTransition extends TransitionSet {
     *
     */
    public AutoTransition() {
        init();
    }

    public AutoTransition(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        setOrdering(ORDERING_SEQUENTIAL);
        addTransition(new Fade(Fade.OUT)).
                addTransition(new ChangeBounds()).
+73 −39

File changed.

Preview size limit exceeded, changes collapsed.

+8 −0
Original line number Diff line number Diff line
@@ -18,7 +18,9 @@ package android.transition;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.RectEvaluator;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

@@ -37,6 +39,12 @@ public class ChangeClipBounds extends Transition {
            PROPNAME_CLIP,
    };

    public ChangeClipBounds() {}

    public ChangeClipBounds(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public String[] getTransitionProperties() {
        return sTransitionProperties;
Loading