Import Android SDK Platform P [4697573]
/google/data/ro/projects/android/fetch_artifact \
--bid 4697573 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4697573.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: If80578c3c657366cc9cf75f8db13d46e2dd4e077
diff --git a/androidx/transition/AnimatorUtils.java b/androidx/transition/AnimatorUtils.java
new file mode 100644
index 0000000..6772a60
--- /dev/null
+++ b/androidx/transition/AnimatorUtils.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+
+class AnimatorUtils {
+
+ static void addPauseListener(@NonNull Animator animator,
+ @NonNull AnimatorListenerAdapter listener) {
+ if (Build.VERSION.SDK_INT >= 19) {
+ animator.addPauseListener(listener);
+ }
+ }
+
+ static void pause(@NonNull Animator animator) {
+ if (Build.VERSION.SDK_INT >= 19) {
+ animator.pause();
+ } else {
+ final ArrayList<Animator.AnimatorListener> listeners = animator.getListeners();
+ if (listeners != null) {
+ for (int i = 0, size = listeners.size(); i < size; i++) {
+ final Animator.AnimatorListener listener = listeners.get(i);
+ if (listener instanceof AnimatorPauseListenerCompat) {
+ ((AnimatorPauseListenerCompat) listener).onAnimationPause(animator);
+ }
+ }
+ }
+ }
+ }
+
+ static void resume(@NonNull Animator animator) {
+ if (Build.VERSION.SDK_INT >= 19) {
+ animator.resume();
+ } else {
+ final ArrayList<Animator.AnimatorListener> listeners = animator.getListeners();
+ if (listeners != null) {
+ for (int i = 0, size = listeners.size(); i < size; i++) {
+ final Animator.AnimatorListener listener = listeners.get(i);
+ if (listener instanceof AnimatorPauseListenerCompat) {
+ ((AnimatorPauseListenerCompat) listener).onAnimationResume(animator);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Listeners can implement this interface in addition to the platform AnimatorPauseListener to
+ * make them compatible with API level 18 and below. Animators will not be paused or resumed,
+ * but the callbacks here are invoked.
+ */
+ interface AnimatorPauseListenerCompat {
+
+ void onAnimationPause(Animator animation);
+
+ void onAnimationResume(Animator animation);
+
+ }
+
+ private AnimatorUtils() {
+ }
+}
diff --git a/androidx/transition/ArcMotion.java b/androidx/transition/ArcMotion.java
new file mode 100644
index 0000000..7953389
--- /dev/null
+++ b/androidx/transition/ArcMotion.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Path;
+import android.util.AttributeSet;
+
+import androidx.core.content.res.TypedArrayUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * 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
+ * <changeBounds>
+ * <arcMotion android:minimumHorizontalAngle="15"
+ * android:minimumVerticalAngle="0"
+ * android:maximumAngle="90"/>
+ * </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, Styleable.ARC_MOTION);
+ XmlPullParser parser = (XmlPullParser) attrs;
+ float minimumVerticalAngle = TypedArrayUtils.getNamedFloat(a, parser,
+ "minimumVerticalAngle", Styleable.ArcMotion.MINIMUM_VERTICAL_ANGLE,
+ DEFAULT_MIN_ANGLE_DEGREES);
+ setMinimumVerticalAngle(minimumVerticalAngle);
+ float minimumHorizontalAngle = TypedArrayUtils.getNamedFloat(a, parser,
+ "minimumHorizontalAngle", Styleable.ArcMotion.MINIMUM_HORIZONTAL_ANGLE,
+ DEFAULT_MIN_ANGLE_DEGREES);
+ setMinimumHorizontalAngle(minimumHorizontalAngle);
+ float maximumAngle = TypedArrayUtils.getNamedFloat(a, parser, "maximumAngle",
+ Styleable.ArcMotion.MAXIMUM_ANGLE, 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.
+ */
+ 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.
+ */
+ 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.
+ */
+ 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.
+ */
+ 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.
+ */
+ 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.
+ */
+ 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;
+ float deltaX = endX - startX;
+ float deltaY = endY - startY;
+
+ // 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;
+
+ boolean isMovingUpwards = startY > endY;
+
+ 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 = Math.abs(h2 / (2 * deltaY));
+ if (isMovingUpwards) {
+ ey = endY + eDistY;
+ ex = endX;
+ } else {
+ ey = startY + eDistY;
+ ex = startX;
+ }
+
+ minimumArcDist2 = midDist2 * mMinimumVerticalTangent
+ * mMinimumVerticalTangent;
+ } else {
+ // Same as above, but flip X & Y and account for negative eDist
+ float eDistX = h2 / (2 * deltaX);
+ if (isMovingUpwards) {
+ ex = startX + eDistX;
+ ey = startY;
+ } else {
+ 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 = (float) Math.sqrt(ratio2);
+ ex = dx + (ratio * (ex - dx));
+ ey = dy + (ratio * (ey - dy));
+ }
+ float control1X = (startX + ex) / 2;
+ float control1Y = (startY + ey) / 2;
+ float control2X = (ex + endX) / 2;
+ float control2Y = (ey + endY) / 2;
+ path.cubicTo(control1X, control1Y, control2X, control2Y, endX, endY);
+ return path;
+ }
+
+}
diff --git a/androidx/transition/ArcMotionTest.java b/androidx/transition/ArcMotionTest.java
new file mode 100644
index 0000000..14a704a
--- /dev/null
+++ b/androidx/transition/ArcMotionTest.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.Path;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ArcMotionTest extends PathMotionTest {
+
+ @Test
+ public void test90Quadrants() {
+ ArcMotion arcMotion = new ArcMotion();
+ arcMotion.setMaximumAngle(90);
+
+ Path expected = arcWithPoint(0, 100, 100, 0, 100, 100);
+ Path path = arcMotion.getPath(0, 100, 100, 0);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(100, 0, 0, -100, 0, 0);
+ path = arcMotion.getPath(100, 0, 0, -100);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(0, -100, -100, 0, 0, 0);
+ path = arcMotion.getPath(0, -100, -100, 0);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(-100, 0, 0, 100, -100, 100);
+ path = arcMotion.getPath(-100, 0, 0, 100);
+ assertPathMatches(expected, path);
+ }
+
+ @Test
+ public void test345Triangles() {
+ // 3-4-5 triangles are easy to calculate the control points
+ ArcMotion arcMotion = new ArcMotion();
+ arcMotion.setMaximumAngle(90);
+ Path expected;
+ Path path;
+
+ expected = arcWithPoint(0, 120, 160, 0, 125, 120);
+ path = arcMotion.getPath(0, 120, 160, 0);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(0, 160, 120, 0, 120, 125);
+ path = arcMotion.getPath(0, 160, 120, 0);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(-120, 0, 0, 160, -120, 125);
+ path = arcMotion.getPath(-120, 0, 0, 160);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(-160, 0, 0, 120, -125, 120);
+ path = arcMotion.getPath(-160, 0, 0, 120);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(0, -120, -160, 0, -35, 0);
+ path = arcMotion.getPath(0, -120, -160, 0);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(0, -160, -120, 0, 0, -35);
+ path = arcMotion.getPath(0, -160, -120, 0);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(120, 0, 0, -160, 0, -35);
+ path = arcMotion.getPath(120, 0, 0, -160);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(160, 0, 0, -120, 35, 0);
+ path = arcMotion.getPath(160, 0, 0, -120);
+ assertPathMatches(expected, path);
+ }
+
+ private static Path arcWithPoint(float startX, float startY, float endX, float endY,
+ float eX, float eY) {
+ float c1x = (eX + startX) / 2;
+ float c1y = (eY + startY) / 2;
+ float c2x = (eX + endX) / 2;
+ float c2y = (eY + endY) / 2;
+ Path path = new Path();
+ path.moveTo(startX, startY);
+ path.cubicTo(c1x, c1y, c2x, c2y, endX, endY);
+ return path;
+ }
+
+ @Test
+ public void testMaximumAngle() {
+ ArcMotion arcMotion = new ArcMotion();
+ arcMotion.setMaximumAngle(45f);
+ assertEquals(45f, arcMotion.getMaximumAngle(), 0.0f);
+
+ float ratio = (float) Math.tan(Math.PI / 8);
+ float ex = 50 + (50 * ratio);
+ float ey = ex;
+
+ Path expected = arcWithPoint(0, 100, 100, 0, ex, ey);
+ Path path = arcMotion.getPath(0, 100, 100, 0);
+ assertPathMatches(expected, path);
+ }
+
+ @Test
+ public void testMinimumHorizontalAngle() {
+ ArcMotion arcMotion = new ArcMotion();
+ arcMotion.setMinimumHorizontalAngle(45);
+ assertEquals(45, arcMotion.getMinimumHorizontalAngle(), 0.0f);
+
+ float ex = 37.5f;
+ float ey = (float) (Math.tan(Math.PI / 4) * 50);
+ Path expected = arcWithPoint(0, 0, 100, 50, ex, ey);
+ Path path = arcMotion.getPath(0, 0, 100, 50);
+ assertPathMatches(expected, path);
+
+ // Pretty much the same, but follows a different path.
+ expected = arcWithPoint(0, 0, 100.001f, 50, ex, ey);
+ path = arcMotion.getPath(0, 0, 100.001f, 50);
+ assertPathMatches(expected, path);
+
+ // Moving in the opposite direction.
+ expected = arcWithPoint(100, 50, 0, 0, ex, ey);
+ path = arcMotion.getPath(100, 50, 0, 0);
+ assertPathMatches(expected, path);
+
+ // With x < y.
+ ex = 0;
+ ey = (float) (Math.tan(Math.PI / 4) * 62.5f);
+ expected = arcWithPoint(0, 0, 50, 100, ex, ey);
+ path = arcMotion.getPath(0, 0, 50, 100);
+ assertPathMatches(expected, path);
+
+ // Pretty much the same, but follows a different path.
+ expected = arcWithPoint(0, 0, 50, 100.001f, ex, ey);
+ path = arcMotion.getPath(0, 0, 50, 100.001f);
+ assertPathMatches(expected, path);
+
+ // Moving in the opposite direction.
+ expected = arcWithPoint(50, 100, 0, 0, ex, ey);
+ path = arcMotion.getPath(50, 100, 0, 0);
+ assertPathMatches(expected, path);
+ }
+
+ @Test
+ public void testMinimumVerticalAngle() {
+ ArcMotion arcMotion = new ArcMotion();
+ arcMotion.setMinimumVerticalAngle(45);
+ assertEquals(45, arcMotion.getMinimumVerticalAngle(), 0.0f);
+
+ float ex = 0;
+ float ey = 62.5f;
+ Path expected = arcWithPoint(0, 0, 50, 100, ex, ey);
+ Path path = arcMotion.getPath(0, 0, 50, 100);
+ assertPathMatches(expected, path);
+
+ // Pretty much the same, but follows a different path.
+ expected = arcWithPoint(0, 0, 50, 100.001f, ex, ey);
+ path = arcMotion.getPath(0, 0, 50, 100.001f);
+ assertPathMatches(expected, path);
+
+ // Moving in opposite direction.
+ expected = arcWithPoint(50, 100, 0, 0, ex, ey);
+ path = arcMotion.getPath(50, 100, 0, 0);
+ assertPathMatches(expected, path);
+
+ // With x > y.
+ ex = (float) (Math.tan(Math.PI / 4) * 37.5f);
+ ey = 50;
+ expected = arcWithPoint(0, 0, 100, 50, ex, ey);
+ path = arcMotion.getPath(0, 0, 100, 50);
+ assertPathMatches(expected, path);
+
+ // Pretty much the same, but follows a different path.
+ expected = arcWithPoint(0, 0, 100.001f, 50, ex, ey);
+ path = arcMotion.getPath(0, 0, 100.001f, 50);
+ assertPathMatches(expected, path);
+
+ // Moving in opposite direction.
+ expected = arcWithPoint(100, 50, 0, 0, ex, ey);
+ path = arcMotion.getPath(100, 50, 0, 0);
+ assertPathMatches(expected, path);
+
+ }
+
+}
diff --git a/androidx/transition/AutoTransition.java b/androidx/transition/AutoTransition.java
new file mode 100644
index 0000000..e1a077e
--- /dev/null
+++ b/androidx/transition/AutoTransition.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 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 androidx.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.
+ *
+ * <p>An AutoTransition can be described in a resource file by using the
+ * tag <code>autoTransition</code>, along with the other standard
+ * attributes of {@link Transition}.</p>
+ */
+public class AutoTransition extends TransitionSet {
+
+ /**
+ * Constructs an AutoTransition object, which is a TransitionSet which
+ * first fades out disappearing targets, then moves and resizes existing
+ * targets, and finally fades in appearing targets.
+ */
+ 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())
+ .addTransition(new Fade(Fade.IN));
+ }
+
+}
diff --git a/androidx/transition/AutoTransitionTest.java b/androidx/transition/AutoTransitionTest.java
new file mode 100644
index 0000000..b5df811
--- /dev/null
+++ b/androidx/transition/AutoTransitionTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import android.graphics.Color;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.MediumTest;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@MediumTest
+public class AutoTransitionTest extends BaseTest {
+
+ private LinearLayout mRoot;
+ private View mView0;
+ private View mView1;
+
+ @UiThreadTest
+ @Before
+ public void setUp() {
+ mRoot = (LinearLayout) rule.getActivity().getRoot();
+ mView0 = new View(rule.getActivity());
+ mView0.setBackgroundColor(Color.RED);
+ mRoot.addView(mView0, new LinearLayout.LayoutParams(100, 100));
+ mView1 = new View(rule.getActivity());
+ mView1.setBackgroundColor(Color.BLUE);
+ mRoot.addView(mView1, new LinearLayout.LayoutParams(100, 100));
+ }
+
+ @LargeTest
+ @Test
+ public void testLayoutBetweenFadeAndChangeBounds() throws Throwable {
+ final LayoutCounter counter = new LayoutCounter();
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ assertThat(mView1.getY(), is(100.f));
+ assertThat(mView0.getVisibility(), is(View.VISIBLE));
+ mView1.addOnLayoutChangeListener(counter);
+ }
+ });
+ final SyncTransitionListener listener = new SyncTransitionListener(
+ SyncTransitionListener.EVENT_END);
+ final Transition transition = new AutoTransition();
+ transition.addListener(listener);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, transition);
+ // This makes view0 fade out and causes view1 to move upwards.
+ mView0.setVisibility(View.GONE);
+ }
+ });
+ assertThat("Timed out waiting for the TransitionListener",
+ listener.await(), is(true));
+ assertThat(mView1.getY(), is(0.f));
+ assertThat(mView0.getVisibility(), is(View.GONE));
+ counter.reset();
+ listener.reset();
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, transition);
+ // Revert
+ mView0.setVisibility(View.VISIBLE);
+ }
+ });
+ assertThat("Timed out waiting for the TransitionListener",
+ listener.await(), is(true));
+ assertThat(mView1.getY(), is(100.f));
+ assertThat(mView0.getVisibility(), is(View.VISIBLE));
+ }
+
+ private static class LayoutCounter implements View.OnLayoutChangeListener {
+
+ private int mCalledCount;
+
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ mCalledCount++;
+ // There should not be more than one layout request to view1.
+ if (mCalledCount > 1) {
+ fail("View layout happened too many times");
+ }
+ }
+
+ void reset() {
+ mCalledCount = 0;
+ }
+
+ }
+
+}
diff --git a/androidx/transition/BaseTest.java b/androidx/transition/BaseTest.java
new file mode 100644
index 0000000..c54b4b3
--- /dev/null
+++ b/androidx/transition/BaseTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public abstract class BaseTest {
+
+ @Rule
+ public final ActivityTestRule<TransitionActivity> rule;
+
+ BaseTest() {
+ rule = new ActivityTestRule<>(TransitionActivity.class);
+ }
+
+}
diff --git a/androidx/transition/BaseTransitionTest.java b/androidx/transition/BaseTransitionTest.java
new file mode 100644
index 0000000..3620100
--- /dev/null
+++ b/androidx/transition/BaseTransitionTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.support.test.InstrumentationRegistry;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import androidx.transition.test.R;
+
+import org.junit.Before;
+
+import java.util.ArrayList;
+
+public abstract class BaseTransitionTest extends BaseTest {
+
+ ArrayList<View> mTransitionTargets = new ArrayList<>();
+ LinearLayout mRoot;
+ Transition mTransition;
+ Transition.TransitionListener mListener;
+ float mAnimatedValue;
+
+ @Before
+ public void setUp() {
+ InstrumentationRegistry.getInstrumentation().setInTouchMode(false);
+ mRoot = (LinearLayout) rule.getActivity().findViewById(R.id.root);
+ mTransitionTargets.clear();
+ mTransition = createTransition();
+ mListener = mock(Transition.TransitionListener.class);
+ mTransition.addListener(mListener);
+ }
+
+ Transition createTransition() {
+ return new TestTransition();
+ }
+
+ void waitForStart() {
+ verify(mListener, timeout(3000)).onTransitionStart(any(Transition.class));
+ }
+
+ void waitForEnd() {
+ verify(mListener, timeout(3000)).onTransitionEnd(any(Transition.class));
+ }
+
+ Scene loadScene(final int layoutId) throws Throwable {
+ final Scene[] scene = new Scene[1];
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ scene[0] = Scene.getSceneForLayout(mRoot, layoutId, rule.getActivity());
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ return scene[0];
+ }
+
+ void startTransition(final int layoutId) throws Throwable {
+ startTransition(loadScene(layoutId));
+ }
+
+ private void startTransition(final Scene scene) throws Throwable {
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.go(scene, mTransition);
+ }
+ });
+ waitForStart();
+ }
+
+ void enterScene(final int layoutId) throws Throwable {
+ enterScene(loadScene(layoutId));
+ }
+
+ void enterScene(final Scene scene) throws Throwable {
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ scene.enter();
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
+
+ void resetListener() {
+ mTransition.removeListener(mListener);
+ mListener = mock(Transition.TransitionListener.class);
+ mTransition.addListener(mListener);
+ }
+
+ void setAnimatedValue(float animatedValue) {
+ mAnimatedValue = animatedValue;
+ }
+
+ public class TestTransition extends Visibility {
+
+ @Override
+ public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
+ TransitionValues endValues) {
+ mTransitionTargets.add(endValues.view);
+ return ObjectAnimator.ofFloat(BaseTransitionTest.this, "animatedValue", 0, 1);
+ }
+
+ @Override
+ public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues,
+ TransitionValues endValues) {
+ mTransitionTargets.add(startValues.view);
+ return ObjectAnimator.ofFloat(BaseTransitionTest.this, "animatedValue", 1, 0);
+ }
+
+ }
+
+}
diff --git a/androidx/transition/ChangeBounds.java b/androidx/transition/ChangeBounds.java
new file mode 100644
index 0000000..fb621ed
--- /dev/null
+++ b/androidx/transition/ChangeBounds.java
@@ -0,0 +1,498 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Property;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.res.TypedArrayUtils;
+import androidx.core.view.ViewCompat;
+
+import java.util.Map;
+
+/**
+ * This transition captures the layout bounds of target views before and after
+ * the scene change and animates those changes during the transition.
+ *
+ * <p>A ChangeBounds transition can be described in a resource file by using the
+ * tag <code>changeBounds</code>, along with the other standard attributes of Transition.</p>
+ */
+public class ChangeBounds extends Transition {
+
+ private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds";
+ private static final String PROPNAME_CLIP = "android:changeBounds:clip";
+ private static final String PROPNAME_PARENT = "android:changeBounds:parent";
+ private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX";
+ private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY";
+ private static final String[] sTransitionProperties = {
+ PROPNAME_BOUNDS,
+ PROPNAME_CLIP,
+ PROPNAME_PARENT,
+ PROPNAME_WINDOW_X,
+ PROPNAME_WINDOW_Y
+ };
+
+ private static final Property<Drawable, PointF> DRAWABLE_ORIGIN_PROPERTY =
+ new Property<Drawable, PointF>(PointF.class, "boundsOrigin") {
+ private Rect mBounds = new Rect();
+
+ @Override
+ public void set(Drawable object, PointF value) {
+ object.copyBounds(mBounds);
+ mBounds.offsetTo(Math.round(value.x), Math.round(value.y));
+ object.setBounds(mBounds);
+ }
+
+ @Override
+ public PointF get(Drawable object) {
+ object.copyBounds(mBounds);
+ return new PointF(mBounds.left, mBounds.top);
+ }
+ };
+
+ private static final Property<ViewBounds, PointF> TOP_LEFT_PROPERTY =
+ new Property<ViewBounds, PointF>(PointF.class, "topLeft") {
+ @Override
+ public void set(ViewBounds viewBounds, PointF topLeft) {
+ viewBounds.setTopLeft(topLeft);
+ }
+
+ @Override
+ public PointF get(ViewBounds viewBounds) {
+ return null;
+ }
+ };
+
+ private static final Property<ViewBounds, PointF> BOTTOM_RIGHT_PROPERTY =
+ new Property<ViewBounds, PointF>(PointF.class, "bottomRight") {
+ @Override
+ public void set(ViewBounds viewBounds, PointF bottomRight) {
+ viewBounds.setBottomRight(bottomRight);
+ }
+
+ @Override
+ public PointF get(ViewBounds viewBounds) {
+ return null;
+ }
+ };
+
+ private static final Property<View, PointF> BOTTOM_RIGHT_ONLY_PROPERTY =
+ new Property<View, PointF>(PointF.class, "bottomRight") {
+ @Override
+ public void set(View view, PointF bottomRight) {
+ int left = view.getLeft();
+ int top = view.getTop();
+ int right = Math.round(bottomRight.x);
+ int bottom = Math.round(bottomRight.y);
+ ViewUtils.setLeftTopRightBottom(view, left, top, right, bottom);
+ }
+
+ @Override
+ public PointF get(View view) {
+ return null;
+ }
+ };
+
+ private static final Property<View, PointF> TOP_LEFT_ONLY_PROPERTY =
+ new Property<View, PointF>(PointF.class, "topLeft") {
+ @Override
+ public void set(View view, PointF topLeft) {
+ int left = Math.round(topLeft.x);
+ int top = Math.round(topLeft.y);
+ int right = view.getRight();
+ int bottom = view.getBottom();
+ ViewUtils.setLeftTopRightBottom(view, left, top, right, bottom);
+ }
+
+ @Override
+ public PointF get(View view) {
+ return null;
+ }
+ };
+
+ private static final Property<View, PointF> POSITION_PROPERTY =
+ new Property<View, PointF>(PointF.class, "position") {
+ @Override
+ public void set(View view, PointF topLeft) {
+ int left = Math.round(topLeft.x);
+ int top = Math.round(topLeft.y);
+ int right = left + view.getWidth();
+ int bottom = top + view.getHeight();
+ ViewUtils.setLeftTopRightBottom(view, left, top, right, bottom);
+ }
+
+ @Override
+ public PointF get(View view) {
+ return null;
+ }
+ };
+
+ private int[] mTempLocation = new int[2];
+ private boolean mResizeClip = false;
+ private boolean mReparent = false;
+
+ private static RectEvaluator sRectEvaluator = new RectEvaluator();
+
+ public ChangeBounds() {
+ }
+
+ public ChangeBounds(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, Styleable.CHANGE_BOUNDS);
+ boolean resizeClip = TypedArrayUtils.getNamedBoolean(a, (XmlResourceParser) attrs,
+ "resizeClip", Styleable.ChangeBounds.RESIZE_CLIP, false);
+ a.recycle();
+ setResizeClip(resizeClip);
+ }
+
+ @Nullable
+ @Override
+ public String[] getTransitionProperties() {
+ return sTransitionProperties;
+ }
+
+ /**
+ * When <code>resizeClip</code> is true, ChangeBounds resizes the view using the clipBounds
+ * instead of changing the dimensions of the view during the animation. When
+ * <code>resizeClip</code> is false, ChangeBounds resizes the View by changing its dimensions.
+ *
+ * <p>When resizeClip is set to true, the clip bounds is modified by ChangeBounds. Therefore,
+ * {@link android.transition.ChangeClipBounds} is not compatible with ChangeBounds
+ * in this mode.</p>
+ *
+ * @param resizeClip Used to indicate whether the view bounds should be modified or the
+ * clip bounds should be modified by ChangeBounds.
+ * @see android.view.View#setClipBounds(android.graphics.Rect)
+ */
+ public void setResizeClip(boolean resizeClip) {
+ mResizeClip = resizeClip;
+ }
+
+ /**
+ * Returns true when the ChangeBounds will resize by changing the clip bounds during the
+ * view animation or false when bounds are changed. The default value is false.
+ *
+ * @return true when the ChangeBounds will resize by changing the clip bounds during the
+ * view animation or false when bounds are changed. The default value is false.
+ */
+ public boolean getResizeClip() {
+ return mResizeClip;
+ }
+
+ private void captureValues(TransitionValues values) {
+ View view = values.view;
+
+ if (ViewCompat.isLaidOut(view) || view.getWidth() != 0 || view.getHeight() != 0) {
+ values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(),
+ view.getRight(), view.getBottom()));
+ values.values.put(PROPNAME_PARENT, values.view.getParent());
+ if (mReparent) {
+ values.view.getLocationInWindow(mTempLocation);
+ values.values.put(PROPNAME_WINDOW_X, mTempLocation[0]);
+ values.values.put(PROPNAME_WINDOW_Y, mTempLocation[1]);
+ }
+ if (mResizeClip) {
+ values.values.put(PROPNAME_CLIP, ViewCompat.getClipBounds(view));
+ }
+ }
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ captureValues(transitionValues);
+ }
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ captureValues(transitionValues);
+ }
+
+ private boolean parentMatches(View startParent, View endParent) {
+ boolean parentMatches = true;
+ if (mReparent) {
+ TransitionValues endValues = getMatchedTransitionValues(startParent, true);
+ if (endValues == null) {
+ parentMatches = startParent == endParent;
+ } else {
+ parentMatches = endParent == endValues.view;
+ }
+ }
+ return parentMatches;
+ }
+
+ @Override
+ @Nullable
+ public Animator createAnimator(@NonNull final ViewGroup sceneRoot,
+ @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) {
+ if (startValues == null || endValues == null) {
+ return null;
+ }
+ Map<String, Object> startParentVals = startValues.values;
+ Map<String, Object> endParentVals = endValues.values;
+ ViewGroup startParent = (ViewGroup) startParentVals.get(PROPNAME_PARENT);
+ ViewGroup endParent = (ViewGroup) endParentVals.get(PROPNAME_PARENT);
+ if (startParent == null || endParent == null) {
+ return null;
+ }
+ final View view = endValues.view;
+ if (parentMatches(startParent, endParent)) {
+ Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS);
+ Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
+ final int startLeft = startBounds.left;
+ final int endLeft = endBounds.left;
+ final int startTop = startBounds.top;
+ final int endTop = endBounds.top;
+ final int startRight = startBounds.right;
+ final int endRight = endBounds.right;
+ final int startBottom = startBounds.bottom;
+ final int endBottom = endBounds.bottom;
+ final int startWidth = startRight - startLeft;
+ final int startHeight = startBottom - startTop;
+ final int endWidth = endRight - endLeft;
+ final int endHeight = endBottom - endTop;
+ Rect startClip = (Rect) startValues.values.get(PROPNAME_CLIP);
+ Rect endClip = (Rect) endValues.values.get(PROPNAME_CLIP);
+ int numChanges = 0;
+ if ((startWidth != 0 && startHeight != 0) || (endWidth != 0 && endHeight != 0)) {
+ if (startLeft != endLeft || startTop != endTop) ++numChanges;
+ if (startRight != endRight || startBottom != endBottom) ++numChanges;
+ }
+ if ((startClip != null && !startClip.equals(endClip))
+ || (startClip == null && endClip != null)) {
+ ++numChanges;
+ }
+ if (numChanges > 0) {
+ Animator anim;
+ if (!mResizeClip) {
+ ViewUtils.setLeftTopRightBottom(view, startLeft, startTop, startRight,
+ startBottom);
+ if (numChanges == 2) {
+ if (startWidth == endWidth && startHeight == endHeight) {
+ Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft,
+ endTop);
+ anim = ObjectAnimatorUtils.ofPointF(view, POSITION_PROPERTY,
+ topLeftPath);
+ } else {
+ final ViewBounds viewBounds = new ViewBounds(view);
+ Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
+ endLeft, endTop);
+ ObjectAnimator topLeftAnimator = ObjectAnimatorUtils
+ .ofPointF(viewBounds, TOP_LEFT_PROPERTY, topLeftPath);
+
+ Path bottomRightPath = getPathMotion().getPath(startRight, startBottom,
+ endRight, endBottom);
+ ObjectAnimator bottomRightAnimator = ObjectAnimatorUtils.ofPointF(
+ viewBounds, BOTTOM_RIGHT_PROPERTY, bottomRightPath);
+ AnimatorSet set = new AnimatorSet();
+ set.playTogether(topLeftAnimator, bottomRightAnimator);
+ anim = set;
+ set.addListener(new AnimatorListenerAdapter() {
+ // We need a strong reference to viewBounds until the
+ // animator ends (The ObjectAnimator holds only a weak reference).
+ @SuppressWarnings("unused")
+ private ViewBounds mViewBounds = viewBounds;
+ });
+ }
+ } else if (startLeft != endLeft || startTop != endTop) {
+ Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
+ endLeft, endTop);
+ anim = ObjectAnimatorUtils.ofPointF(view, TOP_LEFT_ONLY_PROPERTY,
+ topLeftPath);
+ } else {
+ Path bottomRight = getPathMotion().getPath(startRight, startBottom,
+ endRight, endBottom);
+ anim = ObjectAnimatorUtils.ofPointF(view, BOTTOM_RIGHT_ONLY_PROPERTY,
+ bottomRight);
+ }
+ } else {
+ int maxWidth = Math.max(startWidth, endWidth);
+ int maxHeight = Math.max(startHeight, endHeight);
+
+ ViewUtils.setLeftTopRightBottom(view, startLeft, startTop, startLeft + maxWidth,
+ startTop + maxHeight);
+
+ ObjectAnimator positionAnimator = null;
+ if (startLeft != endLeft || startTop != endTop) {
+ Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft,
+ endTop);
+ positionAnimator = ObjectAnimatorUtils.ofPointF(view, POSITION_PROPERTY,
+ topLeftPath);
+ }
+ final Rect finalClip = endClip;
+ if (startClip == null) {
+ startClip = new Rect(0, 0, startWidth, startHeight);
+ }
+ if (endClip == null) {
+ endClip = new Rect(0, 0, endWidth, endHeight);
+ }
+ ObjectAnimator clipAnimator = null;
+ if (!startClip.equals(endClip)) {
+ ViewCompat.setClipBounds(view, startClip);
+ clipAnimator = ObjectAnimator.ofObject(view, "clipBounds", sRectEvaluator,
+ startClip, endClip);
+ clipAnimator.addListener(new AnimatorListenerAdapter() {
+ private boolean mIsCanceled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mIsCanceled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!mIsCanceled) {
+ ViewCompat.setClipBounds(view, finalClip);
+ ViewUtils.setLeftTopRightBottom(view, endLeft, endTop, endRight,
+ endBottom);
+ }
+ }
+ });
+ }
+ anim = TransitionUtils.mergeAnimators(positionAnimator,
+ clipAnimator);
+ }
+ if (view.getParent() instanceof ViewGroup) {
+ final ViewGroup parent = (ViewGroup) view.getParent();
+ ViewGroupUtils.suppressLayout(parent, true);
+ TransitionListener transitionListener = new TransitionListenerAdapter() {
+ boolean mCanceled = false;
+
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ ViewGroupUtils.suppressLayout(parent, false);
+ mCanceled = true;
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ if (!mCanceled) {
+ ViewGroupUtils.suppressLayout(parent, false);
+ }
+ transition.removeListener(this);
+ }
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ ViewGroupUtils.suppressLayout(parent, false);
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ ViewGroupUtils.suppressLayout(parent, true);
+ }
+ };
+ addListener(transitionListener);
+ }
+ return anim;
+ }
+ } else {
+ int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X);
+ int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y);
+ int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X);
+ int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y);
+ // TODO: also handle size changes: check bounds and animate size changes
+ if (startX != endX || startY != endY) {
+ sceneRoot.getLocationInWindow(mTempLocation);
+ Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(),
+ Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ view.draw(canvas);
+ @SuppressWarnings("deprecation") final BitmapDrawable drawable = new BitmapDrawable(
+ bitmap);
+ final float transitionAlpha = ViewUtils.getTransitionAlpha(view);
+ ViewUtils.setTransitionAlpha(view, 0);
+ ViewUtils.getOverlay(sceneRoot).add(drawable);
+ Path topLeftPath = getPathMotion().getPath(startX - mTempLocation[0],
+ startY - mTempLocation[1], endX - mTempLocation[0],
+ endY - mTempLocation[1]);
+ PropertyValuesHolder origin = PropertyValuesHolderUtils.ofPointF(
+ DRAWABLE_ORIGIN_PROPERTY, topLeftPath);
+ ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, origin);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ ViewUtils.getOverlay(sceneRoot).remove(drawable);
+ ViewUtils.setTransitionAlpha(view, transitionAlpha);
+ }
+ });
+ return anim;
+ }
+ }
+ return null;
+ }
+
+ private static class ViewBounds {
+
+ private int mLeft;
+ private int mTop;
+ private int mRight;
+ private int mBottom;
+ private View mView;
+ private int mTopLeftCalls;
+ private int mBottomRightCalls;
+
+ ViewBounds(View view) {
+ mView = view;
+ }
+
+ void setTopLeft(PointF topLeft) {
+ mLeft = Math.round(topLeft.x);
+ mTop = Math.round(topLeft.y);
+ mTopLeftCalls++;
+ if (mTopLeftCalls == mBottomRightCalls) {
+ setLeftTopRightBottom();
+ }
+ }
+
+ void setBottomRight(PointF bottomRight) {
+ mRight = Math.round(bottomRight.x);
+ mBottom = Math.round(bottomRight.y);
+ mBottomRightCalls++;
+ if (mTopLeftCalls == mBottomRightCalls) {
+ setLeftTopRightBottom();
+ }
+ }
+
+ private void setLeftTopRightBottom() {
+ ViewUtils.setLeftTopRightBottom(mView, mLeft, mTop, mRight, mBottom);
+ mTopLeftCalls = 0;
+ mBottomRightCalls = 0;
+ }
+
+ }
+
+}
diff --git a/androidx/transition/ChangeBoundsTest.java b/androidx/transition/ChangeBoundsTest.java
new file mode 100644
index 0000000..345ad0a
--- /dev/null
+++ b/androidx/transition/ChangeBoundsTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+import android.support.test.filters.MediumTest;
+import android.view.View;
+import android.view.animation.LinearInterpolator;
+
+import androidx.transition.test.R;
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Test;
+
+@MediumTest
+public class ChangeBoundsTest extends BaseTransitionTest {
+
+ @Override
+ Transition createTransition() {
+ final ChangeBounds changeBounds = new ChangeBounds();
+ changeBounds.setDuration(400);
+ changeBounds.setInterpolator(new LinearInterpolator());
+ return changeBounds;
+ }
+
+ @Test
+ public void testResizeClip() {
+ ChangeBounds changeBounds = (ChangeBounds) mTransition;
+ assertThat(changeBounds.getResizeClip(), is(false));
+ changeBounds.setResizeClip(true);
+ assertThat(changeBounds.getResizeClip(), is(true));
+ }
+
+ @Test
+ public void testBasic() throws Throwable {
+ enterScene(R.layout.scene1);
+ final ViewHolder startHolder = new ViewHolder(rule.getActivity());
+ assertThat(startHolder.red, is(atTop()));
+ assertThat(startHolder.green, is(below(startHolder.red)));
+ startTransition(R.layout.scene6);
+ waitForEnd();
+ final ViewHolder endHolder = new ViewHolder(rule.getActivity());
+ assertThat(endHolder.green, is(atTop()));
+ assertThat(endHolder.red, is(below(endHolder.green)));
+ }
+
+ private static TypeSafeMatcher<View> atTop() {
+ return new TypeSafeMatcher<View>() {
+ @Override
+ protected boolean matchesSafely(View view) {
+ return view.getTop() == 0;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("is placed at the top of its parent");
+ }
+ };
+ }
+
+ private static TypeSafeMatcher<View> below(final View other) {
+ return new TypeSafeMatcher<View>() {
+ @Override
+ protected boolean matchesSafely(View item) {
+ return other.getBottom() == item.getTop();
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("is placed below the specified view");
+ }
+ };
+ }
+
+ private static class ViewHolder {
+
+ public final View red;
+ public final View green;
+
+ ViewHolder(TransitionActivity activity) {
+ red = activity.findViewById(R.id.redSquare);
+ green = activity.findViewById(R.id.greenSquare);
+ }
+ }
+
+}
diff --git a/androidx/transition/ChangeClipBounds.java b/androidx/transition/ChangeClipBounds.java
new file mode 100644
index 0000000..8762719
--- /dev/null
+++ b/androidx/transition/ChangeClipBounds.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.core.view.ViewCompat;
+
+/**
+ * ChangeClipBounds captures the {@link android.view.View#getClipBounds()} before and after the
+ * scene change and animates those changes during the transition.
+ *
+ * <p>Prior to API 18 this does nothing.</p>
+ */
+public class ChangeClipBounds extends Transition {
+
+ private static final String PROPNAME_CLIP = "android:clipBounds:clip";
+ private static final String PROPNAME_BOUNDS = "android:clipBounds:bounds";
+
+ private static final String[] sTransitionProperties = {
+ PROPNAME_CLIP,
+ };
+
+ @Override
+ public String[] getTransitionProperties() {
+ return sTransitionProperties;
+ }
+
+ public ChangeClipBounds() {
+ }
+
+ public ChangeClipBounds(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ private void captureValues(TransitionValues values) {
+ View view = values.view;
+ if (view.getVisibility() == View.GONE) {
+ return;
+ }
+
+ Rect clip = ViewCompat.getClipBounds(view);
+ values.values.put(PROPNAME_CLIP, clip);
+ if (clip == null) {
+ Rect bounds = new Rect(0, 0, view.getWidth(), view.getHeight());
+ values.values.put(PROPNAME_BOUNDS, bounds);
+ }
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ captureValues(transitionValues);
+ }
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ captureValues(transitionValues);
+ }
+
+ @Override
+ public Animator createAnimator(@NonNull final ViewGroup sceneRoot, TransitionValues startValues,
+ TransitionValues endValues) {
+ if (startValues == null || endValues == null
+ || !startValues.values.containsKey(PROPNAME_CLIP)
+ || !endValues.values.containsKey(PROPNAME_CLIP)) {
+ return null;
+ }
+ Rect start = (Rect) startValues.values.get(PROPNAME_CLIP);
+ Rect end = (Rect) endValues.values.get(PROPNAME_CLIP);
+ final boolean endIsNull = end == null;
+ if (start == null && end == null) {
+ return null; // No animation required since there is no clip.
+ }
+
+ if (start == null) {
+ start = (Rect) startValues.values.get(PROPNAME_BOUNDS);
+ } else if (end == null) {
+ end = (Rect) endValues.values.get(PROPNAME_BOUNDS);
+ }
+ if (start.equals(end)) {
+ return null;
+ }
+
+ ViewCompat.setClipBounds(endValues.view, start);
+ RectEvaluator evaluator = new RectEvaluator(new Rect());
+ ObjectAnimator animator = ObjectAnimator.ofObject(endValues.view, ViewUtils.CLIP_BOUNDS,
+ evaluator, start, end);
+ if (endIsNull) {
+ final View endView = endValues.view;
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ ViewCompat.setClipBounds(endView, null);
+ }
+ });
+ }
+ return animator;
+ }
+}
diff --git a/androidx/transition/ChangeClipBoundsTest.java b/androidx/transition/ChangeClipBoundsTest.java
new file mode 100644
index 0000000..7a416b2
--- /dev/null
+++ b/androidx/transition/ChangeClipBoundsTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Rect;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
+import android.view.View;
+
+import androidx.core.view.ViewCompat;
+import androidx.transition.test.R;
+
+import org.junit.Test;
+
+@MediumTest
+public class ChangeClipBoundsTest extends BaseTransitionTest {
+
+ @Override
+ Transition createTransition() {
+ return new ChangeClipBounds();
+ }
+
+ @SdkSuppress(minSdkVersion = 18)
+ @Test
+ public void testChangeClipBounds() throws Throwable {
+ enterScene(R.layout.scene1);
+
+ final View redSquare = rule.getActivity().findViewById(R.id.redSquare);
+ final Rect newClip = new Rect(redSquare.getLeft() + 10, redSquare.getTop() + 10,
+ redSquare.getRight() - 10, redSquare.getBottom() - 10);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ assertNull(ViewCompat.getClipBounds(redSquare));
+ TransitionManager.beginDelayedTransition(mRoot, mTransition);
+ ViewCompat.setClipBounds(redSquare, newClip);
+ }
+ });
+ waitForStart();
+ Thread.sleep(150);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Rect midClip = ViewCompat.getClipBounds(redSquare);
+ assertNotNull(midClip);
+ assertTrue(midClip.left > 0 && midClip.left < newClip.left);
+ assertTrue(midClip.top > 0 && midClip.top < newClip.top);
+ assertTrue(midClip.right < redSquare.getRight() && midClip.right > newClip.right);
+ assertTrue(midClip.bottom < redSquare.getBottom()
+ && midClip.bottom > newClip.bottom);
+ }
+ });
+ waitForEnd();
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final Rect endRect = ViewCompat.getClipBounds(redSquare);
+ assertNotNull(endRect);
+ assertEquals(newClip, endRect);
+ }
+ });
+
+ resetListener();
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, mTransition);
+ ViewCompat.setClipBounds(redSquare, null);
+ }
+ });
+ waitForStart();
+ Thread.sleep(150);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Rect midClip = ViewCompat.getClipBounds(redSquare);
+ assertNotNull(midClip);
+ assertTrue(midClip.left > 0 && midClip.left < newClip.left);
+ assertTrue(midClip.top > 0 && midClip.top < newClip.top);
+ assertTrue(midClip.right < redSquare.getRight() && midClip.right > newClip.right);
+ assertTrue(midClip.bottom < redSquare.getBottom()
+ && midClip.bottom > newClip.bottom);
+ }
+ });
+ waitForEnd();
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ assertNull(ViewCompat.getClipBounds(redSquare));
+ }
+ });
+
+ }
+
+ @Test
+ public void dummy() {
+ // Avoid "No tests found" on older devices
+ }
+
+}
diff --git a/androidx/transition/ChangeImageTransform.java b/androidx/transition/ChangeImageTransform.java
new file mode 100644
index 0000000..2830097
--- /dev/null
+++ b/androidx/transition/ChangeImageTransform.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.TypeEvaluator;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Property;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+
+import java.util.Map;
+
+/**
+ * This Transition captures an ImageView's matrix before and after the
+ * scene change and animates it during the transition.
+ *
+ * <p>In combination with ChangeBounds, ChangeImageTransform allows ImageViews
+ * that change size, shape, or {@link android.widget.ImageView.ScaleType} to animate contents
+ * smoothly.</p>
+ */
+public class ChangeImageTransform extends Transition {
+
+ private static final String PROPNAME_MATRIX = "android:changeImageTransform:matrix";
+ private static final String PROPNAME_BOUNDS = "android:changeImageTransform:bounds";
+
+ private static final String[] sTransitionProperties = {
+ PROPNAME_MATRIX,
+ PROPNAME_BOUNDS,
+ };
+
+ private static final TypeEvaluator<Matrix> NULL_MATRIX_EVALUATOR = new TypeEvaluator<Matrix>() {
+ @Override
+ public Matrix evaluate(float fraction, Matrix startValue, Matrix endValue) {
+ return null;
+ }
+ };
+
+ private static final Property<ImageView, Matrix> ANIMATED_TRANSFORM_PROPERTY =
+ new Property<ImageView, Matrix>(Matrix.class, "animatedTransform") {
+ @Override
+ public void set(ImageView view, Matrix matrix) {
+ ImageViewUtils.animateTransform(view, matrix);
+ }
+
+ @Override
+ public Matrix get(ImageView object) {
+ return null;
+ }
+ };
+
+ public ChangeImageTransform() {
+ }
+
+ public ChangeImageTransform(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ private void captureValues(TransitionValues transitionValues) {
+ View view = transitionValues.view;
+ if (!(view instanceof ImageView) || view.getVisibility() != View.VISIBLE) {
+ return;
+ }
+ ImageView imageView = (ImageView) view;
+ Drawable drawable = imageView.getDrawable();
+ if (drawable == null) {
+ return;
+ }
+ Map<String, Object> values = transitionValues.values;
+
+ int left = view.getLeft();
+ int top = view.getTop();
+ int right = view.getRight();
+ int bottom = view.getBottom();
+
+ Rect bounds = new Rect(left, top, right, bottom);
+ values.put(PROPNAME_BOUNDS, bounds);
+ values.put(PROPNAME_MATRIX, copyImageMatrix(imageView));
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ captureValues(transitionValues);
+ }
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ captureValues(transitionValues);
+ }
+
+ @Override
+ public String[] getTransitionProperties() {
+ return sTransitionProperties;
+ }
+
+ /**
+ * Creates an Animator for ImageViews moving, changing dimensions, and/or changing
+ * {@link android.widget.ImageView.ScaleType}.
+ *
+ * @param sceneRoot The root of the transition hierarchy.
+ * @param startValues The values for a specific target in the start scene.
+ * @param endValues The values for the target in the end scene.
+ * @return An Animator to move an ImageView or null if the View is not an ImageView,
+ * the Drawable changed, the View is not VISIBLE, or there was no change.
+ */
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot, TransitionValues startValues,
+ final TransitionValues endValues) {
+ if (startValues == null || endValues == null) {
+ return null;
+ }
+ Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS);
+ Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
+ if (startBounds == null || endBounds == null) {
+ return null;
+ }
+
+ Matrix startMatrix = (Matrix) startValues.values.get(PROPNAME_MATRIX);
+ Matrix endMatrix = (Matrix) endValues.values.get(PROPNAME_MATRIX);
+
+ boolean matricesEqual = (startMatrix == null && endMatrix == null)
+ || (startMatrix != null && startMatrix.equals(endMatrix));
+
+ if (startBounds.equals(endBounds) && matricesEqual) {
+ return null;
+ }
+
+ final ImageView imageView = (ImageView) endValues.view;
+ Drawable drawable = imageView.getDrawable();
+ int drawableWidth = drawable.getIntrinsicWidth();
+ int drawableHeight = drawable.getIntrinsicHeight();
+
+ ImageViewUtils.startAnimateTransform(imageView);
+
+ ObjectAnimator animator;
+ if (drawableWidth == 0 || drawableHeight == 0) {
+ animator = createNullAnimator(imageView);
+ } else {
+ if (startMatrix == null) {
+ startMatrix = MatrixUtils.IDENTITY_MATRIX;
+ }
+ if (endMatrix == null) {
+ endMatrix = MatrixUtils.IDENTITY_MATRIX;
+ }
+ ANIMATED_TRANSFORM_PROPERTY.set(imageView, startMatrix);
+ animator = createMatrixAnimator(imageView, startMatrix, endMatrix);
+ }
+
+ ImageViewUtils.reserveEndAnimateTransform(imageView, animator);
+
+ return animator;
+ }
+
+ private ObjectAnimator createNullAnimator(ImageView imageView) {
+ return ObjectAnimator.ofObject(imageView, ANIMATED_TRANSFORM_PROPERTY,
+ NULL_MATRIX_EVALUATOR, null, null);
+ }
+
+ private ObjectAnimator createMatrixAnimator(final ImageView imageView, Matrix startMatrix,
+ final Matrix endMatrix) {
+ return ObjectAnimator.ofObject(imageView, ANIMATED_TRANSFORM_PROPERTY,
+ new TransitionUtils.MatrixEvaluator(), startMatrix, endMatrix);
+ }
+
+ private static Matrix copyImageMatrix(ImageView view) {
+ switch (view.getScaleType()) {
+ case FIT_XY:
+ return fitXYMatrix(view);
+ case CENTER_CROP:
+ return centerCropMatrix(view);
+ default:
+ return new Matrix(view.getImageMatrix());
+ }
+ }
+
+ /**
+ * Calculates the image transformation matrix for an ImageView with ScaleType FIT_XY. This
+ * needs to be manually calculated as the platform does not give us the value for this case.
+ */
+ private static Matrix fitXYMatrix(ImageView view) {
+ final Drawable image = view.getDrawable();
+ final Matrix matrix = new Matrix();
+ matrix.postScale(
+ ((float) view.getWidth()) / image.getIntrinsicWidth(),
+ ((float) view.getHeight()) / image.getIntrinsicHeight());
+ return matrix;
+ }
+
+ /**
+ * Calculates the image transformation matrix for an ImageView with ScaleType CENTER_CROP. This
+ * needs to be manually calculated for consistent behavior across all the API levels.
+ */
+ private static Matrix centerCropMatrix(ImageView view) {
+ final Drawable image = view.getDrawable();
+ final int imageWidth = image.getIntrinsicWidth();
+ final int imageViewWidth = view.getWidth();
+ final float scaleX = ((float) imageViewWidth) / imageWidth;
+
+ final int imageHeight = image.getIntrinsicHeight();
+ final int imageViewHeight = view.getHeight();
+ final float scaleY = ((float) imageViewHeight) / imageHeight;
+
+ final float maxScale = Math.max(scaleX, scaleY);
+
+ final float width = imageWidth * maxScale;
+ final float height = imageHeight * maxScale;
+ final int tx = Math.round((imageViewWidth - width) / 2f);
+ final int ty = Math.round((imageViewHeight - height) / 2f);
+
+ final Matrix matrix = new Matrix();
+ matrix.postScale(maxScale, maxScale);
+ matrix.postTranslate(tx, ty);
+ return matrix;
+ }
+
+}
diff --git a/androidx/transition/ChangeImageTransformTest.java b/androidx/transition/ChangeImageTransformTest.java
new file mode 100644
index 0000000..5879976
--- /dev/null
+++ b/androidx/transition/ChangeImageTransformTest.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.graphics.Matrix;
+import android.graphics.drawable.Drawable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.ActivityCompat;
+import androidx.transition.test.R;
+
+import org.junit.Test;
+
+@MediumTest
+public class ChangeImageTransformTest extends BaseTransitionTest {
+
+ private ChangeImageTransform mChangeImageTransform;
+ private Matrix mStartMatrix;
+ private Matrix mEndMatrix;
+ private Drawable mImage;
+ private ImageView mImageView;
+
+ @Override
+ Transition createTransition() {
+ mChangeImageTransform = new CaptureMatrix();
+ mChangeImageTransform.setDuration(100);
+ mTransition = mChangeImageTransform;
+ resetListener();
+ return mChangeImageTransform;
+ }
+
+ @Test
+ public void testCenterToFitXY() throws Throwable {
+ transformImage(ImageView.ScaleType.CENTER, ImageView.ScaleType.FIT_XY);
+ verifyMatrixMatches(centerMatrix(), mStartMatrix);
+ verifyMatrixMatches(fitXYMatrix(), mEndMatrix);
+ }
+
+ @Test
+ public void testCenterCropToFitCenter() throws Throwable {
+ transformImage(ImageView.ScaleType.CENTER_CROP, ImageView.ScaleType.FIT_CENTER);
+ verifyMatrixMatches(centerCropMatrix(), mStartMatrix);
+ verifyMatrixMatches(fitCenterMatrix(), mEndMatrix);
+ }
+
+ @Test
+ public void testCenterInsideToFitEnd() throws Throwable {
+ transformImage(ImageView.ScaleType.CENTER_INSIDE, ImageView.ScaleType.FIT_END);
+ // CENTER_INSIDE and CENTER are the same when the image is smaller than the View
+ verifyMatrixMatches(centerMatrix(), mStartMatrix);
+ verifyMatrixMatches(fitEndMatrix(), mEndMatrix);
+ }
+
+ @Test
+ public void testFitStartToCenter() throws Throwable {
+ transformImage(ImageView.ScaleType.FIT_START, ImageView.ScaleType.CENTER);
+ verifyMatrixMatches(fitStartMatrix(), mStartMatrix);
+ verifyMatrixMatches(centerMatrix(), mEndMatrix);
+ }
+
+ private Matrix centerMatrix() {
+ int imageWidth = mImage.getIntrinsicWidth();
+ int imageViewWidth = mImageView.getWidth();
+ float tx = Math.round((imageViewWidth - imageWidth) / 2f);
+
+ int imageHeight = mImage.getIntrinsicHeight();
+ int imageViewHeight = mImageView.getHeight();
+ float ty = Math.round((imageViewHeight - imageHeight) / 2f);
+
+ Matrix matrix = new Matrix();
+ matrix.postTranslate(tx, ty);
+ return matrix;
+ }
+
+ private Matrix fitXYMatrix() {
+ int imageWidth = mImage.getIntrinsicWidth();
+ int imageViewWidth = mImageView.getWidth();
+ float scaleX = ((float) imageViewWidth) / imageWidth;
+
+ int imageHeight = mImage.getIntrinsicHeight();
+ int imageViewHeight = mImageView.getHeight();
+ float scaleY = ((float) imageViewHeight) / imageHeight;
+
+ Matrix matrix = new Matrix();
+ matrix.postScale(scaleX, scaleY);
+ return matrix;
+ }
+
+ private Matrix centerCropMatrix() {
+ int imageWidth = mImage.getIntrinsicWidth();
+ int imageViewWidth = mImageView.getWidth();
+ float scaleX = ((float) imageViewWidth) / imageWidth;
+
+ int imageHeight = mImage.getIntrinsicHeight();
+ int imageViewHeight = mImageView.getHeight();
+ float scaleY = ((float) imageViewHeight) / imageHeight;
+
+ float maxScale = Math.max(scaleX, scaleY);
+
+ float width = imageWidth * maxScale;
+ float height = imageHeight * maxScale;
+ int tx = Math.round((imageViewWidth - width) / 2f);
+ int ty = Math.round((imageViewHeight - height) / 2f);
+
+ Matrix matrix = new Matrix();
+ matrix.postScale(maxScale, maxScale);
+ matrix.postTranslate(tx, ty);
+ return matrix;
+ }
+
+ private Matrix fitCenterMatrix() {
+ int imageWidth = mImage.getIntrinsicWidth();
+ int imageViewWidth = mImageView.getWidth();
+ float scaleX = ((float) imageViewWidth) / imageWidth;
+
+ int imageHeight = mImage.getIntrinsicHeight();
+ int imageViewHeight = mImageView.getHeight();
+ float scaleY = ((float) imageViewHeight) / imageHeight;
+
+ float minScale = Math.min(scaleX, scaleY);
+
+ float width = imageWidth * minScale;
+ float height = imageHeight * minScale;
+ float tx = (imageViewWidth - width) / 2f;
+ float ty = (imageViewHeight - height) / 2f;
+
+ Matrix matrix = new Matrix();
+ matrix.postScale(minScale, minScale);
+ matrix.postTranslate(tx, ty);
+ return matrix;
+ }
+
+ private Matrix fitStartMatrix() {
+ int imageWidth = mImage.getIntrinsicWidth();
+ int imageViewWidth = mImageView.getWidth();
+ float scaleX = ((float) imageViewWidth) / imageWidth;
+
+ int imageHeight = mImage.getIntrinsicHeight();
+ int imageViewHeight = mImageView.getHeight();
+ float scaleY = ((float) imageViewHeight) / imageHeight;
+
+ float minScale = Math.min(scaleX, scaleY);
+
+ Matrix matrix = new Matrix();
+ matrix.postScale(minScale, minScale);
+ return matrix;
+ }
+
+ private Matrix fitEndMatrix() {
+ int imageWidth = mImage.getIntrinsicWidth();
+ int imageViewWidth = mImageView.getWidth();
+ float scaleX = ((float) imageViewWidth) / imageWidth;
+
+ int imageHeight = mImage.getIntrinsicHeight();
+ int imageViewHeight = mImageView.getHeight();
+ float scaleY = ((float) imageViewHeight) / imageHeight;
+
+ float minScale = Math.min(scaleX, scaleY);
+
+ float width = imageWidth * minScale;
+ float height = imageHeight * minScale;
+ float tx = imageViewWidth - width;
+ float ty = imageViewHeight - height;
+
+ Matrix matrix = new Matrix();
+ matrix.postScale(minScale, minScale);
+ matrix.postTranslate(tx, ty);
+ return matrix;
+ }
+
+ private void verifyMatrixMatches(Matrix expected, Matrix matrix) {
+ if (expected == null) {
+ assertNull(matrix);
+ return;
+ }
+ assertNotNull(matrix);
+ float[] expectedValues = new float[9];
+ expected.getValues(expectedValues);
+
+ float[] values = new float[9];
+ matrix.getValues(values);
+
+ for (int i = 0; i < values.length; i++) {
+ final float expectedValue = expectedValues[i];
+ final float value = values[i];
+ assertEquals("Value [" + i + "]", expectedValue, value, 0.01f);
+ }
+ }
+
+ private void transformImage(ImageView.ScaleType startScale, final ImageView.ScaleType endScale)
+ throws Throwable {
+ final ImageView imageView = enterImageViewScene(startScale);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, mChangeImageTransform);
+ imageView.setScaleType(endScale);
+ }
+ });
+ waitForStart();
+ verify(mListener, (startScale == endScale) ? times(1) : never())
+ .onTransitionEnd(any(Transition.class));
+ waitForEnd();
+ }
+
+ private ImageView enterImageViewScene(final ImageView.ScaleType scaleType) throws Throwable {
+ enterScene(R.layout.scene4);
+ final ViewGroup container = (ViewGroup) rule.getActivity().findViewById(R.id.holder);
+ final ImageView[] imageViews = new ImageView[1];
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mImageView = new ImageView(rule.getActivity());
+ mImage = ActivityCompat.getDrawable(rule.getActivity(),
+ android.R.drawable.ic_media_play);
+ mImageView.setImageDrawable(mImage);
+ mImageView.setScaleType(scaleType);
+ imageViews[0] = mImageView;
+ container.addView(mImageView);
+ ViewGroup.LayoutParams layoutParams = mImageView.getLayoutParams();
+ DisplayMetrics metrics = rule.getActivity().getResources().getDisplayMetrics();
+ float size = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, metrics);
+ layoutParams.width = Math.round(size);
+ layoutParams.height = Math.round(size * 2);
+ mImageView.setLayoutParams(layoutParams);
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ return imageViews[0];
+ }
+
+ private class CaptureMatrix extends ChangeImageTransform {
+
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot, TransitionValues startValues,
+ TransitionValues endValues) {
+ Animator animator = super.createAnimator(sceneRoot, startValues, endValues);
+ assertNotNull(animator);
+ animator.addListener(new CaptureMatrixListener((ImageView) endValues.view));
+ return animator;
+ }
+
+ }
+
+ private class CaptureMatrixListener extends AnimatorListenerAdapter {
+
+ private final ImageView mImageView;
+
+ CaptureMatrixListener(ImageView view) {
+ mImageView = view;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mStartMatrix = copyMatrix();
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mEndMatrix = copyMatrix();
+ }
+
+ private Matrix copyMatrix() {
+ Matrix matrix = mImageView.getImageMatrix();
+ if (matrix != null) {
+ matrix = new Matrix(matrix);
+ }
+ return matrix;
+ }
+
+ }
+
+}
diff --git a/androidx/transition/ChangeScroll.java b/androidx/transition/ChangeScroll.java
new file mode 100644
index 0000000..bdf918f
--- /dev/null
+++ b/androidx/transition/ChangeScroll.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+
+/**
+ * This transition captures the scroll properties of targets before and after
+ * the scene change and animates any changes.
+ */
+public class ChangeScroll extends Transition {
+
+ private static final String PROPNAME_SCROLL_X = "android:changeScroll:x";
+ private static final String PROPNAME_SCROLL_Y = "android:changeScroll:y";
+
+ private static final String[] PROPERTIES = {
+ PROPNAME_SCROLL_X,
+ PROPNAME_SCROLL_Y,
+ };
+
+ public ChangeScroll() {}
+
+ public ChangeScroll(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ captureValues(transitionValues);
+ }
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ captureValues(transitionValues);
+ }
+
+ @Nullable
+ @Override
+ public String[] getTransitionProperties() {
+ return PROPERTIES;
+ }
+
+ private void captureValues(TransitionValues transitionValues) {
+ transitionValues.values.put(PROPNAME_SCROLL_X, transitionValues.view.getScrollX());
+ transitionValues.values.put(PROPNAME_SCROLL_Y, transitionValues.view.getScrollY());
+ }
+
+ @Nullable
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot,
+ @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) {
+ if (startValues == null || endValues == null) {
+ return null;
+ }
+ final View view = endValues.view;
+ int startX = (Integer) startValues.values.get(PROPNAME_SCROLL_X);
+ int endX = (Integer) endValues.values.get(PROPNAME_SCROLL_X);
+ int startY = (Integer) startValues.values.get(PROPNAME_SCROLL_Y);
+ int endY = (Integer) endValues.values.get(PROPNAME_SCROLL_Y);
+ Animator scrollXAnimator = null;
+ Animator scrollYAnimator = null;
+ if (startX != endX) {
+ view.setScrollX(startX);
+ scrollXAnimator = ObjectAnimator.ofInt(view, "scrollX", startX, endX);
+ }
+ if (startY != endY) {
+ view.setScrollY(startY);
+ scrollYAnimator = ObjectAnimator.ofInt(view, "scrollY", startY, endY);
+ }
+ return TransitionUtils.mergeAnimators(scrollXAnimator, scrollYAnimator);
+ }
+
+}
diff --git a/androidx/transition/ChangeScrollTest.java b/androidx/transition/ChangeScrollTest.java
new file mode 100644
index 0000000..4a01d16
--- /dev/null
+++ b/androidx/transition/ChangeScrollTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.hamcrest.CoreMatchers.both;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.lessThan;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import android.support.test.filters.MediumTest;
+import android.view.View;
+
+import androidx.transition.test.R;
+
+import org.junit.Test;
+
+@MediumTest
+public class ChangeScrollTest extends BaseTransitionTest {
+
+ @Override
+ Transition createTransition() {
+ return new ChangeScroll();
+ }
+
+ @Test
+ public void testChangeScroll() throws Throwable {
+ enterScene(R.layout.scene5);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final View view = rule.getActivity().findViewById(R.id.text);
+ assertEquals(0, view.getScrollX());
+ assertEquals(0, view.getScrollY());
+ TransitionManager.beginDelayedTransition(mRoot, mTransition);
+ view.scrollTo(150, 300);
+ }
+ });
+ waitForStart();
+ Thread.sleep(150);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final View view = rule.getActivity().findViewById(R.id.text);
+ final int scrollX = view.getScrollX();
+ final int scrollY = view.getScrollY();
+ assertThat(scrollX, is(both(greaterThan(0)).and(lessThan(150))));
+ assertThat(scrollY, is(both(greaterThan(0)).and(lessThan(300))));
+ }
+ });
+ waitForEnd();
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final View view = rule.getActivity().findViewById(R.id.text);
+ assertEquals(150, view.getScrollX());
+ assertEquals(300, view.getScrollY());
+ }
+ });
+ }
+
+}
diff --git a/androidx/transition/ChangeTransform.java b/androidx/transition/ChangeTransform.java
new file mode 100644
index 0000000..c91c16b
--- /dev/null
+++ b/androidx/transition/ChangeTransform.java
@@ -0,0 +1,584 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.Property;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.res.TypedArrayUtils;
+import androidx.core.view.ViewCompat;
+
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * This Transition captures scale and rotation for Views before and after the
+ * scene change and animates those changes during the transition.
+ *
+ * A change in parent is handled as well by capturing the transforms from
+ * the parent before and after the scene change and animating those during the
+ * transition.
+ */
+public class ChangeTransform extends Transition {
+
+ private static final String PROPNAME_MATRIX = "android:changeTransform:matrix";
+ private static final String PROPNAME_TRANSFORMS = "android:changeTransform:transforms";
+ private static final String PROPNAME_PARENT = "android:changeTransform:parent";
+ private static final String PROPNAME_PARENT_MATRIX = "android:changeTransform:parentMatrix";
+ private static final String PROPNAME_INTERMEDIATE_PARENT_MATRIX =
+ "android:changeTransform:intermediateParentMatrix";
+ private static final String PROPNAME_INTERMEDIATE_MATRIX =
+ "android:changeTransform:intermediateMatrix";
+
+ private static final String[] sTransitionProperties = {
+ PROPNAME_MATRIX,
+ PROPNAME_TRANSFORMS,
+ PROPNAME_PARENT_MATRIX,
+ };
+
+ /**
+ * This property sets the animation matrix properties that are not translations.
+ */
+ private static final Property<PathAnimatorMatrix, float[]> NON_TRANSLATIONS_PROPERTY =
+ new Property<PathAnimatorMatrix, float[]>(float[].class, "nonTranslations") {
+ @Override
+ public float[] get(PathAnimatorMatrix object) {
+ return null;
+ }
+
+ @Override
+ public void set(PathAnimatorMatrix object, float[] value) {
+ object.setValues(value);
+ }
+ };
+
+ /**
+ * This property sets the translation animation matrix properties.
+ */
+ private static final Property<PathAnimatorMatrix, PointF> TRANSLATIONS_PROPERTY =
+ new Property<PathAnimatorMatrix, PointF>(PointF.class, "translations") {
+ @Override
+ public PointF get(PathAnimatorMatrix object) {
+ return null;
+ }
+
+ @Override
+ public void set(PathAnimatorMatrix object, PointF value) {
+ object.setTranslation(value);
+ }
+ };
+
+ /**
+ * Newer platforms suppress view removal at the beginning of the animation.
+ */
+ private static final boolean SUPPORTS_VIEW_REMOVAL_SUPPRESSION = Build.VERSION.SDK_INT >= 21;
+
+ private boolean mUseOverlay = true;
+ private boolean mReparent = true;
+ private Matrix mTempMatrix = new Matrix();
+
+ public ChangeTransform() {
+ }
+
+ public ChangeTransform(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = context.obtainStyledAttributes(attrs, Styleable.CHANGE_TRANSFORM);
+ mUseOverlay = TypedArrayUtils.getNamedBoolean(a, (XmlPullParser) attrs,
+ "reparentWithOverlay", Styleable.ChangeTransform.REPARENT_WITH_OVERLAY, true);
+ mReparent = TypedArrayUtils.getNamedBoolean(a, (XmlPullParser) attrs,
+ "reparent", Styleable.ChangeTransform.REPARENT, true);
+ a.recycle();
+ }
+
+ /**
+ * Returns whether changes to parent should use an overlay or not. When the parent
+ * change doesn't use an overlay, it affects the transforms of the child. The
+ * default value is <code>true</code>.
+ *
+ * <p>Note: when Overlays are not used when a parent changes, a view can be clipped when
+ * it moves outside the bounds of its parent. Setting
+ * {@link android.view.ViewGroup#setClipChildren(boolean)} and
+ * {@link android.view.ViewGroup#setClipToPadding(boolean)} can help. Also, when
+ * Overlays are not used and the parent is animating its location, the position of the
+ * child view will be relative to its parent's final position, so it may appear to "jump"
+ * at the beginning.</p>
+ *
+ * @return <code>true</code> when a changed parent should execute the transition
+ * inside the scene root's overlay or <code>false</code> if a parent change only
+ * affects the transform of the transitioning view.
+ */
+ public boolean getReparentWithOverlay() {
+ return mUseOverlay;
+ }
+
+ /**
+ * Sets whether changes to parent should use an overlay or not. When the parent
+ * change doesn't use an overlay, it affects the transforms of the child. The
+ * default value is <code>true</code>.
+ *
+ * <p>Note: when Overlays are not used when a parent changes, a view can be clipped when
+ * it moves outside the bounds of its parent. Setting
+ * {@link android.view.ViewGroup#setClipChildren(boolean)} and
+ * {@link android.view.ViewGroup#setClipToPadding(boolean)} can help. Also, when
+ * Overlays are not used and the parent is animating its location, the position of the
+ * child view will be relative to its parent's final position, so it may appear to "jump"
+ * at the beginning.</p>
+ *
+ * @param reparentWithOverlay <code>true</code> when a changed parent should execute the
+ * transition inside the scene root's overlay or <code>false</code>
+ * if a parent change only affects the transform of the
+ * transitioning view.
+ */
+ public void setReparentWithOverlay(boolean reparentWithOverlay) {
+ mUseOverlay = reparentWithOverlay;
+ }
+
+ /**
+ * Returns whether parent changes will be tracked by the ChangeTransform. If parent
+ * changes are tracked, then the transform will adjust to the transforms of the
+ * different parents. If they aren't tracked, only the transforms of the transitioning
+ * view will be tracked. Default is true.
+ *
+ * @return whether parent changes will be tracked by the ChangeTransform.
+ */
+ public boolean getReparent() {
+ return mReparent;
+ }
+
+ /**
+ * Sets whether parent changes will be tracked by the ChangeTransform. If parent
+ * changes are tracked, then the transform will adjust to the transforms of the
+ * different parents. If they aren't tracked, only the transforms of the transitioning
+ * view will be tracked. Default is true.
+ *
+ * @param reparent Set to true to track parent changes or false to only track changes
+ * of the transitioning view without considering the parent change.
+ */
+ public void setReparent(boolean reparent) {
+ mReparent = reparent;
+ }
+
+ @Override
+ public String[] getTransitionProperties() {
+ return sTransitionProperties;
+ }
+
+ private void captureValues(TransitionValues transitionValues) {
+ View view = transitionValues.view;
+ if (view.getVisibility() == View.GONE) {
+ return;
+ }
+ transitionValues.values.put(PROPNAME_PARENT, view.getParent());
+ Transforms transforms = new Transforms(view);
+ transitionValues.values.put(PROPNAME_TRANSFORMS, transforms);
+ Matrix matrix = view.getMatrix();
+ if (matrix == null || matrix.isIdentity()) {
+ matrix = null;
+ } else {
+ matrix = new Matrix(matrix);
+ }
+ transitionValues.values.put(PROPNAME_MATRIX, matrix);
+ if (mReparent) {
+ Matrix parentMatrix = new Matrix();
+ ViewGroup parent = (ViewGroup) view.getParent();
+ ViewUtils.transformMatrixToGlobal(parent, parentMatrix);
+ parentMatrix.preTranslate(-parent.getScrollX(), -parent.getScrollY());
+ transitionValues.values.put(PROPNAME_PARENT_MATRIX, parentMatrix);
+ transitionValues.values.put(PROPNAME_INTERMEDIATE_MATRIX,
+ view.getTag(R.id.transition_transform));
+ transitionValues.values.put(PROPNAME_INTERMEDIATE_PARENT_MATRIX,
+ view.getTag(R.id.parent_matrix));
+ }
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ captureValues(transitionValues);
+ if (!SUPPORTS_VIEW_REMOVAL_SUPPRESSION) {
+ // We still don't know if the view is removed or not, but we need to do this here, or
+ // the view will be actually removed, resulting in flickering at the beginning of the
+ // animation. We are canceling this afterwards.
+ ((ViewGroup) transitionValues.view.getParent()).startViewTransition(
+ transitionValues.view);
+ }
+ }
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ captureValues(transitionValues);
+ }
+
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot, TransitionValues startValues,
+ TransitionValues endValues) {
+ if (startValues == null || endValues == null
+ || !startValues.values.containsKey(PROPNAME_PARENT)
+ || !endValues.values.containsKey(PROPNAME_PARENT)) {
+ return null;
+ }
+
+ ViewGroup startParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT);
+ ViewGroup endParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT);
+ boolean handleParentChange = mReparent && !parentsMatch(startParent, endParent);
+
+ Matrix startMatrix = (Matrix) startValues.values.get(PROPNAME_INTERMEDIATE_MATRIX);
+ if (startMatrix != null) {
+ startValues.values.put(PROPNAME_MATRIX, startMatrix);
+ }
+
+ Matrix startParentMatrix = (Matrix)
+ startValues.values.get(PROPNAME_INTERMEDIATE_PARENT_MATRIX);
+ if (startParentMatrix != null) {
+ startValues.values.put(PROPNAME_PARENT_MATRIX, startParentMatrix);
+ }
+
+ // First handle the parent change:
+ if (handleParentChange) {
+ setMatricesForParent(startValues, endValues);
+ }
+
+ // Next handle the normal matrix transform:
+ ObjectAnimator transformAnimator = createTransformAnimator(startValues, endValues,
+ handleParentChange);
+
+ if (handleParentChange && transformAnimator != null && mUseOverlay) {
+ createGhostView(sceneRoot, startValues, endValues);
+ } else if (!SUPPORTS_VIEW_REMOVAL_SUPPRESSION) {
+ // We didn't need to suppress the view removal in this case. Cancel the suppression.
+ startParent.endViewTransition(startValues.view);
+ }
+
+ return transformAnimator;
+ }
+
+ private ObjectAnimator createTransformAnimator(TransitionValues startValues,
+ TransitionValues endValues, final boolean handleParentChange) {
+ Matrix startMatrix = (Matrix) startValues.values.get(PROPNAME_MATRIX);
+ Matrix endMatrix = (Matrix) endValues.values.get(PROPNAME_MATRIX);
+
+ if (startMatrix == null) {
+ startMatrix = MatrixUtils.IDENTITY_MATRIX;
+ }
+
+ if (endMatrix == null) {
+ endMatrix = MatrixUtils.IDENTITY_MATRIX;
+ }
+
+ if (startMatrix.equals(endMatrix)) {
+ return null;
+ }
+
+ final Transforms transforms = (Transforms) endValues.values.get(PROPNAME_TRANSFORMS);
+
+ // clear the transform properties so that we can use the animation matrix instead
+ final View view = endValues.view;
+ setIdentityTransforms(view);
+
+ final float[] startMatrixValues = new float[9];
+ startMatrix.getValues(startMatrixValues);
+ final float[] endMatrixValues = new float[9];
+ endMatrix.getValues(endMatrixValues);
+ final PathAnimatorMatrix pathAnimatorMatrix =
+ new PathAnimatorMatrix(view, startMatrixValues);
+
+ PropertyValuesHolder valuesProperty = PropertyValuesHolder.ofObject(
+ NON_TRANSLATIONS_PROPERTY, new FloatArrayEvaluator(new float[9]),
+ startMatrixValues, endMatrixValues);
+ Path path = getPathMotion().getPath(startMatrixValues[Matrix.MTRANS_X],
+ startMatrixValues[Matrix.MTRANS_Y], endMatrixValues[Matrix.MTRANS_X],
+ endMatrixValues[Matrix.MTRANS_Y]);
+ PropertyValuesHolder translationProperty = PropertyValuesHolderUtils.ofPointF(
+ TRANSLATIONS_PROPERTY, path);
+ ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(pathAnimatorMatrix,
+ valuesProperty, translationProperty);
+
+ final Matrix finalEndMatrix = endMatrix;
+
+ AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
+ private boolean mIsCanceled;
+ private Matrix mTempMatrix = new Matrix();
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mIsCanceled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!mIsCanceled) {
+ if (handleParentChange && mUseOverlay) {
+ setCurrentMatrix(finalEndMatrix);
+ } else {
+ view.setTag(R.id.transition_transform, null);
+ view.setTag(R.id.parent_matrix, null);
+ }
+ }
+ ViewUtils.setAnimationMatrix(view, null);
+ transforms.restore(view);
+ }
+
+ @Override
+ public void onAnimationPause(Animator animation) {
+ Matrix currentMatrix = pathAnimatorMatrix.getMatrix();
+ setCurrentMatrix(currentMatrix);
+ }
+
+ @Override
+ public void onAnimationResume(Animator animation) {
+ setIdentityTransforms(view);
+ }
+
+ private void setCurrentMatrix(Matrix currentMatrix) {
+ mTempMatrix.set(currentMatrix);
+ view.setTag(R.id.transition_transform, mTempMatrix);
+ transforms.restore(view);
+ }
+ };
+
+ animator.addListener(listener);
+ AnimatorUtils.addPauseListener(animator, listener);
+ return animator;
+ }
+
+ private boolean parentsMatch(ViewGroup startParent, ViewGroup endParent) {
+ boolean parentsMatch = false;
+ if (!isValidTarget(startParent) || !isValidTarget(endParent)) {
+ parentsMatch = startParent == endParent;
+ } else {
+ TransitionValues endValues = getMatchedTransitionValues(startParent, true);
+ if (endValues != null) {
+ parentsMatch = endParent == endValues.view;
+ }
+ }
+ return parentsMatch;
+ }
+
+ private void createGhostView(final ViewGroup sceneRoot, TransitionValues startValues,
+ TransitionValues endValues) {
+ View view = endValues.view;
+
+ Matrix endMatrix = (Matrix) endValues.values.get(PROPNAME_PARENT_MATRIX);
+ Matrix localEndMatrix = new Matrix(endMatrix);
+ ViewUtils.transformMatrixToLocal(sceneRoot, localEndMatrix);
+
+ GhostViewImpl ghostView = GhostViewUtils.addGhost(view, sceneRoot, localEndMatrix);
+ if (ghostView == null) {
+ return;
+ }
+ // Ask GhostView to actually remove the start view when it starts drawing the animation.
+ ghostView.reserveEndViewTransition((ViewGroup) startValues.values.get(PROPNAME_PARENT),
+ startValues.view);
+
+ Transition outerTransition = this;
+ while (outerTransition.mParent != null) {
+ outerTransition = outerTransition.mParent;
+ }
+
+ GhostListener listener = new GhostListener(view, ghostView);
+ outerTransition.addListener(listener);
+
+ // We cannot do this for older platforms or it invalidates the view and results in
+ // flickering, but the view will still be invisible by actually removing it from the parent.
+ if (SUPPORTS_VIEW_REMOVAL_SUPPRESSION) {
+ if (startValues.view != endValues.view) {
+ ViewUtils.setTransitionAlpha(startValues.view, 0);
+ }
+ ViewUtils.setTransitionAlpha(view, 1);
+ }
+ }
+
+ private void setMatricesForParent(TransitionValues startValues, TransitionValues endValues) {
+ Matrix endParentMatrix = (Matrix) endValues.values.get(PROPNAME_PARENT_MATRIX);
+ endValues.view.setTag(R.id.parent_matrix, endParentMatrix);
+
+ Matrix toLocal = mTempMatrix;
+ toLocal.reset();
+ endParentMatrix.invert(toLocal);
+
+ Matrix startLocal = (Matrix) startValues.values.get(PROPNAME_MATRIX);
+ if (startLocal == null) {
+ startLocal = new Matrix();
+ startValues.values.put(PROPNAME_MATRIX, startLocal);
+ }
+
+ Matrix startParentMatrix = (Matrix) startValues.values.get(PROPNAME_PARENT_MATRIX);
+ startLocal.postConcat(startParentMatrix);
+ startLocal.postConcat(toLocal);
+ }
+
+ private static void setIdentityTransforms(View view) {
+ setTransforms(view, 0, 0, 0, 1, 1, 0, 0, 0);
+ }
+
+ private static void setTransforms(View view, float translationX, float translationY,
+ float translationZ, float scaleX, float scaleY, float rotationX,
+ float rotationY, float rotationZ) {
+ view.setTranslationX(translationX);
+ view.setTranslationY(translationY);
+ ViewCompat.setTranslationZ(view, translationZ);
+ view.setScaleX(scaleX);
+ view.setScaleY(scaleY);
+ view.setRotationX(rotationX);
+ view.setRotationY(rotationY);
+ view.setRotation(rotationZ);
+ }
+
+ private static class Transforms {
+
+ final float mTranslationX;
+ final float mTranslationY;
+ final float mTranslationZ;
+ final float mScaleX;
+ final float mScaleY;
+ final float mRotationX;
+ final float mRotationY;
+ final float mRotationZ;
+
+ Transforms(View view) {
+ mTranslationX = view.getTranslationX();
+ mTranslationY = view.getTranslationY();
+ mTranslationZ = ViewCompat.getTranslationZ(view);
+ mScaleX = view.getScaleX();
+ mScaleY = view.getScaleY();
+ mRotationX = view.getRotationX();
+ mRotationY = view.getRotationY();
+ mRotationZ = view.getRotation();
+ }
+
+ public void restore(View view) {
+ setTransforms(view, mTranslationX, mTranslationY, mTranslationZ, mScaleX, mScaleY,
+ mRotationX, mRotationY, mRotationZ);
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ if (!(that instanceof Transforms)) {
+ return false;
+ }
+ Transforms thatTransform = (Transforms) that;
+ return thatTransform.mTranslationX == mTranslationX
+ && thatTransform.mTranslationY == mTranslationY
+ && thatTransform.mTranslationZ == mTranslationZ
+ && thatTransform.mScaleX == mScaleX
+ && thatTransform.mScaleY == mScaleY
+ && thatTransform.mRotationX == mRotationX
+ && thatTransform.mRotationY == mRotationY
+ && thatTransform.mRotationZ == mRotationZ;
+ }
+
+ @Override
+ public int hashCode() {
+ int code = mTranslationX != +0.0f ? Float.floatToIntBits(mTranslationX) : 0;
+ code = 31 * code + (mTranslationY != +0.0f ? Float.floatToIntBits(mTranslationY) : 0);
+ code = 31 * code + (mTranslationZ != +0.0f ? Float.floatToIntBits(mTranslationZ) : 0);
+ code = 31 * code + (mScaleX != +0.0f ? Float.floatToIntBits(mScaleX) : 0);
+ code = 31 * code + (mScaleY != +0.0f ? Float.floatToIntBits(mScaleY) : 0);
+ code = 31 * code + (mRotationX != +0.0f ? Float.floatToIntBits(mRotationX) : 0);
+ code = 31 * code + (mRotationY != +0.0f ? Float.floatToIntBits(mRotationY) : 0);
+ code = 31 * code + (mRotationZ != +0.0f ? Float.floatToIntBits(mRotationZ) : 0);
+ return code;
+ }
+
+ }
+
+ private static class GhostListener extends TransitionListenerAdapter {
+
+ private View mView;
+ private GhostViewImpl mGhostView;
+
+ GhostListener(View view, GhostViewImpl ghostView) {
+ mView = view;
+ mGhostView = ghostView;
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ transition.removeListener(this);
+ GhostViewUtils.removeGhost(mView);
+ mView.setTag(R.id.transition_transform, null);
+ mView.setTag(R.id.parent_matrix, null);
+ }
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ mGhostView.setVisibility(View.INVISIBLE);
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ mGhostView.setVisibility(View.VISIBLE);
+ }
+
+ }
+
+ /**
+ * PathAnimatorMatrix allows the translations and the rest of the matrix to be set
+ * separately. This allows the PathMotion to affect the translations while scale
+ * and rotation are evaluated separately.
+ */
+ private static class PathAnimatorMatrix {
+
+ private final Matrix mMatrix = new Matrix();
+ private final View mView;
+ private final float[] mValues;
+ private float mTranslationX;
+ private float mTranslationY;
+
+ PathAnimatorMatrix(View view, float[] values) {
+ mView = view;
+ mValues = values.clone();
+ mTranslationX = mValues[Matrix.MTRANS_X];
+ mTranslationY = mValues[Matrix.MTRANS_Y];
+ setAnimationMatrix();
+ }
+
+ void setValues(float[] values) {
+ System.arraycopy(values, 0, mValues, 0, values.length);
+ setAnimationMatrix();
+ }
+
+ void setTranslation(PointF translation) {
+ mTranslationX = translation.x;
+ mTranslationY = translation.y;
+ setAnimationMatrix();
+ }
+
+ private void setAnimationMatrix() {
+ mValues[Matrix.MTRANS_X] = mTranslationX;
+ mValues[Matrix.MTRANS_Y] = mTranslationY;
+ mMatrix.setValues(mValues);
+ ViewUtils.setAnimationMatrix(mView, mMatrix);
+ }
+
+ Matrix getMatrix() {
+ return mMatrix;
+ }
+ }
+
+}
diff --git a/androidx/transition/ChangeTransformTest.java b/androidx/transition/ChangeTransformTest.java
new file mode 100644
index 0000000..b13f0b8
--- /dev/null
+++ b/androidx/transition/ChangeTransformTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.support.test.filters.MediumTest;
+import android.view.View;
+
+import androidx.transition.test.R;
+
+import org.junit.Test;
+
+@MediumTest
+public class ChangeTransformTest extends BaseTransitionTest {
+
+ @Override
+ Transition createTransition() {
+ return new ChangeTransform();
+ }
+
+ @Test
+ public void testTranslation() throws Throwable {
+ enterScene(R.layout.scene1);
+
+ final View redSquare = rule.getActivity().findViewById(R.id.redSquare);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, mTransition);
+ redSquare.setTranslationX(500);
+ redSquare.setTranslationY(600);
+ }
+ });
+ waitForStart();
+
+ verify(mListener, never()).onTransitionEnd(any(Transition.class)); // still running
+ // There is no way to validate the intermediate matrix because it uses
+ // hidden properties of the View to execute.
+ waitForEnd();
+ assertEquals(500f, redSquare.getTranslationX(), 0.0f);
+ assertEquals(600f, redSquare.getTranslationY(), 0.0f);
+ }
+
+ @Test
+ public void testRotation() throws Throwable {
+ enterScene(R.layout.scene1);
+
+ final View redSquare = rule.getActivity().findViewById(R.id.redSquare);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, mTransition);
+ redSquare.setRotation(45);
+ }
+ });
+ waitForStart();
+
+ verify(mListener, never()).onTransitionEnd(any(Transition.class)); // still running
+ // There is no way to validate the intermediate matrix because it uses
+ // hidden properties of the View to execute.
+ waitForEnd();
+ assertEquals(45f, redSquare.getRotation(), 0.0f);
+ }
+
+ @Test
+ public void testScale() throws Throwable {
+ enterScene(R.layout.scene1);
+
+ final View redSquare = rule.getActivity().findViewById(R.id.redSquare);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, mTransition);
+ redSquare.setScaleX(2f);
+ redSquare.setScaleY(3f);
+ }
+ });
+ waitForStart();
+
+ verify(mListener, never()).onTransitionEnd(any(Transition.class)); // still running
+ // There is no way to validate the intermediate matrix because it uses
+ // hidden properties of the View to execute.
+ waitForEnd();
+ assertEquals(2f, redSquare.getScaleX(), 0.0f);
+ assertEquals(3f, redSquare.getScaleY(), 0.0f);
+ }
+
+ @Test
+ public void testReparent() throws Throwable {
+ final ChangeTransform changeTransform = (ChangeTransform) mTransition;
+ assertEquals(true, changeTransform.getReparent());
+ enterScene(R.layout.scene5);
+ startTransition(R.layout.scene9);
+ verify(mListener, never()).onTransitionEnd(any(Transition.class)); // still running
+ waitForEnd();
+
+ resetListener();
+ changeTransform.setReparent(false);
+ assertEquals(false, changeTransform.getReparent());
+ startTransition(R.layout.scene5);
+ waitForEnd(); // no transition to run because reparent == false
+ }
+
+}
diff --git a/androidx/transition/CheckCalledRunnable.java b/androidx/transition/CheckCalledRunnable.java
new file mode 100644
index 0000000..f33b72c
--- /dev/null
+++ b/androidx/transition/CheckCalledRunnable.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+class CheckCalledRunnable implements Runnable {
+
+ private boolean mWasCalled = false;
+
+ @Override
+ public void run() {
+ mWasCalled = true;
+ }
+
+ /**
+ * @return {@code true} if {@link #run()} was called at least once.
+ */
+ boolean wasCalled() {
+ return mWasCalled;
+ }
+
+}
diff --git a/androidx/transition/CircularPropagation.java b/androidx/transition/CircularPropagation.java
new file mode 100644
index 0000000..685d596
--- /dev/null
+++ b/androidx/transition/CircularPropagation.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A propagation that varies with the distance to the epicenter of the Transition
+ * or center of the scene if no epicenter exists. When a View is visible in the
+ * start of the transition, Views farther from the epicenter will transition
+ * sooner than Views closer to the epicenter. When a View is not in the start
+ * of the transition or is not visible at the start of the transition, it will
+ * transition sooner when closer to the epicenter and later when farther from
+ * the epicenter. This is the default TransitionPropagation used with
+ * {@link Explode}.
+ */
+public class CircularPropagation extends VisibilityPropagation {
+
+ private float mPropagationSpeed = 3.0f;
+
+ /**
+ * Sets the speed at which transition propagation happens, relative to the duration of the
+ * Transition. A <code>propagationSpeed</code> of 1 means that a View centered farthest from
+ * the epicenter and View centered at the epicenter will have a difference
+ * in start delay of approximately the duration of the Transition. A speed of 2 means the
+ * start delay difference will be approximately half of the duration of the transition. A
+ * value of 0 is illegal, but negative values will invert the propagation.
+ *
+ * @param propagationSpeed The speed at which propagation occurs, relative to the duration
+ * of the transition. A speed of 4 means it works 4 times as fast
+ * as the duration of the transition. May not be 0.
+ */
+ public void setPropagationSpeed(float propagationSpeed) {
+ if (propagationSpeed == 0) {
+ throw new IllegalArgumentException("propagationSpeed may not be 0");
+ }
+ mPropagationSpeed = propagationSpeed;
+ }
+
+ @Override
+ public long getStartDelay(ViewGroup sceneRoot, Transition transition,
+ TransitionValues startValues, TransitionValues endValues) {
+ if (startValues == null && endValues == null) {
+ return 0;
+ }
+ int directionMultiplier = 1;
+ TransitionValues positionValues;
+ if (endValues == null || getViewVisibility(startValues) == View.VISIBLE) {
+ positionValues = startValues;
+ directionMultiplier = -1;
+ } else {
+ positionValues = endValues;
+ }
+
+ int viewCenterX = getViewX(positionValues);
+ int viewCenterY = getViewY(positionValues);
+
+ Rect epicenter = transition.getEpicenter();
+ int epicenterX;
+ int epicenterY;
+ if (epicenter != null) {
+ epicenterX = epicenter.centerX();
+ epicenterY = epicenter.centerY();
+ } else {
+ int[] loc = new int[2];
+ sceneRoot.getLocationOnScreen(loc);
+ epicenterX = Math.round(loc[0] + (sceneRoot.getWidth() / 2)
+ + sceneRoot.getTranslationX());
+ epicenterY = Math.round(loc[1] + (sceneRoot.getHeight() / 2)
+ + sceneRoot.getTranslationY());
+ }
+ float distance = distance(viewCenterX, viewCenterY, epicenterX, epicenterY);
+ float maxDistance = distance(0, 0, sceneRoot.getWidth(), sceneRoot.getHeight());
+ float distanceFraction = distance / maxDistance;
+
+ long duration = transition.getDuration();
+ if (duration < 0) {
+ duration = 300;
+ }
+
+ return Math.round(duration * directionMultiplier / mPropagationSpeed * distanceFraction);
+ }
+
+ private static float distance(float x1, float y1, float x2, float y2) {
+ float x = x2 - x1;
+ float y = y2 - y1;
+ return (float) Math.sqrt((x * x) + (y * y));
+ }
+
+}
diff --git a/androidx/transition/Explode.java b/androidx/transition/Explode.java
new file mode 100644
index 0000000..1dc6b3a
--- /dev/null
+++ b/androidx/transition/Explode.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.animation.Animator;
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+
+import androidx.annotation.NonNull;
+
+/**
+ * This transition tracks changes to the visibility of target views in the
+ * start and end scenes and moves views in or out from the edges of the
+ * scene. Visibility is determined by both the
+ * {@link View#setVisibility(int)} state of the view as well as whether it
+ * is parented in the current view hierarchy. Disappearing Views are
+ * limited as described in {@link Visibility#onDisappear(android.view.ViewGroup,
+ * TransitionValues, int, TransitionValues, int)}.
+ * <p>Views move away from the focal View or the center of the Scene if
+ * no epicenter was provided.</p>
+ */
+public class Explode extends Visibility {
+
+ private static final TimeInterpolator sDecelerate = new DecelerateInterpolator();
+ private static final TimeInterpolator sAccelerate = new AccelerateInterpolator();
+ private static final String PROPNAME_SCREEN_BOUNDS = "android:explode:screenBounds";
+
+ private int[] mTempLoc = new int[2];
+
+ public Explode() {
+ setPropagation(new CircularPropagation());
+ }
+
+ public Explode(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setPropagation(new CircularPropagation());
+ }
+
+ private void captureValues(TransitionValues transitionValues) {
+ View view = transitionValues.view;
+ view.getLocationOnScreen(mTempLoc);
+ int left = mTempLoc[0];
+ int top = mTempLoc[1];
+ int right = left + view.getWidth();
+ int bottom = top + view.getHeight();
+ transitionValues.values.put(PROPNAME_SCREEN_BOUNDS, new Rect(left, top, right, bottom));
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ super.captureStartValues(transitionValues);
+ captureValues(transitionValues);
+ }
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ super.captureEndValues(transitionValues);
+ captureValues(transitionValues);
+ }
+
+ @Override
+ public Animator onAppear(ViewGroup sceneRoot, View view,
+ TransitionValues startValues, TransitionValues endValues) {
+ if (endValues == null) {
+ return null;
+ }
+ Rect bounds = (Rect) endValues.values.get(PROPNAME_SCREEN_BOUNDS);
+ float endX = view.getTranslationX();
+ float endY = view.getTranslationY();
+ calculateOut(sceneRoot, bounds, mTempLoc);
+ float startX = endX + mTempLoc[0];
+ float startY = endY + mTempLoc[1];
+
+ return TranslationAnimationCreator.createAnimation(view, endValues, bounds.left, bounds.top,
+ startX, startY, endX, endY, sDecelerate);
+ }
+
+ @Override
+ public Animator onDisappear(ViewGroup sceneRoot, View view,
+ TransitionValues startValues, TransitionValues endValues) {
+ if (startValues == null) {
+ return null;
+ }
+ Rect bounds = (Rect) startValues.values.get(PROPNAME_SCREEN_BOUNDS);
+ int viewPosX = bounds.left;
+ int viewPosY = bounds.top;
+ float startX = view.getTranslationX();
+ float startY = view.getTranslationY();
+ float endX = startX;
+ float endY = startY;
+ int[] interruptedPosition = (int[]) startValues.view.getTag(R.id.transition_position);
+ if (interruptedPosition != null) {
+ // We want to have the end position relative to the interrupted position, not
+ // the position it was supposed to start at.
+ endX += interruptedPosition[0] - bounds.left;
+ endY += interruptedPosition[1] - bounds.top;
+ bounds.offsetTo(interruptedPosition[0], interruptedPosition[1]);
+ }
+ calculateOut(sceneRoot, bounds, mTempLoc);
+ endX += mTempLoc[0];
+ endY += mTempLoc[1];
+
+ return TranslationAnimationCreator.createAnimation(view, startValues,
+ viewPosX, viewPosY, startX, startY, endX, endY, sAccelerate);
+ }
+
+ private void calculateOut(View sceneRoot, Rect bounds, int[] outVector) {
+ sceneRoot.getLocationOnScreen(mTempLoc);
+ int sceneRootX = mTempLoc[0];
+ int sceneRootY = mTempLoc[1];
+ int focalX;
+ int focalY;
+
+ Rect epicenter = getEpicenter();
+ if (epicenter == null) {
+ focalX = sceneRootX + (sceneRoot.getWidth() / 2)
+ + Math.round(sceneRoot.getTranslationX());
+ focalY = sceneRootY + (sceneRoot.getHeight() / 2)
+ + Math.round(sceneRoot.getTranslationY());
+ } else {
+ focalX = epicenter.centerX();
+ focalY = epicenter.centerY();
+ }
+
+ int centerX = bounds.centerX();
+ int centerY = bounds.centerY();
+ float xVector = centerX - focalX;
+ float yVector = centerY - focalY;
+
+ if (xVector == 0 && yVector == 0) {
+ // Random direction when View is centered on focal View.
+ xVector = (float) (Math.random() * 2) - 1;
+ yVector = (float) (Math.random() * 2) - 1;
+ }
+ float vectorSize = calculateDistance(xVector, yVector);
+ xVector /= vectorSize;
+ yVector /= vectorSize;
+
+ float maxDistance =
+ calculateMaxDistance(sceneRoot, focalX - sceneRootX, focalY - sceneRootY);
+
+ outVector[0] = Math.round(maxDistance * xVector);
+ outVector[1] = Math.round(maxDistance * yVector);
+ }
+
+ private static float calculateMaxDistance(View sceneRoot, int focalX, int focalY) {
+ int maxX = Math.max(focalX, sceneRoot.getWidth() - focalX);
+ int maxY = Math.max(focalY, sceneRoot.getHeight() - focalY);
+ return calculateDistance(maxX, maxY);
+ }
+
+ private static float calculateDistance(float x, float y) {
+ return (float) Math.sqrt((x * x) + (y * y));
+ }
+
+}
diff --git a/androidx/transition/ExplodeTest.java b/androidx/transition/ExplodeTest.java
new file mode 100644
index 0000000..330d0bc
--- /dev/null
+++ b/androidx/transition/ExplodeTest.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.lessThan;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.view.View;
+
+import androidx.transition.test.R;
+
+import org.junit.Test;
+
+@MediumTest
+public class ExplodeTest extends BaseTransitionTest {
+
+ @Override
+ Transition createTransition() {
+ return new Explode();
+ }
+
+ @Test
+ public void testExplode() throws Throwable {
+ enterScene(R.layout.scene10);
+ final View redSquare = rule.getActivity().findViewById(R.id.redSquare);
+ final View greenSquare = rule.getActivity().findViewById(R.id.greenSquare);
+ final View blueSquare = rule.getActivity().findViewById(R.id.blueSquare);
+ final View yellowSquare = rule.getActivity().findViewById(R.id.yellowSquare);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, mTransition);
+ redSquare.setVisibility(View.INVISIBLE);
+ greenSquare.setVisibility(View.INVISIBLE);
+ blueSquare.setVisibility(View.INVISIBLE);
+ yellowSquare.setVisibility(View.INVISIBLE);
+ }
+ });
+ waitForStart();
+ verify(mListener, never()).onTransitionEnd(any(Transition.class));
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(View.VISIBLE, greenSquare.getVisibility());
+ assertEquals(View.VISIBLE, blueSquare.getVisibility());
+ assertEquals(View.VISIBLE, yellowSquare.getVisibility());
+ float redStartX = redSquare.getTranslationX();
+ float redStartY = redSquare.getTranslationY();
+
+ SystemClock.sleep(100);
+ verifyTranslation(redSquare, true, true);
+ verifyTranslation(greenSquare, false, true);
+ verifyTranslation(blueSquare, false, false);
+ verifyTranslation(yellowSquare, true, false);
+ assertThat(redStartX, is(greaterThan(redSquare.getTranslationX()))); // moving left
+ assertThat(redStartY, is(greaterThan(redSquare.getTranslationY()))); // moving up
+ waitForEnd();
+
+ verifyNoTranslation(redSquare);
+ verifyNoTranslation(greenSquare);
+ verifyNoTranslation(blueSquare);
+ verifyNoTranslation(yellowSquare);
+ assertEquals(View.INVISIBLE, redSquare.getVisibility());
+ assertEquals(View.INVISIBLE, greenSquare.getVisibility());
+ assertEquals(View.INVISIBLE, blueSquare.getVisibility());
+ assertEquals(View.INVISIBLE, yellowSquare.getVisibility());
+ }
+
+ @Test
+ public void testImplode() throws Throwable {
+ enterScene(R.layout.scene10);
+ final View redSquare = rule.getActivity().findViewById(R.id.redSquare);
+ final View greenSquare = rule.getActivity().findViewById(R.id.greenSquare);
+ final View blueSquare = rule.getActivity().findViewById(R.id.blueSquare);
+ final View yellowSquare = rule.getActivity().findViewById(R.id.yellowSquare);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ redSquare.setVisibility(View.INVISIBLE);
+ greenSquare.setVisibility(View.INVISIBLE);
+ blueSquare.setVisibility(View.INVISIBLE);
+ yellowSquare.setVisibility(View.INVISIBLE);
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, mTransition);
+ redSquare.setVisibility(View.VISIBLE);
+ greenSquare.setVisibility(View.VISIBLE);
+ blueSquare.setVisibility(View.VISIBLE);
+ yellowSquare.setVisibility(View.VISIBLE);
+ }
+ });
+ waitForStart();
+
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(View.VISIBLE, greenSquare.getVisibility());
+ assertEquals(View.VISIBLE, blueSquare.getVisibility());
+ assertEquals(View.VISIBLE, yellowSquare.getVisibility());
+ float redStartX = redSquare.getTranslationX();
+ float redStartY = redSquare.getTranslationY();
+
+ SystemClock.sleep(100);
+ verifyTranslation(redSquare, true, true);
+ verifyTranslation(greenSquare, false, true);
+ verifyTranslation(blueSquare, false, false);
+ verifyTranslation(yellowSquare, true, false);
+ assertThat(redStartX, is(lessThan(redSquare.getTranslationX()))); // moving right
+ assertThat(redStartY, is(lessThan(redSquare.getTranslationY()))); // moving down
+ waitForEnd();
+
+ verifyNoTranslation(redSquare);
+ verifyNoTranslation(greenSquare);
+ verifyNoTranslation(blueSquare);
+ verifyNoTranslation(yellowSquare);
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(View.VISIBLE, greenSquare.getVisibility());
+ assertEquals(View.VISIBLE, blueSquare.getVisibility());
+ assertEquals(View.VISIBLE, yellowSquare.getVisibility());
+ }
+
+ private void verifyTranslation(View view, boolean goLeft, boolean goUp) {
+ float translationX = view.getTranslationX();
+ float translationY = view.getTranslationY();
+
+ if (goLeft) {
+ assertThat(translationX, is(lessThan(0.f)));
+ } else {
+ assertThat(translationX, is(greaterThan(0.f)));
+ }
+
+ if (goUp) {
+ assertThat(translationY, is(lessThan(0.f)));
+ } else {
+ assertThat(translationY, is(greaterThan(0.f)));
+ }
+ }
+
+ private void verifyNoTranslation(View view) {
+ assertEquals(0f, view.getTranslationX(), 0.0f);
+ assertEquals(0f, view.getTranslationY(), 0.0f);
+ }
+
+}
diff --git a/androidx/transition/Fade.java b/androidx/transition/Fade.java
new file mode 100644
index 0000000..ab484f5
--- /dev/null
+++ b/androidx/transition/Fade.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.res.TypedArrayUtils;
+import androidx.core.view.ViewCompat;
+
+/**
+ * This transition tracks changes to the visibility of target views in the
+ * start and end scenes and fades views in or out when they become visible
+ * or non-visible. Visibility is determined by both the
+ * {@link View#setVisibility(int)} state of the view as well as whether it
+ * is parented in the current view hierarchy.
+ *
+ * <p>The ability of this transition to fade out a particular view, and the
+ * way that that fading operation takes place, is based on
+ * the situation of the view in the view hierarchy. For example, if a view was
+ * simply removed from its parent, then the view will be added into a {@link
+ * android.view.ViewGroupOverlay} while fading. If a visible view is
+ * changed to be {@link View#GONE} or {@link View#INVISIBLE}, then the
+ * visibility will be changed to {@link View#VISIBLE} for the duration of
+ * the animation. However, if a view is in a hierarchy which is also altering
+ * its visibility, the situation can be more complicated. In general, if a
+ * view that is no longer in the hierarchy in the end scene still has a
+ * parent (so its parent hierarchy was removed, but it was not removed from
+ * its parent), then it will be left alone to avoid side-effects from
+ * improperly removing it from its parent. The only exception to this is if
+ * the previous {@link Scene} was
+ * {@link Scene#getSceneForLayout(android.view.ViewGroup, int, android.content.Context)
+ * created from a layout resource file}, then it is considered safe to un-parent
+ * the starting scene view in order to fade it out.</p>
+ *
+ * <p>A Fade transition can be described in a resource file by using the
+ * tag <code>fade</code>, along with the standard
+ * attributes of {@code Fade} and {@link Transition}.</p>
+ */
+public class Fade extends Visibility {
+
+ private static final String PROPNAME_TRANSITION_ALPHA = "android:fade:transitionAlpha";
+
+ private static final String LOG_TAG = "Fade";
+
+ /**
+ * Fading mode used in {@link #Fade(int)} to make the transition
+ * operate on targets that are appearing. Maybe be combined with
+ * {@link #OUT} to fade both in and out.
+ */
+ public static final int IN = Visibility.MODE_IN;
+
+ /**
+ * Fading mode used in {@link #Fade(int)} to make the transition
+ * operate on targets that are disappearing. Maybe be combined with
+ * {@link #IN} to fade both in and out.
+ */
+ public static final int OUT = Visibility.MODE_OUT;
+
+ /**
+ * Constructs a Fade transition that will fade targets in
+ * and/or out, according to the value of fadingMode.
+ *
+ * @param fadingMode The behavior of this transition, a combination of
+ * {@link #IN} and {@link #OUT}.
+ */
+ public Fade(int fadingMode) {
+ setMode(fadingMode);
+ }
+
+ /**
+ * Constructs a Fade transition that will fade targets in and out.
+ */
+ public Fade() {
+ }
+
+ public Fade(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = context.obtainStyledAttributes(attrs, Styleable.FADE);
+ @Mode
+ int fadingMode = TypedArrayUtils.getNamedInt(a, (XmlResourceParser) attrs, "fadingMode",
+ Styleable.Fade.FADING_MODE, getMode());
+ setMode(fadingMode);
+ a.recycle();
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ super.captureStartValues(transitionValues);
+ transitionValues.values.put(PROPNAME_TRANSITION_ALPHA,
+ ViewUtils.getTransitionAlpha(transitionValues.view));
+ }
+
+ /**
+ * Utility method to handle creating and running the Animator.
+ */
+ private Animator createAnimation(final View view, float startAlpha, float endAlpha) {
+ if (startAlpha == endAlpha) {
+ return null;
+ }
+ ViewUtils.setTransitionAlpha(view, startAlpha);
+ final ObjectAnimator anim = ObjectAnimator.ofFloat(view, ViewUtils.TRANSITION_ALPHA,
+ endAlpha);
+ if (DBG) {
+ Log.d(LOG_TAG, "Created animator " + anim);
+ }
+ FadeAnimatorListener listener = new FadeAnimatorListener(view);
+ anim.addListener(listener);
+ addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ ViewUtils.setTransitionAlpha(view, 1);
+ ViewUtils.clearNonTransitionAlpha(view);
+ transition.removeListener(this);
+ }
+ });
+ return anim;
+ }
+
+ @Override
+ public Animator onAppear(ViewGroup sceneRoot, View view,
+ TransitionValues startValues,
+ TransitionValues endValues) {
+ if (DBG) {
+ View startView = (startValues != null) ? startValues.view : null;
+ Log.d(LOG_TAG, "Fade.onAppear: startView, startVis, endView, endVis = "
+ + startView + ", " + view);
+ }
+ float startAlpha = getStartAlpha(startValues, 0);
+ if (startAlpha == 1) {
+ startAlpha = 0;
+ }
+ return createAnimation(view, startAlpha, 1);
+ }
+
+ @Override
+ public Animator onDisappear(ViewGroup sceneRoot, final View view, TransitionValues startValues,
+ TransitionValues endValues) {
+ ViewUtils.saveNonTransitionAlpha(view);
+ float startAlpha = getStartAlpha(startValues, 1);
+ return createAnimation(view, startAlpha, 0);
+ }
+
+ private static float getStartAlpha(TransitionValues startValues, float fallbackValue) {
+ float startAlpha = fallbackValue;
+ if (startValues != null) {
+ Float startAlphaFloat = (Float) startValues.values.get(PROPNAME_TRANSITION_ALPHA);
+ if (startAlphaFloat != null) {
+ startAlpha = startAlphaFloat;
+ }
+ }
+ return startAlpha;
+ }
+
+ private static class FadeAnimatorListener extends AnimatorListenerAdapter {
+
+ private final View mView;
+ private boolean mLayerTypeChanged = false;
+
+ FadeAnimatorListener(View view) {
+ mView = view;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (ViewCompat.hasOverlappingRendering(mView)
+ && mView.getLayerType() == View.LAYER_TYPE_NONE) {
+ mLayerTypeChanged = true;
+ mView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ ViewUtils.setTransitionAlpha(mView, 1);
+ if (mLayerTypeChanged) {
+ mView.setLayerType(View.LAYER_TYPE_NONE, null);
+ }
+ }
+
+ }
+
+}
diff --git a/androidx/transition/FadeTest.java b/androidx/transition/FadeTest.java
new file mode 100644
index 0000000..af507be
--- /dev/null
+++ b/androidx/transition/FadeTest.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.lessThan;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.os.Build;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@MediumTest
+public class FadeTest extends BaseTest {
+
+ private View mView;
+ private ViewGroup mRoot;
+
+ @UiThreadTest
+ @Before
+ public void setUp() {
+ mRoot = rule.getActivity().getRoot();
+ mView = new View(rule.getActivity());
+ mRoot.addView(mView, new ViewGroup.LayoutParams(100, 100));
+ }
+
+ @Test
+ public void testMode() {
+ assertThat(Fade.IN, is(Visibility.MODE_IN));
+ assertThat(Fade.OUT, is(Visibility.MODE_OUT));
+ final Fade fade = new Fade();
+ assertThat(fade.getMode(), is(Visibility.MODE_IN | Visibility.MODE_OUT));
+ fade.setMode(Visibility.MODE_IN);
+ assertThat(fade.getMode(), is(Visibility.MODE_IN));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testDisappear() {
+ final Fade fade = new Fade();
+ final TransitionValues startValues = new TransitionValues();
+ startValues.view = mView;
+ fade.captureStartValues(startValues);
+ mView.setVisibility(View.INVISIBLE);
+ final TransitionValues endValues = new TransitionValues();
+ endValues.view = mView;
+ fade.captureEndValues(endValues);
+ Animator animator = fade.createAnimator(mRoot, startValues, endValues);
+ assertThat(animator, is(notNullValue()));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testAppear() {
+ mView.setVisibility(View.INVISIBLE);
+ final Fade fade = new Fade();
+ final TransitionValues startValues = new TransitionValues();
+ startValues.view = mView;
+ fade.captureStartValues(startValues);
+ mView.setVisibility(View.VISIBLE);
+ final TransitionValues endValues = new TransitionValues();
+ endValues.view = mView;
+ fade.captureEndValues(endValues);
+ Animator animator = fade.createAnimator(mRoot, startValues, endValues);
+ assertThat(animator, is(notNullValue()));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testNoChange() {
+ final Fade fade = new Fade();
+ final TransitionValues startValues = new TransitionValues();
+ startValues.view = mView;
+ fade.captureStartValues(startValues);
+ final TransitionValues endValues = new TransitionValues();
+ endValues.view = mView;
+ fade.captureEndValues(endValues);
+ Animator animator = fade.createAnimator(mRoot, startValues, endValues);
+ // No visibility change; no animation should happen
+ assertThat(animator, is(nullValue()));
+ }
+
+ @Test
+ public void testFadeOutThenIn() throws Throwable {
+ // Fade out
+ final Runnable interrupt = mock(Runnable.class);
+ float[] valuesOut = new float[2];
+ final InterruptibleFade fadeOut = new InterruptibleFade(Fade.MODE_OUT, interrupt,
+ valuesOut);
+ final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class);
+ fadeOut.addListener(listenerOut);
+ changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE);
+ verify(listenerOut, timeout(3000)).onTransitionStart(any(Transition.class));
+
+ // The view is in the middle of fading out
+ verify(interrupt, timeout(3000)).run();
+
+ // Fade in
+ float[] valuesIn = new float[2];
+ final InterruptibleFade fadeIn = new InterruptibleFade(Fade.MODE_IN, null, valuesIn);
+ final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class);
+ fadeIn.addListener(listenerIn);
+ changeVisibility(fadeIn, mRoot, mView, View.VISIBLE);
+ verify(listenerOut, timeout(3000)).onTransitionPause(any(Transition.class));
+ verify(listenerIn, timeout(3000)).onTransitionStart(any(Transition.class));
+ assertThat(valuesOut[1], allOf(greaterThan(0f), lessThan(1f)));
+ if (Build.VERSION.SDK_INT >= 19 && fadeOut.mInitialAlpha >= 0) {
+ // These won't match on API levels 18 and below due to lack of Animator pause.
+ assertEquals(valuesOut[1], valuesIn[0], 0.01f);
+ }
+
+ verify(listenerIn, timeout(3000)).onTransitionEnd(any(Transition.class));
+ assertThat(mView.getVisibility(), is(View.VISIBLE));
+ assertEquals(valuesIn[1], 1.f, 0.01f);
+ }
+
+ @Test
+ public void testFadeInThenOut() throws Throwable {
+ changeVisibility(null, mRoot, mView, View.INVISIBLE);
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ // Fade in
+ final Runnable interrupt = mock(Runnable.class);
+ float[] valuesIn = new float[2];
+ final InterruptibleFade fadeIn = new InterruptibleFade(Fade.MODE_IN, interrupt, valuesIn);
+ final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class);
+ fadeIn.addListener(listenerIn);
+ changeVisibility(fadeIn, mRoot, mView, View.VISIBLE);
+ verify(listenerIn, timeout(3000)).onTransitionStart(any(Transition.class));
+
+ // The view is in the middle of fading in
+ verify(interrupt, timeout(3000)).run();
+
+ // Fade out
+ float[] valuesOut = new float[2];
+ final InterruptibleFade fadeOut = new InterruptibleFade(Fade.MODE_OUT, null, valuesOut);
+ final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class);
+ fadeOut.addListener(listenerOut);
+ changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE);
+ verify(listenerIn, timeout(3000)).onTransitionPause(any(Transition.class));
+ verify(listenerOut, timeout(3000)).onTransitionStart(any(Transition.class));
+ assertThat(valuesIn[1], allOf(greaterThan(0f), lessThan(1f)));
+ if (Build.VERSION.SDK_INT >= 19 && fadeIn.mInitialAlpha >= 0) {
+ // These won't match on API levels 18 and below due to lack of Animator pause.
+ assertEquals(valuesIn[1], valuesOut[0], 0.01f);
+ }
+
+ verify(listenerOut, timeout(3000)).onTransitionEnd(any(Transition.class));
+ assertThat(mView.getVisibility(), is(View.INVISIBLE));
+ }
+
+ @Test
+ public void testFadeWithAlpha() throws Throwable {
+ // Set the view alpha to 0.5
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mView.setAlpha(0.5f);
+ }
+ });
+ // Fade out
+ final Fade fadeOut = new Fade(Fade.OUT);
+ final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class);
+ fadeOut.addListener(listenerOut);
+ changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE);
+ verify(listenerOut, timeout(3000)).onTransitionStart(any(Transition.class));
+ verify(listenerOut, timeout(3000)).onTransitionEnd(any(Transition.class));
+ // Fade in
+ final Fade fadeIn = new Fade(Fade.IN);
+ final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class);
+ fadeIn.addListener(listenerIn);
+ changeVisibility(fadeIn, mRoot, mView, View.VISIBLE);
+ verify(listenerIn, timeout(3000)).onTransitionStart(any(Transition.class));
+ verify(listenerIn, timeout(3000)).onTransitionEnd(any(Transition.class));
+ // Confirm that the view still has the original alpha value
+ assertThat(mView.getVisibility(), is(View.VISIBLE));
+ assertEquals(0.5f, mView.getAlpha(), 0.01f);
+ }
+
+ private void changeVisibility(final Fade fade, final ViewGroup container, final View target,
+ final int visibility) throws Throwable {
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (fade != null) {
+ TransitionManager.beginDelayedTransition(container, fade);
+ }
+ target.setVisibility(visibility);
+ }
+ });
+ }
+
+ /**
+ * A special version of {@link Fade} that runs a specified {@link Runnable} soon after the
+ * target starts fading in or out.
+ */
+ private static class InterruptibleFade extends Fade {
+
+ static final float ALPHA_THRESHOLD = 0.2f;
+
+ float mInitialAlpha = -1;
+ Runnable mMiddle;
+ final float[] mAlphaValues;
+
+ InterruptibleFade(int mode, Runnable middle, float[] alphaValues) {
+ super(mode);
+ mMiddle = middle;
+ mAlphaValues = alphaValues;
+ }
+
+ @Nullable
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot,
+ @Nullable final TransitionValues startValues,
+ @Nullable final TransitionValues endValues) {
+ final Animator animator = super.createAnimator(sceneRoot, startValues, endValues);
+ if (animator instanceof ObjectAnimator) {
+ ((ObjectAnimator) animator).addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final float alpha = (float) animation.getAnimatedValue();
+ mAlphaValues[1] = alpha;
+ if (mInitialAlpha < 0) {
+ mInitialAlpha = alpha;
+ mAlphaValues[0] = mInitialAlpha;
+ } else if (Math.abs(alpha - mInitialAlpha) > ALPHA_THRESHOLD) {
+ if (mMiddle != null) {
+ mMiddle.run();
+ mMiddle = null;
+ }
+ }
+ }
+ });
+ }
+ return animator;
+ }
+
+ }
+
+}
diff --git a/androidx/transition/FloatArrayEvaluator.java b/androidx/transition/FloatArrayEvaluator.java
new file mode 100644
index 0000000..9947921
--- /dev/null
+++ b/androidx/transition/FloatArrayEvaluator.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.animation.TypeEvaluator;
+
+/**
+ * This evaluator can be used to perform type interpolation between <code>float[]</code> values.
+ * Each index into the array is treated as a separate value to interpolate. For example,
+ * evaluating <code>{100, 200}</code> and <code>{300, 400}</code> will interpolate the value at
+ * the first index between 100 and 300 and the value at the second index value between 200 and 400.
+ */
+class FloatArrayEvaluator implements TypeEvaluator<float[]> {
+
+ private float[] mArray;
+
+ /**
+ * Create a FloatArrayEvaluator that reuses <code>reuseArray</code> for every evaluate() call.
+ * Caution must be taken to ensure that the value returned from
+ * {@link android.animation.ValueAnimator#getAnimatedValue()} is not cached, modified, or
+ * used across threads. The value will be modified on each <code>evaluate()</code> call.
+ *
+ * @param reuseArray The array to modify and return from <code>evaluate</code>.
+ */
+ FloatArrayEvaluator(float[] reuseArray) {
+ mArray = reuseArray;
+ }
+
+ /**
+ * Interpolates the value at each index by the fraction. If
+ * {@link #FloatArrayEvaluator(float[])} was used to construct this object,
+ * <code>reuseArray</code> will be returned, otherwise a new <code>float[]</code>
+ * will be returned.
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue The start value.
+ * @param endValue The end value.
+ * @return A <code>float[]</code> where each element is an interpolation between
+ * the same index in startValue and endValue.
+ */
+ @Override
+ public float[] evaluate(float fraction, float[] startValue, float[] endValue) {
+ float[] array = mArray;
+ if (array == null) {
+ array = new float[startValue.length];
+ }
+
+ for (int i = 0; i < array.length; i++) {
+ float start = startValue[i];
+ float end = endValue[i];
+ array[i] = start + (fraction * (end - start));
+ }
+ return array;
+ }
+
+}
diff --git a/androidx/transition/FragmentTransitionSupport.java b/androidx/transition/FragmentTransitionSupport.java
new file mode 100644
index 0000000..9e72ff3
--- /dev/null
+++ b/androidx/transition/FragmentTransitionSupport.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.fragment.app.FragmentTransitionImpl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * @hide
+ */
+// This is instantiated in androidx.fragment.app.FragmentTransition
+@SuppressWarnings("unused")
+@RestrictTo(LIBRARY_GROUP)
+public class FragmentTransitionSupport extends FragmentTransitionImpl {
+
+ @Override
+ public boolean canHandle(Object transition) {
+ return transition instanceof Transition;
+ }
+
+ @Override
+ public Object cloneTransition(Object transition) {
+ Transition copy = null;
+ if (transition != null) {
+ copy = ((Transition) transition).clone();
+ }
+ return copy;
+ }
+
+ @Override
+ public Object wrapTransitionInSet(Object transition) {
+ if (transition == null) {
+ return null;
+ }
+ TransitionSet transitionSet = new TransitionSet();
+ transitionSet.addTransition((Transition) transition);
+ return transitionSet;
+ }
+
+ @Override
+ public void setSharedElementTargets(Object transitionObj,
+ View nonExistentView, ArrayList<View> sharedViews) {
+ TransitionSet transition = (TransitionSet) transitionObj;
+ final List<View> views = transition.getTargets();
+ views.clear();
+ final int count = sharedViews.size();
+ for (int i = 0; i < count; i++) {
+ final View view = sharedViews.get(i);
+ bfsAddViewChildren(views, view);
+ }
+ views.add(nonExistentView);
+ sharedViews.add(nonExistentView);
+ addTargets(transition, sharedViews);
+ }
+
+ @Override
+ public void setEpicenter(Object transitionObj, View view) {
+ if (view != null) {
+ Transition transition = (Transition) transitionObj;
+ final Rect epicenter = new Rect();
+ getBoundsOnScreen(view, epicenter);
+
+ transition.setEpicenterCallback(new Transition.EpicenterCallback() {
+ @Override
+ public Rect onGetEpicenter(@NonNull Transition transition) {
+ return epicenter;
+ }
+ });
+ }
+ }
+
+ @Override
+ public void addTargets(Object transitionObj, ArrayList<View> views) {
+ Transition transition = (Transition) transitionObj;
+ if (transition == null) {
+ return;
+ }
+ if (transition instanceof TransitionSet) {
+ TransitionSet set = (TransitionSet) transition;
+ int numTransitions = set.getTransitionCount();
+ for (int i = 0; i < numTransitions; i++) {
+ Transition child = set.getTransitionAt(i);
+ addTargets(child, views);
+ }
+ } else if (!hasSimpleTarget(transition)) {
+ List<View> targets = transition.getTargets();
+ if (isNullOrEmpty(targets)) {
+ // We can just add the target views
+ int numViews = views.size();
+ for (int i = 0; i < numViews; i++) {
+ transition.addTarget(views.get(i));
+ }
+ }
+ }
+ }
+
+ private static boolean hasSimpleTarget(Transition transition) {
+ return !isNullOrEmpty(transition.getTargetIds())
+ || !isNullOrEmpty(transition.getTargetNames())
+ || !isNullOrEmpty(transition.getTargetTypes());
+ }
+
+ @Override
+ public Object mergeTransitionsTogether(Object transition1, Object transition2,
+ Object transition3) {
+ TransitionSet transitionSet = new TransitionSet();
+ if (transition1 != null) {
+ transitionSet.addTransition((Transition) transition1);
+ }
+ if (transition2 != null) {
+ transitionSet.addTransition((Transition) transition2);
+ }
+ if (transition3 != null) {
+ transitionSet.addTransition((Transition) transition3);
+ }
+ return transitionSet;
+ }
+
+ @Override
+ public void scheduleHideFragmentView(Object exitTransitionObj, final View fragmentView,
+ final ArrayList<View> exitingViews) {
+ Transition exitTransition = (Transition) exitTransitionObj;
+ exitTransition.addListener(new Transition.TransitionListener() {
+ @Override
+ public void onTransitionStart(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ transition.removeListener(this);
+ fragmentView.setVisibility(View.GONE);
+ final int numViews = exitingViews.size();
+ for (int i = 0; i < numViews; i++) {
+ exitingViews.get(i).setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ }
+ });
+ }
+
+ @Override
+ public Object mergeTransitionsInSequence(Object exitTransitionObj,
+ Object enterTransitionObj, Object sharedElementTransitionObj) {
+ // First do exit, then enter, but allow shared element transition to happen
+ // during both.
+ Transition staggered = null;
+ final Transition exitTransition = (Transition) exitTransitionObj;
+ final Transition enterTransition = (Transition) enterTransitionObj;
+ final Transition sharedElementTransition = (Transition) sharedElementTransitionObj;
+ if (exitTransition != null && enterTransition != null) {
+ staggered = new TransitionSet()
+ .addTransition(exitTransition)
+ .addTransition(enterTransition)
+ .setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+ } else if (exitTransition != null) {
+ staggered = exitTransition;
+ } else if (enterTransition != null) {
+ staggered = enterTransition;
+ }
+ if (sharedElementTransition != null) {
+ TransitionSet together = new TransitionSet();
+ if (staggered != null) {
+ together.addTransition(staggered);
+ }
+ together.addTransition(sharedElementTransition);
+ return together;
+ } else {
+ return staggered;
+ }
+ }
+
+ @Override
+ public void beginDelayedTransition(ViewGroup sceneRoot, Object transition) {
+ TransitionManager.beginDelayedTransition(sceneRoot, (Transition) transition);
+ }
+
+ @Override
+ public void scheduleRemoveTargets(final Object overallTransitionObj,
+ final Object enterTransition, final ArrayList<View> enteringViews,
+ final Object exitTransition, final ArrayList<View> exitingViews,
+ final Object sharedElementTransition, final ArrayList<View> sharedElementsIn) {
+ final Transition overallTransition = (Transition) overallTransitionObj;
+ overallTransition.addListener(new Transition.TransitionListener() {
+ @Override
+ public void onTransitionStart(@NonNull Transition transition) {
+ if (enterTransition != null) {
+ replaceTargets(enterTransition, enteringViews, null);
+ }
+ if (exitTransition != null) {
+ replaceTargets(exitTransition, exitingViews, null);
+ }
+ if (sharedElementTransition != null) {
+ replaceTargets(sharedElementTransition, sharedElementsIn, null);
+ }
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ }
+ });
+ }
+
+ @Override
+ public void swapSharedElementTargets(Object sharedElementTransitionObj,
+ ArrayList<View> sharedElementsOut, ArrayList<View> sharedElementsIn) {
+ TransitionSet sharedElementTransition = (TransitionSet) sharedElementTransitionObj;
+ if (sharedElementTransition != null) {
+ sharedElementTransition.getTargets().clear();
+ sharedElementTransition.getTargets().addAll(sharedElementsIn);
+ replaceTargets(sharedElementTransition, sharedElementsOut, sharedElementsIn);
+ }
+ }
+
+ @Override
+ public void replaceTargets(Object transitionObj, ArrayList<View> oldTargets,
+ ArrayList<View> newTargets) {
+ Transition transition = (Transition) transitionObj;
+ if (transition instanceof TransitionSet) {
+ TransitionSet set = (TransitionSet) transition;
+ int numTransitions = set.getTransitionCount();
+ for (int i = 0; i < numTransitions; i++) {
+ Transition child = set.getTransitionAt(i);
+ replaceTargets(child, oldTargets, newTargets);
+ }
+ } else if (!hasSimpleTarget(transition)) {
+ List<View> targets = transition.getTargets();
+ if (targets.size() == oldTargets.size()
+ && targets.containsAll(oldTargets)) {
+ // We have an exact match. We must have added these earlier in addTargets
+ final int targetCount = newTargets == null ? 0 : newTargets.size();
+ for (int i = 0; i < targetCount; i++) {
+ transition.addTarget(newTargets.get(i));
+ }
+ for (int i = oldTargets.size() - 1; i >= 0; i--) {
+ transition.removeTarget(oldTargets.get(i));
+ }
+ }
+ }
+ }
+
+ @Override
+ public void addTarget(Object transitionObj, View view) {
+ if (transitionObj != null) {
+ Transition transition = (Transition) transitionObj;
+ transition.addTarget(view);
+ }
+ }
+
+ @Override
+ public void removeTarget(Object transitionObj, View view) {
+ if (transitionObj != null) {
+ Transition transition = (Transition) transitionObj;
+ transition.removeTarget(view);
+ }
+ }
+
+ @Override
+ public void setEpicenter(Object transitionObj, final Rect epicenter) {
+ if (transitionObj != null) {
+ Transition transition = (Transition) transitionObj;
+ transition.setEpicenterCallback(new Transition.EpicenterCallback() {
+ @Override
+ public Rect onGetEpicenter(@NonNull Transition transition) {
+ if (epicenter == null || epicenter.isEmpty()) {
+ return null;
+ }
+ return epicenter;
+ }
+ });
+ }
+ }
+
+}
diff --git a/androidx/transition/FragmentTransitionTest.java b/androidx/transition/FragmentTransitionTest.java
new file mode 100644
index 0000000..4ad47aa
--- /dev/null
+++ b/androidx/transition/FragmentTransitionTest.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.Nullable;
+import androidx.collection.SparseArrayCompat;
+import androidx.core.util.Pair;
+import androidx.core.view.ViewCompat;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.transition.test.R;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+
+@MediumTest
+@RunWith(Parameterized.class)
+public class FragmentTransitionTest extends BaseTest {
+
+ @Parameterized.Parameters
+ public static Object[] data() {
+ return new Boolean[]{
+ false, true
+ };
+ }
+
+ private final boolean mReorderingAllowed;
+
+ public FragmentTransitionTest(boolean reorderingAllowed) {
+ mReorderingAllowed = reorderingAllowed;
+ }
+
+ @Test
+ public void preconditions() {
+ final TransitionFragment fragment1 = TransitionFragment.newInstance(R.layout.scene2);
+ final TransitionFragment fragment2 = TransitionFragment.newInstance(R.layout.scene3);
+ showFragment(fragment1, false, null);
+ assertNull(fragment1.mRed);
+ assertNotNull(fragment1.mGreen);
+ assertNotNull(fragment1.mBlue);
+ showFragment(fragment2, true, new Pair<>(fragment1.mGreen, "green"));
+ assertNotNull(fragment2.mRed);
+ assertNotNull(fragment2.mGreen);
+ assertNotNull(fragment2.mBlue);
+ }
+
+ @Test
+ public void nonSharedTransition() {
+ final TransitionFragment fragment1 = TransitionFragment.newInstance(R.layout.scene2);
+ final TransitionFragment fragment2 = TransitionFragment.newInstance(R.layout.scene3);
+ showFragment(fragment1, false, null);
+ showFragment(fragment2, true, null);
+ verify(fragment1.mListeners.get(TransitionFragment.TRANSITION_EXIT))
+ .onTransitionStart(any(Transition.class));
+ verify(fragment1.mListeners.get(TransitionFragment.TRANSITION_EXIT), timeout(3000))
+ .onTransitionEnd(any(Transition.class));
+ verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_ENTER))
+ .onTransitionStart(any(Transition.class));
+ verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_ENTER), timeout(3000))
+ .onTransitionEnd(any(Transition.class));
+ popBackStack();
+ verify(fragment1.mListeners.get(TransitionFragment.TRANSITION_REENTER))
+ .onTransitionStart(any(Transition.class));
+ verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_RETURN))
+ .onTransitionStart(any(Transition.class));
+ }
+
+ @Test
+ public void sharedTransition() {
+ final TransitionFragment fragment1 = TransitionFragment.newInstance(R.layout.scene2);
+ final TransitionFragment fragment2 = TransitionFragment.newInstance(R.layout.scene3);
+ showFragment(fragment1, false, null);
+ showFragment(fragment2, true, new Pair<>(fragment1.mGreen, "green"));
+ verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_SHARED_ENTER))
+ .onTransitionStart(any(Transition.class));
+ verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_SHARED_ENTER), timeout(3000))
+ .onTransitionEnd(any(Transition.class));
+ popBackStack();
+ verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_SHARED_RETURN))
+ .onTransitionStart(any(Transition.class));
+ }
+
+ private void showFragment(final Fragment fragment, final boolean addToBackStack,
+ final Pair<View, String> sharedElement) {
+ final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ instrumentation.runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ final FragmentTransaction transaction = getFragmentManager().beginTransaction();
+ transaction.replace(R.id.root, fragment);
+ transaction.setReorderingAllowed(mReorderingAllowed);
+ if (sharedElement != null) {
+ transaction.addSharedElement(sharedElement.first, sharedElement.second);
+ }
+ if (addToBackStack) {
+ transaction.addToBackStack(null);
+ transaction.commit();
+ getFragmentManager().executePendingTransactions();
+ } else {
+ transaction.commitNow();
+ }
+ }
+ });
+ instrumentation.waitForIdleSync();
+ }
+
+ private void popBackStack() {
+ final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ instrumentation.runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ getFragmentManager().popBackStackImmediate();
+ }
+ });
+ instrumentation.waitForIdleSync();
+ }
+
+ private FragmentManager getFragmentManager() {
+ return rule.getActivity().getSupportFragmentManager();
+ }
+
+ /**
+ * A {@link Fragment} with all kinds of {@link Transition} with tracking listeners.
+ */
+ public static class TransitionFragment extends Fragment {
+
+ static final int TRANSITION_ENTER = 1;
+ static final int TRANSITION_EXIT = 2;
+ static final int TRANSITION_REENTER = 3;
+ static final int TRANSITION_RETURN = 4;
+ static final int TRANSITION_SHARED_ENTER = 5;
+ static final int TRANSITION_SHARED_RETURN = 6;
+
+ private static final String ARG_LAYOUT_ID = "layout_id";
+
+ View mRed;
+ View mGreen;
+ View mBlue;
+
+ SparseArrayCompat<Transition.TransitionListener> mListeners = new SparseArrayCompat<>();
+
+ public static TransitionFragment newInstance(@LayoutRes int layout) {
+ final Bundle args = new Bundle();
+ args.putInt(ARG_LAYOUT_ID, layout);
+ final TransitionFragment fragment = new TransitionFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ public TransitionFragment() {
+ setEnterTransition(createTransition(TRANSITION_ENTER));
+ setExitTransition(createTransition(TRANSITION_EXIT));
+ setReenterTransition(createTransition(TRANSITION_REENTER));
+ setReturnTransition(createTransition(TRANSITION_RETURN));
+ setSharedElementEnterTransition(createTransition(TRANSITION_SHARED_ENTER));
+ setSharedElementReturnTransition(createTransition(TRANSITION_SHARED_RETURN));
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(getArguments().getInt(ARG_LAYOUT_ID), container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ mRed = view.findViewById(R.id.redSquare);
+ mGreen = view.findViewById(R.id.greenSquare);
+ mBlue = view.findViewById(R.id.blueSquare);
+ if (mRed != null) {
+ ViewCompat.setTransitionName(mRed, "red");
+ }
+ if (mGreen != null) {
+ ViewCompat.setTransitionName(mGreen, "green");
+ }
+ if (mBlue != null) {
+ ViewCompat.setTransitionName(mBlue, "blue");
+ }
+ }
+
+ private Transition createTransition(int type) {
+ final Transition.TransitionListener listener = mock(
+ Transition.TransitionListener.class);
+ final AutoTransition transition = new AutoTransition();
+ transition.addListener(listener);
+ transition.setDuration(10);
+ mListeners.put(type, listener);
+ return transition;
+ }
+
+ }
+
+}
diff --git a/androidx/transition/GhostViewApi14.java b/androidx/transition/GhostViewApi14.java
new file mode 100644
index 0000000..fa577d9
--- /dev/null
+++ b/androidx/transition/GhostViewApi14.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.annotation.SuppressLint;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.core.view.ViewCompat;
+
+/**
+ * Backport of android.view.GhostView introduced in API level 21.
+ * <p>
+ * While the platform version uses ViewOverlay, this ghost view finds the closest FrameLayout in
+ * the hierarchy and adds itself there.
+ * <p>
+ * Since we cannot use RenderNode to delegate drawing, we instead use {@link View#draw(Canvas)} to
+ * draw the target view. We apply the same transformation matrix applied to the target view. For
+ * that, this view is sized as large as the parent FrameLayout (except padding) while the platform
+ * version becomes as large as the target view.
+ */
+@SuppressLint("ViewConstructor")
+class GhostViewApi14 extends View implements GhostViewImpl {
+
+ static GhostViewImpl addGhost(View view, ViewGroup viewGroup) {
+ GhostViewApi14 ghostView = getGhostView(view);
+ if (ghostView == null) {
+ FrameLayout frameLayout = findFrameLayout(viewGroup);
+ if (frameLayout == null) {
+ return null;
+ }
+ ghostView = new GhostViewApi14(view);
+ frameLayout.addView(ghostView);
+ }
+ ghostView.mReferences++;
+ return ghostView;
+ }
+
+ static void removeGhost(View view) {
+ GhostViewApi14 ghostView = getGhostView(view);
+ if (ghostView != null) {
+ ghostView.mReferences--;
+ if (ghostView.mReferences <= 0) {
+ ViewParent parent = ghostView.getParent();
+ if (parent instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) parent;
+ group.endViewTransition(ghostView);
+ group.removeView(ghostView);
+ }
+ }
+ }
+ }
+
+ /**
+ * Find the closest FrameLayout in the ascendant hierarchy from the specified {@code
+ * viewGroup}.
+ */
+ private static FrameLayout findFrameLayout(ViewGroup viewGroup) {
+ while (!(viewGroup instanceof FrameLayout)) {
+ ViewParent parent = viewGroup.getParent();
+ if (!(parent instanceof ViewGroup)) {
+ return null;
+ }
+ viewGroup = (ViewGroup) parent;
+ }
+ return (FrameLayout) viewGroup;
+ }
+
+ /** The target view */
+ final View mView;
+
+ /** The parent of the view that is disappearing at the beginning of the animation */
+ ViewGroup mStartParent;
+
+ /** The view that is disappearing at the beginning of the animation */
+ View mStartView;
+
+ /** The number of references to this ghost view */
+ int mReferences;
+
+ /** The horizontal distance from the ghost view to the target view */
+ private int mDeltaX;
+
+ /** The horizontal distance from the ghost view to the target view */
+ private int mDeltaY;
+
+ /** The current transformation matrix of the target view */
+ Matrix mCurrentMatrix;
+
+ /** The matrix applied to the ghost view canvas */
+ private final Matrix mMatrix = new Matrix();
+
+ private final ViewTreeObserver.OnPreDrawListener mOnPreDrawListener =
+ new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ // The target view was invalidated; get the transformation.
+ mCurrentMatrix = mView.getMatrix();
+ // We draw the view.
+ ViewCompat.postInvalidateOnAnimation(GhostViewApi14.this);
+ if (mStartParent != null && mStartView != null) {
+ mStartParent.endViewTransition(mStartView);
+ ViewCompat.postInvalidateOnAnimation(mStartParent);
+ mStartParent = null;
+ mStartView = null;
+ }
+ return true;
+ }
+ };
+
+ GhostViewApi14(View view) {
+ super(view.getContext());
+ mView = view;
+ setLayerType(LAYER_TYPE_HARDWARE, null);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ setGhostView(mView, this);
+ // Calculate the deltas
+ final int[] location = new int[2];
+ final int[] viewLocation = new int[2];
+ getLocationOnScreen(location);
+ mView.getLocationOnScreen(viewLocation);
+ viewLocation[0] = (int) (viewLocation[0] - mView.getTranslationX());
+ viewLocation[1] = (int) (viewLocation[1] - mView.getTranslationY());
+ mDeltaX = viewLocation[0] - location[0];
+ mDeltaY = viewLocation[1] - location[1];
+ // Monitor invalidation of the target view.
+ mView.getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener);
+ // Make the target view invisible because we draw it instead.
+ mView.setVisibility(INVISIBLE);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ mView.getViewTreeObserver().removeOnPreDrawListener(mOnPreDrawListener);
+ mView.setVisibility(VISIBLE);
+ setGhostView(mView, null);
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ // Apply the matrix while adjusting the coordinates
+ mMatrix.set(mCurrentMatrix);
+ mMatrix.postTranslate(mDeltaX, mDeltaY);
+ canvas.setMatrix(mMatrix);
+ // Draw the target
+ mView.draw(canvas);
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+ mView.setVisibility(visibility == VISIBLE ? INVISIBLE : VISIBLE);
+ }
+
+ @Override
+ public void reserveEndViewTransition(ViewGroup viewGroup, View view) {
+ mStartParent = viewGroup;
+ mStartView = view;
+ }
+
+ private static void setGhostView(@NonNull View view, GhostViewApi14 ghostView) {
+ view.setTag(R.id.ghost_view, ghostView);
+ }
+
+ static GhostViewApi14 getGhostView(@NonNull View view) {
+ return (GhostViewApi14) view.getTag(R.id.ghost_view);
+ }
+
+}
diff --git a/androidx/transition/GhostViewApi21.java b/androidx/transition/GhostViewApi21.java
new file mode 100644
index 0000000..4cf5ae8
--- /dev/null
+++ b/androidx/transition/GhostViewApi21.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.graphics.Matrix;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+@RequiresApi(21)
+class GhostViewApi21 implements GhostViewImpl {
+
+ private static final String TAG = "GhostViewApi21";
+
+ private static Class<?> sGhostViewClass;
+ private static boolean sGhostViewClassFetched;
+ private static Method sAddGhostMethod;
+ private static boolean sAddGhostMethodFetched;
+ private static Method sRemoveGhostMethod;
+ private static boolean sRemoveGhostMethodFetched;
+
+ static GhostViewImpl addGhost(View view, ViewGroup viewGroup, Matrix matrix) {
+ fetchAddGhostMethod();
+ if (sAddGhostMethod != null) {
+ try {
+ return new GhostViewApi21(
+ (View) sAddGhostMethod.invoke(null, view, viewGroup, matrix));
+ } catch (IllegalAccessException e) {
+ // Do nothing
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ }
+ }
+ return null;
+ }
+
+ static void removeGhost(View view) {
+ fetchRemoveGhostMethod();
+ if (sRemoveGhostMethod != null) {
+ try {
+ sRemoveGhostMethod.invoke(null, view);
+ } catch (IllegalAccessException e) {
+ // Do nothing
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ }
+ }
+ }
+
+ /** A handle to the platform android.view.GhostView. */
+ private final View mGhostView;
+
+ private GhostViewApi21(@NonNull View ghostView) {
+ mGhostView = ghostView;
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ mGhostView.setVisibility(visibility);
+ }
+
+ @Override
+ public void reserveEndViewTransition(ViewGroup viewGroup, View view) {
+ // No need
+ }
+
+ private static void fetchGhostViewClass() {
+ if (!sGhostViewClassFetched) {
+ try {
+ sGhostViewClass = Class.forName("android.view.GhostView");
+ } catch (ClassNotFoundException e) {
+ Log.i(TAG, "Failed to retrieve GhostView class", e);
+ }
+ sGhostViewClassFetched = true;
+ }
+ }
+
+ private static void fetchAddGhostMethod() {
+ if (!sAddGhostMethodFetched) {
+ try {
+ fetchGhostViewClass();
+ sAddGhostMethod = sGhostViewClass.getDeclaredMethod("addGhost", View.class,
+ ViewGroup.class, Matrix.class);
+ sAddGhostMethod.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ Log.i(TAG, "Failed to retrieve addGhost method", e);
+ }
+ sAddGhostMethodFetched = true;
+ }
+ }
+
+ private static void fetchRemoveGhostMethod() {
+ if (!sRemoveGhostMethodFetched) {
+ try {
+ fetchGhostViewClass();
+ sRemoveGhostMethod = sGhostViewClass.getDeclaredMethod("removeGhost", View.class);
+ sRemoveGhostMethod.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ Log.i(TAG, "Failed to retrieve removeGhost method", e);
+ }
+ sRemoveGhostMethodFetched = true;
+ }
+ }
+
+}
diff --git a/androidx/transition/GhostViewImpl.java b/androidx/transition/GhostViewImpl.java
new file mode 100644
index 0000000..5a4d6cf
--- /dev/null
+++ b/androidx/transition/GhostViewImpl.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+interface GhostViewImpl {
+
+ void setVisibility(int visibility);
+
+ /**
+ * Reserves a call to {@link ViewGroup#endViewTransition(View)} at the time when the GhostView
+ * starts drawing its real view.
+ */
+ void reserveEndViewTransition(ViewGroup viewGroup, View view);
+
+}
diff --git a/androidx/transition/GhostViewUtils.java b/androidx/transition/GhostViewUtils.java
new file mode 100644
index 0000000..9e460ca
--- /dev/null
+++ b/androidx/transition/GhostViewUtils.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.graphics.Matrix;
+import android.os.Build;
+import android.view.View;
+import android.view.ViewGroup;
+
+class GhostViewUtils {
+
+ static GhostViewImpl addGhost(View view, ViewGroup viewGroup, Matrix matrix) {
+ if (Build.VERSION.SDK_INT >= 21) {
+ return GhostViewApi21.addGhost(view, viewGroup, matrix);
+ }
+ return GhostViewApi14.addGhost(view, viewGroup);
+ }
+
+ static void removeGhost(View view) {
+ if (Build.VERSION.SDK_INT >= 21) {
+ GhostViewApi21.removeGhost(view);
+ } else {
+ GhostViewApi14.removeGhost(view);
+ }
+ }
+
+ private GhostViewUtils() {
+ }
+}
diff --git a/androidx/transition/ImageViewUtils.java b/androidx/transition/ImageViewUtils.java
new file mode 100644
index 0000000..9d73e9e
--- /dev/null
+++ b/androidx/transition/ImageViewUtils.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.graphics.Matrix;
+import android.os.Build;
+import android.util.Log;
+import android.widget.ImageView;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+class ImageViewUtils {
+ private static final String TAG = "ImageViewUtils";
+
+ private static Method sAnimateTransformMethod;
+ private static boolean sAnimateTransformMethodFetched;
+
+ /**
+ * Starts animating the transformation of the image view. This has to be called before calling
+ * {@link #animateTransform(ImageView, Matrix)}.
+ */
+ static void startAnimateTransform(ImageView view) {
+ if (Build.VERSION.SDK_INT < 21) {
+ final ImageView.ScaleType scaleType = view.getScaleType();
+ view.setTag(R.id.save_scale_type, scaleType);
+ if (scaleType == ImageView.ScaleType.MATRIX) {
+ view.setTag(R.id.save_image_matrix, view.getImageMatrix());
+ } else {
+ view.setScaleType(ImageView.ScaleType.MATRIX);
+ }
+ view.setImageMatrix(MatrixUtils.IDENTITY_MATRIX);
+ }
+ }
+
+ /**
+ * Sets the matrix to animate the content of the image view.
+ */
+ static void animateTransform(ImageView view, Matrix matrix) {
+ if (Build.VERSION.SDK_INT < 21) {
+ view.setImageMatrix(matrix);
+ } else {
+ fetchAnimateTransformMethod();
+ if (sAnimateTransformMethod != null) {
+ try {
+ sAnimateTransformMethod.invoke(view, matrix);
+ } catch (IllegalAccessException e) {
+ // Do nothing
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ }
+ }
+ }
+ }
+
+ private static void fetchAnimateTransformMethod() {
+ if (!sAnimateTransformMethodFetched) {
+ try {
+ sAnimateTransformMethod = ImageView.class.getDeclaredMethod("animateTransform",
+ Matrix.class);
+ sAnimateTransformMethod.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ Log.i(TAG, "Failed to retrieve animateTransform method", e);
+ }
+ sAnimateTransformMethodFetched = true;
+ }
+ }
+
+ /**
+ * Reserves that the caller will stop calling {@link #animateTransform(ImageView, Matrix)} when
+ * the specified animator ends.
+ */
+ static void reserveEndAnimateTransform(final ImageView view, Animator animator) {
+ if (Build.VERSION.SDK_INT < 21) {
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ final ImageView.ScaleType scaleType = (ImageView.ScaleType)
+ view.getTag(R.id.save_scale_type);
+ view.setScaleType(scaleType);
+ view.setTag(R.id.save_scale_type, null);
+ if (scaleType == ImageView.ScaleType.MATRIX) {
+ view.setImageMatrix((Matrix) view.getTag(R.id.save_image_matrix));
+ view.setTag(R.id.save_image_matrix, null);
+ }
+ animation.removeListener(this);
+ }
+ });
+ }
+ }
+
+ private ImageViewUtils() {
+ }
+}
diff --git a/androidx/transition/MatrixUtils.java b/androidx/transition/MatrixUtils.java
new file mode 100644
index 0000000..9a13796
--- /dev/null
+++ b/androidx/transition/MatrixUtils.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.graphics.Matrix;
+import android.graphics.RectF;
+
+class MatrixUtils {
+
+ static final Matrix IDENTITY_MATRIX = new Matrix() {
+
+ void oops() {
+ throw new IllegalStateException("Matrix can not be modified");
+ }
+
+ @Override
+ public void set(Matrix src) {
+ oops();
+ }
+
+ @Override
+ public void reset() {
+ oops();
+ }
+
+ @Override
+ public void setTranslate(float dx, float dy) {
+ oops();
+ }
+
+ @Override
+ public void setScale(float sx, float sy, float px, float py) {
+ oops();
+ }
+
+ @Override
+ public void setScale(float sx, float sy) {
+ oops();
+ }
+
+ @Override
+ public void setRotate(float degrees, float px, float py) {
+ oops();
+ }
+
+ @Override
+ public void setRotate(float degrees) {
+ oops();
+ }
+
+ @Override
+ public void setSinCos(float sinValue, float cosValue, float px, float py) {
+ oops();
+ }
+
+ @Override
+ public void setSinCos(float sinValue, float cosValue) {
+ oops();
+ }
+
+ @Override
+ public void setSkew(float kx, float ky, float px, float py) {
+ oops();
+ }
+
+ @Override
+ public void setSkew(float kx, float ky) {
+ oops();
+ }
+
+ @Override
+ public boolean setConcat(Matrix a, Matrix b) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean preTranslate(float dx, float dy) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean preScale(float sx, float sy, float px, float py) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean preScale(float sx, float sy) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean preRotate(float degrees, float px, float py) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean preRotate(float degrees) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean preSkew(float kx, float ky, float px, float py) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean preSkew(float kx, float ky) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean preConcat(Matrix other) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean postTranslate(float dx, float dy) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean postScale(float sx, float sy, float px, float py) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean postScale(float sx, float sy) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean postRotate(float degrees, float px, float py) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean postRotate(float degrees) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean postSkew(float kx, float ky, float px, float py) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean postSkew(float kx, float ky) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean postConcat(Matrix other) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex,
+ int pointCount) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public void setValues(float[] values) {
+ oops();
+ }
+
+ };
+
+ private MatrixUtils() {
+ }
+}
diff --git a/androidx/transition/ObjectAnimatorUtils.java b/androidx/transition/ObjectAnimatorUtils.java
new file mode 100644
index 0000000..bd73b58
--- /dev/null
+++ b/androidx/transition/ObjectAnimatorUtils.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.animation.ObjectAnimator;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.os.Build;
+import android.util.Property;
+
+class ObjectAnimatorUtils {
+
+ static <T> ObjectAnimator ofPointF(T target, Property<T, PointF> property, Path path) {
+ if (Build.VERSION.SDK_INT >= 21) {
+ return ObjectAnimator.ofObject(target, property, null, path);
+ }
+ return ObjectAnimator.ofFloat(target, new PathProperty<>(property, path), 0f, 1f);
+ }
+
+ private ObjectAnimatorUtils() {
+ }
+}
diff --git a/androidx/transition/PathMotion.java b/androidx/transition/PathMotion.java
new file mode 100644
index 0000000..3e05665
--- /dev/null
+++ b/androidx/transition/PathMotion.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.content.Context;
+import android.graphics.Path;
+import android.util.AttributeSet;
+
+/**
+ * This base class can be extended to provide motion along a Path to Transitions.
+ *
+ * <p>
+ * Transitions such as {@link android.transition.ChangeBounds} move Views, typically
+ * in a straight path between the start and end positions. Applications that desire to
+ * have these motions move in a curve can change how Views interpolate in two dimensions
+ * by extending PathMotion and implementing {@link #getPath(float, float, float, float)}.
+ * </p>
+ * <p>This may be used in XML as an element inside a transition.</p>
+ * <pre>
+ * {@code
+ * <changeBounds>
+ * <pathMotion class="my.app.transition.MyPathMotion"/>
+ * </changeBounds>
+ * }
+ * </pre>
+ */
+public abstract class PathMotion {
+
+ public PathMotion() {
+ }
+
+ public PathMotion(Context context, AttributeSet attrs) {
+ }
+
+ /**
+ * Provide a Path to interpolate between two points <code>(startX, startY)</code> and
+ * <code>(endX, endY)</code>. This allows controlled curved motion along two dimensions.
+ *
+ * @param startX The x coordinate of the starting point.
+ * @param startY The y coordinate of the starting point.
+ * @param endX The x coordinate of the ending point.
+ * @param endY The y coordinate of the ending point.
+ * @return A Path along which the points should be interpolated. The returned Path
+ * must start at point <code>(startX, startY)</code>, typically using
+ * {@link android.graphics.Path#moveTo(float, float)} and end at <code>(endX, endY)</code>.
+ */
+ public abstract Path getPath(float startX, float startY, float endX, float endY);
+}
diff --git a/androidx/transition/PathMotionTest.java b/androidx/transition/PathMotionTest.java
new file mode 100644
index 0000000..53ae77b
--- /dev/null
+++ b/androidx/transition/PathMotionTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+
+public abstract class PathMotionTest {
+
+ public static void assertPathMatches(Path expectedPath, Path path) {
+ PathMeasure expectedMeasure = new PathMeasure(expectedPath, false);
+ PathMeasure pathMeasure = new PathMeasure(path, false);
+
+ boolean expectedNextContour;
+ boolean pathNextContour;
+ int contourIndex = 0;
+ do {
+ float expectedLength = expectedMeasure.getLength();
+ assertEquals("Lengths differ", expectedLength, pathMeasure.getLength(), 0.01f);
+
+ float minLength = Math.min(expectedLength, pathMeasure.getLength());
+
+ float[] pos = new float[2];
+
+ float increment = minLength / 5f;
+ for (float along = 0; along <= minLength; along += increment) {
+ expectedMeasure.getPosTan(along, pos, null);
+ float expectedX = pos[0];
+ float expectedY = pos[1];
+
+ pathMeasure.getPosTan(along, pos, null);
+ assertEquals("Failed at " + increment + " in contour " + contourIndex,
+ expectedX, pos[0], 0.01f);
+ assertEquals("Failed at " + increment + " in contour " + contourIndex,
+ expectedY, pos[1], 0.01f);
+ }
+ expectedNextContour = expectedMeasure.nextContour();
+ pathNextContour = pathMeasure.nextContour();
+ contourIndex++;
+ } while (expectedNextContour && pathNextContour);
+ assertFalse(expectedNextContour);
+ assertFalse(pathNextContour);
+ }
+
+}
diff --git a/androidx/transition/PathProperty.java b/androidx/transition/PathProperty.java
new file mode 100644
index 0000000..be2dddb
--- /dev/null
+++ b/androidx/transition/PathProperty.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.graphics.PointF;
+import android.util.Property;
+
+/**
+ * A special {@link Property} that can animate a pair of properties bi-dimensionally along the
+ * specified path.
+ * <p>
+ * This property should always be used with Animator that sets float fractions between
+ * {@code 0.f} and {@code 1.f}. For example, setting {@code 0.5f} to this property sets the
+ * values right in the middle of the specified path to the underlying properties.
+ * <p>
+ * Unlike many of the platform built-in properties, instances of this class cannot be reused
+ * for later animations.
+ */
+class PathProperty<T> extends Property<T, Float> {
+
+ private final Property<T, PointF> mProperty;
+ private final PathMeasure mPathMeasure;
+ private final float mPathLength;
+ private final float[] mPosition = new float[2];
+ private final PointF mPointF = new PointF();
+ private float mCurrentFraction;
+
+ PathProperty(Property<T, PointF> property, Path path) {
+ super(Float.class, property.getName());
+ mProperty = property;
+ mPathMeasure = new PathMeasure(path, false);
+ mPathLength = mPathMeasure.getLength();
+ }
+
+ @Override
+ public Float get(T object) {
+ return mCurrentFraction;
+ }
+
+ @Override
+ public void set(T target, Float fraction) {
+ mCurrentFraction = fraction;
+ mPathMeasure.getPosTan(mPathLength * fraction, mPosition, null);
+ mPointF.x = mPosition[0];
+ mPointF.y = mPosition[1];
+ mProperty.set(target, mPointF);
+ }
+
+}
diff --git a/androidx/transition/PatternPathMotion.java b/androidx/transition/PatternPathMotion.java
new file mode 100644
index 0000000..7f6153d
--- /dev/null
+++ b/androidx/transition/PatternPathMotion.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.util.AttributeSet;
+
+import androidx.core.content.res.TypedArrayUtils;
+import androidx.core.graphics.PathParser;
+
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * A PathMotion that takes a Path pattern and applies it to the separation between two points.
+ * The starting point of the Path will be moved to the origin and the end point will be scaled
+ * and rotated so that it matches with the target end point.
+ * <p>This may be used in XML as an element inside a transition.</p>
+ * <pre>{@code
+ * <changeBounds>
+ * <patternPathMotion android:patternPathData="M0 0 L0 100 L100 100"/>
+ * </changeBounds>}
+ * </pre>
+ */
+public class PatternPathMotion extends PathMotion {
+
+ private Path mOriginalPatternPath;
+
+ private final Path mPatternPath = new Path();
+
+ private final Matrix mTempMatrix = new Matrix();
+
+ /**
+ * Constructs a PatternPathMotion with a straight-line pattern.
+ */
+ public PatternPathMotion() {
+ mPatternPath.lineTo(1, 0);
+ mOriginalPatternPath = mPatternPath;
+ }
+
+ public PatternPathMotion(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs, Styleable.PATTERN_PATH_MOTION);
+ try {
+ String pathData = TypedArrayUtils.getNamedString(a, (XmlPullParser) attrs,
+ "patternPathData", Styleable.PatternPathMotion.PATTERN_PATH_DATA);
+ if (pathData == null) {
+ throw new RuntimeException("pathData must be supplied for patternPathMotion");
+ }
+ Path pattern = PathParser.createPathFromPathData(pathData);
+ setPatternPath(pattern);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ /**
+ * Creates a PatternPathMotion with the Path defining a pattern of motion between two
+ * coordinates. The pattern will be translated, rotated, and scaled to fit between the start
+ * and end points. The pattern must not be empty and must have the end point differ from the
+ * start point.
+ *
+ * @param patternPath A Path to be used as a pattern for two-dimensional motion.
+ */
+ public PatternPathMotion(Path patternPath) {
+ setPatternPath(patternPath);
+ }
+
+ /**
+ * Returns the Path defining a pattern of motion between two coordinates.
+ * The pattern will be translated, rotated, and scaled to fit between the start and end points.
+ * The pattern must not be empty and must have the end point differ from the start point.
+ *
+ * @return the Path defining a pattern of motion between two coordinates.
+ */
+ public Path getPatternPath() {
+ return mOriginalPatternPath;
+ }
+
+ /**
+ * Sets the Path defining a pattern of motion between two coordinates.
+ * The pattern will be translated, rotated, and scaled to fit between the start and end points.
+ * The pattern must not be empty and must have the end point differ from the start point.
+ *
+ * @param patternPath A Path to be used as a pattern for two-dimensional motion.
+ */
+ public void setPatternPath(Path patternPath) {
+ PathMeasure pathMeasure = new PathMeasure(patternPath, false);
+ float length = pathMeasure.getLength();
+ float[] pos = new float[2];
+ pathMeasure.getPosTan(length, pos, null);
+ float endX = pos[0];
+ float endY = pos[1];
+ pathMeasure.getPosTan(0, pos, null);
+ float startX = pos[0];
+ float startY = pos[1];
+
+ if (startX == endX && startY == endY) {
+ throw new IllegalArgumentException("pattern must not end at the starting point");
+ }
+
+ mTempMatrix.setTranslate(-startX, -startY);
+ float dx = endX - startX;
+ float dy = endY - startY;
+ float distance = distance(dx, dy);
+ float scale = 1 / distance;
+ mTempMatrix.postScale(scale, scale);
+ double angle = Math.atan2(dy, dx);
+ mTempMatrix.postRotate((float) Math.toDegrees(-angle));
+ patternPath.transform(mTempMatrix, mPatternPath);
+ mOriginalPatternPath = patternPath;
+ }
+
+ @Override
+ public Path getPath(float startX, float startY, float endX, float endY) {
+ float dx = endX - startX;
+ float dy = endY - startY;
+ float length = distance(dx, dy);
+ double angle = Math.atan2(dy, dx);
+
+ mTempMatrix.setScale(length, length);
+ mTempMatrix.postRotate((float) Math.toDegrees(angle));
+ mTempMatrix.postTranslate(startX, startY);
+ Path path = new Path();
+ mPatternPath.transform(mTempMatrix, path);
+ return path;
+ }
+
+ private static float distance(float x, float y) {
+ return (float) Math.sqrt((x * x) + (y * y));
+ }
+
+}
diff --git a/androidx/transition/PatternPathMotionTest.java b/androidx/transition/PatternPathMotionTest.java
new file mode 100644
index 0000000..e4fe206
--- /dev/null
+++ b/androidx/transition/PatternPathMotionTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.junit.Assert.assertSame;
+
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PatternPathMotionTest extends PathMotionTest {
+
+ @Test
+ public void testStraightPath() {
+ Path pattern = new Path();
+ pattern.moveTo(100, 500);
+ pattern.lineTo(300, 1000);
+
+ PatternPathMotion pathMotion = new PatternPathMotion(pattern);
+ assertPathMatches(pattern, pathMotion.getPatternPath());
+
+ Path expected = new Path();
+ expected.moveTo(0, 0);
+ expected.lineTo(100, 100);
+
+ assertPathMatches(expected, pathMotion.getPath(0, 0, 100, 100));
+ }
+
+ @Test
+ public void testCurve() {
+ RectF oval = new RectF();
+ Path pattern = new Path();
+ oval.set(0, 0, 100, 100);
+ pattern.addArc(oval, 0, 180);
+
+ PatternPathMotion pathMotion = new PatternPathMotion(pattern);
+ assertPathMatches(pattern, pathMotion.getPatternPath());
+
+ Path expected = new Path();
+ oval.set(-50, 0, 50, 100);
+ expected.addArc(oval, -90, 180);
+
+ assertPathMatches(expected, pathMotion.getPath(0, 0, 0, 100));
+ }
+
+ @Test
+ public void testSetPatternPath() {
+ Path pattern = new Path();
+ RectF oval = new RectF(0, 0, 100, 100);
+ pattern.addArc(oval, 0, 180);
+
+ PatternPathMotion patternPathMotion = new PatternPathMotion();
+ patternPathMotion.setPatternPath(pattern);
+ assertSame(pattern, patternPathMotion.getPatternPath());
+ }
+
+}
diff --git a/androidx/transition/PropagationTest.java b/androidx/transition/PropagationTest.java
new file mode 100644
index 0000000..8efb856
--- /dev/null
+++ b/androidx/transition/PropagationTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Rect;
+import android.support.test.filters.MediumTest;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.transition.test.R;
+
+import org.junit.Test;
+
+@MediumTest
+public class PropagationTest extends BaseTransitionTest {
+
+ @Test
+ public void testCircularPropagation() throws Throwable {
+ enterScene(R.layout.scene10);
+ CircularPropagation propagation = new CircularPropagation();
+ mTransition.setPropagation(propagation);
+ final TransitionValues redValues = new TransitionValues();
+ redValues.view = mRoot.findViewById(R.id.redSquare);
+ propagation.captureValues(redValues);
+
+ // Only the reported propagation properties are set
+ for (String prop : propagation.getPropagationProperties()) {
+ assertTrue(redValues.values.keySet().contains(prop));
+ }
+ assertEquals(propagation.getPropagationProperties().length, redValues.values.size());
+
+ // check the visibility
+ assertEquals(View.VISIBLE, propagation.getViewVisibility(redValues));
+ assertEquals(View.GONE, propagation.getViewVisibility(null));
+
+ // Check the positions
+ int[] pos = new int[2];
+ redValues.view.getLocationOnScreen(pos);
+ pos[0] += redValues.view.getWidth() / 2;
+ pos[1] += redValues.view.getHeight() / 2;
+ assertEquals(pos[0], propagation.getViewX(redValues));
+ assertEquals(pos[1], propagation.getViewY(redValues));
+
+ mTransition.setEpicenterCallback(new Transition.EpicenterCallback() {
+ @Override
+ public Rect onGetEpicenter(@NonNull Transition transition) {
+ return new Rect(0, 0, redValues.view.getWidth(), redValues.view.getHeight());
+ }
+ });
+
+ long redDelay = getDelay(R.id.redSquare);
+ // red square's delay should be roughly 0 since it is at the epicenter
+ assertEquals(0f, redDelay, 30f);
+
+ // The green square is on the upper-right
+ long greenDelay = getDelay(R.id.greenSquare);
+ assertTrue(greenDelay < redDelay);
+
+ // The blue square is on the lower-right
+ long blueDelay = getDelay(R.id.blueSquare);
+ assertTrue(blueDelay < greenDelay);
+
+ // Test propagation speed
+ propagation.setPropagationSpeed(1000000000f);
+ assertEquals(0, getDelay(R.id.blueSquare));
+ }
+
+ private TransitionValues capturePropagationValues(int viewId) {
+ TransitionValues transitionValues = new TransitionValues();
+ transitionValues.view = mRoot.findViewById(viewId);
+ TransitionPropagation propagation = mTransition.getPropagation();
+ assertNotNull(propagation);
+ propagation.captureValues(transitionValues);
+ return transitionValues;
+ }
+
+ private long getDelay(int viewId) {
+ TransitionValues transitionValues = capturePropagationValues(viewId);
+ TransitionPropagation propagation = mTransition.getPropagation();
+ assertNotNull(propagation);
+ return propagation.getStartDelay(mRoot, mTransition, transitionValues, null);
+ }
+
+}
diff --git a/androidx/transition/PropertyValuesHolderUtils.java b/androidx/transition/PropertyValuesHolderUtils.java
new file mode 100644
index 0000000..42527c4
--- /dev/null
+++ b/androidx/transition/PropertyValuesHolderUtils.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.animation.PropertyValuesHolder;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.os.Build;
+import android.util.Property;
+
+class PropertyValuesHolderUtils {
+
+ /**
+ * Constructs and returns a PropertyValuesHolder with a given property and
+ * a Path along which the values should be animated. This variant supports a
+ * <code>TypeConverter</code> to convert from <code>PointF</code> to the target
+ * type.
+ *
+ * @param property The property being animated. Should not be null.
+ * @param path The Path along which the values should be animated.
+ * @return PropertyValuesHolder The constructed PropertyValuesHolder object.
+ */
+ static PropertyValuesHolder ofPointF(Property<?, PointF> property, Path path) {
+ if (Build.VERSION.SDK_INT >= 21) {
+ return PropertyValuesHolder.ofObject(property, null, path);
+ }
+ return PropertyValuesHolder.ofFloat(new PathProperty<>(property, path), 0f, 1f);
+ }
+
+ private PropertyValuesHolderUtils() {
+ }
+}
diff --git a/androidx/transition/RectEvaluator.java b/androidx/transition/RectEvaluator.java
new file mode 100644
index 0000000..6dab422
--- /dev/null
+++ b/androidx/transition/RectEvaluator.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.animation.TypeEvaluator;
+import android.graphics.Rect;
+
+/**
+ * This evaluator can be used to perform type interpolation between <code>Rect</code> values.
+ */
+class RectEvaluator implements TypeEvaluator<Rect> {
+
+ /**
+ * When null, a new Rect is returned on every evaluate call. When non-null,
+ * mRect will be modified and returned on every evaluate.
+ */
+ private Rect mRect;
+
+ /**
+ * Construct a RectEvaluator that returns a new Rect on every evaluate call.
+ * To avoid creating an object for each evaluate call,
+ * {@link RectEvaluator#RectEvaluator(android.graphics.Rect)} should be used
+ * whenever possible.
+ */
+ RectEvaluator() {
+ }
+
+ /**
+ * Constructs a RectEvaluator that modifies and returns <code>reuseRect</code>
+ * in {@link #evaluate(float, android.graphics.Rect, android.graphics.Rect)} calls.
+ * The value returned from
+ * {@link #evaluate(float, android.graphics.Rect, android.graphics.Rect)} should
+ * not be cached because it will change over time as the object is reused on each
+ * call.
+ *
+ * @param reuseRect A Rect to be modified and returned by evaluate.
+ */
+ RectEvaluator(Rect reuseRect) {
+ mRect = reuseRect;
+ }
+
+ /**
+ * This function returns the result of linearly interpolating the start and
+ * end Rect values, with <code>fraction</code> representing the proportion
+ * between the start and end values. The calculation is a simple parametric
+ * calculation on each of the separate components in the Rect objects
+ * (left, top, right, and bottom).
+ *
+ * <p>If {@link #RectEvaluator(android.graphics.Rect)} was used to construct
+ * this RectEvaluator, the object returned will be the <code>reuseRect</code>
+ * passed into the constructor.</p>
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue The start Rect
+ * @param endValue The end Rect
+ * @return A linear interpolation between the start and end values, given the
+ * <code>fraction</code> parameter.
+ */
+ @Override
+ public Rect evaluate(float fraction, Rect startValue, Rect endValue) {
+ int left = startValue.left + (int) ((endValue.left - startValue.left) * fraction);
+ int top = startValue.top + (int) ((endValue.top - startValue.top) * fraction);
+ int right = startValue.right + (int) ((endValue.right - startValue.right) * fraction);
+ int bottom = startValue.bottom + (int) ((endValue.bottom - startValue.bottom) * fraction);
+ if (mRect == null) {
+ return new Rect(left, top, right, bottom);
+ } else {
+ mRect.set(left, top, right, bottom);
+ return mRect;
+ }
+ }
+}
diff --git a/androidx/transition/Scene.java b/androidx/transition/Scene.java
new file mode 100644
index 0000000..9239ef6
--- /dev/null
+++ b/androidx/transition/Scene.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.content.Context;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * A scene represents the collection of values that various properties in the
+ * View hierarchy will have when the scene is applied. A Scene can be
+ * configured to automatically run a Transition when it is applied, which will
+ * animate the various property changes that take place during the
+ * scene change.
+ */
+public class Scene {
+
+ private Context mContext;
+ private int mLayoutId = -1;
+ private ViewGroup mSceneRoot;
+ private View mLayout; // alternative to layoutId
+ private Runnable mEnterAction, mExitAction;
+
+ /**
+ * Returns a Scene described by the resource file associated with the given
+ * <code>layoutId</code> parameter. If such a Scene has already been created for
+ * the given <code>sceneRoot</code>, that same Scene will be returned.
+ * This caching of layoutId-based scenes enables sharing of common scenes
+ * between those created in code and those referenced by {@link TransitionManager}
+ * XML resource files.
+ *
+ * @param sceneRoot The root of the hierarchy in which scene changes
+ * and transitions will take place.
+ * @param layoutId The id of a standard layout resource file.
+ * @param context The context used in the process of inflating
+ * the layout resource.
+ * @return The scene for the given root and layout id
+ */
+ @NonNull
+ public static Scene getSceneForLayout(@NonNull ViewGroup sceneRoot, @LayoutRes int layoutId,
+ @NonNull Context context) {
+ @SuppressWarnings("unchecked")
+ SparseArray<Scene> scenes =
+ (SparseArray<Scene>) sceneRoot.getTag(R.id.transition_scene_layoutid_cache);
+ if (scenes == null) {
+ scenes = new SparseArray<>();
+ sceneRoot.setTag(R.id.transition_scene_layoutid_cache, scenes);
+ }
+ Scene scene = scenes.get(layoutId);
+ if (scene != null) {
+ return scene;
+ } else {
+ scene = new Scene(sceneRoot, layoutId, context);
+ scenes.put(layoutId, scene);
+ return scene;
+ }
+ }
+
+ /**
+ * Constructs a Scene with no information about how values will change
+ * when this scene is applied. This constructor might be used when
+ * a Scene is created with the intention of being dynamically configured,
+ * through setting {@link #setEnterAction(Runnable)} and possibly
+ * {@link #setExitAction(Runnable)}.
+ *
+ * @param sceneRoot The root of the hierarchy in which scene changes
+ * and transitions will take place.
+ */
+ public Scene(@NonNull ViewGroup sceneRoot) {
+ mSceneRoot = sceneRoot;
+ }
+
+ /**
+ * Constructs a Scene which, when entered, will remove any
+ * children from the sceneRoot container and will inflate and add
+ * the hierarchy specified by the layoutId resource file.
+ *
+ * <p>This method is hidden because layoutId-based scenes should be
+ * created by the caching factory method {@link Scene#getCurrentScene(View)}.</p>
+ *
+ * @param sceneRoot The root of the hierarchy in which scene changes
+ * and transitions will take place.
+ * @param layoutId The id of a resource file that defines the view
+ * hierarchy of this scene.
+ * @param context The context used in the process of inflating
+ * the layout resource.
+ */
+ private Scene(ViewGroup sceneRoot, int layoutId, Context context) {
+ mContext = context;
+ mSceneRoot = sceneRoot;
+ mLayoutId = layoutId;
+ }
+
+ /**
+ * Constructs a Scene which, when entered, will remove any
+ * children from the sceneRoot container and add the layout
+ * object as a new child of that container.
+ *
+ * @param sceneRoot The root of the hierarchy in which scene changes
+ * and transitions will take place.
+ * @param layout The view hierarchy of this scene, added as a child
+ * of sceneRoot when this scene is entered.
+ */
+ public Scene(@NonNull ViewGroup sceneRoot, @NonNull View layout) {
+ mSceneRoot = sceneRoot;
+ mLayout = layout;
+ }
+
+ /**
+ * Gets the root of the scene, which is the root of the view hierarchy
+ * affected by changes due to this scene, and which will be animated
+ * when this scene is entered.
+ *
+ * @return The root of the view hierarchy affected by this scene.
+ */
+ @NonNull
+ public ViewGroup getSceneRoot() {
+ return mSceneRoot;
+ }
+
+ /**
+ * Exits this scene, if it is the current scene
+ * on the scene's {@link #getSceneRoot() scene root}. The current scene is
+ * set when {@link #enter() entering} a scene.
+ * Exiting a scene runs the {@link #setExitAction(Runnable) exit action}
+ * if there is one.
+ */
+ public void exit() {
+ if (getCurrentScene(mSceneRoot) == this) {
+ if (mExitAction != null) {
+ mExitAction.run();
+ }
+ }
+ }
+
+ /**
+ * Enters this scene, which entails changing all values that
+ * are specified by this scene. These may be values associated
+ * with a layout view group or layout resource file which will
+ * now be added to the scene root, or it may be values changed by
+ * an {@link #setEnterAction(Runnable)} enter action}, or a
+ * combination of the these. No transition will be run when the
+ * scene is entered. To get transition behavior in scene changes,
+ * use one of the methods in {@link androidx.transition.TransitionManager} instead.
+ */
+ public void enter() {
+ // Apply layout change, if any
+ if (mLayoutId > 0 || mLayout != null) {
+ // empty out parent container before adding to it
+ getSceneRoot().removeAllViews();
+
+ if (mLayoutId > 0) {
+ LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot);
+ } else {
+ mSceneRoot.addView(mLayout);
+ }
+ }
+
+ // Notify next scene that it is entering. Subclasses may override to configure scene.
+ if (mEnterAction != null) {
+ mEnterAction.run();
+ }
+
+ setCurrentScene(mSceneRoot, this);
+ }
+
+ /**
+ * Set the scene that the given view is in. The current scene is set only
+ * on the root view of a scene, not for every view in that hierarchy. This
+ * information is used by Scene to determine whether there is a previous
+ * scene which should be exited before the new scene is entered.
+ *
+ * @param view The view on which the current scene is being set
+ */
+ static void setCurrentScene(View view, Scene scene) {
+ view.setTag(R.id.transition_current_scene, scene);
+ }
+
+ /**
+ * Gets the current {@link Scene} set on the given view. A scene is set on a view
+ * only if that view is the scene root.
+ *
+ * @return The current Scene set on this view. A value of null indicates that
+ * no Scene is currently set.
+ */
+ static Scene getCurrentScene(View view) {
+ return (Scene) view.getTag(R.id.transition_current_scene);
+ }
+
+ /**
+ * Scenes that are not defined with layout resources or
+ * hierarchies, or which need to perform additional steps
+ * after those hierarchies are changed to, should set an enter
+ * action, and possibly an exit action as well. An enter action
+ * will cause Scene to call back into application code to do
+ * anything else the application needs after transitions have
+ * captured pre-change values and after any other scene changes
+ * have been applied, such as the layout (if any) being added to
+ * the view hierarchy. After this method is called, Transitions will
+ * be played.
+ *
+ * @param action The runnable whose {@link Runnable#run() run()} method will
+ * be called when this scene is entered
+ * @see #setExitAction(Runnable)
+ * @see androidx.transition.Scene(android.view.ViewGroup, android.view.ViewGroup)
+ */
+ public void setEnterAction(@Nullable Runnable action) {
+ mEnterAction = action;
+ }
+
+ /**
+ * Scenes that are not defined with layout resources or
+ * hierarchies, or which need to perform additional steps
+ * after those hierarchies are changed to, should set an enter
+ * action, and possibly an exit action as well. An exit action
+ * will cause Scene to call back into application code to do
+ * anything the application needs to do after applicable transitions have
+ * captured pre-change values, but before any other scene changes
+ * have been applied, such as the new layout (if any) being added to
+ * the view hierarchy. After this method is called, the next scene
+ * will be entered, including a call to {@link #setEnterAction(Runnable)}
+ * if an enter action is set.
+ *
+ * @see #setEnterAction(Runnable)
+ * @see androidx.transition.Scene(android.view.ViewGroup, android.view.ViewGroup)
+ */
+ public void setExitAction(@Nullable Runnable action) {
+ mExitAction = action;
+ }
+
+ /**
+ * Returns whether this Scene was created by a layout resource file, determined
+ * by the layoutId passed into
+ * {@link #getSceneForLayout(ViewGroup, int, Context)}.
+ */
+ boolean isCreatedFromLayoutResource() {
+ return (mLayoutId > 0);
+ }
+
+}
diff --git a/androidx/transition/SceneTest.java b/androidx/transition/SceneTest.java
new file mode 100644
index 0000000..2c63b98
--- /dev/null
+++ b/androidx/transition/SceneTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.sameInstance;
+
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.transition.test.R;
+
+import org.junit.Test;
+
+@MediumTest
+public class SceneTest extends BaseTest {
+
+ @Test
+ public void testGetSceneRoot() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ Scene scene = new Scene(root);
+ assertThat(scene.getSceneRoot(), is(sameInstance(root)));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testSceneWithViewGroup() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ FrameLayout layout = new FrameLayout(activity);
+ Scene scene = new Scene(root, layout);
+ CheckCalledRunnable enterAction = new CheckCalledRunnable();
+ CheckCalledRunnable exitAction = new CheckCalledRunnable();
+ scene.setEnterAction(enterAction);
+ scene.setExitAction(exitAction);
+ scene.enter();
+ assertThat(enterAction.wasCalled(), is(true));
+ assertThat(exitAction.wasCalled(), is(false));
+ assertThat(root.getChildCount(), is(1));
+ assertThat(root.getChildAt(0), is((View) layout));
+ scene.exit();
+ assertThat(exitAction.wasCalled(), is(true));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testSceneWithView() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ View view = new View(activity);
+ Scene scene = new Scene(root, view);
+ CheckCalledRunnable enterAction = new CheckCalledRunnable();
+ CheckCalledRunnable exitAction = new CheckCalledRunnable();
+ scene.setEnterAction(enterAction);
+ scene.setExitAction(exitAction);
+ scene.enter();
+ assertThat(enterAction.wasCalled(), is(true));
+ assertThat(exitAction.wasCalled(), is(false));
+ assertThat(root.getChildCount(), is(1));
+ assertThat(root.getChildAt(0), is(view));
+ scene.exit();
+ assertThat(exitAction.wasCalled(), is(true));
+ }
+
+ @Test
+ public void testEnterAction() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ Scene scene = new Scene(root);
+ CheckCalledRunnable runnable = new CheckCalledRunnable();
+ scene.setEnterAction(runnable);
+ scene.enter();
+ assertThat(runnable.wasCalled(), is(true));
+ }
+
+ @Test
+ public void testExitAction() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ Scene scene = new Scene(root);
+ scene.enter();
+ CheckCalledRunnable runnable = new CheckCalledRunnable();
+ scene.setExitAction(runnable);
+ scene.exit();
+ assertThat(runnable.wasCalled(), is(true));
+ }
+
+ @Test
+ public void testExitAction_withoutEnter() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ Scene scene = new Scene(root);
+ CheckCalledRunnable runnable = new CheckCalledRunnable();
+ scene.setExitAction(runnable);
+ scene.exit();
+ assertThat(runnable.wasCalled(), is(false));
+ }
+
+ @Test
+ public void testGetSceneForLayout_cache() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ Scene scene = Scene.getSceneForLayout(root, R.layout.support_scene0, activity);
+ assertThat("getSceneForLayout should return the same instance for subsequent calls",
+ Scene.getSceneForLayout(root, R.layout.support_scene0, activity),
+ is(sameInstance(scene)));
+ }
+
+}
diff --git a/androidx/transition/SidePropagation.java b/androidx/transition/SidePropagation.java
new file mode 100644
index 0000000..20d3b5d
--- /dev/null
+++ b/androidx/transition/SidePropagation.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.graphics.Rect;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.core.view.ViewCompat;
+
+/**
+ * A <code>TransitionPropagation</code> that propagates based on the distance to the side
+ * and, orthogonally, the distance to epicenter. If the transitioning View is visible in
+ * the start of the transition, then it will transition sooner when closer to the side and
+ * later when farther. If the view is not visible in the start of the transition, then
+ * it will transition later when closer to the side and sooner when farther from the edge.
+ * This is the default TransitionPropagation used with {@link android.transition.Slide}.
+ */
+public class SidePropagation extends VisibilityPropagation {
+
+ private float mPropagationSpeed = 3.0f;
+ private int mSide = Gravity.BOTTOM;
+
+ /**
+ * Sets the side that is used to calculate the transition propagation. If the transitioning
+ * View is visible in the start of the transition, then it will transition sooner when
+ * closer to the side and later when farther. If the view is not visible in the start of
+ * the transition, then it will transition later when closer to the side and sooner when
+ * farther from the edge. The default is {@link Gravity#BOTTOM}.
+ *
+ * @param side The side that is used to calculate the transition propagation. Must be one of
+ * {@link Gravity#LEFT}, {@link Gravity#TOP}, {@link Gravity#RIGHT},
+ * {@link Gravity#BOTTOM}, {@link Gravity#START}, or {@link Gravity#END}.
+ */
+ public void setSide(@Slide.GravityFlag int side) {
+ mSide = side;
+ }
+
+ /**
+ * Sets the speed at which transition propagation happens, relative to the duration of the
+ * Transition. A <code>propagationSpeed</code> of 1 means that a View centered at the side
+ * set in {@link #setSide(int)} and View centered at the opposite edge will have a difference
+ * in start delay of approximately the duration of the Transition. A speed of 2 means the
+ * start delay difference will be approximately half of the duration of the transition. A
+ * value of 0 is illegal, but negative values will invert the propagation.
+ *
+ * @param propagationSpeed The speed at which propagation occurs, relative to the duration
+ * of the transition. A speed of 4 means it works 4 times as fast
+ * as the duration of the transition. May not be 0.
+ */
+ public void setPropagationSpeed(float propagationSpeed) {
+ if (propagationSpeed == 0) {
+ throw new IllegalArgumentException("propagationSpeed may not be 0");
+ }
+ mPropagationSpeed = propagationSpeed;
+ }
+
+ @Override
+ public long getStartDelay(ViewGroup sceneRoot, Transition transition,
+ TransitionValues startValues, TransitionValues endValues) {
+ if (startValues == null && endValues == null) {
+ return 0;
+ }
+ int directionMultiplier = 1;
+ Rect epicenter = transition.getEpicenter();
+ TransitionValues positionValues;
+ if (endValues == null || getViewVisibility(startValues) == View.VISIBLE) {
+ positionValues = startValues;
+ directionMultiplier = -1;
+ } else {
+ positionValues = endValues;
+ }
+
+ int viewCenterX = getViewX(positionValues);
+ int viewCenterY = getViewY(positionValues);
+
+ int[] loc = new int[2];
+ sceneRoot.getLocationOnScreen(loc);
+ int left = loc[0] + Math.round(sceneRoot.getTranslationX());
+ int top = loc[1] + Math.round(sceneRoot.getTranslationY());
+ int right = left + sceneRoot.getWidth();
+ int bottom = top + sceneRoot.getHeight();
+
+ int epicenterX;
+ int epicenterY;
+ if (epicenter != null) {
+ epicenterX = epicenter.centerX();
+ epicenterY = epicenter.centerY();
+ } else {
+ epicenterX = (left + right) / 2;
+ epicenterY = (top + bottom) / 2;
+ }
+
+ float distance = distance(sceneRoot, viewCenterX, viewCenterY, epicenterX, epicenterY,
+ left, top, right, bottom);
+ float maxDistance = getMaxDistance(sceneRoot);
+ float distanceFraction = distance / maxDistance;
+
+ long duration = transition.getDuration();
+ if (duration < 0) {
+ duration = 300;
+ }
+
+ return Math.round(duration * directionMultiplier / mPropagationSpeed * distanceFraction);
+ }
+
+ private int distance(View sceneRoot, int viewX, int viewY, int epicenterX, int epicenterY,
+ int left, int top, int right, int bottom) {
+ final int side;
+ if (mSide == Gravity.START) {
+ final boolean isRtl = ViewCompat.getLayoutDirection(sceneRoot)
+ == ViewCompat.LAYOUT_DIRECTION_RTL;
+ side = isRtl ? Gravity.RIGHT : Gravity.LEFT;
+ } else if (mSide == Gravity.END) {
+ final boolean isRtl = ViewCompat.getLayoutDirection(sceneRoot)
+ == ViewCompat.LAYOUT_DIRECTION_RTL;
+ side = isRtl ? Gravity.LEFT : Gravity.RIGHT;
+ } else {
+ side = mSide;
+ }
+ int distance = 0;
+ switch (side) {
+ case Gravity.LEFT:
+ distance = right - viewX + Math.abs(epicenterY - viewY);
+ break;
+ case Gravity.TOP:
+ distance = bottom - viewY + Math.abs(epicenterX - viewX);
+ break;
+ case Gravity.RIGHT:
+ distance = viewX - left + Math.abs(epicenterY - viewY);
+ break;
+ case Gravity.BOTTOM:
+ distance = viewY - top + Math.abs(epicenterX - viewX);
+ break;
+ }
+ return distance;
+ }
+
+ private int getMaxDistance(ViewGroup sceneRoot) {
+ switch (mSide) {
+ case Gravity.LEFT:
+ case Gravity.RIGHT:
+ case Gravity.START:
+ case Gravity.END:
+ return sceneRoot.getWidth();
+ default:
+ return sceneRoot.getHeight();
+ }
+ }
+
+}
diff --git a/androidx/transition/Slide.java b/androidx/transition/Slide.java
new file mode 100644
index 0000000..b38dc29
--- /dev/null
+++ b/androidx/transition/Slide.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.animation.Animator;
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.content.res.TypedArrayUtils;
+import androidx.core.view.ViewCompat;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This transition tracks changes to the visibility of target views in the
+ * start and end scenes and moves views in or out from one of the edges of the
+ * scene. Visibility is determined by both the
+ * {@link View#setVisibility(int)} state of the view as well as whether it
+ * is parented in the current view hierarchy. Disappearing Views are
+ * limited as described in {@link Visibility#onDisappear(android.view.ViewGroup,
+ * TransitionValues, int, TransitionValues, int)}.
+ */
+public class Slide extends Visibility {
+
+ private static final TimeInterpolator sDecelerate = new DecelerateInterpolator();
+ private static final TimeInterpolator sAccelerate = new AccelerateInterpolator();
+ private static final String PROPNAME_SCREEN_POSITION = "android:slide:screenPosition";
+ private CalculateSlide mSlideCalculator = sCalculateBottom;
+ private int mSlideEdge = Gravity.BOTTOM;
+
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({Gravity.LEFT, Gravity.TOP, Gravity.RIGHT, Gravity.BOTTOM, Gravity.START, Gravity.END})
+ public @interface GravityFlag {
+ }
+
+ private interface CalculateSlide {
+
+ /** Returns the translation value for view when it goes out of the scene */
+ float getGoneX(ViewGroup sceneRoot, View view);
+
+ /** Returns the translation value for view when it goes out of the scene */
+ float getGoneY(ViewGroup sceneRoot, View view);
+ }
+
+ private abstract static class CalculateSlideHorizontal implements CalculateSlide {
+
+ @Override
+ public float getGoneY(ViewGroup sceneRoot, View view) {
+ return view.getTranslationY();
+ }
+ }
+
+ private abstract static class CalculateSlideVertical implements CalculateSlide {
+
+ @Override
+ public float getGoneX(ViewGroup sceneRoot, View view) {
+ return view.getTranslationX();
+ }
+ }
+
+ private static final CalculateSlide sCalculateLeft = new CalculateSlideHorizontal() {
+ @Override
+ public float getGoneX(ViewGroup sceneRoot, View view) {
+ return view.getTranslationX() - sceneRoot.getWidth();
+ }
+ };
+
+ private static final CalculateSlide sCalculateStart = new CalculateSlideHorizontal() {
+ @Override
+ public float getGoneX(ViewGroup sceneRoot, View view) {
+ final boolean isRtl = ViewCompat.getLayoutDirection(sceneRoot)
+ == ViewCompat.LAYOUT_DIRECTION_RTL;
+ final float x;
+ if (isRtl) {
+ x = view.getTranslationX() + sceneRoot.getWidth();
+ } else {
+ x = view.getTranslationX() - sceneRoot.getWidth();
+ }
+ return x;
+ }
+ };
+
+ private static final CalculateSlide sCalculateTop = new CalculateSlideVertical() {
+ @Override
+ public float getGoneY(ViewGroup sceneRoot, View view) {
+ return view.getTranslationY() - sceneRoot.getHeight();
+ }
+ };
+
+ private static final CalculateSlide sCalculateRight = new CalculateSlideHorizontal() {
+ @Override
+ public float getGoneX(ViewGroup sceneRoot, View view) {
+ return view.getTranslationX() + sceneRoot.getWidth();
+ }
+ };
+
+ private static final CalculateSlide sCalculateEnd = new CalculateSlideHorizontal() {
+ @Override
+ public float getGoneX(ViewGroup sceneRoot, View view) {
+ final boolean isRtl = ViewCompat.getLayoutDirection(sceneRoot)
+ == ViewCompat.LAYOUT_DIRECTION_RTL;
+ final float x;
+ if (isRtl) {
+ x = view.getTranslationX() - sceneRoot.getWidth();
+ } else {
+ x = view.getTranslationX() + sceneRoot.getWidth();
+ }
+ return x;
+ }
+ };
+
+ private static final CalculateSlide sCalculateBottom = new CalculateSlideVertical() {
+ @Override
+ public float getGoneY(ViewGroup sceneRoot, View view) {
+ return view.getTranslationY() + sceneRoot.getHeight();
+ }
+ };
+
+ /**
+ * Constructor using the default {@link Gravity#BOTTOM}
+ * slide edge direction.
+ */
+ public Slide() {
+ setSlideEdge(Gravity.BOTTOM);
+ }
+
+ /**
+ * Constructor using the provided slide edge direction.
+ */
+ public Slide(int slideEdge) {
+ setSlideEdge(slideEdge);
+ }
+
+ public Slide(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = context.obtainStyledAttributes(attrs, Styleable.SLIDE);
+ int edge = TypedArrayUtils.getNamedInt(a, (XmlPullParser) attrs, "slideEdge",
+ Styleable.Slide.SLIDE_EDGE, Gravity.BOTTOM);
+ a.recycle();
+ //noinspection WrongConstant
+ setSlideEdge(edge);
+ }
+
+ private void captureValues(TransitionValues transitionValues) {
+ View view = transitionValues.view;
+ int[] position = new int[2];
+ view.getLocationOnScreen(position);
+ transitionValues.values.put(PROPNAME_SCREEN_POSITION, position);
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ super.captureStartValues(transitionValues);
+ captureValues(transitionValues);
+ }
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ super.captureEndValues(transitionValues);
+ captureValues(transitionValues);
+ }
+
+ /**
+ * Change the edge that Views appear and disappear from.
+ *
+ * @param slideEdge The edge of the scene to use for Views appearing and disappearing. One of
+ * {@link android.view.Gravity#LEFT}, {@link android.view.Gravity#TOP},
+ * {@link android.view.Gravity#RIGHT}, {@link android.view.Gravity#BOTTOM},
+ * {@link android.view.Gravity#START}, {@link android.view.Gravity#END}.
+ */
+ public void setSlideEdge(@GravityFlag int slideEdge) {
+ switch (slideEdge) {
+ case Gravity.LEFT:
+ mSlideCalculator = sCalculateLeft;
+ break;
+ case Gravity.TOP:
+ mSlideCalculator = sCalculateTop;
+ break;
+ case Gravity.RIGHT:
+ mSlideCalculator = sCalculateRight;
+ break;
+ case Gravity.BOTTOM:
+ mSlideCalculator = sCalculateBottom;
+ break;
+ case Gravity.START:
+ mSlideCalculator = sCalculateStart;
+ break;
+ case Gravity.END:
+ mSlideCalculator = sCalculateEnd;
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid slide direction");
+ }
+ mSlideEdge = slideEdge;
+ SidePropagation propagation = new SidePropagation();
+ propagation.setSide(slideEdge);
+ setPropagation(propagation);
+ }
+
+ /**
+ * Returns the edge that Views appear and disappear from.
+ *
+ * @return the edge of the scene to use for Views appearing and disappearing. One of
+ * {@link android.view.Gravity#LEFT}, {@link android.view.Gravity#TOP},
+ * {@link android.view.Gravity#RIGHT}, {@link android.view.Gravity#BOTTOM},
+ * {@link android.view.Gravity#START}, {@link android.view.Gravity#END}.
+ */
+ @GravityFlag
+ public int getSlideEdge() {
+ return mSlideEdge;
+ }
+
+ @Override
+ public Animator onAppear(ViewGroup sceneRoot, View view,
+ TransitionValues startValues, TransitionValues endValues) {
+ if (endValues == null) {
+ return null;
+ }
+ int[] position = (int[]) endValues.values.get(PROPNAME_SCREEN_POSITION);
+ float endX = view.getTranslationX();
+ float endY = view.getTranslationY();
+ float startX = mSlideCalculator.getGoneX(sceneRoot, view);
+ float startY = mSlideCalculator.getGoneY(sceneRoot, view);
+ return TranslationAnimationCreator
+ .createAnimation(view, endValues, position[0], position[1],
+ startX, startY, endX, endY, sDecelerate);
+ }
+
+ @Override
+ public Animator onDisappear(ViewGroup sceneRoot, View view,
+ TransitionValues startValues, TransitionValues endValues) {
+ if (startValues == null) {
+ return null;
+ }
+ int[] position = (int[]) startValues.values.get(PROPNAME_SCREEN_POSITION);
+ float startX = view.getTranslationX();
+ float startY = view.getTranslationY();
+ float endX = mSlideCalculator.getGoneX(sceneRoot, view);
+ float endY = mSlideCalculator.getGoneY(sceneRoot, view);
+ return TranslationAnimationCreator
+ .createAnimation(view, startValues, position[0], position[1],
+ startX, startY, endX, endY, sAccelerate);
+ }
+
+}
diff --git a/androidx/transition/SlideBadEdgeTest.java b/androidx/transition/SlideBadEdgeTest.java
new file mode 100644
index 0000000..7e7c983
--- /dev/null
+++ b/androidx/transition/SlideBadEdgeTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.junit.Assert.fail;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.Gravity;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SlideBadEdgeTest {
+
+ private static final Object[][] sBadGravity = {
+ {Gravity.AXIS_CLIP, "AXIS_CLIP"},
+ {Gravity.AXIS_PULL_AFTER, "AXIS_PULL_AFTER"},
+ {Gravity.AXIS_PULL_BEFORE, "AXIS_PULL_BEFORE"},
+ {Gravity.AXIS_SPECIFIED, "AXIS_SPECIFIED"},
+ {Gravity.AXIS_Y_SHIFT, "AXIS_Y_SHIFT"},
+ {Gravity.AXIS_X_SHIFT, "AXIS_X_SHIFT"},
+ {Gravity.CENTER, "CENTER"},
+ {Gravity.CLIP_VERTICAL, "CLIP_VERTICAL"},
+ {Gravity.CLIP_HORIZONTAL, "CLIP_HORIZONTAL"},
+ {Gravity.CENTER_VERTICAL, "CENTER_VERTICAL"},
+ {Gravity.CENTER_HORIZONTAL, "CENTER_HORIZONTAL"},
+ {Gravity.DISPLAY_CLIP_VERTICAL, "DISPLAY_CLIP_VERTICAL"},
+ {Gravity.DISPLAY_CLIP_HORIZONTAL, "DISPLAY_CLIP_HORIZONTAL"},
+ {Gravity.FILL_VERTICAL, "FILL_VERTICAL"},
+ {Gravity.FILL, "FILL"},
+ {Gravity.FILL_HORIZONTAL, "FILL_HORIZONTAL"},
+ {Gravity.HORIZONTAL_GRAVITY_MASK, "HORIZONTAL_GRAVITY_MASK"},
+ {Gravity.NO_GRAVITY, "NO_GRAVITY"},
+ {Gravity.RELATIVE_LAYOUT_DIRECTION, "RELATIVE_LAYOUT_DIRECTION"},
+ {Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK, "RELATIVE_HORIZONTAL_GRAVITY_MASK"},
+ {Gravity.VERTICAL_GRAVITY_MASK, "VERTICAL_GRAVITY_MASK"},
+ };
+
+ @Test
+ public void testBadSide() {
+ for (int i = 0; i < sBadGravity.length; i++) {
+ int badEdge = (Integer) sBadGravity[i][0];
+ String edgeName = (String) sBadGravity[i][1];
+ try {
+ new Slide(badEdge);
+ fail("Should not be able to set slide edge to " + edgeName);
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ Slide slide = new Slide();
+ slide.setSlideEdge(badEdge);
+ fail("Should not be able to set slide edge to " + edgeName);
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+ }
+
+}
diff --git a/androidx/transition/SlideDefaultEdgeTest.java b/androidx/transition/SlideDefaultEdgeTest.java
new file mode 100644
index 0000000..03b14eb
--- /dev/null
+++ b/androidx/transition/SlideDefaultEdgeTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.junit.Assert.assertEquals;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.Gravity;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SlideDefaultEdgeTest {
+
+ @Test
+ public void testDefaultSide() {
+ // default to bottom
+ Slide slide = new Slide();
+ assertEquals(Gravity.BOTTOM, slide.getSlideEdge());
+ }
+
+}
diff --git a/androidx/transition/SlideEdgeTest.java b/androidx/transition/SlideEdgeTest.java
new file mode 100644
index 0000000..8f08288
--- /dev/null
+++ b/androidx/transition/SlideEdgeTest.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.MediumTest;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.transition.test.R;
+
+import org.junit.Test;
+
+@MediumTest
+public class SlideEdgeTest extends BaseTransitionTest {
+
+ private static final Object[][] sSlideEdgeArray = {
+ {Gravity.START, "START"},
+ {Gravity.END, "END"},
+ {Gravity.LEFT, "LEFT"},
+ {Gravity.TOP, "TOP"},
+ {Gravity.RIGHT, "RIGHT"},
+ {Gravity.BOTTOM, "BOTTOM"},
+ };
+
+ @Test
+ public void testSetSide() throws Throwable {
+ for (int i = 0; i < sSlideEdgeArray.length; i++) {
+ int slideEdge = (Integer) (sSlideEdgeArray[i][0]);
+ String edgeName = (String) (sSlideEdgeArray[i][1]);
+ Slide slide = new Slide(slideEdge);
+ assertEquals("Edge not set properly in constructor " + edgeName,
+ slideEdge, slide.getSlideEdge());
+
+ slide = new Slide();
+ slide.setSlideEdge(slideEdge);
+ assertEquals("Edge not set properly with setter " + edgeName,
+ slideEdge, slide.getSlideEdge());
+ }
+ }
+
+ @LargeTest
+ @Test
+ public void testSlideOut() throws Throwable {
+ for (int i = 0; i < sSlideEdgeArray.length; i++) {
+ final int slideEdge = (Integer) (sSlideEdgeArray[i][0]);
+ final Slide slide = new Slide(slideEdge);
+ final Transition.TransitionListener listener =
+ mock(Transition.TransitionListener.class);
+ slide.addListener(listener);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ rule.getActivity().setContentView(R.layout.scene1);
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ final View redSquare = rule.getActivity().findViewById(R.id.redSquare);
+ final View greenSquare = rule.getActivity().findViewById(R.id.greenSquare);
+ final View hello = rule.getActivity().findViewById(R.id.hello);
+ final ViewGroup sceneRoot = (ViewGroup) rule.getActivity().findViewById(R.id.holder);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(sceneRoot, slide);
+ redSquare.setVisibility(View.INVISIBLE);
+ greenSquare.setVisibility(View.INVISIBLE);
+ hello.setVisibility(View.INVISIBLE);
+ }
+ });
+ verify(listener, timeout(1000)).onTransitionStart(any(Transition.class));
+ verify(listener, never()).onTransitionEnd(any(Transition.class));
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(View.VISIBLE, greenSquare.getVisibility());
+ assertEquals(View.VISIBLE, hello.getVisibility());
+
+ float redStartX = redSquare.getTranslationX();
+ float redStartY = redSquare.getTranslationY();
+
+ Thread.sleep(200);
+ verifyTranslation(slideEdge, redSquare);
+ verifyTranslation(slideEdge, greenSquare);
+ verifyTranslation(slideEdge, hello);
+
+ final float redMidX = redSquare.getTranslationX();
+ final float redMidY = redSquare.getTranslationY();
+
+ switch (slideEdge) {
+ case Gravity.LEFT:
+ case Gravity.START:
+ assertTrue(
+ "isn't sliding out to left. Expecting " + redStartX + " > " + redMidX,
+ redStartX > redMidX);
+ break;
+ case Gravity.RIGHT:
+ case Gravity.END:
+ assertTrue(
+ "isn't sliding out to right. Expecting " + redStartX + " < " + redMidX,
+ redStartX < redMidX);
+ break;
+ case Gravity.TOP:
+ assertTrue("isn't sliding out to top. Expecting " + redStartY + " > " + redMidY,
+ redStartY > redSquare.getTranslationY());
+ break;
+ case Gravity.BOTTOM:
+ assertTrue(
+ "isn't sliding out to bottom. Expecting " + redStartY + " < " + redMidY,
+ redStartY < redSquare.getTranslationY());
+ break;
+ }
+ verify(listener, timeout(1000)).onTransitionEnd(any(Transition.class));
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ verifyNoTranslation(redSquare);
+ verifyNoTranslation(greenSquare);
+ verifyNoTranslation(hello);
+ assertEquals(View.INVISIBLE, redSquare.getVisibility());
+ assertEquals(View.INVISIBLE, greenSquare.getVisibility());
+ assertEquals(View.INVISIBLE, hello.getVisibility());
+ }
+ }
+
+ @LargeTest
+ @Test
+ public void testSlideIn() throws Throwable {
+ for (int i = 0; i < sSlideEdgeArray.length; i++) {
+ final int slideEdge = (Integer) (sSlideEdgeArray[i][0]);
+ final Slide slide = new Slide(slideEdge);
+ final Transition.TransitionListener listener =
+ mock(Transition.TransitionListener.class);
+ slide.addListener(listener);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ rule.getActivity().setContentView(R.layout.scene1);
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ final View redSquare = rule.getActivity().findViewById(R.id.redSquare);
+ final View greenSquare = rule.getActivity().findViewById(R.id.greenSquare);
+ final View hello = rule.getActivity().findViewById(R.id.hello);
+ final ViewGroup sceneRoot = (ViewGroup) rule.getActivity().findViewById(R.id.holder);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ redSquare.setVisibility(View.INVISIBLE);
+ greenSquare.setVisibility(View.INVISIBLE);
+ hello.setVisibility(View.INVISIBLE);
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ // now slide in
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(sceneRoot, slide);
+ redSquare.setVisibility(View.VISIBLE);
+ greenSquare.setVisibility(View.VISIBLE);
+ hello.setVisibility(View.VISIBLE);
+ }
+ });
+ verify(listener, timeout(1000)).onTransitionStart(any(Transition.class));
+
+ verify(listener, never()).onTransitionEnd(any(Transition.class));
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(View.VISIBLE, greenSquare.getVisibility());
+ assertEquals(View.VISIBLE, hello.getVisibility());
+
+ final float redStartX = redSquare.getTranslationX();
+ final float redStartY = redSquare.getTranslationY();
+
+ Thread.sleep(200);
+ verifyTranslation(slideEdge, redSquare);
+ verifyTranslation(slideEdge, greenSquare);
+ verifyTranslation(slideEdge, hello);
+ final float redMidX = redSquare.getTranslationX();
+ final float redMidY = redSquare.getTranslationY();
+
+ switch (slideEdge) {
+ case Gravity.LEFT:
+ case Gravity.START:
+ assertTrue(
+ "isn't sliding in from left. Expecting " + redStartX + " < " + redMidX,
+ redStartX < redMidX);
+ break;
+ case Gravity.RIGHT:
+ case Gravity.END:
+ assertTrue(
+ "isn't sliding in from right. Expecting " + redStartX + " > " + redMidX,
+ redStartX > redMidX);
+ break;
+ case Gravity.TOP:
+ assertTrue(
+ "isn't sliding in from top. Expecting " + redStartY + " < " + redMidY,
+ redStartY < redSquare.getTranslationY());
+ break;
+ case Gravity.BOTTOM:
+ assertTrue("isn't sliding in from bottom. Expecting " + redStartY + " > "
+ + redMidY,
+ redStartY > redSquare.getTranslationY());
+ break;
+ }
+ verify(listener, timeout(1000)).onTransitionEnd(any(Transition.class));
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ verifyNoTranslation(redSquare);
+ verifyNoTranslation(greenSquare);
+ verifyNoTranslation(hello);
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(View.VISIBLE, greenSquare.getVisibility());
+ assertEquals(View.VISIBLE, hello.getVisibility());
+ }
+ }
+
+ private void verifyTranslation(int slideEdge, View view) {
+ switch (slideEdge) {
+ case Gravity.LEFT:
+ case Gravity.START:
+ assertTrue(view.getTranslationX() < 0);
+ assertEquals(0f, view.getTranslationY(), 0.01f);
+ break;
+ case Gravity.RIGHT:
+ case Gravity.END:
+ assertTrue(view.getTranslationX() > 0);
+ assertEquals(0f, view.getTranslationY(), 0.01f);
+ break;
+ case Gravity.TOP:
+ assertTrue(view.getTranslationY() < 0);
+ assertEquals(0f, view.getTranslationX(), 0.01f);
+ break;
+ case Gravity.BOTTOM:
+ assertTrue(view.getTranslationY() > 0);
+ assertEquals(0f, view.getTranslationX(), 0.01f);
+ break;
+ }
+ }
+
+ private void verifyNoTranslation(View view) {
+ assertEquals(0f, view.getTranslationX(), 0.01f);
+ assertEquals(0f, view.getTranslationY(), 0.01f);
+ }
+
+}
diff --git a/androidx/transition/Styleable.java b/androidx/transition/Styleable.java
new file mode 100644
index 0000000..40f3cca
--- /dev/null
+++ b/androidx/transition/Styleable.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.StyleableRes;
+
+/**
+ * Copies of styleable ID values generated in the platform R.java.
+ */
+@SuppressLint("InlinedApi")
+class Styleable {
+
+ @StyleableRes
+ static final int[] TRANSITION_TARGET = {
+ android.R.attr.targetClass,
+ android.R.attr.targetId,
+ android.R.attr.excludeId,
+ android.R.attr.excludeClass,
+ android.R.attr.targetName,
+ android.R.attr.excludeName,
+ };
+
+ interface TransitionTarget {
+ @StyleableRes
+ int TARGET_CLASS = 0;
+ @StyleableRes
+ int TARGET_ID = 1;
+ @StyleableRes
+ int EXCLUDE_ID = 2;
+ @StyleableRes
+ int EXCLUDE_CLASS = 3;
+ @StyleableRes
+ int TARGET_NAME = 4;
+ @StyleableRes
+ int EXCLUDE_NAME = 5;
+ }
+
+ @StyleableRes
+ static final int[] TRANSITION_MANAGER = {
+ android.R.attr.fromScene,
+ android.R.attr.toScene,
+ android.R.attr.transition,
+ };
+
+ interface TransitionManager {
+ @StyleableRes
+ int FROM_SCENE = 0;
+ @StyleableRes
+ int TO_SCENE = 1;
+ @StyleableRes
+ int TRANSITION = 2;
+ }
+
+ @StyleableRes
+ static final int[] TRANSITION = {
+ android.R.attr.interpolator,
+ android.R.attr.duration,
+ android.R.attr.startDelay,
+ android.R.attr.matchOrder,
+ };
+
+ interface Transition {
+ @StyleableRes
+ int INTERPOLATOR = 0;
+ @StyleableRes
+ int DURATION = 1;
+ @StyleableRes
+ int START_DELAY = 2;
+ @StyleableRes
+ int MATCH_ORDER = 3;
+ }
+
+ @StyleableRes
+ static final int[] CHANGE_BOUNDS = {
+ android.R.attr.resizeClip,
+ };
+
+ interface ChangeBounds {
+ @StyleableRes
+ int RESIZE_CLIP = 0;
+ }
+
+ @StyleableRes
+ static final int[] VISIBILITY_TRANSITION = {
+ android.R.attr.transitionVisibilityMode,
+ };
+
+ interface VisibilityTransition {
+ @StyleableRes
+ int TRANSITION_VISIBILITY_MODE = 0;
+ }
+
+ @StyleableRes
+ static final int[] FADE = {
+ android.R.attr.fadingMode,
+ };
+
+ interface Fade {
+ @StyleableRes
+ int FADING_MODE = 0;
+ }
+
+ @StyleableRes
+ static final int[] CHANGE_TRANSFORM = {
+ android.R.attr.reparent,
+ android.R.attr.reparentWithOverlay,
+ };
+
+ interface ChangeTransform {
+ @StyleableRes
+ int REPARENT = 0;
+ @StyleableRes
+ int REPARENT_WITH_OVERLAY = 1;
+ }
+
+ @StyleableRes
+ static final int[] SLIDE = {
+ android.R.attr.slideEdge,
+ };
+
+ interface Slide {
+ @StyleableRes
+ int SLIDE_EDGE = 0;
+ }
+
+ @StyleableRes
+ static final int[] TRANSITION_SET = {
+ android.R.attr.transitionOrdering,
+ };
+
+ interface TransitionSet {
+ @StyleableRes
+ int TRANSITION_ORDERING = 0;
+ }
+
+ @StyleableRes
+ static final int[] ARC_MOTION = {
+ android.R.attr.minimumHorizontalAngle,
+ android.R.attr.minimumVerticalAngle,
+ android.R.attr.maximumAngle,
+ };
+
+ interface ArcMotion {
+ @StyleableRes
+ int MINIMUM_HORIZONTAL_ANGLE = 0;
+ @StyleableRes
+ int MINIMUM_VERTICAL_ANGLE = 1;
+ @StyleableRes
+ int MAXIMUM_ANGLE = 2;
+ }
+
+ @StyleableRes
+ static final int[] PATTERN_PATH_MOTION = {
+ android.R.attr.patternPathData,
+ };
+
+ interface PatternPathMotion {
+ @StyleableRes
+ int PATTERN_PATH_DATA = 0;
+ }
+
+ private Styleable() {
+ }
+}
diff --git a/androidx/transition/SyncRunnable.java b/androidx/transition/SyncRunnable.java
new file mode 100644
index 0000000..0cd391a
--- /dev/null
+++ b/androidx/transition/SyncRunnable.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+class SyncRunnable implements Runnable {
+
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+
+ @Override
+ public void run() {
+ mLatch.countDown();
+ }
+
+ boolean await() {
+ try {
+ return mLatch.await(3000, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ return false;
+ }
+
+}
diff --git a/androidx/transition/SyncTransitionListener.java b/androidx/transition/SyncTransitionListener.java
new file mode 100644
index 0000000..204c05f
--- /dev/null
+++ b/androidx/transition/SyncTransitionListener.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import androidx.annotation.NonNull;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This {@link Transition.TransitionListener} synchronously waits for the specified callback.
+ */
+class SyncTransitionListener implements Transition.TransitionListener {
+
+ static final int EVENT_START = 1;
+ static final int EVENT_END = 2;
+ static final int EVENT_CANCEL = 3;
+ static final int EVENT_PAUSE = 4;
+ static final int EVENT_RESUME = 5;
+
+ private final int mTargetEvent;
+ private CountDownLatch mLatch = new CountDownLatch(1);
+
+ SyncTransitionListener(int event) {
+ mTargetEvent = event;
+ }
+
+ boolean await() {
+ try {
+ return mLatch.await(3000, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ return false;
+ }
+ }
+
+ void reset() {
+ mLatch = new CountDownLatch(1);
+ }
+
+ @Override
+ public void onTransitionStart(@NonNull Transition transition) {
+ if (mTargetEvent == EVENT_START) {
+ mLatch.countDown();
+ }
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ if (mTargetEvent == EVENT_END) {
+ mLatch.countDown();
+ }
+ }
+
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ if (mTargetEvent == EVENT_CANCEL) {
+ mLatch.countDown();
+ }
+ }
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ if (mTargetEvent == EVENT_PAUSE) {
+ mLatch.countDown();
+ }
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ if (mTargetEvent == EVENT_RESUME) {
+ mLatch.countDown();
+ }
+ }
+}
diff --git a/androidx/transition/Transition.java b/androidx/transition/Transition.java
new file mode 100644
index 0000000..621da2f
--- /dev/null
+++ b/androidx/transition/Transition.java
@@ -0,0 +1,2438 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.view.InflateException;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.widget.ListView;
+import android.widget.Spinner;
+
+import androidx.annotation.IdRes;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArrayMap;
+import androidx.collection.LongSparseArray;
+import androidx.core.content.res.TypedArrayUtils;
+import androidx.core.view.ViewCompat;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * A Transition holds information about animations that will be run on its
+ * targets during a scene change. Subclasses of this abstract class may
+ * choreograph several child transitions ({@link TransitionSet} or they may
+ * perform custom animations themselves. Any Transition has two main jobs:
+ * (1) capture property values, and (2) play animations based on changes to
+ * captured property values. A custom transition knows what property values
+ * on View objects are of interest to it, and also knows how to animate
+ * changes to those values. For example, the {@link Fade} transition tracks
+ * changes to visibility-related properties and is able to construct and run
+ * animations that fade items in or out based on changes to those properties.
+ *
+ * <p>Note: Transitions may not work correctly with either {@link SurfaceView}
+ * or {@link TextureView}, due to the way that these views are displayed
+ * on the screen. For SurfaceView, the problem is that the view is updated from
+ * a non-UI thread, so changes to the view due to transitions (such as moving
+ * and resizing the view) may be out of sync with the display inside those bounds.
+ * TextureView is more compatible with transitions in general, but some
+ * specific transitions (such as {@link Fade}) may not be compatible
+ * with TextureView because they rely on {@link android.view.ViewOverlay}
+ * functionality, which does not currently work with TextureView.</p>
+ *
+ * <p>Transitions can be declared in XML resource files inside the <code>res/transition</code>
+ * directory. Transition resources consist of a tag name for one of the Transition
+ * subclasses along with attributes to define some of the attributes of that transition.
+ * For example, here is a minimal resource file that declares a {@link ChangeBounds}
+ * transition:</p>
+ *
+ * <pre>
+ * <changeBounds/>
+ * </pre>
+ *
+ * <p>Note that attributes for the transition are not required, just as they are
+ * optional when declared in code; Transitions created from XML resources will use
+ * the same defaults as their code-created equivalents. Here is a slightly more
+ * elaborate example which declares a {@link TransitionSet} transition with
+ * {@link ChangeBounds} and {@link Fade} child transitions:</p>
+ *
+ * <pre>
+ * <transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:transitionOrdering="sequential">
+ * <changeBounds/>
+ * <fade android:fadingMode="fade_out">
+ * <targets>
+ * <target android:targetId="@id/grayscaleContainer"/>
+ * </targets>
+ * </fade>
+ * </transitionSet>
+ * </pre>
+ *
+ * <p>In this example, the transitionOrdering attribute is used on the TransitionSet
+ * object to change from the default {@link TransitionSet#ORDERING_TOGETHER} behavior
+ * to be {@link TransitionSet#ORDERING_SEQUENTIAL} instead. Also, the {@link Fade}
+ * transition uses a fadingMode of {@link Fade#OUT} instead of the default
+ * out-in behavior. Finally, note the use of the <code>targets</code> sub-tag, which
+ * takes a set of {code target} tags, each of which lists a specific <code>targetId</code> which
+ * this transition acts upon. Use of targets is optional, but can be used to either limit the time
+ * spent checking attributes on unchanging views, or limiting the types of animations run on
+ * specific views. In this case, we know that only the <code>grayscaleContainer</code> will be
+ * disappearing, so we choose to limit the {@link Fade} transition to only that view.</p>
+ */
+public abstract class Transition implements Cloneable {
+
+ private static final String LOG_TAG = "Transition";
+ static final boolean DBG = false;
+
+ /**
+ * With {@link #setMatchOrder(int...)}, chooses to match by View instance.
+ */
+ public static final int MATCH_INSTANCE = 0x1;
+ private static final int MATCH_FIRST = MATCH_INSTANCE;
+
+ /**
+ * With {@link #setMatchOrder(int...)}, chooses to match by
+ * {@link android.view.View#getTransitionName()}. Null names will not be matched.
+ */
+ public static final int MATCH_NAME = 0x2;
+
+ /**
+ * With {@link #setMatchOrder(int...)}, chooses to match by
+ * {@link android.view.View#getId()}. Negative IDs will not be matched.
+ */
+ public static final int MATCH_ID = 0x3;
+
+ /**
+ * With {@link #setMatchOrder(int...)}, chooses to match by the {@link android.widget.Adapter}
+ * item id. When {@link android.widget.Adapter#hasStableIds()} returns false, no match
+ * will be made for items.
+ */
+ public static final int MATCH_ITEM_ID = 0x4;
+
+ private static final int MATCH_LAST = MATCH_ITEM_ID;
+
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({MATCH_INSTANCE, MATCH_NAME, MATCH_ID, MATCH_ITEM_ID})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface MatchOrder {
+ }
+
+ private static final String MATCH_INSTANCE_STR = "instance";
+ private static final String MATCH_NAME_STR = "name";
+ private static final String MATCH_ID_STR = "id";
+ private static final String MATCH_ITEM_ID_STR = "itemId";
+
+ private static final int[] DEFAULT_MATCH_ORDER = {
+ MATCH_NAME,
+ MATCH_INSTANCE,
+ MATCH_ID,
+ MATCH_ITEM_ID,
+ };
+
+ private static final PathMotion STRAIGHT_PATH_MOTION = new PathMotion() {
+ @Override
+ public Path getPath(float startX, float startY, float endX, float endY) {
+ Path path = new Path();
+ path.moveTo(startX, startY);
+ path.lineTo(endX, endY);
+ return path;
+ }
+ };
+
+ private String mName = getClass().getName();
+
+ private long mStartDelay = -1;
+ long mDuration = -1;
+ private TimeInterpolator mInterpolator = null;
+ ArrayList<Integer> mTargetIds = new ArrayList<>();
+ ArrayList<View> mTargets = new ArrayList<>();
+ private ArrayList<String> mTargetNames = null;
+ private ArrayList<Class> mTargetTypes = null;
+ private ArrayList<Integer> mTargetIdExcludes = null;
+ private ArrayList<View> mTargetExcludes = null;
+ private ArrayList<Class> mTargetTypeExcludes = null;
+ private ArrayList<String> mTargetNameExcludes = null;
+ private ArrayList<Integer> mTargetIdChildExcludes = null;
+ private ArrayList<View> mTargetChildExcludes = null;
+ private ArrayList<Class> mTargetTypeChildExcludes = null;
+ private TransitionValuesMaps mStartValues = new TransitionValuesMaps();
+ private TransitionValuesMaps mEndValues = new TransitionValuesMaps();
+ TransitionSet mParent = null;
+ private int[] mMatchOrder = DEFAULT_MATCH_ORDER;
+ private ArrayList<TransitionValues> mStartValuesList; // only valid after playTransition starts
+ private ArrayList<TransitionValues> mEndValuesList; // only valid after playTransitions starts
+
+ // Per-animator information used for later canceling when future transitions overlap
+ private static ThreadLocal<ArrayMap<Animator, Transition.AnimationInfo>> sRunningAnimators =
+ new ThreadLocal<>();
+
+ // Scene Root is set at createAnimator() time in the cloned Transition
+ private ViewGroup mSceneRoot = null;
+
+ // Whether removing views from their parent is possible. This is only for views
+ // in the start scene, which are no longer in the view hierarchy. This property
+ // is determined by whether the previous Scene was created from a layout
+ // resource, and thus the views from the exited scene are going away anyway
+ // and can be removed as necessary to achieve a particular effect, such as
+ // removing them from parents to add them to overlays.
+ boolean mCanRemoveViews = false;
+
+ // Track all animators in use in case the transition gets canceled and needs to
+ // cancel running animators
+ private ArrayList<Animator> mCurrentAnimators = new ArrayList<>();
+
+ // Number of per-target instances of this Transition currently running. This count is
+ // determined by calls to start() and end()
+ private int mNumInstances = 0;
+
+ // Whether this transition is currently paused, due to a call to pause()
+ private boolean mPaused = false;
+
+ // Whether this transition has ended. Used to avoid pause/resume on transitions
+ // that have completed
+ private boolean mEnded = false;
+
+ // The set of listeners to be sent transition lifecycle events.
+ private ArrayList<Transition.TransitionListener> mListeners = null;
+
+ // The set of animators collected from calls to createAnimator(),
+ // to be run in runAnimators()
+ private ArrayList<Animator> mAnimators = new ArrayList<>();
+
+ // The function for calculating the Animation start delay.
+ TransitionPropagation mPropagation;
+
+ // The rectangular region for Transitions like Explode and TransitionPropagations
+ // like CircularPropagation
+ private EpicenterCallback mEpicenterCallback;
+
+ // For Fragment shared element transitions, linking views explicitly by mismatching
+ // transitionNames.
+ private ArrayMap<String, String> mNameOverrides;
+
+ // The function used to interpolate along two-dimensional points. Typically used
+ // for adding curves to x/y View motion.
+ private PathMotion mPathMotion = STRAIGHT_PATH_MOTION;
+
+ /**
+ * Constructs a Transition object with no target objects. A transition with
+ * no targets defaults to running on all target objects in the scene hierarchy
+ * (if the transition is not contained in a TransitionSet), or all target
+ * objects passed down from its parent (if it is in a TransitionSet).
+ */
+ public Transition() {
+ }
+
+ /**
+ * Perform inflation from XML and apply a class-specific base style from a
+ * theme attribute or style resource. This constructor of Transition allows
+ * subclasses to use their own base style when they are inflating.
+ *
+ * @param context The Context the transition is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the transition.
+ */
+ public Transition(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs, Styleable.TRANSITION);
+ XmlResourceParser parser = (XmlResourceParser) attrs;
+ long duration = TypedArrayUtils.getNamedInt(a, parser, "duration",
+ Styleable.Transition.DURATION, -1);
+ if (duration >= 0) {
+ setDuration(duration);
+ }
+ long startDelay = TypedArrayUtils.getNamedInt(a, parser, "startDelay",
+ Styleable.Transition.START_DELAY, -1);
+ if (startDelay > 0) {
+ setStartDelay(startDelay);
+ }
+ final int resId = TypedArrayUtils.getNamedResourceId(a, parser, "interpolator",
+ Styleable.Transition.INTERPOLATOR, 0);
+ if (resId > 0) {
+ setInterpolator(AnimationUtils.loadInterpolator(context, resId));
+ }
+ String matchOrder = TypedArrayUtils.getNamedString(a, parser, "matchOrder",
+ Styleable.Transition.MATCH_ORDER);
+ if (matchOrder != null) {
+ setMatchOrder(parseMatchOrder(matchOrder));
+ }
+ a.recycle();
+ }
+
+ @MatchOrder
+ private static int[] parseMatchOrder(String matchOrderString) {
+ StringTokenizer st = new StringTokenizer(matchOrderString, ",");
+ @MatchOrder
+ int[] matches = new int[st.countTokens()];
+ int index = 0;
+ while (st.hasMoreTokens()) {
+ String token = st.nextToken().trim();
+ if (MATCH_ID_STR.equalsIgnoreCase(token)) {
+ matches[index] = Transition.MATCH_ID;
+ } else if (MATCH_INSTANCE_STR.equalsIgnoreCase(token)) {
+ matches[index] = Transition.MATCH_INSTANCE;
+ } else if (MATCH_NAME_STR.equalsIgnoreCase(token)) {
+ matches[index] = Transition.MATCH_NAME;
+ } else if (MATCH_ITEM_ID_STR.equalsIgnoreCase(token)) {
+ matches[index] = Transition.MATCH_ITEM_ID;
+ } else if (token.isEmpty()) {
+ @MatchOrder
+ int[] smallerMatches = new int[matches.length - 1];
+ System.arraycopy(matches, 0, smallerMatches, 0, index);
+ matches = smallerMatches;
+ index--;
+ } else {
+ throw new InflateException("Unknown match type in matchOrder: '" + token + "'");
+ }
+ index++;
+ }
+ return matches;
+ }
+
+ /**
+ * Sets the duration of this transition. By default, there is no duration
+ * (indicated by a negative number), which means that the Animator created by
+ * the transition will have its own specified duration. If the duration of a
+ * Transition is set, that duration will override the Animator duration.
+ *
+ * @param duration The length of the animation, in milliseconds.
+ * @return This transition object.
+ */
+ @NonNull
+ public Transition setDuration(long duration) {
+ mDuration = duration;
+ return this;
+ }
+
+ /**
+ * Returns the duration set on this transition. If no duration has been set,
+ * the returned value will be negative, indicating that resulting animators will
+ * retain their own durations.
+ *
+ * @return The duration set on this transition, in milliseconds, if one has been
+ * set, otherwise returns a negative number.
+ */
+ public long getDuration() {
+ return mDuration;
+ }
+
+ /**
+ * Sets the startDelay of this transition. By default, there is no delay
+ * (indicated by a negative number), which means that the Animator created by
+ * the transition will have its own specified startDelay. If the delay of a
+ * Transition is set, that delay will override the Animator delay.
+ *
+ * @param startDelay The length of the delay, in milliseconds.
+ * @return This transition object.
+ */
+ @NonNull
+ public Transition setStartDelay(long startDelay) {
+ mStartDelay = startDelay;
+ return this;
+ }
+
+ /**
+ * Returns the startDelay set on this transition. If no startDelay has been set,
+ * the returned value will be negative, indicating that resulting animators will
+ * retain their own startDelays.
+ *
+ * @return The startDelay set on this transition, in milliseconds, if one has
+ * been set, otherwise returns a negative number.
+ */
+ public long getStartDelay() {
+ return mStartDelay;
+ }
+
+ /**
+ * Sets the interpolator of this transition. By default, the interpolator
+ * is null, which means that the Animator created by the transition
+ * will have its own specified interpolator. If the interpolator of a
+ * Transition is set, that interpolator will override the Animator interpolator.
+ *
+ * @param interpolator The time interpolator used by the transition
+ * @return This transition object.
+ */
+ @NonNull
+ public Transition setInterpolator(@Nullable TimeInterpolator interpolator) {
+ mInterpolator = interpolator;
+ return this;
+ }
+
+ /**
+ * Returns the interpolator set on this transition. If no interpolator has been set,
+ * the returned value will be null, indicating that resulting animators will
+ * retain their own interpolators.
+ *
+ * @return The interpolator set on this transition, if one has been set, otherwise
+ * returns null.
+ */
+ @Nullable
+ public TimeInterpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ /**
+ * Returns the set of property names used stored in the {@link TransitionValues}
+ * object passed into {@link #captureStartValues(TransitionValues)} that
+ * this transition cares about for the purposes of canceling overlapping animations.
+ * When any transition is started on a given scene root, all transitions
+ * currently running on that same scene root are checked to see whether the
+ * properties on which they based their animations agree with the end values of
+ * the same properties in the new transition. If the end values are not equal,
+ * then the old animation is canceled since the new transition will start a new
+ * animation to these new values. If the values are equal, the old animation is
+ * allowed to continue and no new animation is started for that transition.
+ *
+ * <p>A transition does not need to override this method. However, not doing so
+ * will mean that the cancellation logic outlined in the previous paragraph
+ * will be skipped for that transition, possibly leading to artifacts as
+ * old transitions and new transitions on the same targets run in parallel,
+ * animating views toward potentially different end values.</p>
+ *
+ * @return An array of property names as described in the class documentation for
+ * {@link TransitionValues}. The default implementation returns <code>null</code>.
+ */
+ @Nullable
+ public String[] getTransitionProperties() {
+ return null;
+ }
+
+ /**
+ * This method creates an animation that will be run for this transition
+ * given the information in the startValues and endValues structures captured
+ * earlier for the start and end scenes. Subclasses of Transition should override
+ * this method. The method should only be called by the transition system; it is
+ * not intended to be called from external classes.
+ *
+ * <p>This method is called by the transition's parent (all the way up to the
+ * topmost Transition in the hierarchy) with the sceneRoot and start/end
+ * values that the transition may need to set up initial target values
+ * and construct an appropriate animation. For example, if an overall
+ * Transition is a {@link TransitionSet} consisting of several
+ * child transitions in sequence, then some of the child transitions may
+ * want to set initial values on target views prior to the overall
+ * Transition commencing, to put them in an appropriate state for the
+ * delay between that start and the child Transition start time. For
+ * example, a transition that fades an item in may wish to set the starting
+ * alpha value to 0, to avoid it blinking in prior to the transition
+ * actually starting the animation. This is necessary because the scene
+ * change that triggers the Transition will automatically set the end-scene
+ * on all target views, so a Transition that wants to animate from a
+ * different value should set that value prior to returning from this method.</p>
+ *
+ * <p>Additionally, a Transition can perform logic to determine whether
+ * the transition needs to run on the given target and start/end values.
+ * For example, a transition that resizes objects on the screen may wish
+ * to avoid running for views which are not present in either the start
+ * or end scenes.</p>
+ *
+ * <p>If there is an animator created and returned from this method, the
+ * transition mechanism will apply any applicable duration, startDelay,
+ * and interpolator to that animation and start it. A return value of
+ * <code>null</code> indicates that no animation should run. The default
+ * implementation returns null.</p>
+ *
+ * <p>The method is called for every applicable target object, which is
+ * stored in the {@link TransitionValues#view} field.</p>
+ *
+ * @param sceneRoot The root of the transition hierarchy.
+ * @param startValues The values for a specific target in the start scene.
+ * @param endValues The values for the target in the end scene.
+ * @return A Animator to be started at the appropriate time in the
+ * overall transition for this scene change. A null value means no animation
+ * should be run.
+ */
+ @Nullable
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot,
+ @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) {
+ return null;
+ }
+
+ /**
+ * Sets the order in which Transition matches View start and end values.
+ * <p>
+ * The default behavior is to match first by {@link android.view.View#getTransitionName()},
+ * then by View instance, then by {@link android.view.View#getId()} and finally
+ * by its item ID if it is in a direct child of ListView. The caller can
+ * choose to have only some or all of the values of {@link #MATCH_INSTANCE},
+ * {@link #MATCH_NAME}, {@link #MATCH_ITEM_ID}, and {@link #MATCH_ID}. Only
+ * the match algorithms supplied will be used to determine whether Views are the
+ * the same in both the start and end Scene. Views that do not match will be considered
+ * as entering or leaving the Scene.
+ * </p>
+ *
+ * @param matches A list of zero or more of {@link #MATCH_INSTANCE},
+ * {@link #MATCH_NAME}, {@link #MATCH_ITEM_ID}, and {@link #MATCH_ID}.
+ * If none are provided, then the default match order will be set.
+ */
+ public void setMatchOrder(@MatchOrder int... matches) {
+ if (matches == null || matches.length == 0) {
+ mMatchOrder = DEFAULT_MATCH_ORDER;
+ } else {
+ for (int i = 0; i < matches.length; i++) {
+ int match = matches[i];
+ if (!isValidMatch(match)) {
+ throw new IllegalArgumentException("matches contains invalid value");
+ }
+ if (alreadyContains(matches, i)) {
+ throw new IllegalArgumentException("matches contains a duplicate value");
+ }
+ }
+ mMatchOrder = matches.clone();
+ }
+ }
+
+ private static boolean isValidMatch(int match) {
+ return (match >= MATCH_FIRST && match <= MATCH_LAST);
+ }
+
+ private static boolean alreadyContains(int[] array, int searchIndex) {
+ int value = array[searchIndex];
+ for (int i = 0; i < searchIndex; i++) {
+ if (array[i] == value) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Match start/end values by View instance. Adds matched values to mStartValuesList
+ * and mEndValuesList and removes them from unmatchedStart and unmatchedEnd.
+ */
+ private void matchInstances(ArrayMap<View, TransitionValues> unmatchedStart,
+ ArrayMap<View, TransitionValues> unmatchedEnd) {
+ for (int i = unmatchedStart.size() - 1; i >= 0; i--) {
+ View view = unmatchedStart.keyAt(i);
+ if (view != null && isValidTarget(view)) {
+ TransitionValues end = unmatchedEnd.remove(view);
+ if (end != null && end.view != null && isValidTarget(end.view)) {
+ TransitionValues start = unmatchedStart.removeAt(i);
+ mStartValuesList.add(start);
+ mEndValuesList.add(end);
+ }
+ }
+ }
+ }
+
+ /**
+ * Match start/end values by Adapter item ID. Adds matched values to mStartValuesList
+ * and mEndValuesList and removes them from unmatchedStart and unmatchedEnd, using
+ * startItemIds and endItemIds as a guide for which Views have unique item IDs.
+ */
+ private void matchItemIds(ArrayMap<View, TransitionValues> unmatchedStart,
+ ArrayMap<View, TransitionValues> unmatchedEnd,
+ LongSparseArray<View> startItemIds, LongSparseArray<View> endItemIds) {
+ int numStartIds = startItemIds.size();
+ for (int i = 0; i < numStartIds; i++) {
+ View startView = startItemIds.valueAt(i);
+ if (startView != null && isValidTarget(startView)) {
+ View endView = endItemIds.get(startItemIds.keyAt(i));
+ if (endView != null && isValidTarget(endView)) {
+ TransitionValues startValues = unmatchedStart.get(startView);
+ TransitionValues endValues = unmatchedEnd.get(endView);
+ if (startValues != null && endValues != null) {
+ mStartValuesList.add(startValues);
+ mEndValuesList.add(endValues);
+ unmatchedStart.remove(startView);
+ unmatchedEnd.remove(endView);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Match start/end values by Adapter view ID. Adds matched values to mStartValuesList
+ * and mEndValuesList and removes them from unmatchedStart and unmatchedEnd, using
+ * startIds and endIds as a guide for which Views have unique IDs.
+ */
+ private void matchIds(ArrayMap<View, TransitionValues> unmatchedStart,
+ ArrayMap<View, TransitionValues> unmatchedEnd,
+ SparseArray<View> startIds, SparseArray<View> endIds) {
+ int numStartIds = startIds.size();
+ for (int i = 0; i < numStartIds; i++) {
+ View startView = startIds.valueAt(i);
+ if (startView != null && isValidTarget(startView)) {
+ View endView = endIds.get(startIds.keyAt(i));
+ if (endView != null && isValidTarget(endView)) {
+ TransitionValues startValues = unmatchedStart.get(startView);
+ TransitionValues endValues = unmatchedEnd.get(endView);
+ if (startValues != null && endValues != null) {
+ mStartValuesList.add(startValues);
+ mEndValuesList.add(endValues);
+ unmatchedStart.remove(startView);
+ unmatchedEnd.remove(endView);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Match start/end values by Adapter transitionName. Adds matched values to mStartValuesList
+ * and mEndValuesList and removes them from unmatchedStart and unmatchedEnd, using
+ * startNames and endNames as a guide for which Views have unique transitionNames.
+ */
+ private void matchNames(ArrayMap<View, TransitionValues> unmatchedStart,
+ ArrayMap<View, TransitionValues> unmatchedEnd,
+ ArrayMap<String, View> startNames, ArrayMap<String, View> endNames) {
+ int numStartNames = startNames.size();
+ for (int i = 0; i < numStartNames; i++) {
+ View startView = startNames.valueAt(i);
+ if (startView != null && isValidTarget(startView)) {
+ View endView = endNames.get(startNames.keyAt(i));
+ if (endView != null && isValidTarget(endView)) {
+ TransitionValues startValues = unmatchedStart.get(startView);
+ TransitionValues endValues = unmatchedEnd.get(endView);
+ if (startValues != null && endValues != null) {
+ mStartValuesList.add(startValues);
+ mEndValuesList.add(endValues);
+ unmatchedStart.remove(startView);
+ unmatchedEnd.remove(endView);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds all values from unmatchedStart and unmatchedEnd to mStartValuesList and mEndValuesList,
+ * assuming that there is no match between values in the list.
+ */
+ private void addUnmatched(ArrayMap<View, TransitionValues> unmatchedStart,
+ ArrayMap<View, TransitionValues> unmatchedEnd) {
+ // Views that only exist in the start Scene
+ for (int i = 0; i < unmatchedStart.size(); i++) {
+ final TransitionValues start = unmatchedStart.valueAt(i);
+ if (isValidTarget(start.view)) {
+ mStartValuesList.add(start);
+ mEndValuesList.add(null);
+ }
+ }
+
+ // Views that only exist in the end Scene
+ for (int i = 0; i < unmatchedEnd.size(); i++) {
+ final TransitionValues end = unmatchedEnd.valueAt(i);
+ if (isValidTarget(end.view)) {
+ mEndValuesList.add(end);
+ mStartValuesList.add(null);
+ }
+ }
+ }
+
+ private void matchStartAndEnd(TransitionValuesMaps startValues,
+ TransitionValuesMaps endValues) {
+ ArrayMap<View, TransitionValues> unmatchedStart = new ArrayMap<>(startValues.mViewValues);
+ ArrayMap<View, TransitionValues> unmatchedEnd = new ArrayMap<>(endValues.mViewValues);
+
+ for (int i = 0; i < mMatchOrder.length; i++) {
+ switch (mMatchOrder[i]) {
+ case MATCH_INSTANCE:
+ matchInstances(unmatchedStart, unmatchedEnd);
+ break;
+ case MATCH_NAME:
+ matchNames(unmatchedStart, unmatchedEnd,
+ startValues.mNameValues, endValues.mNameValues);
+ break;
+ case MATCH_ID:
+ matchIds(unmatchedStart, unmatchedEnd,
+ startValues.mIdValues, endValues.mIdValues);
+ break;
+ case MATCH_ITEM_ID:
+ matchItemIds(unmatchedStart, unmatchedEnd,
+ startValues.mItemIdValues, endValues.mItemIdValues);
+ break;
+ }
+ }
+ addUnmatched(unmatchedStart, unmatchedEnd);
+ }
+
+ /**
+ * This method, essentially a wrapper around all calls to createAnimator for all
+ * possible target views, is called with the entire set of start/end
+ * values. The implementation in Transition iterates through these lists
+ * and calls {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}
+ * with each set of start/end values on this transition. The
+ * TransitionSet subclass overrides this method and delegates it to
+ * each of its children in succession.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ protected void createAnimators(ViewGroup sceneRoot, TransitionValuesMaps startValues,
+ TransitionValuesMaps endValues, ArrayList<TransitionValues> startValuesList,
+ ArrayList<TransitionValues> endValuesList) {
+ if (DBG) {
+ Log.d(LOG_TAG, "createAnimators() for " + this);
+ }
+ ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
+ long minStartDelay = Long.MAX_VALUE;
+ SparseIntArray startDelays = new SparseIntArray();
+ int startValuesListCount = startValuesList.size();
+ for (int i = 0; i < startValuesListCount; ++i) {
+ TransitionValues start = startValuesList.get(i);
+ TransitionValues end = endValuesList.get(i);
+ if (start != null && !start.mTargetedTransitions.contains(this)) {
+ start = null;
+ }
+ if (end != null && !end.mTargetedTransitions.contains(this)) {
+ end = null;
+ }
+ if (start == null && end == null) {
+ continue;
+ }
+ // Only bother trying to animate with values that differ between start/end
+ boolean isChanged = start == null || end == null || isTransitionRequired(start, end);
+ if (isChanged) {
+ if (DBG) {
+ View view = (end != null) ? end.view : start.view;
+ Log.d(LOG_TAG, " differing start/end values for view " + view);
+ if (start == null || end == null) {
+ Log.d(LOG_TAG, " " + ((start == null)
+ ? "start null, end non-null" : "start non-null, end null"));
+ } else {
+ for (String key : start.values.keySet()) {
+ Object startValue = start.values.get(key);
+ Object endValue = end.values.get(key);
+ if (startValue != endValue && !startValue.equals(endValue)) {
+ Log.d(LOG_TAG, " " + key + ": start(" + startValue
+ + "), end(" + endValue + ")");
+ }
+ }
+ }
+ }
+ // TODO: what to do about targetIds and itemIds?
+ Animator animator = createAnimator(sceneRoot, start, end);
+ if (animator != null) {
+ // Save animation info for future cancellation purposes
+ View view;
+ TransitionValues infoValues = null;
+ if (end != null) {
+ view = end.view;
+ String[] properties = getTransitionProperties();
+ if (view != null && properties != null && properties.length > 0) {
+ infoValues = new TransitionValues();
+ infoValues.view = view;
+ TransitionValues newValues = endValues.mViewValues.get(view);
+ if (newValues != null) {
+ for (int j = 0; j < properties.length; ++j) {
+ infoValues.values.put(properties[j],
+ newValues.values.get(properties[j]));
+ }
+ }
+ int numExistingAnims = runningAnimators.size();
+ for (int j = 0; j < numExistingAnims; ++j) {
+ Animator anim = runningAnimators.keyAt(j);
+ AnimationInfo info = runningAnimators.get(anim);
+ if (info.mValues != null && info.mView == view
+ && info.mName.equals(getName())) {
+ if (info.mValues.equals(infoValues)) {
+ // Favor the old animator
+ animator = null;
+ break;
+ }
+ }
+ }
+ }
+ } else {
+ view = start.view;
+ }
+ if (animator != null) {
+ if (mPropagation != null) {
+ long delay = mPropagation.getStartDelay(sceneRoot, this, start, end);
+ startDelays.put(mAnimators.size(), (int) delay);
+ minStartDelay = Math.min(delay, minStartDelay);
+ }
+ AnimationInfo info = new AnimationInfo(view, getName(), this,
+ ViewUtils.getWindowId(sceneRoot), infoValues);
+ runningAnimators.put(animator, info);
+ mAnimators.add(animator);
+ }
+ }
+ }
+ }
+ if (minStartDelay != 0) {
+ for (int i = 0; i < startDelays.size(); i++) {
+ int index = startDelays.keyAt(i);
+ Animator animator = mAnimators.get(index);
+ long delay = startDelays.valueAt(i) - minStartDelay + animator.getStartDelay();
+ animator.setStartDelay(delay);
+ }
+ }
+ }
+
+ /**
+ * Internal utility method for checking whether a given view/id
+ * is valid for this transition, where "valid" means that either
+ * the Transition has no target/targetId list (the default, in which
+ * cause the transition should act on all views in the hiearchy), or
+ * the given view is in the target list or the view id is in the
+ * targetId list. If the target parameter is null, then the target list
+ * is not checked (this is in the case of ListView items, where the
+ * views are ignored and only the ids are used).
+ */
+ boolean isValidTarget(View target) {
+ int targetId = target.getId();
+ if (mTargetIdExcludes != null && mTargetIdExcludes.contains(targetId)) {
+ return false;
+ }
+ if (mTargetExcludes != null && mTargetExcludes.contains(target)) {
+ return false;
+ }
+ if (mTargetTypeExcludes != null) {
+ int numTypes = mTargetTypeExcludes.size();
+ for (int i = 0; i < numTypes; ++i) {
+ Class type = mTargetTypeExcludes.get(i);
+ if (type.isInstance(target)) {
+ return false;
+ }
+ }
+ }
+ if (mTargetNameExcludes != null && ViewCompat.getTransitionName(target) != null) {
+ if (mTargetNameExcludes.contains(ViewCompat.getTransitionName(target))) {
+ return false;
+ }
+ }
+ if (mTargetIds.size() == 0 && mTargets.size() == 0
+ && (mTargetTypes == null || mTargetTypes.isEmpty())
+ && (mTargetNames == null || mTargetNames.isEmpty())) {
+ return true;
+ }
+ if (mTargetIds.contains(targetId) || mTargets.contains(target)) {
+ return true;
+ }
+ if (mTargetNames != null && mTargetNames.contains(ViewCompat.getTransitionName(target))) {
+ return true;
+ }
+ if (mTargetTypes != null) {
+ for (int i = 0; i < mTargetTypes.size(); ++i) {
+ if (mTargetTypes.get(i).isInstance(target)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static ArrayMap<Animator, AnimationInfo> getRunningAnimators() {
+ ArrayMap<Animator, AnimationInfo> runningAnimators = sRunningAnimators.get();
+ if (runningAnimators == null) {
+ runningAnimators = new ArrayMap<>();
+ sRunningAnimators.set(runningAnimators);
+ }
+ return runningAnimators;
+ }
+
+ /**
+ * This is called internally once all animations have been set up by the
+ * transition hierarchy. \
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ protected void runAnimators() {
+ if (DBG) {
+ Log.d(LOG_TAG, "runAnimators() on " + this);
+ }
+ start();
+ ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
+ // Now start every Animator that was previously created for this transition
+ for (Animator anim : mAnimators) {
+ if (DBG) {
+ Log.d(LOG_TAG, " anim: " + anim);
+ }
+ if (runningAnimators.containsKey(anim)) {
+ start();
+ runAnimator(anim, runningAnimators);
+ }
+ }
+ mAnimators.clear();
+ end();
+ }
+
+ private void runAnimator(Animator animator,
+ final ArrayMap<Animator, AnimationInfo> runningAnimators) {
+ if (animator != null) {
+ // TODO: could be a single listener instance for all of them since it uses the param
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mCurrentAnimators.add(animation);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ runningAnimators.remove(animation);
+ mCurrentAnimators.remove(animation);
+ }
+ });
+ animate(animator);
+ }
+ }
+
+ /**
+ * Captures the values in the start scene for the properties that this
+ * transition monitors. These values are then passed as the startValues
+ * structure in a later call to
+ * {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}.
+ * The main concern for an implementation is what the
+ * properties are that the transition cares about and what the values are
+ * for all of those properties. The start and end values will be compared
+ * later during the
+ * {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}
+ * method to determine what, if any, animations, should be run.
+ *
+ * <p>Subclasses must implement this method. The method should only be called by the
+ * transition system; it is not intended to be called from external classes.</p>
+ *
+ * @param transitionValues The holder for any values that the Transition
+ * wishes to store. Values are stored in the <code>values</code> field
+ * of this TransitionValues object and are keyed from
+ * a String value. For example, to store a view's rotation value,
+ * a transition might call
+ * <code>transitionValues.values.put("appname:transitionname:rotation",
+ * view.getRotation())</code>. The target view will already be stored
+ * in
+ * the transitionValues structure when this method is called.
+ * @see #captureEndValues(TransitionValues)
+ * @see #createAnimator(ViewGroup, TransitionValues, TransitionValues)
+ */
+ public abstract void captureStartValues(@NonNull TransitionValues transitionValues);
+
+ /**
+ * Captures the values in the end scene for the properties that this
+ * transition monitors. These values are then passed as the endValues
+ * structure in a later call to
+ * {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}.
+ * The main concern for an implementation is what the
+ * properties are that the transition cares about and what the values are
+ * for all of those properties. The start and end values will be compared
+ * later during the
+ * {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}
+ * method to determine what, if any, animations, should be run.
+ *
+ * <p>Subclasses must implement this method. The method should only be called by the
+ * transition system; it is not intended to be called from external classes.</p>
+ *
+ * @param transitionValues The holder for any values that the Transition
+ * wishes to store. Values are stored in the <code>values</code> field
+ * of this TransitionValues object and are keyed from
+ * a String value. For example, to store a view's rotation value,
+ * a transition might call
+ * <code>transitionValues.values.put("appname:transitionname:rotation",
+ * view.getRotation())</code>. The target view will already be stored
+ * in
+ * the transitionValues structure when this method is called.
+ * @see #captureStartValues(TransitionValues)
+ * @see #createAnimator(ViewGroup, TransitionValues, TransitionValues)
+ */
+ public abstract void captureEndValues(@NonNull TransitionValues transitionValues);
+
+ /**
+ * Sets the target view instances that this Transition is interested in
+ * animating. By default, there are no targets, and a Transition will
+ * listen for changes on every view in the hierarchy below the sceneRoot
+ * of the Scene being transitioned into. Setting targets constrains
+ * the Transition to only listen for, and act on, these views.
+ * All other views will be ignored.
+ *
+ * <p>The target list is like the {@link #addTarget(int) targetId}
+ * list except this list specifies the actual View instances, not the ids
+ * of the views. This is an important distinction when scene changes involve
+ * view hierarchies which have been inflated separately; different views may
+ * share the same id but not actually be the same instance. If the transition
+ * should treat those views as the same, then {@link #addTarget(int)} should be used
+ * instead of {@link #addTarget(View)}. If, on the other hand, scene changes involve
+ * changes all within the same view hierarchy, among views which do not
+ * necessarily have ids set on them, then the target list of views may be more
+ * convenient.</p>
+ *
+ * @param target A View on which the Transition will act, must be non-null.
+ * @return The Transition to which the target is added.
+ * Returning the same object makes it easier to chain calls during
+ * construction, such as
+ * <code>transitionSet.addTransitions(new Fade()).addTarget(someView);</code>
+ * @see #addTarget(int)
+ */
+ @NonNull
+ public Transition addTarget(@NonNull View target) {
+ mTargets.add(target);
+ return this;
+ }
+
+ /**
+ * Adds the id of a target view that this Transition is interested in
+ * animating. By default, there are no targetIds, and a Transition will
+ * listen for changes on every view in the hierarchy below the sceneRoot
+ * of the Scene being transitioned into. Setting targetIds constrains
+ * the Transition to only listen for, and act on, views with these IDs.
+ * Views with different IDs, or no IDs whatsoever, will be ignored.
+ *
+ * <p>Note that using ids to specify targets implies that ids should be unique
+ * within the view hierarchy underneath the scene root.</p>
+ *
+ * @param targetId The id of a target view, must be a positive number.
+ * @return The Transition to which the targetId is added.
+ * Returning the same object makes it easier to chain calls during
+ * construction, such as
+ * <code>transitionSet.addTransitions(new Fade()).addTarget(someId);</code>
+ * @see View#getId()
+ */
+ @NonNull
+ public Transition addTarget(@IdRes int targetId) {
+ if (targetId != 0) {
+ mTargetIds.add(targetId);
+ }
+ return this;
+ }
+
+ /**
+ * Adds the transitionName of a target view that this Transition is interested in
+ * animating. By default, there are no targetNames, and a Transition will
+ * listen for changes on every view in the hierarchy below the sceneRoot
+ * of the Scene being transitioned into. Setting targetNames constrains
+ * the Transition to only listen for, and act on, views with these transitionNames.
+ * Views with different transitionNames, or no transitionName whatsoever, will be ignored.
+ *
+ * <p>Note that transitionNames should be unique within the view hierarchy.</p>
+ *
+ * @param targetName The transitionName of a target view, must be non-null.
+ * @return The Transition to which the target transitionName is added.
+ * Returning the same object makes it easier to chain calls during
+ * construction, such as
+ * <code>transitionSet.addTransitions(new Fade()).addTarget(someName);</code>
+ * @see ViewCompat#getTransitionName(View)
+ */
+ @NonNull
+ public Transition addTarget(@NonNull String targetName) {
+ if (mTargetNames == null) {
+ mTargetNames = new ArrayList<>();
+ }
+ mTargetNames.add(targetName);
+ return this;
+ }
+
+ /**
+ * Adds the Class of a target view that this Transition is interested in
+ * animating. By default, there are no targetTypes, and a Transition will
+ * listen for changes on every view in the hierarchy below the sceneRoot
+ * of the Scene being transitioned into. Setting targetTypes constrains
+ * the Transition to only listen for, and act on, views with these classes.
+ * Views with different classes will be ignored.
+ *
+ * <p>Note that any View that can be cast to targetType will be included, so
+ * if targetType is <code>View.class</code>, all Views will be included.</p>
+ *
+ * @param targetType The type to include when running this transition.
+ * @return The Transition to which the target class was added.
+ * Returning the same object makes it easier to chain calls during
+ * construction, such as
+ * <code>transitionSet.addTransitions(new Fade()).addTarget(ImageView.class);</code>
+ * @see #addTarget(int)
+ * @see #addTarget(android.view.View)
+ * @see #excludeTarget(Class, boolean)
+ * @see #excludeChildren(Class, boolean)
+ */
+ @NonNull
+ public Transition addTarget(@NonNull Class targetType) {
+ if (mTargetTypes == null) {
+ mTargetTypes = new ArrayList<>();
+ }
+ mTargetTypes.add(targetType);
+ return this;
+ }
+
+ /**
+ * Removes the given target from the list of targets that this Transition
+ * is interested in animating.
+ *
+ * @param target The target view, must be non-null.
+ * @return Transition The Transition from which the target is removed.
+ * Returning the same object makes it easier to chain calls during
+ * construction, such as
+ * <code>transitionSet.addTransitions(new Fade()).removeTarget(someView);</code>
+ */
+ @NonNull
+ public Transition removeTarget(@NonNull View target) {
+ mTargets.remove(target);
+ return this;
+ }
+
+ /**
+ * Removes the given targetId from the list of ids that this Transition
+ * is interested in animating.
+ *
+ * @param targetId The id of a target view, must be a positive number.
+ * @return The Transition from which the targetId is removed.
+ * Returning the same object makes it easier to chain calls during
+ * construction, such as
+ * <code>transitionSet.addTransitions(new Fade()).removeTargetId(someId);</code>
+ */
+ @NonNull
+ public Transition removeTarget(@IdRes int targetId) {
+ if (targetId != 0) {
+ mTargetIds.remove((Integer) targetId);
+ }
+ return this;
+ }
+
+ /**
+ * Removes the given targetName from the list of transitionNames that this Transition
+ * is interested in animating.
+ *
+ * @param targetName The transitionName of a target view, must not be null.
+ * @return The Transition from which the targetName is removed.
+ * Returning the same object makes it easier to chain calls during
+ * construction, such as
+ * <code>transitionSet.addTransitions(new Fade()).removeTargetName(someName);</code>
+ */
+ @NonNull
+ public Transition removeTarget(@NonNull String targetName) {
+ if (mTargetNames != null) {
+ mTargetNames.remove(targetName);
+ }
+ return this;
+ }
+
+ /**
+ * Removes the given target from the list of targets that this Transition
+ * is interested in animating.
+ *
+ * @param target The type of the target view, must be non-null.
+ * @return Transition The Transition from which the target is removed.
+ * Returning the same object makes it easier to chain calls during
+ * construction, such as
+ * <code>transitionSet.addTransitions(new Fade()).removeTarget(someType);</code>
+ */
+ @NonNull
+ public Transition removeTarget(@NonNull Class target) {
+ if (mTargetTypes != null) {
+ mTargetTypes.remove(target);
+ }
+ return this;
+ }
+
+ /**
+ * Utility method to manage the boilerplate code that is the same whether we
+ * are excluding targets or their children.
+ */
+ private static <T> ArrayList<T> excludeObject(ArrayList<T> list, T target, boolean exclude) {
+ if (target != null) {
+ if (exclude) {
+ list = ArrayListManager.add(list, target);
+ } else {
+ list = ArrayListManager.remove(list, target);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Whether to add the given target to the list of targets to exclude from this
+ * transition. The <code>exclude</code> parameter specifies whether the target
+ * should be added to or removed from the excluded list.
+ *
+ * <p>Excluding targets is a general mechanism for allowing transitions to run on
+ * a view hierarchy while skipping target views that should not be part of
+ * the transition. For example, you may want to avoid animating children
+ * of a specific ListView or Spinner. Views can be excluded either by their
+ * id, or by their instance reference, or by the Class of that view
+ * (eg, {@link Spinner}).</p>
+ *
+ * @param target The target to ignore when running this transition.
+ * @param exclude Whether to add the target to or remove the target from the
+ * current list of excluded targets.
+ * @return This transition object.
+ * @see #excludeChildren(View, boolean)
+ * @see #excludeTarget(int, boolean)
+ * @see #excludeTarget(Class, boolean)
+ */
+ @NonNull
+ public Transition excludeTarget(@NonNull View target, boolean exclude) {
+ mTargetExcludes = excludeView(mTargetExcludes, target, exclude);
+ return this;
+ }
+
+ /**
+ * Whether to add the given id to the list of target ids to exclude from this
+ * transition. The <code>exclude</code> parameter specifies whether the target
+ * should be added to or removed from the excluded list.
+ *
+ * <p>Excluding targets is a general mechanism for allowing transitions to run on
+ * a view hierarchy while skipping target views that should not be part of
+ * the transition. For example, you may want to avoid animating children
+ * of a specific ListView or Spinner. Views can be excluded either by their
+ * id, or by their instance reference, or by the Class of that view
+ * (eg, {@link Spinner}).</p>
+ *
+ * @param targetId The id of a target to ignore when running this transition.
+ * @param exclude Whether to add the target to or remove the target from the
+ * current list of excluded targets.
+ * @return This transition object.
+ * @see #excludeChildren(int, boolean)
+ * @see #excludeTarget(View, boolean)
+ * @see #excludeTarget(Class, boolean)
+ */
+ @NonNull
+ public Transition excludeTarget(@IdRes int targetId, boolean exclude) {
+ mTargetIdExcludes = excludeId(mTargetIdExcludes, targetId, exclude);
+ return this;
+ }
+
+ /**
+ * Whether to add the given transitionName to the list of target transitionNames to exclude
+ * from this transition. The <code>exclude</code> parameter specifies whether the target
+ * should be added to or removed from the excluded list.
+ *
+ * <p>Excluding targets is a general mechanism for allowing transitions to run on
+ * a view hierarchy while skipping target views that should not be part of
+ * the transition. For example, you may want to avoid animating children
+ * of a specific ListView or Spinner. Views can be excluded by their
+ * id, their instance reference, their transitionName, or by the Class of that view
+ * (eg, {@link Spinner}).</p>
+ *
+ * @param targetName The name of a target to ignore when running this transition.
+ * @param exclude Whether to add the target to or remove the target from the
+ * current list of excluded targets.
+ * @return This transition object.
+ * @see #excludeTarget(View, boolean)
+ * @see #excludeTarget(int, boolean)
+ * @see #excludeTarget(Class, boolean)
+ */
+ @NonNull
+ public Transition excludeTarget(@NonNull String targetName, boolean exclude) {
+ mTargetNameExcludes = excludeObject(mTargetNameExcludes, targetName, exclude);
+ return this;
+ }
+
+ /**
+ * Whether to add the children of given target to the list of target children
+ * to exclude from this transition. The <code>exclude</code> parameter specifies
+ * whether the target should be added to or removed from the excluded list.
+ *
+ * <p>Excluding targets is a general mechanism for allowing transitions to run on
+ * a view hierarchy while skipping target views that should not be part of
+ * the transition. For example, you may want to avoid animating children
+ * of a specific ListView or Spinner. Views can be excluded either by their
+ * id, or by their instance reference, or by the Class of that view
+ * (eg, {@link Spinner}).</p>
+ *
+ * @param target The target to ignore when running this transition.
+ * @param exclude Whether to add the target to or remove the target from the
+ * current list of excluded targets.
+ * @return This transition object.
+ * @see #excludeTarget(View, boolean)
+ * @see #excludeChildren(int, boolean)
+ * @see #excludeChildren(Class, boolean)
+ */
+ @NonNull
+ public Transition excludeChildren(@NonNull View target, boolean exclude) {
+ mTargetChildExcludes = excludeView(mTargetChildExcludes, target, exclude);
+ return this;
+ }
+
+ /**
+ * Whether to add the children of the given id to the list of targets to exclude
+ * from this transition. The <code>exclude</code> parameter specifies whether
+ * the children of the target should be added to or removed from the excluded list.
+ * Excluding children in this way provides a simple mechanism for excluding all
+ * children of specific targets, rather than individually excluding each
+ * child individually.
+ *
+ * <p>Excluding targets is a general mechanism for allowing transitions to run on
+ * a view hierarchy while skipping target views that should not be part of
+ * the transition. For example, you may want to avoid animating children
+ * of a specific ListView or Spinner. Views can be excluded either by their
+ * id, or by their instance reference, or by the Class of that view
+ * (eg, {@link Spinner}).</p>
+ *
+ * @param targetId The id of a target whose children should be ignored when running
+ * this transition.
+ * @param exclude Whether to add the target to or remove the target from the
+ * current list of excluded-child targets.
+ * @return This transition object.
+ * @see #excludeTarget(int, boolean)
+ * @see #excludeChildren(View, boolean)
+ * @see #excludeChildren(Class, boolean)
+ */
+ @NonNull
+ public Transition excludeChildren(@IdRes int targetId, boolean exclude) {
+ mTargetIdChildExcludes = excludeId(mTargetIdChildExcludes, targetId, exclude);
+ return this;
+ }
+
+ /**
+ * Utility method to manage the boilerplate code that is the same whether we
+ * are excluding targets or their children.
+ */
+ private ArrayList<Integer> excludeId(ArrayList<Integer> list, int targetId, boolean exclude) {
+ if (targetId > 0) {
+ if (exclude) {
+ list = ArrayListManager.add(list, targetId);
+ } else {
+ list = ArrayListManager.remove(list, targetId);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Utility method to manage the boilerplate code that is the same whether we
+ * are excluding targets or their children.
+ */
+ private ArrayList<View> excludeView(ArrayList<View> list, View target, boolean exclude) {
+ if (target != null) {
+ if (exclude) {
+ list = ArrayListManager.add(list, target);
+ } else {
+ list = ArrayListManager.remove(list, target);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Whether to add the given type to the list of types to exclude from this
+ * transition. The <code>exclude</code> parameter specifies whether the target
+ * type should be added to or removed from the excluded list.
+ *
+ * <p>Excluding targets is a general mechanism for allowing transitions to run on
+ * a view hierarchy while skipping target views that should not be part of
+ * the transition. For example, you may want to avoid animating children
+ * of a specific ListView or Spinner. Views can be excluded either by their
+ * id, or by their instance reference, or by the Class of that view
+ * (eg, {@link Spinner}).</p>
+ *
+ * @param type The type to ignore when running this transition.
+ * @param exclude Whether to add the target type to or remove it from the
+ * current list of excluded target types.
+ * @return This transition object.
+ * @see #excludeChildren(Class, boolean)
+ * @see #excludeTarget(int, boolean)
+ * @see #excludeTarget(View, boolean)
+ */
+ @NonNull
+ public Transition excludeTarget(@NonNull Class type, boolean exclude) {
+ mTargetTypeExcludes = excludeType(mTargetTypeExcludes, type, exclude);
+ return this;
+ }
+
+ /**
+ * Whether to add the given type to the list of types whose children should
+ * be excluded from this transition. The <code>exclude</code> parameter
+ * specifies whether the target type should be added to or removed from
+ * the excluded list.
+ *
+ * <p>Excluding targets is a general mechanism for allowing transitions to run on
+ * a view hierarchy while skipping target views that should not be part of
+ * the transition. For example, you may want to avoid animating children
+ * of a specific ListView or Spinner. Views can be excluded either by their
+ * id, or by their instance reference, or by the Class of that view
+ * (eg, {@link Spinner}).</p>
+ *
+ * @param type The type to ignore when running this transition.
+ * @param exclude Whether to add the target type to or remove it from the
+ * current list of excluded target types.
+ * @return This transition object.
+ * @see #excludeTarget(Class, boolean)
+ * @see #excludeChildren(int, boolean)
+ * @see #excludeChildren(View, boolean)
+ */
+ @NonNull
+ public Transition excludeChildren(@NonNull Class type, boolean exclude) {
+ mTargetTypeChildExcludes = excludeType(mTargetTypeChildExcludes, type, exclude);
+ return this;
+ }
+
+ /**
+ * Utility method to manage the boilerplate code that is the same whether we
+ * are excluding targets or their children.
+ */
+ private ArrayList<Class> excludeType(ArrayList<Class> list, Class type, boolean exclude) {
+ if (type != null) {
+ if (exclude) {
+ list = ArrayListManager.add(list, type);
+ } else {
+ list = ArrayListManager.remove(list, type);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Returns the array of target IDs that this transition limits itself to
+ * tracking and animating. If the array is null for both this method and
+ * {@link #getTargets()}, then this transition is
+ * not limited to specific views, and will handle changes to any views
+ * in the hierarchy of a scene change.
+ *
+ * @return the list of target IDs
+ */
+ @NonNull
+ public List<Integer> getTargetIds() {
+ return mTargetIds;
+ }
+
+ /**
+ * Returns the array of target views that this transition limits itself to
+ * tracking and animating. If the array is null for both this method and
+ * {@link #getTargetIds()}, then this transition is
+ * not limited to specific views, and will handle changes to any views
+ * in the hierarchy of a scene change.
+ *
+ * @return the list of target views
+ */
+ @NonNull
+ public List<View> getTargets() {
+ return mTargets;
+ }
+
+ /**
+ * Returns the list of target transitionNames that this transition limits itself to
+ * tracking and animating. If the list is null or empty for
+ * {@link #getTargetIds()}, {@link #getTargets()}, {@link #getTargetNames()}, and
+ * {@link #getTargetTypes()} then this transition is
+ * not limited to specific views, and will handle changes to any views
+ * in the hierarchy of a scene change.
+ *
+ * @return the list of target transitionNames
+ */
+ @Nullable
+ public List<String> getTargetNames() {
+ return mTargetNames;
+ }
+
+ /**
+ * Returns the list of target transitionNames that this transition limits itself to
+ * tracking and animating. If the list is null or empty for
+ * {@link #getTargetIds()}, {@link #getTargets()}, {@link #getTargetNames()}, and
+ * {@link #getTargetTypes()} then this transition is
+ * not limited to specific views, and will handle changes to any views
+ * in the hierarchy of a scene change.
+ *
+ * @return the list of target Types
+ */
+ @Nullable
+ public List<Class> getTargetTypes() {
+ return mTargetTypes;
+ }
+
+ /**
+ * Recursive method that captures values for the given view and the
+ * hierarchy underneath it.
+ *
+ * @param sceneRoot The root of the view hierarchy being captured
+ * @param start true if this capture is happening before the scene change,
+ * false otherwise
+ */
+ void captureValues(ViewGroup sceneRoot, boolean start) {
+ clearValues(start);
+ if ((mTargetIds.size() > 0 || mTargets.size() > 0)
+ && (mTargetNames == null || mTargetNames.isEmpty())
+ && (mTargetTypes == null || mTargetTypes.isEmpty())) {
+ for (int i = 0; i < mTargetIds.size(); ++i) {
+ int id = mTargetIds.get(i);
+ View view = sceneRoot.findViewById(id);
+ if (view != null) {
+ TransitionValues values = new TransitionValues();
+ values.view = view;
+ if (start) {
+ captureStartValues(values);
+ } else {
+ captureEndValues(values);
+ }
+ values.mTargetedTransitions.add(this);
+ capturePropagationValues(values);
+ if (start) {
+ addViewValues(mStartValues, view, values);
+ } else {
+ addViewValues(mEndValues, view, values);
+ }
+ }
+ }
+ for (int i = 0; i < mTargets.size(); ++i) {
+ View view = mTargets.get(i);
+ TransitionValues values = new TransitionValues();
+ values.view = view;
+ if (start) {
+ captureStartValues(values);
+ } else {
+ captureEndValues(values);
+ }
+ values.mTargetedTransitions.add(this);
+ capturePropagationValues(values);
+ if (start) {
+ addViewValues(mStartValues, view, values);
+ } else {
+ addViewValues(mEndValues, view, values);
+ }
+ }
+ } else {
+ captureHierarchy(sceneRoot, start);
+ }
+ if (!start && mNameOverrides != null) {
+ int numOverrides = mNameOverrides.size();
+ ArrayList<View> overriddenViews = new ArrayList<>(numOverrides);
+ for (int i = 0; i < numOverrides; i++) {
+ String fromName = mNameOverrides.keyAt(i);
+ overriddenViews.add(mStartValues.mNameValues.remove(fromName));
+ }
+ for (int i = 0; i < numOverrides; i++) {
+ View view = overriddenViews.get(i);
+ if (view != null) {
+ String toName = mNameOverrides.valueAt(i);
+ mStartValues.mNameValues.put(toName, view);
+ }
+ }
+ }
+ }
+
+ private static void addViewValues(TransitionValuesMaps transitionValuesMaps,
+ View view, TransitionValues transitionValues) {
+ transitionValuesMaps.mViewValues.put(view, transitionValues);
+ int id = view.getId();
+ if (id >= 0) {
+ if (transitionValuesMaps.mIdValues.indexOfKey(id) >= 0) {
+ // Duplicate IDs cannot match by ID.
+ transitionValuesMaps.mIdValues.put(id, null);
+ } else {
+ transitionValuesMaps.mIdValues.put(id, view);
+ }
+ }
+ String name = ViewCompat.getTransitionName(view);
+ if (name != null) {
+ if (transitionValuesMaps.mNameValues.containsKey(name)) {
+ // Duplicate transitionNames: cannot match by transitionName.
+ transitionValuesMaps.mNameValues.put(name, null);
+ } else {
+ transitionValuesMaps.mNameValues.put(name, view);
+ }
+ }
+ if (view.getParent() instanceof ListView) {
+ ListView listview = (ListView) view.getParent();
+ if (listview.getAdapter().hasStableIds()) {
+ int position = listview.getPositionForView(view);
+ long itemId = listview.getItemIdAtPosition(position);
+ if (transitionValuesMaps.mItemIdValues.indexOfKey(itemId) >= 0) {
+ // Duplicate item IDs: cannot match by item ID.
+ View alreadyMatched = transitionValuesMaps.mItemIdValues.get(itemId);
+ if (alreadyMatched != null) {
+ ViewCompat.setHasTransientState(alreadyMatched, false);
+ transitionValuesMaps.mItemIdValues.put(itemId, null);
+ }
+ } else {
+ ViewCompat.setHasTransientState(view, true);
+ transitionValuesMaps.mItemIdValues.put(itemId, view);
+ }
+ }
+ }
+ }
+
+ /**
+ * Clear valuesMaps for specified start/end state
+ *
+ * @param start true if the start values should be cleared, false otherwise
+ */
+ void clearValues(boolean start) {
+ if (start) {
+ mStartValues.mViewValues.clear();
+ mStartValues.mIdValues.clear();
+ mStartValues.mItemIdValues.clear();
+ } else {
+ mEndValues.mViewValues.clear();
+ mEndValues.mIdValues.clear();
+ mEndValues.mItemIdValues.clear();
+ }
+ }
+
+ /**
+ * Recursive method which captures values for an entire view hierarchy,
+ * starting at some root view. Transitions without targetIDs will use this
+ * method to capture values for all possible views.
+ *
+ * @param view The view for which to capture values. Children of this View
+ * will also be captured, recursively down to the leaf nodes.
+ * @param start true if values are being captured in the start scene, false
+ * otherwise.
+ */
+ private void captureHierarchy(View view, boolean start) {
+ if (view == null) {
+ return;
+ }
+ int id = view.getId();
+ if (mTargetIdExcludes != null && mTargetIdExcludes.contains(id)) {
+ return;
+ }
+ if (mTargetExcludes != null && mTargetExcludes.contains(view)) {
+ return;
+ }
+ if (mTargetTypeExcludes != null) {
+ int numTypes = mTargetTypeExcludes.size();
+ for (int i = 0; i < numTypes; ++i) {
+ if (mTargetTypeExcludes.get(i).isInstance(view)) {
+ return;
+ }
+ }
+ }
+ if (view.getParent() instanceof ViewGroup) {
+ TransitionValues values = new TransitionValues();
+ values.view = view;
+ if (start) {
+ captureStartValues(values);
+ } else {
+ captureEndValues(values);
+ }
+ values.mTargetedTransitions.add(this);
+ capturePropagationValues(values);
+ if (start) {
+ addViewValues(mStartValues, view, values);
+ } else {
+ addViewValues(mEndValues, view, values);
+ }
+ }
+ if (view instanceof ViewGroup) {
+ // Don't traverse child hierarchy if there are any child-excludes on this view
+ if (mTargetIdChildExcludes != null && mTargetIdChildExcludes.contains(id)) {
+ return;
+ }
+ if (mTargetChildExcludes != null && mTargetChildExcludes.contains(view)) {
+ return;
+ }
+ if (mTargetTypeChildExcludes != null) {
+ int numTypes = mTargetTypeChildExcludes.size();
+ for (int i = 0; i < numTypes; ++i) {
+ if (mTargetTypeChildExcludes.get(i).isInstance(view)) {
+ return;
+ }
+ }
+ }
+ ViewGroup parent = (ViewGroup) view;
+ for (int i = 0; i < parent.getChildCount(); ++i) {
+ captureHierarchy(parent.getChildAt(i), start);
+ }
+ }
+ }
+
+ /**
+ * This method can be called by transitions to get the TransitionValues for
+ * any particular view during the transition-playing process. This might be
+ * necessary, for example, to query the before/after state of related views
+ * for a given transition.
+ */
+ @Nullable
+ public TransitionValues getTransitionValues(@NonNull View view, boolean start) {
+ if (mParent != null) {
+ return mParent.getTransitionValues(view, start);
+ }
+ TransitionValuesMaps valuesMaps = start ? mStartValues : mEndValues;
+ return valuesMaps.mViewValues.get(view);
+ }
+
+ /**
+ * Find the matched start or end value for a given View. This is only valid
+ * after playTransition starts. For example, it will be valid in
+ * {@link #createAnimator(android.view.ViewGroup, TransitionValues, TransitionValues)}, but not
+ * in {@link #captureStartValues(TransitionValues)}.
+ *
+ * @param view The view to find the match for.
+ * @param viewInStart Is View from the start values or end values.
+ * @return The matching TransitionValues for view in either start or end values, depending
+ * on viewInStart or null if there is no match for the given view.
+ */
+ TransitionValues getMatchedTransitionValues(View view, boolean viewInStart) {
+ if (mParent != null) {
+ return mParent.getMatchedTransitionValues(view, viewInStart);
+ }
+ ArrayList<TransitionValues> lookIn = viewInStart ? mStartValuesList : mEndValuesList;
+ if (lookIn == null) {
+ return null;
+ }
+ int count = lookIn.size();
+ int index = -1;
+ for (int i = 0; i < count; i++) {
+ TransitionValues values = lookIn.get(i);
+ if (values == null) {
+ return null;
+ }
+ if (values.view == view) {
+ index = i;
+ break;
+ }
+ }
+ TransitionValues values = null;
+ if (index >= 0) {
+ ArrayList<TransitionValues> matchIn = viewInStart ? mEndValuesList : mStartValuesList;
+ values = matchIn.get(index);
+ }
+ return values;
+ }
+
+ /**
+ * Pauses this transition, sending out calls to {@link
+ * TransitionListener#onTransitionPause(Transition)} to all listeners
+ * and pausing all running animators started by this transition.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void pause(View sceneRoot) {
+ if (!mEnded) {
+ ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
+ int numOldAnims = runningAnimators.size();
+ WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
+ for (int i = numOldAnims - 1; i >= 0; i--) {
+ AnimationInfo info = runningAnimators.valueAt(i);
+ if (info.mView != null && windowId.equals(info.mWindowId)) {
+ Animator anim = runningAnimators.keyAt(i);
+ AnimatorUtils.pause(anim);
+ }
+ }
+ if (mListeners != null && mListeners.size() > 0) {
+ @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onTransitionPause(this);
+ }
+ }
+ mPaused = true;
+ }
+ }
+
+ /**
+ * Resumes this transition, sending out calls to {@link
+ * TransitionListener#onTransitionPause(Transition)} to all listeners
+ * and pausing all running animators started by this transition.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void resume(View sceneRoot) {
+ if (mPaused) {
+ if (!mEnded) {
+ ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
+ int numOldAnims = runningAnimators.size();
+ WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
+ for (int i = numOldAnims - 1; i >= 0; i--) {
+ AnimationInfo info = runningAnimators.valueAt(i);
+ if (info.mView != null && windowId.equals(info.mWindowId)) {
+ Animator anim = runningAnimators.keyAt(i);
+ AnimatorUtils.resume(anim);
+ }
+ }
+ if (mListeners != null && mListeners.size() > 0) {
+ @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onTransitionResume(this);
+ }
+ }
+ }
+ mPaused = false;
+ }
+ }
+
+ /**
+ * Called by TransitionManager to play the transition. This calls
+ * createAnimators() to set things up and create all of the animations and then
+ * runAnimations() to actually start the animations.
+ */
+ void playTransition(ViewGroup sceneRoot) {
+ mStartValuesList = new ArrayList<>();
+ mEndValuesList = new ArrayList<>();
+ matchStartAndEnd(mStartValues, mEndValues);
+
+ ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
+ int numOldAnims = runningAnimators.size();
+ WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
+ for (int i = numOldAnims - 1; i >= 0; i--) {
+ Animator anim = runningAnimators.keyAt(i);
+ if (anim != null) {
+ AnimationInfo oldInfo = runningAnimators.get(anim);
+ if (oldInfo != null && oldInfo.mView != null
+ && windowId.equals(oldInfo.mWindowId)) {
+ TransitionValues oldValues = oldInfo.mValues;
+ View oldView = oldInfo.mView;
+ TransitionValues startValues = getTransitionValues(oldView, true);
+ TransitionValues endValues = getMatchedTransitionValues(oldView, true);
+ boolean cancel = (startValues != null || endValues != null)
+ && oldInfo.mTransition.isTransitionRequired(oldValues, endValues);
+ if (cancel) {
+ if (anim.isRunning() || anim.isStarted()) {
+ if (DBG) {
+ Log.d(LOG_TAG, "Canceling anim " + anim);
+ }
+ anim.cancel();
+ } else {
+ if (DBG) {
+ Log.d(LOG_TAG, "removing anim from info list: " + anim);
+ }
+ runningAnimators.remove(anim);
+ }
+ }
+ }
+ }
+ }
+
+ createAnimators(sceneRoot, mStartValues, mEndValues, mStartValuesList, mEndValuesList);
+ runAnimators();
+ }
+
+ /**
+ * Returns whether or not the transition should create an Animator, based on the values
+ * captured during {@link #captureStartValues(TransitionValues)} and
+ * {@link #captureEndValues(TransitionValues)}. The default implementation compares the
+ * property values returned from {@link #getTransitionProperties()}, or all property values if
+ * {@code getTransitionProperties()} returns null. Subclasses may override this method to
+ * provide logic more specific to the transition implementation.
+ *
+ * @param startValues the values from captureStartValues, This may be {@code null} if the
+ * View did not exist in the start state.
+ * @param endValues the values from captureEndValues. This may be {@code null} if the View
+ * did not exist in the end state.
+ */
+ public boolean isTransitionRequired(@Nullable TransitionValues startValues,
+ @Nullable TransitionValues endValues) {
+ boolean valuesChanged = false;
+ // if startValues null, then transition didn't care to stash values,
+ // and won't get canceled
+ if (startValues != null && endValues != null) {
+ String[] properties = getTransitionProperties();
+ if (properties != null) {
+ for (String property : properties) {
+ if (isValueChanged(startValues, endValues, property)) {
+ valuesChanged = true;
+ break;
+ }
+ }
+ } else {
+ for (String key : startValues.values.keySet()) {
+ if (isValueChanged(startValues, endValues, key)) {
+ valuesChanged = true;
+ break;
+ }
+ }
+ }
+ }
+ return valuesChanged;
+ }
+
+ private static boolean isValueChanged(TransitionValues oldValues, TransitionValues newValues,
+ String key) {
+ Object oldValue = oldValues.values.get(key);
+ Object newValue = newValues.values.get(key);
+ boolean changed;
+ if (oldValue == null && newValue == null) {
+ // both are null
+ changed = false;
+ } else if (oldValue == null || newValue == null) {
+ // one is null
+ changed = true;
+ } else {
+ // neither is null
+ changed = !oldValue.equals(newValue);
+ }
+ if (DBG && changed) {
+ Log.d(LOG_TAG, "Transition.playTransition: "
+ + "oldValue != newValue for " + key
+ + ": old, new = " + oldValue + ", " + newValue);
+ }
+ return changed;
+ }
+
+ /**
+ * This is a utility method used by subclasses to handle standard parts of
+ * setting up and running an Animator: it sets the {@link #getDuration()
+ * duration} and the {@link #getStartDelay() startDelay}, starts the
+ * animation, and, when the animator ends, calls {@link #end()}.
+ *
+ * @param animator The Animator to be run during this transition.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ protected void animate(Animator animator) {
+ // TODO: maybe pass auto-end as a boolean parameter?
+ if (animator == null) {
+ end();
+ } else {
+ if (getDuration() >= 0) {
+ animator.setDuration(getDuration());
+ }
+ if (getStartDelay() >= 0) {
+ animator.setStartDelay(getStartDelay());
+ }
+ if (getInterpolator() != null) {
+ animator.setInterpolator(getInterpolator());
+ }
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ end();
+ animation.removeListener(this);
+ }
+ });
+ animator.start();
+ }
+ }
+
+ /**
+ * This method is called automatically by the transition and
+ * TransitionSet classes prior to a Transition subclass starting;
+ * subclasses should not need to call it directly.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ protected void start() {
+ if (mNumInstances == 0) {
+ if (mListeners != null && mListeners.size() > 0) {
+ @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onTransitionStart(this);
+ }
+ }
+ mEnded = false;
+ }
+ mNumInstances++;
+ }
+
+ /**
+ * This method is called automatically by the Transition and
+ * TransitionSet classes when a transition finishes, either because
+ * a transition did nothing (returned a null Animator from
+ * {@link Transition#createAnimator(ViewGroup, TransitionValues,
+ * TransitionValues)}) or because the transition returned a valid
+ * Animator and end() was called in the onAnimationEnd()
+ * callback of the AnimatorListener.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ protected void end() {
+ --mNumInstances;
+ if (mNumInstances == 0) {
+ if (mListeners != null && mListeners.size() > 0) {
+ @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onTransitionEnd(this);
+ }
+ }
+ for (int i = 0; i < mStartValues.mItemIdValues.size(); ++i) {
+ View view = mStartValues.mItemIdValues.valueAt(i);
+ if (view != null) {
+ ViewCompat.setHasTransientState(view, false);
+ }
+ }
+ for (int i = 0; i < mEndValues.mItemIdValues.size(); ++i) {
+ View view = mEndValues.mItemIdValues.valueAt(i);
+ if (view != null) {
+ ViewCompat.setHasTransientState(view, false);
+ }
+ }
+ mEnded = true;
+ }
+ }
+
+ /**
+ * Force the transition to move to its end state, ending all the animators.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ void forceToEnd(ViewGroup sceneRoot) {
+ ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
+ int numOldAnims = runningAnimators.size();
+ if (sceneRoot != null) {
+ WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
+ for (int i = numOldAnims - 1; i >= 0; i--) {
+ AnimationInfo info = runningAnimators.valueAt(i);
+ if (info.mView != null && windowId != null && windowId.equals(info.mWindowId)) {
+ Animator anim = runningAnimators.keyAt(i);
+ anim.end();
+ }
+ }
+ }
+ }
+
+ /**
+ * This method cancels a transition that is currently running.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ protected void cancel() {
+ int numAnimators = mCurrentAnimators.size();
+ for (int i = numAnimators - 1; i >= 0; i--) {
+ Animator animator = mCurrentAnimators.get(i);
+ animator.cancel();
+ }
+ if (mListeners != null && mListeners.size() > 0) {
+ @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onTransitionCancel(this);
+ }
+ }
+ }
+
+ /**
+ * Adds a listener to the set of listeners that are sent events through the
+ * life of an animation, such as start, repeat, and end.
+ *
+ * @param listener the listener to be added to the current set of listeners
+ * for this animation.
+ * @return This transition object.
+ */
+ @NonNull
+ public Transition addListener(@NonNull TransitionListener listener) {
+ if (mListeners == null) {
+ mListeners = new ArrayList<>();
+ }
+ mListeners.add(listener);
+ return this;
+ }
+
+ /**
+ * Removes a listener from the set listening to this animation.
+ *
+ * @param listener the listener to be removed from the current set of
+ * listeners for this transition.
+ * @return This transition object.
+ */
+ @NonNull
+ public Transition removeListener(@NonNull TransitionListener listener) {
+ if (mListeners == null) {
+ return this;
+ }
+ mListeners.remove(listener);
+ if (mListeners.size() == 0) {
+ mListeners = null;
+ }
+ return this;
+ }
+
+ /**
+ * Sets the algorithm used to calculate two-dimensional interpolation.
+ * <p>
+ * Transitions such as {@link android.transition.ChangeBounds} move Views, typically
+ * in a straight path between the start and end positions. Applications that desire to
+ * have these motions move in a curve can change how Views interpolate in two dimensions
+ * by extending PathMotion and implementing
+ * {@link android.transition.PathMotion#getPath(float, float, float, float)}.
+ * </p>
+ *
+ * @param pathMotion Algorithm object to use for determining how to interpolate in two
+ * dimensions. If null, a straight-path algorithm will be used.
+ * @see android.transition.ArcMotion
+ * @see PatternPathMotion
+ * @see android.transition.PathMotion
+ */
+ public void setPathMotion(@Nullable PathMotion pathMotion) {
+ if (pathMotion == null) {
+ mPathMotion = STRAIGHT_PATH_MOTION;
+ } else {
+ mPathMotion = pathMotion;
+ }
+ }
+
+ /**
+ * Returns the algorithm object used to interpolate along two dimensions. This is typically
+ * used to determine the View motion between two points.
+ *
+ * @return The algorithm object used to interpolate along two dimensions.
+ * @see android.transition.ArcMotion
+ * @see PatternPathMotion
+ * @see android.transition.PathMotion
+ */
+ @NonNull
+ public PathMotion getPathMotion() {
+ return mPathMotion;
+ }
+
+ /**
+ * Sets the callback to use to find the epicenter of a Transition. A null value indicates
+ * that there is no epicenter in the Transition and onGetEpicenter() will return null.
+ * Transitions like {@link android.transition.Explode} use a point or Rect to orient
+ * the direction of travel. This is called the epicenter of the Transition and is
+ * typically centered on a touched View. The
+ * {@link android.transition.Transition.EpicenterCallback} allows a Transition to
+ * dynamically retrieve the epicenter during a Transition.
+ *
+ * @param epicenterCallback The callback to use to find the epicenter of the Transition.
+ */
+ public void setEpicenterCallback(@Nullable EpicenterCallback epicenterCallback) {
+ mEpicenterCallback = epicenterCallback;
+ }
+
+ /**
+ * Returns the callback used to find the epicenter of the Transition.
+ * Transitions like {@link android.transition.Explode} use a point or Rect to orient
+ * the direction of travel. This is called the epicenter of the Transition and is
+ * typically centered on a touched View. The
+ * {@link android.transition.Transition.EpicenterCallback} allows a Transition to
+ * dynamically retrieve the epicenter during a Transition.
+ *
+ * @return the callback used to find the epicenter of the Transition.
+ */
+ @Nullable
+ public EpicenterCallback getEpicenterCallback() {
+ return mEpicenterCallback;
+ }
+
+ /**
+ * Returns the epicenter as specified by the
+ * {@link android.transition.Transition.EpicenterCallback} or null if no callback exists.
+ *
+ * @return the epicenter as specified by the
+ * {@link android.transition.Transition.EpicenterCallback} or null if no callback exists.
+ * @see #setEpicenterCallback(EpicenterCallback)
+ */
+ @Nullable
+ public Rect getEpicenter() {
+ if (mEpicenterCallback == null) {
+ return null;
+ }
+ return mEpicenterCallback.onGetEpicenter(this);
+ }
+
+ /**
+ * Sets the method for determining Animator start delays.
+ * When a Transition affects several Views like {@link android.transition.Explode} or
+ * {@link android.transition.Slide}, there may be a desire to have a "wave-front" effect
+ * such that the Animator start delay depends on position of the View. The
+ * TransitionPropagation specifies how the start delays are calculated.
+ *
+ * @param transitionPropagation The class used to determine the start delay of
+ * Animators created by this Transition. A null value
+ * indicates that no delay should be used.
+ */
+ public void setPropagation(@Nullable TransitionPropagation transitionPropagation) {
+ mPropagation = transitionPropagation;
+ }
+
+ /**
+ * Returns the {@link android.transition.TransitionPropagation} used to calculate Animator
+ * start
+ * delays.
+ * When a Transition affects several Views like {@link android.transition.Explode} or
+ * {@link android.transition.Slide}, there may be a desire to have a "wave-front" effect
+ * such that the Animator start delay depends on position of the View. The
+ * TransitionPropagation specifies how the start delays are calculated.
+ *
+ * @return the {@link android.transition.TransitionPropagation} used to calculate Animator start
+ * delays. This is null by default.
+ */
+ @Nullable
+ public TransitionPropagation getPropagation() {
+ return mPropagation;
+ }
+
+ /**
+ * Captures TransitionPropagation values for the given view and the
+ * hierarchy underneath it.
+ */
+ void capturePropagationValues(TransitionValues transitionValues) {
+ if (mPropagation != null && !transitionValues.values.isEmpty()) {
+ String[] propertyNames = mPropagation.getPropagationProperties();
+ if (propertyNames == null) {
+ return;
+ }
+ boolean containsAll = true;
+ for (int i = 0; i < propertyNames.length; i++) {
+ if (!transitionValues.values.containsKey(propertyNames[i])) {
+ containsAll = false;
+ break;
+ }
+ }
+ if (!containsAll) {
+ mPropagation.captureValues(transitionValues);
+ }
+ }
+ }
+
+ Transition setSceneRoot(ViewGroup sceneRoot) {
+ mSceneRoot = sceneRoot;
+ return this;
+ }
+
+ void setCanRemoveViews(boolean canRemoveViews) {
+ mCanRemoveViews = canRemoveViews;
+ }
+
+ @Override
+ public String toString() {
+ return toString("");
+ }
+
+ @Override
+ public Transition clone() {
+ try {
+ Transition clone = (Transition) super.clone();
+ clone.mAnimators = new ArrayList<>();
+ clone.mStartValues = new TransitionValuesMaps();
+ clone.mEndValues = new TransitionValuesMaps();
+ clone.mStartValuesList = null;
+ clone.mEndValuesList = null;
+ return clone;
+ } catch (CloneNotSupportedException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the name of this Transition. This name is used internally to distinguish
+ * between different transitions to determine when interrupting transitions overlap.
+ * For example, a ChangeBounds running on the same target view as another ChangeBounds
+ * should determine whether the old transition is animating to different end values
+ * and should be canceled in favor of the new transition.
+ *
+ * <p>By default, a Transition's name is simply the value of {@link Class#getName()},
+ * but subclasses are free to override and return something different.</p>
+ *
+ * @return The name of this transition.
+ */
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ String toString(String indent) {
+ String result = indent + getClass().getSimpleName() + "@"
+ + Integer.toHexString(hashCode()) + ": ";
+ if (mDuration != -1) {
+ result += "dur(" + mDuration + ") ";
+ }
+ if (mStartDelay != -1) {
+ result += "dly(" + mStartDelay + ") ";
+ }
+ if (mInterpolator != null) {
+ result += "interp(" + mInterpolator + ") ";
+ }
+ if (mTargetIds.size() > 0 || mTargets.size() > 0) {
+ result += "tgts(";
+ if (mTargetIds.size() > 0) {
+ for (int i = 0; i < mTargetIds.size(); ++i) {
+ if (i > 0) {
+ result += ", ";
+ }
+ result += mTargetIds.get(i);
+ }
+ }
+ if (mTargets.size() > 0) {
+ for (int i = 0; i < mTargets.size(); ++i) {
+ if (i > 0) {
+ result += ", ";
+ }
+ result += mTargets.get(i);
+ }
+ }
+ result += ")";
+ }
+ return result;
+ }
+
+ /**
+ * A transition listener receives notifications from a transition.
+ * Notifications indicate transition lifecycle events.
+ */
+ public interface TransitionListener {
+
+ /**
+ * Notification about the start of the transition.
+ *
+ * @param transition The started transition.
+ */
+ void onTransitionStart(@NonNull Transition transition);
+
+ /**
+ * Notification about the end of the transition. Canceled transitions
+ * will always notify listeners of both the cancellation and end
+ * events. That is, {@link #onTransitionEnd(Transition)} is always called,
+ * regardless of whether the transition was canceled or played
+ * through to completion.
+ *
+ * @param transition The transition which reached its end.
+ */
+ void onTransitionEnd(@NonNull Transition transition);
+
+ /**
+ * Notification about the cancellation of the transition.
+ * Note that cancel may be called by a parent {@link TransitionSet} on
+ * a child transition which has not yet started. This allows the child
+ * transition to restore state on target objects which was set at
+ * {@link #createAnimator(android.view.ViewGroup, TransitionValues, TransitionValues)
+ * createAnimator()} time.
+ *
+ * @param transition The transition which was canceled.
+ */
+ void onTransitionCancel(@NonNull Transition transition);
+
+ /**
+ * Notification when a transition is paused.
+ * Note that createAnimator() may be called by a parent {@link TransitionSet} on
+ * a child transition which has not yet started. This allows the child
+ * transition to restore state on target objects which was set at
+ * {@link #createAnimator(android.view.ViewGroup, TransitionValues, TransitionValues)
+ * createAnimator()} time.
+ *
+ * @param transition The transition which was paused.
+ */
+ void onTransitionPause(@NonNull Transition transition);
+
+ /**
+ * Notification when a transition is resumed.
+ * Note that resume() may be called by a parent {@link TransitionSet} on
+ * a child transition which has not yet started. This allows the child
+ * transition to restore state which may have changed in an earlier call
+ * to {@link #onTransitionPause(Transition)}.
+ *
+ * @param transition The transition which was resumed.
+ */
+ void onTransitionResume(@NonNull Transition transition);
+ }
+
+ /**
+ * Holds information about each animator used when a new transition starts
+ * while other transitions are still running to determine whether a running
+ * animation should be canceled or a new animation noop'd. The structure holds
+ * information about the state that an animation is going to, to be compared to
+ * end state of a new animation.
+ */
+ private static class AnimationInfo {
+
+ View mView;
+
+ String mName;
+
+ TransitionValues mValues;
+
+ WindowIdImpl mWindowId;
+
+ Transition mTransition;
+
+ AnimationInfo(View view, String name, Transition transition, WindowIdImpl windowId,
+ TransitionValues values) {
+ mView = view;
+ mName = name;
+ mValues = values;
+ mWindowId = windowId;
+ mTransition = transition;
+ }
+ }
+
+ /**
+ * Utility class for managing typed ArrayLists efficiently. In particular, this
+ * can be useful for lists that we don't expect to be used often (eg, the exclude
+ * lists), so we'd like to keep them nulled out by default. This causes the code to
+ * become tedious, with constant null checks, code to allocate when necessary,
+ * and code to null out the reference when the list is empty. This class encapsulates
+ * all of that functionality into simple add()/remove() methods which perform the
+ * necessary checks, allocation/null-out as appropriate, and return the
+ * resulting list.
+ */
+ private static class ArrayListManager {
+
+ /**
+ * Add the specified item to the list, returning the resulting list.
+ * The returned list can either the be same list passed in or, if that
+ * list was null, the new list that was created.
+ *
+ * Note that the list holds unique items; if the item already exists in the
+ * list, the list is not modified.
+ */
+ static <T> ArrayList<T> add(ArrayList<T> list, T item) {
+ if (list == null) {
+ list = new ArrayList<>();
+ }
+ if (!list.contains(item)) {
+ list.add(item);
+ }
+ return list;
+ }
+
+ /**
+ * Remove the specified item from the list, returning the resulting list.
+ * The returned list can either the be same list passed in or, if that
+ * list becomes empty as a result of the remove(), the new list was created.
+ */
+ static <T> ArrayList<T> remove(ArrayList<T> list, T item) {
+ if (list != null) {
+ list.remove(item);
+ if (list.isEmpty()) {
+ list = null;
+ }
+ }
+ return list;
+ }
+ }
+
+ /**
+ * Class to get the epicenter of Transition. Use
+ * {@link #setEpicenterCallback(EpicenterCallback)} to set the callback used to calculate the
+ * epicenter of the Transition. Override {@link #getEpicenter()} to return the rectangular
+ * region in screen coordinates of the epicenter of the transition.
+ *
+ * @see #setEpicenterCallback(EpicenterCallback)
+ */
+ public abstract static class EpicenterCallback {
+
+ /**
+ * Implementers must override to return the epicenter of the Transition in screen
+ * coordinates. Transitions like {@link android.transition.Explode} depend upon
+ * an epicenter for the Transition. In Explode, Views move toward or away from the
+ * center of the epicenter Rect along the vector between the epicenter and the center
+ * of the View appearing and disappearing. Some Transitions, such as
+ * {@link android.transition.Fade} pay no attention to the epicenter.
+ *
+ * @param transition The transition for which the epicenter applies.
+ * @return The Rect region of the epicenter of <code>transition</code> or null if
+ * there is no epicenter.
+ */
+ public abstract Rect onGetEpicenter(@NonNull Transition transition);
+ }
+
+}
diff --git a/androidx/transition/TransitionActivity.java b/androidx/transition/TransitionActivity.java
new file mode 100644
index 0000000..a155a65
--- /dev/null
+++ b/androidx/transition/TransitionActivity.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.os.Bundle;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import androidx.fragment.app.FragmentActivity;
+import androidx.transition.test.R;
+
+public class TransitionActivity extends FragmentActivity {
+
+ private LinearLayout mRoot;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_transition);
+ mRoot = findViewById(R.id.root);
+ }
+
+ ViewGroup getRoot() {
+ return mRoot;
+ }
+
+}
diff --git a/androidx/transition/TransitionInflater.java b/androidx/transition/TransitionInflater.java
new file mode 100644
index 0000000..4570fe9
--- /dev/null
+++ b/androidx/transition/TransitionInflater.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.Xml;
+import android.view.InflateException;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.collection.ArrayMap;
+import androidx.core.content.res.TypedArrayUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+
+/**
+ * This class inflates scenes and transitions from resource files.
+ */
+public class TransitionInflater {
+
+ private static final Class<?>[] CONSTRUCTOR_SIGNATURE =
+ new Class[]{Context.class, AttributeSet.class};
+ private static final ArrayMap<String, Constructor> CONSTRUCTORS = new ArrayMap<>();
+
+ private final Context mContext;
+
+ private TransitionInflater(@NonNull Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Obtains the TransitionInflater from the given context.
+ */
+ public static TransitionInflater from(Context context) {
+ return new TransitionInflater(context);
+ }
+
+ /**
+ * Loads a {@link Transition} object from a resource
+ *
+ * @param resource The resource id of the transition to load
+ * @return The loaded Transition object
+ * @throws android.content.res.Resources.NotFoundException when the
+ * transition cannot be loaded
+ */
+ public Transition inflateTransition(int resource) {
+ XmlResourceParser parser = mContext.getResources().getXml(resource);
+ try {
+ return createTransitionFromXml(parser, Xml.asAttributeSet(parser), null);
+ } catch (XmlPullParserException e) {
+ throw new InflateException(e.getMessage(), e);
+ } catch (IOException e) {
+ throw new InflateException(
+ parser.getPositionDescription() + ": " + e.getMessage(), e);
+ } finally {
+ parser.close();
+ }
+ }
+
+ /**
+ * Loads a {@link TransitionManager} object from a resource
+ *
+ * @param resource The resource id of the transition manager to load
+ * @return The loaded TransitionManager object
+ * @throws android.content.res.Resources.NotFoundException when the
+ * transition manager cannot be loaded
+ */
+ public TransitionManager inflateTransitionManager(int resource, ViewGroup sceneRoot) {
+ XmlResourceParser parser = mContext.getResources().getXml(resource);
+ try {
+ return createTransitionManagerFromXml(parser, Xml.asAttributeSet(parser), sceneRoot);
+ } catch (XmlPullParserException e) {
+ InflateException ex = new InflateException(e.getMessage());
+ ex.initCause(e);
+ throw ex;
+ } catch (IOException e) {
+ InflateException ex = new InflateException(
+ parser.getPositionDescription()
+ + ": " + e.getMessage());
+ ex.initCause(e);
+ throw ex;
+ } finally {
+ parser.close();
+ }
+ }
+
+ //
+ // Transition loading
+ //
+ private Transition createTransitionFromXml(XmlPullParser parser,
+ AttributeSet attrs, Transition parent)
+ throws XmlPullParserException, IOException {
+
+ Transition transition = null;
+
+ // Make sure we are on a start tag.
+ int type;
+ int depth = parser.getDepth();
+
+ TransitionSet transitionSet = (parent instanceof TransitionSet)
+ ? (TransitionSet) parent : null;
+
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ String name = parser.getName();
+ if ("fade".equals(name)) {
+ transition = new Fade(mContext, attrs);
+ } else if ("changeBounds".equals(name)) {
+ transition = new ChangeBounds(mContext, attrs);
+ } else if ("slide".equals(name)) {
+ transition = new Slide(mContext, attrs);
+ } else if ("explode".equals(name)) {
+ transition = new Explode(mContext, attrs);
+ } else if ("changeImageTransform".equals(name)) {
+ transition = new ChangeImageTransform(mContext, attrs);
+ } else if ("changeTransform".equals(name)) {
+ transition = new ChangeTransform(mContext, attrs);
+ } else if ("changeClipBounds".equals(name)) {
+ transition = new ChangeClipBounds(mContext, attrs);
+ } else if ("autoTransition".equals(name)) {
+ transition = new AutoTransition(mContext, attrs);
+ } else if ("changeScroll".equals(name)) {
+ transition = new ChangeScroll(mContext, attrs);
+ } else if ("transitionSet".equals(name)) {
+ transition = new TransitionSet(mContext, attrs);
+ } else if ("transition".equals(name)) {
+ transition = (Transition) createCustom(attrs, Transition.class, "transition");
+ } else if ("targets".equals(name)) {
+ getTargetIds(parser, attrs, parent);
+ } else if ("arcMotion".equals(name)) {
+ if (parent == null) {
+ throw new RuntimeException("Invalid use of arcMotion element");
+ }
+ parent.setPathMotion(new ArcMotion(mContext, attrs));
+ } else if ("pathMotion".equals(name)) {
+ if (parent == null) {
+ throw new RuntimeException("Invalid use of pathMotion element");
+ }
+ parent.setPathMotion((PathMotion) createCustom(attrs, PathMotion.class,
+ "pathMotion"));
+ } else if ("patternPathMotion".equals(name)) {
+ if (parent == null) {
+ throw new RuntimeException("Invalid use of patternPathMotion element");
+ }
+ parent.setPathMotion(new PatternPathMotion(mContext, attrs));
+ } else {
+ throw new RuntimeException("Unknown scene name: " + parser.getName());
+ }
+ if (transition != null) {
+ if (!parser.isEmptyElementTag()) {
+ createTransitionFromXml(parser, attrs, transition);
+ }
+ if (transitionSet != null) {
+ transitionSet.addTransition(transition);
+ transition = null;
+ } else if (parent != null) {
+ throw new InflateException("Could not add transition to another transition.");
+ }
+ }
+ }
+
+ return transition;
+ }
+
+ private Object createCustom(AttributeSet attrs, Class expectedType, String tag) {
+ String className = attrs.getAttributeValue(null, "class");
+
+ if (className == null) {
+ throw new InflateException(tag + " tag must have a 'class' attribute");
+ }
+
+ try {
+ synchronized (CONSTRUCTORS) {
+ Constructor constructor = CONSTRUCTORS.get(className);
+ if (constructor == null) {
+ @SuppressWarnings("unchecked")
+ Class<?> c = mContext.getClassLoader().loadClass(className)
+ .asSubclass(expectedType);
+ if (c != null) {
+ constructor = c.getConstructor(CONSTRUCTOR_SIGNATURE);
+ constructor.setAccessible(true);
+ CONSTRUCTORS.put(className, constructor);
+ }
+ }
+ //noinspection ConstantConditions
+ return constructor.newInstance(mContext, attrs);
+ }
+ } catch (Exception e) {
+ throw new InflateException("Could not instantiate " + expectedType + " class "
+ + className, e);
+ }
+ }
+
+ private void getTargetIds(XmlPullParser parser,
+ AttributeSet attrs, Transition transition) throws XmlPullParserException, IOException {
+
+ // Make sure we are on a start tag.
+ int type;
+ int depth = parser.getDepth();
+
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ String name = parser.getName();
+ if (name.equals("target")) {
+ TypedArray a = mContext.obtainStyledAttributes(attrs, Styleable.TRANSITION_TARGET);
+ int id = TypedArrayUtils.getNamedResourceId(a, parser, "targetId",
+ Styleable.TransitionTarget.TARGET_ID, 0);
+ String transitionName;
+ if (id != 0) {
+ transition.addTarget(id);
+ } else if ((id = TypedArrayUtils.getNamedResourceId(a, parser, "excludeId",
+ Styleable.TransitionTarget.EXCLUDE_ID, 0)) != 0) {
+ transition.excludeTarget(id, true);
+ } else if ((transitionName = TypedArrayUtils.getNamedString(a, parser, "targetName",
+ Styleable.TransitionTarget.TARGET_NAME)) != null) {
+ transition.addTarget(transitionName);
+ } else if ((transitionName = TypedArrayUtils.getNamedString(a, parser,
+ "excludeName", Styleable.TransitionTarget.EXCLUDE_NAME)) != null) {
+ transition.excludeTarget(transitionName, true);
+ } else {
+ String className = TypedArrayUtils.getNamedString(a, parser,
+ "excludeClass", Styleable.TransitionTarget.EXCLUDE_CLASS);
+ try {
+ if (className != null) {
+ Class clazz = Class.forName(className);
+ transition.excludeTarget(clazz, true);
+ } else if ((className = TypedArrayUtils.getNamedString(a, parser,
+ "targetClass", Styleable.TransitionTarget.TARGET_CLASS)) != null) {
+ Class clazz = Class.forName(className);
+ transition.addTarget(clazz);
+ }
+ } catch (ClassNotFoundException e) {
+ a.recycle();
+ throw new RuntimeException("Could not create " + className, e);
+ }
+ }
+ a.recycle();
+ } else {
+ throw new RuntimeException("Unknown scene name: " + parser.getName());
+ }
+ }
+ }
+
+ //
+ // TransitionManager loading
+ //
+
+ private TransitionManager createTransitionManagerFromXml(XmlPullParser parser,
+ AttributeSet attrs, ViewGroup sceneRoot) throws XmlPullParserException, IOException {
+
+ // Make sure we are on a start tag.
+ int type;
+ int depth = parser.getDepth();
+ TransitionManager transitionManager = null;
+
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ String name = parser.getName();
+ if (name.equals("transitionManager")) {
+ transitionManager = new TransitionManager();
+ } else if (name.equals("transition") && (transitionManager != null)) {
+ loadTransition(attrs, parser, sceneRoot, transitionManager);
+ } else {
+ throw new RuntimeException("Unknown scene name: " + parser.getName());
+ }
+ }
+ return transitionManager;
+ }
+
+ private void loadTransition(AttributeSet attrs, XmlPullParser parser, ViewGroup sceneRoot,
+ TransitionManager transitionManager) throws Resources.NotFoundException {
+
+ TypedArray a = mContext.obtainStyledAttributes(attrs, Styleable.TRANSITION_MANAGER);
+ int transitionId = TypedArrayUtils.getNamedResourceId(a, parser, "transition",
+ Styleable.TransitionManager.TRANSITION, -1);
+ int fromId = TypedArrayUtils.getNamedResourceId(a, parser, "fromScene",
+ Styleable.TransitionManager.FROM_SCENE, -1);
+ Scene fromScene = (fromId < 0) ? null : Scene.getSceneForLayout(sceneRoot, fromId,
+ mContext);
+ int toId = TypedArrayUtils.getNamedResourceId(a, parser, "toScene",
+ Styleable.TransitionManager.TO_SCENE, -1);
+ Scene toScene = (toId < 0) ? null : Scene.getSceneForLayout(sceneRoot, toId, mContext);
+
+ if (transitionId >= 0) {
+ Transition transition = inflateTransition(transitionId);
+ if (transition != null) {
+ if (toScene == null) {
+ throw new RuntimeException("No toScene for transition ID " + transitionId);
+ }
+ if (fromScene == null) {
+ transitionManager.setTransition(toScene, transition);
+ } else {
+ transitionManager.setTransition(fromScene, toScene, transition);
+ }
+ }
+ }
+ a.recycle();
+ }
+
+}
diff --git a/androidx/transition/TransitionInflaterTest.java b/androidx/transition/TransitionInflaterTest.java
new file mode 100644
index 0000000..a0cc8d8
--- /dev/null
+++ b/androidx/transition/TransitionInflaterTest.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.support.test.filters.MediumTest;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.transition.test.R;
+
+import org.junit.Test;
+
+import java.util.List;
+
+@MediumTest
+public class TransitionInflaterTest extends BaseTest {
+
+ @Test
+ public void testInflationConstructors() throws Throwable {
+ TransitionInflater inflater = TransitionInflater.from(rule.getActivity());
+ Transition transition = inflater.inflateTransition(R.transition.transition_constructors);
+ assertTrue(transition instanceof TransitionSet);
+ TransitionSet set = (TransitionSet) transition;
+ assertEquals(10, set.getTransitionCount());
+ }
+
+ @Test
+ public void testInflation() {
+ TransitionInflater inflater = TransitionInflater.from(rule.getActivity());
+ verifyFadeProperties(inflater.inflateTransition(R.transition.fade));
+ verifyChangeBoundsProperties(inflater.inflateTransition(R.transition.change_bounds));
+ verifySlideProperties(inflater.inflateTransition(R.transition.slide));
+ verifyExplodeProperties(inflater.inflateTransition(R.transition.explode));
+ verifyChangeImageTransformProperties(
+ inflater.inflateTransition(R.transition.change_image_transform));
+ verifyChangeTransformProperties(inflater.inflateTransition(R.transition.change_transform));
+ verifyChangeClipBoundsProperties(
+ inflater.inflateTransition(R.transition.change_clip_bounds));
+ verifyAutoTransitionProperties(inflater.inflateTransition(R.transition.auto_transition));
+ verifyChangeScrollProperties(inflater.inflateTransition(R.transition.change_scroll));
+ verifyTransitionSetProperties(inflater.inflateTransition(R.transition.transition_set));
+ verifyCustomTransitionProperties(
+ inflater.inflateTransition(R.transition.custom_transition));
+ verifyTargetIds(inflater.inflateTransition(R.transition.target_ids));
+ verifyTargetNames(inflater.inflateTransition(R.transition.target_names));
+ verifyTargetClass(inflater.inflateTransition(R.transition.target_classes));
+ verifyArcMotion(inflater.inflateTransition(R.transition.arc_motion));
+ verifyCustomPathMotion(inflater.inflateTransition(R.transition.custom_path_motion));
+ verifyPatternPathMotion(inflater.inflateTransition(R.transition.pattern_path_motion));
+ }
+
+ // TODO: Add test for TransitionManager
+
+ private void verifyFadeProperties(Transition transition) {
+ assertTrue(transition instanceof Fade);
+ Fade fade = (Fade) transition;
+ assertEquals(Fade.OUT, fade.getMode());
+ }
+
+ private void verifyChangeBoundsProperties(Transition transition) {
+ assertTrue(transition instanceof ChangeBounds);
+ ChangeBounds changeBounds = (ChangeBounds) transition;
+ assertTrue(changeBounds.getResizeClip());
+ }
+
+ private void verifySlideProperties(Transition transition) {
+ assertTrue(transition instanceof Slide);
+ Slide slide = (Slide) transition;
+ assertEquals(Gravity.TOP, slide.getSlideEdge());
+ }
+
+ private void verifyExplodeProperties(Transition transition) {
+ assertTrue(transition instanceof Explode);
+ Visibility visibility = (Visibility) transition;
+ assertEquals(Visibility.MODE_IN, visibility.getMode());
+ }
+
+ private void verifyChangeImageTransformProperties(Transition transition) {
+ assertTrue(transition instanceof ChangeImageTransform);
+ }
+
+ private void verifyChangeTransformProperties(Transition transition) {
+ assertTrue(transition instanceof ChangeTransform);
+ ChangeTransform changeTransform = (ChangeTransform) transition;
+ assertFalse(changeTransform.getReparent());
+ assertFalse(changeTransform.getReparentWithOverlay());
+ }
+
+ private void verifyChangeClipBoundsProperties(Transition transition) {
+ assertTrue(transition instanceof ChangeClipBounds);
+ }
+
+ private void verifyAutoTransitionProperties(Transition transition) {
+ assertTrue(transition instanceof AutoTransition);
+ }
+
+ private void verifyChangeScrollProperties(Transition transition) {
+ assertTrue(transition instanceof ChangeScroll);
+ }
+
+ private void verifyTransitionSetProperties(Transition transition) {
+ assertTrue(transition instanceof TransitionSet);
+ TransitionSet set = (TransitionSet) transition;
+ assertEquals(TransitionSet.ORDERING_SEQUENTIAL, set.getOrdering());
+ assertEquals(2, set.getTransitionCount());
+ assertTrue(set.getTransitionAt(0) instanceof ChangeBounds);
+ assertTrue(set.getTransitionAt(1) instanceof Fade);
+ }
+
+ private void verifyCustomTransitionProperties(Transition transition) {
+ assertTrue(transition instanceof CustomTransition);
+ }
+
+ private void verifyTargetIds(Transition transition) {
+ List<Integer> targets = transition.getTargetIds();
+ assertNotNull(targets);
+ assertEquals(2, targets.size());
+ assertEquals(R.id.hello, (int) targets.get(0));
+ assertEquals(R.id.world, (int) targets.get(1));
+ }
+
+ private void verifyTargetNames(Transition transition) {
+ List<String> targets = transition.getTargetNames();
+ assertNotNull(targets);
+ assertEquals(2, targets.size());
+ assertEquals("hello", targets.get(0));
+ assertEquals("world", targets.get(1));
+ }
+
+ private void verifyTargetClass(Transition transition) {
+ List<Class> targets = transition.getTargetTypes();
+ assertNotNull(targets);
+ assertEquals(2, targets.size());
+ assertEquals(TextView.class, targets.get(0));
+ assertEquals(ImageView.class, targets.get(1));
+ }
+
+ private void verifyArcMotion(Transition transition) {
+ assertNotNull(transition);
+ PathMotion motion = transition.getPathMotion();
+ assertNotNull(motion);
+ assertTrue(motion instanceof ArcMotion);
+ ArcMotion arcMotion = (ArcMotion) motion;
+ assertEquals(1f, arcMotion.getMinimumVerticalAngle(), 0.01f);
+ assertEquals(2f, arcMotion.getMinimumHorizontalAngle(), 0.01f);
+ assertEquals(53f, arcMotion.getMaximumAngle(), 0.01f);
+ }
+
+ private void verifyCustomPathMotion(Transition transition) {
+ assertNotNull(transition);
+ PathMotion motion = transition.getPathMotion();
+ assertNotNull(motion);
+ assertTrue(motion instanceof CustomPathMotion);
+ }
+
+ private void verifyPatternPathMotion(Transition transition) {
+ assertNotNull(transition);
+ PathMotion motion = transition.getPathMotion();
+ assertNotNull(motion);
+ assertTrue(motion instanceof PatternPathMotion);
+ PatternPathMotion pattern = (PatternPathMotion) motion;
+ Path path = pattern.getPatternPath();
+ PathMeasure measure = new PathMeasure(path, false);
+ assertEquals(200f, measure.getLength(), 0.1f);
+ }
+
+ public static class CustomTransition extends Transition {
+ public CustomTransition() {
+ fail("Default constructor was not expected");
+ }
+
+ @SuppressWarnings("unused") // This constructor is used in XML
+ public CustomTransition(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ }
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ }
+ }
+
+ public static class CustomPathMotion extends PathMotion {
+ public CustomPathMotion() {
+ fail("default constructor shouldn't be called.");
+ }
+
+ public CustomPathMotion(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public Path getPath(float startX, float startY, float endX, float endY) {
+ return null;
+ }
+ }
+
+ public static class InflationFade extends Fade {
+ public InflationFade(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationChangeBounds extends ChangeBounds {
+ public InflationChangeBounds(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationSlide extends Slide {
+ public InflationSlide(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationTransitionSet extends TransitionSet {
+ public InflationTransitionSet(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationChangeImageTransform extends ChangeImageTransform {
+ public InflationChangeImageTransform(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationChangeTransform extends ChangeTransform {
+ public InflationChangeTransform(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationAutoTransition extends AutoTransition {
+ public InflationAutoTransition(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationChangeClipBounds extends ChangeClipBounds {
+ public InflationChangeClipBounds(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationChangeScroll extends ChangeScroll {
+ public InflationChangeScroll(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationExplode extends Explode {
+ public InflationExplode(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+}
diff --git a/androidx/transition/TransitionListenerAdapter.java b/androidx/transition/TransitionListenerAdapter.java
new file mode 100644
index 0000000..6f93260
--- /dev/null
+++ b/androidx/transition/TransitionListenerAdapter.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import androidx.annotation.NonNull;
+
+/**
+ * This adapter class provides empty implementations of the methods from {@link
+ * Transition.TransitionListener}.
+ * Any custom listener that cares only about a subset of the methods of this listener can
+ * simply subclass this adapter class instead of implementing the interface directly.
+ */
+public class TransitionListenerAdapter implements Transition.TransitionListener {
+
+ @Override
+ public void onTransitionStart(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ }
+
+}
diff --git a/androidx/transition/TransitionManager.java b/androidx/transition/TransitionManager.java
new file mode 100644
index 0000000..517d2e3
--- /dev/null
+++ b/androidx/transition/TransitionManager.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.collection.ArrayMap;
+import androidx.core.view.ViewCompat;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * This class manages the set of transitions that fire when there is a
+ * change of {@link Scene}. To use the manager, add scenes along with
+ * transition objects with calls to {@link #setTransition(Scene, Transition)}
+ * or {@link #setTransition(Scene, Scene, Transition)}. Setting specific
+ * transitions for scene changes is not required; by default, a Scene change
+ * will use {@link AutoTransition} to do something reasonable for most
+ * situations. Specifying other transitions for particular scene changes is
+ * only necessary if the application wants different transition behavior
+ * in these situations.
+ *
+ * <p>TransitionManagers can be declared in XML resource files inside the
+ * <code>res/transition</code> directory. TransitionManager resources consist of
+ * the <code>transitionManager</code>tag name, containing one or more
+ * <code>transition</code> tags, each of which describe the relationship of
+ * that transition to the from/to scene information in that tag.
+ * For example, here is a resource file that declares several scene
+ * transitions:</p>
+ *
+ * <pre>
+ * <transitionManager xmlns:android="http://schemas.android.com/apk/res/android">
+ * <transition android:fromScene="@layout/transition_scene1"
+ * android:toScene="@layout/transition_scene2"
+ * android:transition="@transition/changebounds"/>
+ * <transition android:fromScene="@layout/transition_scene2"
+ * android:toScene="@layout/transition_scene1"
+ * android:transition="@transition/changebounds"/>
+ * <transition android:toScene="@layout/transition_scene3"
+ * android:transition="@transition/changebounds_fadein_together"/>
+ * <transition android:fromScene="@layout/transition_scene3"
+ * android:toScene="@layout/transition_scene1"
+ * android:transition="@transition/changebounds_fadeout_sequential"/>
+ * <transition android:fromScene="@layout/transition_scene3"
+ * android:toScene="@layout/transition_scene2"
+ * android:transition="@transition/changebounds_fadeout_sequential"/>
+ * </transitionManager>
+ * </pre>
+ *
+ * <p>For each of the <code>fromScene</code> and <code>toScene</code> attributes,
+ * there is a reference to a standard XML layout file. This is equivalent to
+ * creating a scene from a layout in code by calling
+ * {@link Scene#getSceneForLayout(ViewGroup, int, Context)}. For the
+ * <code>transition</code> attribute, there is a reference to a resource
+ * file in the <code>res/transition</code> directory which describes that
+ * transition.</p>
+ */
+public class TransitionManager {
+
+ private static final String LOG_TAG = "TransitionManager";
+
+ private static Transition sDefaultTransition = new AutoTransition();
+
+ private ArrayMap<Scene, Transition> mSceneTransitions = new ArrayMap<>();
+ private ArrayMap<Scene, ArrayMap<Scene, Transition>> mScenePairTransitions = new ArrayMap<>();
+ private static ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>>
+ sRunningTransitions = new ThreadLocal<>();
+ private static ArrayList<ViewGroup> sPendingTransitions = new ArrayList<>();
+
+ /**
+ * Sets a specific transition to occur when the given scene is entered.
+ *
+ * @param scene The scene which, when applied, will cause the given
+ * transition to run.
+ * @param transition The transition that will play when the given scene is
+ * entered. A value of null will result in the default behavior of
+ * using the default transition instead.
+ */
+ public void setTransition(@NonNull Scene scene, @Nullable Transition transition) {
+ mSceneTransitions.put(scene, transition);
+ }
+
+ /**
+ * Sets a specific transition to occur when the given pair of scenes is
+ * exited/entered.
+ *
+ * @param fromScene The scene being exited when the given transition will
+ * be run
+ * @param toScene The scene being entered when the given transition will
+ * be run
+ * @param transition The transition that will play when the given scene is
+ * entered. A value of null will result in the default behavior of
+ * using the default transition instead.
+ */
+ public void setTransition(@NonNull Scene fromScene, @NonNull Scene toScene,
+ @Nullable Transition transition) {
+ ArrayMap<Scene, Transition> sceneTransitionMap = mScenePairTransitions.get(toScene);
+ if (sceneTransitionMap == null) {
+ sceneTransitionMap = new ArrayMap<>();
+ mScenePairTransitions.put(toScene, sceneTransitionMap);
+ }
+ sceneTransitionMap.put(fromScene, transition);
+ }
+
+ /**
+ * Returns the Transition for the given scene being entered. The result
+ * depends not only on the given scene, but also the scene which the
+ * {@link Scene#getSceneRoot() sceneRoot} of the Scene is currently in.
+ *
+ * @param scene The scene being entered
+ * @return The Transition to be used for the given scene change. If no
+ * Transition was specified for this scene change, the default transition
+ * will be used instead.
+ */
+ private Transition getTransition(Scene scene) {
+ Transition transition;
+ ViewGroup sceneRoot = scene.getSceneRoot();
+ if (sceneRoot != null) {
+ // TODO: cached in Scene instead? long-term, cache in View itself
+ Scene currScene = Scene.getCurrentScene(sceneRoot);
+ if (currScene != null) {
+ ArrayMap<Scene, Transition> sceneTransitionMap = mScenePairTransitions
+ .get(scene);
+ if (sceneTransitionMap != null) {
+ transition = sceneTransitionMap.get(currScene);
+ if (transition != null) {
+ return transition;
+ }
+ }
+ }
+ }
+ transition = mSceneTransitions.get(scene);
+ return (transition != null) ? transition : sDefaultTransition;
+ }
+
+ /**
+ * This is where all of the work of a transition/scene-change is
+ * orchestrated. This method captures the start values for the given
+ * transition, exits the current Scene, enters the new scene, captures
+ * the end values for the transition, and finally plays the
+ * resulting values-populated transition.
+ *
+ * @param scene The scene being entered
+ * @param transition The transition to play for this scene change
+ */
+ private static void changeScene(Scene scene, Transition transition) {
+ final ViewGroup sceneRoot = scene.getSceneRoot();
+
+ if (!sPendingTransitions.contains(sceneRoot)) {
+ if (transition == null) {
+ scene.enter();
+ } else {
+ sPendingTransitions.add(sceneRoot);
+
+ Transition transitionClone = transition.clone();
+ transitionClone.setSceneRoot(sceneRoot);
+
+ Scene oldScene = Scene.getCurrentScene(sceneRoot);
+ if (oldScene != null && oldScene.isCreatedFromLayoutResource()) {
+ transitionClone.setCanRemoveViews(true);
+ }
+
+ sceneChangeSetup(sceneRoot, transitionClone);
+
+ scene.enter();
+
+ sceneChangeRunTransition(sceneRoot, transitionClone);
+ }
+ }
+ }
+
+ static ArrayMap<ViewGroup, ArrayList<Transition>> getRunningTransitions() {
+ WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>> runningTransitions =
+ sRunningTransitions.get();
+ if (runningTransitions == null || runningTransitions.get() == null) {
+ ArrayMap<ViewGroup, ArrayList<Transition>> transitions = new ArrayMap<>();
+ runningTransitions = new WeakReference<>(transitions);
+ sRunningTransitions.set(runningTransitions);
+ }
+ return runningTransitions.get();
+ }
+
+ private static void sceneChangeRunTransition(final ViewGroup sceneRoot,
+ final Transition transition) {
+ if (transition != null && sceneRoot != null) {
+ MultiListener listener = new MultiListener(transition, sceneRoot);
+ sceneRoot.addOnAttachStateChangeListener(listener);
+ sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener);
+ }
+ }
+
+ /**
+ * This private utility class is used to listen for both OnPreDraw and
+ * OnAttachStateChange events. OnPreDraw events are the main ones we care
+ * about since that's what triggers the transition to take place.
+ * OnAttachStateChange events are also important in case the view is removed
+ * from the hierarchy before the OnPreDraw event takes place; it's used to
+ * clean up things since the OnPreDraw listener didn't get called in time.
+ */
+ private static class MultiListener implements ViewTreeObserver.OnPreDrawListener,
+ View.OnAttachStateChangeListener {
+
+ Transition mTransition;
+
+ ViewGroup mSceneRoot;
+
+ MultiListener(Transition transition, ViewGroup sceneRoot) {
+ mTransition = transition;
+ mSceneRoot = sceneRoot;
+ }
+
+ private void removeListeners() {
+ mSceneRoot.getViewTreeObserver().removeOnPreDrawListener(this);
+ mSceneRoot.removeOnAttachStateChangeListener(this);
+ }
+
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ removeListeners();
+
+ sPendingTransitions.remove(mSceneRoot);
+ ArrayList<Transition> runningTransitions = getRunningTransitions().get(mSceneRoot);
+ if (runningTransitions != null && runningTransitions.size() > 0) {
+ for (Transition runningTransition : runningTransitions) {
+ runningTransition.resume(mSceneRoot);
+ }
+ }
+ mTransition.clearValues(true);
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ removeListeners();
+
+ // Don't start the transition if it's no longer pending.
+ if (!sPendingTransitions.remove(mSceneRoot)) {
+ return true;
+ }
+
+ // Add to running list, handle end to remove it
+ final ArrayMap<ViewGroup, ArrayList<Transition>> runningTransitions =
+ getRunningTransitions();
+ ArrayList<Transition> currentTransitions = runningTransitions.get(mSceneRoot);
+ ArrayList<Transition> previousRunningTransitions = null;
+ if (currentTransitions == null) {
+ currentTransitions = new ArrayList<>();
+ runningTransitions.put(mSceneRoot, currentTransitions);
+ } else if (currentTransitions.size() > 0) {
+ previousRunningTransitions = new ArrayList<>(currentTransitions);
+ }
+ currentTransitions.add(mTransition);
+ mTransition.addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ ArrayList<Transition> currentTransitions = runningTransitions.get(mSceneRoot);
+ currentTransitions.remove(transition);
+ }
+ });
+ mTransition.captureValues(mSceneRoot, false);
+ if (previousRunningTransitions != null) {
+ for (Transition runningTransition : previousRunningTransitions) {
+ runningTransition.resume(mSceneRoot);
+ }
+ }
+ mTransition.playTransition(mSceneRoot);
+
+ return true;
+ }
+ }
+
+ private static void sceneChangeSetup(ViewGroup sceneRoot, Transition transition) {
+ // Capture current values
+ ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
+
+ if (runningTransitions != null && runningTransitions.size() > 0) {
+ for (Transition runningTransition : runningTransitions) {
+ runningTransition.pause(sceneRoot);
+ }
+ }
+
+ if (transition != null) {
+ transition.captureValues(sceneRoot, true);
+ }
+
+ // Notify previous scene that it is being exited
+ Scene previousScene = Scene.getCurrentScene(sceneRoot);
+ if (previousScene != null) {
+ previousScene.exit();
+ }
+ }
+
+ /**
+ * Change to the given scene, using the
+ * appropriate transition for this particular scene change
+ * (as specified to the TransitionManager, or the default
+ * if no such transition exists).
+ *
+ * @param scene The Scene to change to
+ */
+ public void transitionTo(@NonNull Scene scene) {
+ // Auto transition if there is no transition declared for the Scene, but there is
+ // a root or parent view
+ changeScene(scene, getTransition(scene));
+ }
+
+ /**
+ * Convenience method to simply change to the given scene using
+ * the default transition for TransitionManager.
+ *
+ * @param scene The Scene to change to
+ */
+ public static void go(@NonNull Scene scene) {
+ changeScene(scene, sDefaultTransition);
+ }
+
+ /**
+ * Convenience method to simply change to the given scene using
+ * the given transition.
+ *
+ * <p>Passing in <code>null</code> for the transition parameter will
+ * result in the scene changing without any transition running, and is
+ * equivalent to calling {@link Scene#exit()} on the scene root's
+ * current scene, followed by {@link Scene#enter()} on the scene
+ * specified by the <code>scene</code> parameter.</p>
+ *
+ * @param scene The Scene to change to
+ * @param transition The transition to use for this scene change. A
+ * value of null causes the scene change to happen with no transition.
+ */
+ public static void go(@NonNull Scene scene, @Nullable Transition transition) {
+ changeScene(scene, transition);
+ }
+
+ /**
+ * Convenience method to animate, using the default transition,
+ * to a new scene defined by all changes within the given scene root between
+ * calling this method and the next rendering frame.
+ * Equivalent to calling {@link #beginDelayedTransition(ViewGroup, Transition)}
+ * with a value of <code>null</code> for the <code>transition</code> parameter.
+ *
+ * @param sceneRoot The root of the View hierarchy to run the transition on.
+ */
+ public static void beginDelayedTransition(@NonNull final ViewGroup sceneRoot) {
+ beginDelayedTransition(sceneRoot, null);
+ }
+
+ /**
+ * Convenience method to animate to a new scene defined by all changes within
+ * the given scene root between calling this method and the next rendering frame.
+ * Calling this method causes TransitionManager to capture current values in the
+ * scene root and then post a request to run a transition on the next frame.
+ * At that time, the new values in the scene root will be captured and changes
+ * will be animated. There is no need to create a Scene; it is implied by
+ * changes which take place between calling this method and the next frame when
+ * the transition begins.
+ *
+ * <p>Calling this method several times before the next frame (for example, if
+ * unrelated code also wants to make dynamic changes and run a transition on
+ * the same scene root), only the first call will trigger capturing values
+ * and exiting the current scene. Subsequent calls to the method with the
+ * same scene root during the same frame will be ignored.</p>
+ *
+ * <p>Passing in <code>null</code> for the transition parameter will
+ * cause the TransitionManager to use its default transition.</p>
+ *
+ * @param sceneRoot The root of the View hierarchy to run the transition on.
+ * @param transition The transition to use for this change. A
+ * value of null causes the TransitionManager to use the default transition.
+ */
+ public static void beginDelayedTransition(@NonNull final ViewGroup sceneRoot,
+ @Nullable Transition transition) {
+ if (!sPendingTransitions.contains(sceneRoot) && ViewCompat.isLaidOut(sceneRoot)) {
+ if (Transition.DBG) {
+ Log.d(LOG_TAG, "beginDelayedTransition: root, transition = "
+ + sceneRoot + ", " + transition);
+ }
+ sPendingTransitions.add(sceneRoot);
+ if (transition == null) {
+ transition = sDefaultTransition;
+ }
+ final Transition transitionClone = transition.clone();
+ sceneChangeSetup(sceneRoot, transitionClone);
+ Scene.setCurrentScene(sceneRoot, null);
+ sceneChangeRunTransition(sceneRoot, transitionClone);
+ }
+ }
+
+ /**
+ * Ends all pending and ongoing transitions on the specified scene root.
+ *
+ * @param sceneRoot The root of the View hierarchy to end transitions on.
+ */
+ public static void endTransitions(final ViewGroup sceneRoot) {
+ sPendingTransitions.remove(sceneRoot);
+ final ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
+ if (runningTransitions != null && !runningTransitions.isEmpty()) {
+ // Make a copy in case this is called by an onTransitionEnd listener
+ ArrayList<Transition> copy = new ArrayList<>(runningTransitions);
+ for (int i = copy.size() - 1; i >= 0; i--) {
+ final Transition transition = copy.get(i);
+ transition.forceToEnd(sceneRoot);
+ }
+ }
+ }
+
+}
diff --git a/androidx/transition/TransitionManagerTest.java b/androidx/transition/TransitionManagerTest.java
new file mode 100644
index 0000000..87b490a
--- /dev/null
+++ b/androidx/transition/TransitionManagerTest.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
+import android.view.ViewGroup;
+
+import androidx.transition.test.R;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@MediumTest
+public class TransitionManagerTest extends BaseTest {
+
+ private Scene[] mScenes = new Scene[2];
+
+ @Before
+ public void prepareScenes() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ mScenes[0] = Scene.getSceneForLayout(root, R.layout.support_scene0, activity);
+ mScenes[1] = Scene.getSceneForLayout(root, R.layout.support_scene1, activity);
+ }
+
+ @Test
+ public void testSetup() {
+ assertThat(mScenes[0], is(notNullValue()));
+ assertThat(mScenes[1], is(notNullValue()));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testGo_enterAction() {
+ CheckCalledRunnable runnable = new CheckCalledRunnable();
+ mScenes[0].setEnterAction(runnable);
+ assertThat(runnable.wasCalled(), is(false));
+ TransitionManager.go(mScenes[0]);
+ assertThat(runnable.wasCalled(), is(true));
+ }
+
+ @Test
+ public void testGo_exitAction() throws Throwable {
+ final CheckCalledRunnable enter = new CheckCalledRunnable();
+ final CheckCalledRunnable exit = new CheckCalledRunnable();
+ mScenes[0].setEnterAction(enter);
+ mScenes[0].setExitAction(exit);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ assertThat(enter.wasCalled(), is(false));
+ assertThat(exit.wasCalled(), is(false));
+ TransitionManager.go(mScenes[0]);
+ assertThat(enter.wasCalled(), is(true));
+ assertThat(exit.wasCalled(), is(false));
+ }
+ });
+ // Let the main thread catch up with the scene change
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.go(mScenes[1]);
+ assertThat(exit.wasCalled(), is(true));
+ }
+ });
+ }
+
+ @Test
+ public void testGo_transitionListenerStart() throws Throwable {
+ final SyncTransitionListener listener =
+ new SyncTransitionListener(SyncTransitionListener.EVENT_START);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Transition transition = new AutoTransition();
+ transition.setDuration(0);
+ assertThat(transition.addListener(listener), is(sameInstance(transition)));
+ TransitionManager.go(mScenes[0], transition);
+ }
+ });
+ assertThat("Timed out waiting for the TransitionListener",
+ listener.await(), is(true));
+ }
+
+ @Test
+ public void testGo_transitionListenerEnd() throws Throwable {
+ final SyncTransitionListener listener =
+ new SyncTransitionListener(SyncTransitionListener.EVENT_END);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Transition transition = new AutoTransition();
+ transition.setDuration(0);
+ assertThat(transition.addListener(listener), is(sameInstance(transition)));
+ TransitionManager.go(mScenes[0], transition);
+ }
+ });
+ assertThat("Timed out waiting for the TransitionListener",
+ listener.await(), is(true));
+ }
+
+ @Test
+ public void testGo_nullParameter() throws Throwable {
+ final ViewGroup root = rule.getActivity().getRoot();
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.go(mScenes[0], null);
+ assertThat(Scene.getCurrentScene(root), is(mScenes[0]));
+ TransitionManager.go(mScenes[1], null);
+ assertThat(Scene.getCurrentScene(root), is(mScenes[1]));
+ }
+ });
+ }
+
+ @Test
+ public void testEndTransitions() throws Throwable {
+ final ViewGroup root = rule.getActivity().getRoot();
+ final Transition transition = new AutoTransition();
+ // This transition is very long, but will be forced to end as soon as it starts
+ transition.setDuration(30000);
+ final Transition.TransitionListener listener = mock(Transition.TransitionListener.class);
+ transition.addListener(listener);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.go(mScenes[0], transition);
+ }
+ });
+ verify(listener, timeout(3000)).onTransitionStart(any(Transition.class));
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.endTransitions(root);
+ }
+ });
+ verify(listener, timeout(3000)).onTransitionEnd(any(Transition.class));
+ }
+
+ @Test
+ public void testEndTransitionsBeforeStarted() throws Throwable {
+ final ViewGroup root = rule.getActivity().getRoot();
+ final Transition transition = new AutoTransition();
+ transition.setDuration(0);
+ final Transition.TransitionListener listener = mock(Transition.TransitionListener.class);
+ transition.addListener(listener);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.go(mScenes[0], transition);
+ // This terminates the transition before it starts
+ TransitionManager.endTransitions(root);
+ }
+ });
+ verify(listener, never()).onTransitionStart(any(Transition.class));
+ verify(listener, never()).onTransitionEnd(any(Transition.class));
+ }
+
+}
diff --git a/androidx/transition/TransitionPropagation.java b/androidx/transition/TransitionPropagation.java
new file mode 100644
index 0000000..004aee5
--- /dev/null
+++ b/androidx/transition/TransitionPropagation.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.view.ViewGroup;
+
+/**
+ * Extend <code>TransitionPropagation</code> to customize start delays for Animators created
+ * in {@link Transition#createAnimator(ViewGroup, TransitionValues, TransitionValues)}.
+ * A Transition such as {@link Explode} defaults to using {@link CircularPropagation} and Views
+ * closer to the epicenter will move out of the scene later and into the scene sooner than Views
+ * farther from the epicenter, giving the appearance of inertia. With no TransitionPropagation, all
+ * Views will react simultaneously to the start of the transition.
+ *
+ * @see Transition#setPropagation(TransitionPropagation)
+ * @see Transition#getEpicenter()
+ */
+public abstract class TransitionPropagation {
+
+ /**
+ * Called by Transition to alter the Animator start delay. All start delays will be adjusted
+ * such that the minimum becomes zero.
+ *
+ * @param sceneRoot The root of the View hierarchy running the transition.
+ * @param transition The transition that created the Animator
+ * @param startValues The values for a specific target in the start scene.
+ * @param endValues The values for the target in the end scene.
+ * @return A start delay to use with the Animator created by <code>transition</code>. The
+ * delay will be offset by the minimum delay of all <code>TransitionPropagation</code>s
+ * used in the Transition so that the smallest delay will be 0. Returned values may be
+ * negative.
+ */
+ public abstract long getStartDelay(ViewGroup sceneRoot, Transition transition,
+ TransitionValues startValues, TransitionValues endValues);
+
+ /**
+ * Captures the values in the start or end scene for the properties that this
+ * transition propagation monitors. These values are then passed as the startValues
+ * or endValues structure in a later call to
+ * {@link #getStartDelay(ViewGroup, Transition, TransitionValues, TransitionValues)}.
+ * The main concern for an implementation is what the
+ * properties are that the transition cares about and what the values are
+ * for all of those properties. The start and end values will be compared
+ * later during the
+ * {@link #getStartDelay(ViewGroup, Transition, TransitionValues, TransitionValues)}.
+ * method to determine the start delay.
+ *
+ * <p>Subclasses must implement this method. The method should only be called by the
+ * transition system; it is not intended to be called from external classes.</p>
+ *
+ * @param transitionValues The holder for any values that the Transition
+ * wishes to store. Values are stored in the <code>values</code> field
+ * of this TransitionValues object and are keyed from
+ * a String value. For example, to store a view's rotation value,
+ * a transition might call
+ * <code>transitionValues.values.put("appname:transitionname:rotation",
+ * view.getRotation())</code>. The target view will already be stored
+ * in
+ * the transitionValues structure when this method is called.
+ */
+ public abstract void captureValues(TransitionValues transitionValues);
+
+ /**
+ * Returns the set of property names stored in the {@link TransitionValues}
+ * object passed into {@link #captureValues(TransitionValues)} that
+ * this transition propagation cares about for the purposes of preventing
+ * duplicate capturing of property values.
+ *
+ * <p>A <code>TransitionPropagation</code> must override this method to prevent
+ * duplicate capturing of values and must contain at least one </p>
+ *
+ * @return An array of property names as described in the class documentation for
+ * {@link TransitionValues}.
+ */
+ public abstract String[] getPropagationProperties();
+
+}
diff --git a/androidx/transition/TransitionSet.java b/androidx/transition/TransitionSet.java
new file mode 100644
index 0000000..bf945fd
--- /dev/null
+++ b/androidx/transition/TransitionSet.java
@@ -0,0 +1,602 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AndroidRuntimeException;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.IdRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.content.res.TypedArrayUtils;
+
+import java.util.ArrayList;
+
+/**
+ * A TransitionSet is a parent of child transitions (including other
+ * TransitionSets). Using TransitionSets enables more complex
+ * choreography of transitions, where some sets play {@link #ORDERING_TOGETHER} and
+ * others play {@link #ORDERING_SEQUENTIAL}. For example, {@link AutoTransition}
+ * uses a TransitionSet to sequentially play a Fade(Fade.OUT), followed by
+ * a {@link ChangeBounds}, followed by a Fade(Fade.OUT) transition.
+ *
+ * <p>A TransitionSet can be described in a resource file by using the
+ * tag <code>transitionSet</code>, along with the standard
+ * attributes of {@code TransitionSet} and {@link Transition}. Child transitions of the
+ * TransitionSet object can be loaded by adding those child tags inside the
+ * enclosing <code>transitionSet</code> tag. For example, the following xml
+ * describes a TransitionSet that plays a Fade and then a ChangeBounds
+ * transition on the affected view targets:</p>
+ * <pre>
+ * <transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:transitionOrdering="sequential">
+ * <fade/>
+ * <changeBounds/>
+ * </transitionSet>
+ * </pre>
+ */
+public class TransitionSet extends Transition {
+
+ private ArrayList<Transition> mTransitions = new ArrayList<>();
+ private boolean mPlayTogether = true;
+ private int mCurrentListeners;
+ private boolean mStarted = false;
+
+ /**
+ * A flag used to indicate that the child transitions of this set
+ * should all start at the same time.
+ */
+ public static final int ORDERING_TOGETHER = 0;
+
+ /**
+ * A flag used to indicate that the child transitions of this set should
+ * play in sequence; when one child transition ends, the next child
+ * transition begins. Note that a transition does not end until all
+ * instances of it (which are playing on all applicable targets of the
+ * transition) end.
+ */
+ public static final int ORDERING_SEQUENTIAL = 1;
+
+ /**
+ * Constructs an empty transition set. Add child transitions to the
+ * set by calling {@link #addTransition(Transition)} )}. By default,
+ * child transitions will play {@link #ORDERING_TOGETHER together}.
+ */
+ public TransitionSet() {
+ }
+
+ public TransitionSet(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = context.obtainStyledAttributes(attrs, Styleable.TRANSITION_SET);
+ int ordering = TypedArrayUtils.getNamedInt(a, (XmlResourceParser) attrs,
+ "transitionOrdering", Styleable.TransitionSet.TRANSITION_ORDERING,
+ TransitionSet.ORDERING_TOGETHER);
+ setOrdering(ordering);
+ a.recycle();
+ }
+
+ /**
+ * Sets the play order of this set's child transitions.
+ *
+ * @param ordering {@link #ORDERING_TOGETHER} to play this set's child
+ * transitions together, {@link #ORDERING_SEQUENTIAL} to play the child
+ * transitions in sequence.
+ * @return This transitionSet object.
+ */
+ @NonNull
+ public TransitionSet setOrdering(int ordering) {
+ switch (ordering) {
+ case ORDERING_SEQUENTIAL:
+ mPlayTogether = false;
+ break;
+ case ORDERING_TOGETHER:
+ mPlayTogether = true;
+ break;
+ default:
+ throw new AndroidRuntimeException("Invalid parameter for TransitionSet "
+ + "ordering: " + ordering);
+ }
+ return this;
+ }
+
+ /**
+ * Returns the ordering of this TransitionSet. By default, the value is
+ * {@link #ORDERING_TOGETHER}.
+ *
+ * @return {@link #ORDERING_TOGETHER} if child transitions will play at the same
+ * time, {@link #ORDERING_SEQUENTIAL} if they will play in sequence.
+ * @see #setOrdering(int)
+ */
+ public int getOrdering() {
+ return mPlayTogether ? ORDERING_TOGETHER : ORDERING_SEQUENTIAL;
+ }
+
+ /**
+ * Adds child transition to this set. The order in which this child transition
+ * is added relative to other child transitions that are added, in addition to
+ * the {@link #getOrdering() ordering} property, determines the
+ * order in which the transitions are started.
+ *
+ * <p>If this transitionSet has a {@link #getDuration() duration} set on it, the
+ * child transition will inherit that duration. Transitions are assumed to have
+ * a maximum of one transitionSet parent.</p>
+ *
+ * @param transition A non-null child transition to be added to this set.
+ * @return This transitionSet object.
+ */
+ @NonNull
+ public TransitionSet addTransition(@NonNull Transition transition) {
+ mTransitions.add(transition);
+ transition.mParent = this;
+ if (mDuration >= 0) {
+ transition.setDuration(mDuration);
+ }
+ return this;
+ }
+
+ /**
+ * Returns the number of child transitions in the TransitionSet.
+ *
+ * @return The number of child transitions in the TransitionSet.
+ * @see #addTransition(Transition)
+ * @see #getTransitionAt(int)
+ */
+ public int getTransitionCount() {
+ return mTransitions.size();
+ }
+
+ /**
+ * Returns the child Transition at the specified position in the TransitionSet.
+ *
+ * @param index The position of the Transition to retrieve.
+ * @see #addTransition(Transition)
+ * @see #getTransitionCount()
+ */
+ public Transition getTransitionAt(int index) {
+ if (index < 0 || index >= mTransitions.size()) {
+ return null;
+ }
+ return mTransitions.get(index);
+ }
+
+ /**
+ * Setting a non-negative duration on a TransitionSet causes all of the child
+ * transitions (current and future) to inherit this duration.
+ *
+ * @param duration The length of the animation, in milliseconds.
+ * @return This transitionSet object.
+ */
+ @NonNull
+ @Override
+ public TransitionSet setDuration(long duration) {
+ super.setDuration(duration);
+ if (mDuration >= 0) {
+ int numTransitions = mTransitions.size();
+ for (int i = 0; i < numTransitions; ++i) {
+ mTransitions.get(i).setDuration(duration);
+ }
+ }
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public TransitionSet setStartDelay(long startDelay) {
+ return (TransitionSet) super.setStartDelay(startDelay);
+ }
+
+ @NonNull
+ @Override
+ public TransitionSet setInterpolator(@Nullable TimeInterpolator interpolator) {
+ return (TransitionSet) super.setInterpolator(interpolator);
+ }
+
+ @NonNull
+ @Override
+ public TransitionSet addTarget(@NonNull View target) {
+ for (int i = 0; i < mTransitions.size(); i++) {
+ mTransitions.get(i).addTarget(target);
+ }
+ return (TransitionSet) super.addTarget(target);
+ }
+
+ @NonNull
+ @Override
+ public TransitionSet addTarget(@IdRes int targetId) {
+ for (int i = 0; i < mTransitions.size(); i++) {
+ mTransitions.get(i).addTarget(targetId);
+ }
+ return (TransitionSet) super.addTarget(targetId);
+ }
+
+ @NonNull
+ @Override
+ public TransitionSet addTarget(@NonNull String targetName) {
+ for (int i = 0; i < mTransitions.size(); i++) {
+ mTransitions.get(i).addTarget(targetName);
+ }
+ return (TransitionSet) super.addTarget(targetName);
+ }
+
+ @NonNull
+ @Override
+ public TransitionSet addTarget(@NonNull Class targetType) {
+ for (int i = 0; i < mTransitions.size(); i++) {
+ mTransitions.get(i).addTarget(targetType);
+ }
+ return (TransitionSet) super.addTarget(targetType);
+ }
+
+ @NonNull
+ @Override
+ public TransitionSet addListener(@NonNull TransitionListener listener) {
+ return (TransitionSet) super.addListener(listener);
+ }
+
+ @NonNull
+ @Override
+ public TransitionSet removeTarget(@IdRes int targetId) {
+ for (int i = 0; i < mTransitions.size(); i++) {
+ mTransitions.get(i).removeTarget(targetId);
+ }
+ return (TransitionSet) super.removeTarget(targetId);
+ }
+
+ @NonNull
+ @Override
+ public TransitionSet removeTarget(@NonNull View target) {
+ for (int i = 0; i < mTransitions.size(); i++) {
+ mTransitions.get(i).removeTarget(target);
+ }
+ return (TransitionSet) super.removeTarget(target);
+ }
+
+ @NonNull
+ @Override
+ public TransitionSet removeTarget(@NonNull Class target) {
+ for (int i = 0; i < mTransitions.size(); i++) {
+ mTransitions.get(i).removeTarget(target);
+ }
+ return (TransitionSet) super.removeTarget(target);
+ }
+
+ @NonNull
+ @Override
+ public TransitionSet removeTarget(@NonNull String target) {
+ for (int i = 0; i < mTransitions.size(); i++) {
+ mTransitions.get(i).removeTarget(target);
+ }
+ return (TransitionSet) super.removeTarget(target);
+ }
+
+ @NonNull
+ @Override
+ public Transition excludeTarget(@NonNull View target, boolean exclude) {
+ for (int i = 0; i < mTransitions.size(); i++) {
+ mTransitions.get(i).excludeTarget(target, exclude);
+ }
+ return super.excludeTarget(target, exclude);
+ }
+
+ @NonNull
+ @Override
+ public Transition excludeTarget(@NonNull String targetName, boolean exclude) {
+ for (int i = 0; i < mTransitions.size(); i++) {
+ mTransitions.get(i).excludeTarget(targetName, exclude);
+ }
+ return super.excludeTarget(targetName, exclude);
+ }
+
+ @NonNull
+ @Override
+ public Transition excludeTarget(int targetId, boolean exclude) {
+ for (int i = 0; i < mTransitions.size(); i++) {
+ mTransitions.get(i).excludeTarget(targetId, exclude);
+ }
+ return super.excludeTarget(targetId, exclude);
+ }
+
+ @NonNull
+ @Override
+ public Transition excludeTarget(@NonNull Class type, boolean exclude) {
+ for (int i = 0; i < mTransitions.size(); i++) {
+ mTransitions.get(i).excludeTarget(type, exclude);
+ }
+ return super.excludeTarget(type, exclude);
+ }
+
+ @NonNull
+ @Override
+ public TransitionSet removeListener(@NonNull TransitionListener listener) {
+ return (TransitionSet) super.removeListener(listener);
+ }
+
+ @Override
+ public void setPathMotion(PathMotion pathMotion) {
+ super.setPathMotion(pathMotion);
+ for (int i = 0; i < mTransitions.size(); i++) {
+ mTransitions.get(i).setPathMotion(pathMotion);
+ }
+ }
+
+ /**
+ * Removes the specified child transition from this set.
+ *
+ * @param transition The transition to be removed.
+ * @return This transitionSet object.
+ */
+ @NonNull
+ public TransitionSet removeTransition(@NonNull Transition transition) {
+ mTransitions.remove(transition);
+ transition.mParent = null;
+ return this;
+ }
+
+ /**
+ * Sets up listeners for each of the child transitions. This is used to
+ * determine when this transition set is finished (all child transitions
+ * must finish first).
+ */
+ private void setupStartEndListeners() {
+ TransitionSetListener listener = new TransitionSetListener(this);
+ for (Transition childTransition : mTransitions) {
+ childTransition.addListener(listener);
+ }
+ mCurrentListeners = mTransitions.size();
+ }
+
+ /**
+ * This listener is used to detect when all child transitions are done, at
+ * which point this transition set is also done.
+ */
+ static class TransitionSetListener extends TransitionListenerAdapter {
+
+ TransitionSet mTransitionSet;
+
+ TransitionSetListener(TransitionSet transitionSet) {
+ mTransitionSet = transitionSet;
+ }
+
+ @Override
+ public void onTransitionStart(@NonNull Transition transition) {
+ if (!mTransitionSet.mStarted) {
+ mTransitionSet.start();
+ mTransitionSet.mStarted = true;
+ }
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ --mTransitionSet.mCurrentListeners;
+ if (mTransitionSet.mCurrentListeners == 0) {
+ // All child trans
+ mTransitionSet.mStarted = false;
+ mTransitionSet.end();
+ }
+ transition.removeListener(this);
+ }
+
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ protected void createAnimators(ViewGroup sceneRoot, TransitionValuesMaps startValues,
+ TransitionValuesMaps endValues, ArrayList<TransitionValues> startValuesList,
+ ArrayList<TransitionValues> endValuesList) {
+ long startDelay = getStartDelay();
+ int numTransitions = mTransitions.size();
+ for (int i = 0; i < numTransitions; i++) {
+ Transition childTransition = mTransitions.get(i);
+ // We only set the start delay on the first transition if we are playing
+ // the transitions sequentially.
+ if (startDelay > 0 && (mPlayTogether || i == 0)) {
+ long childStartDelay = childTransition.getStartDelay();
+ if (childStartDelay > 0) {
+ childTransition.setStartDelay(startDelay + childStartDelay);
+ } else {
+ childTransition.setStartDelay(startDelay);
+ }
+ }
+ childTransition.createAnimators(sceneRoot, startValues, endValues, startValuesList,
+ endValuesList);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ protected void runAnimators() {
+ if (mTransitions.isEmpty()) {
+ start();
+ end();
+ return;
+ }
+ setupStartEndListeners();
+ if (!mPlayTogether) {
+ // Setup sequence with listeners
+ // TODO: Need to add listeners in such a way that we can remove them later if canceled
+ for (int i = 1; i < mTransitions.size(); ++i) {
+ Transition previousTransition = mTransitions.get(i - 1);
+ final Transition nextTransition = mTransitions.get(i);
+ previousTransition.addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ nextTransition.runAnimators();
+ transition.removeListener(this);
+ }
+ });
+ }
+ Transition firstTransition = mTransitions.get(0);
+ if (firstTransition != null) {
+ firstTransition.runAnimators();
+ }
+ } else {
+ for (Transition childTransition : mTransitions) {
+ childTransition.runAnimators();
+ }
+ }
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ if (isValidTarget(transitionValues.view)) {
+ for (Transition childTransition : mTransitions) {
+ if (childTransition.isValidTarget(transitionValues.view)) {
+ childTransition.captureStartValues(transitionValues);
+ transitionValues.mTargetedTransitions.add(childTransition);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ if (isValidTarget(transitionValues.view)) {
+ for (Transition childTransition : mTransitions) {
+ if (childTransition.isValidTarget(transitionValues.view)) {
+ childTransition.captureEndValues(transitionValues);
+ transitionValues.mTargetedTransitions.add(childTransition);
+ }
+ }
+ }
+ }
+
+ @Override
+ void capturePropagationValues(TransitionValues transitionValues) {
+ super.capturePropagationValues(transitionValues);
+ int numTransitions = mTransitions.size();
+ for (int i = 0; i < numTransitions; ++i) {
+ mTransitions.get(i).capturePropagationValues(transitionValues);
+ }
+ }
+
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void pause(View sceneRoot) {
+ super.pause(sceneRoot);
+ int numTransitions = mTransitions.size();
+ for (int i = 0; i < numTransitions; ++i) {
+ mTransitions.get(i).pause(sceneRoot);
+ }
+ }
+
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void resume(View sceneRoot) {
+ super.resume(sceneRoot);
+ int numTransitions = mTransitions.size();
+ for (int i = 0; i < numTransitions; ++i) {
+ mTransitions.get(i).resume(sceneRoot);
+ }
+ }
+
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ protected void cancel() {
+ super.cancel();
+ int numTransitions = mTransitions.size();
+ for (int i = 0; i < numTransitions; ++i) {
+ mTransitions.get(i).cancel();
+ }
+ }
+
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ void forceToEnd(ViewGroup sceneRoot) {
+ super.forceToEnd(sceneRoot);
+ int numTransitions = mTransitions.size();
+ for (int i = 0; i < numTransitions; ++i) {
+ mTransitions.get(i).forceToEnd(sceneRoot);
+ }
+ }
+
+ @Override
+ TransitionSet setSceneRoot(ViewGroup sceneRoot) {
+ super.setSceneRoot(sceneRoot);
+ int numTransitions = mTransitions.size();
+ for (int i = 0; i < numTransitions; ++i) {
+ mTransitions.get(i).setSceneRoot(sceneRoot);
+ }
+ return this;
+ }
+
+ @Override
+ void setCanRemoveViews(boolean canRemoveViews) {
+ super.setCanRemoveViews(canRemoveViews);
+ int numTransitions = mTransitions.size();
+ for (int i = 0; i < numTransitions; ++i) {
+ mTransitions.get(i).setCanRemoveViews(canRemoveViews);
+ }
+ }
+
+ @Override
+ public void setPropagation(TransitionPropagation propagation) {
+ super.setPropagation(propagation);
+ int numTransitions = mTransitions.size();
+ for (int i = 0; i < numTransitions; ++i) {
+ mTransitions.get(i).setPropagation(propagation);
+ }
+ }
+
+ @Override
+ public void setEpicenterCallback(EpicenterCallback epicenterCallback) {
+ super.setEpicenterCallback(epicenterCallback);
+ int numTransitions = mTransitions.size();
+ for (int i = 0; i < numTransitions; ++i) {
+ mTransitions.get(i).setEpicenterCallback(epicenterCallback);
+ }
+ }
+
+ @Override
+ String toString(String indent) {
+ String result = super.toString(indent);
+ for (int i = 0; i < mTransitions.size(); ++i) {
+ result += "\n" + mTransitions.get(i).toString(indent + " ");
+ }
+ return result;
+ }
+
+ @Override
+ public Transition clone() {
+ TransitionSet clone = (TransitionSet) super.clone();
+ clone.mTransitions = new ArrayList<>();
+ int numTransitions = mTransitions.size();
+ for (int i = 0; i < numTransitions; ++i) {
+ clone.addTransition(mTransitions.get(i).clone());
+ }
+ return clone;
+ }
+
+}
diff --git a/androidx/transition/TransitionSetTest.java b/androidx/transition/TransitionSetTest.java
new file mode 100644
index 0000000..194d227
--- /dev/null
+++ b/androidx/transition/TransitionSetTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.sameInstance;
+
+import android.support.test.filters.MediumTest;
+import android.view.View;
+
+import androidx.transition.test.R;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@MediumTest
+public class TransitionSetTest extends BaseTest {
+
+ private final TransitionSet mTransitionSet = new TransitionSet();
+ private final Transition mTransition = new TransitionTest.EmptyTransition();
+
+ @Before
+ public void setUp() {
+ // mTransitionSet has 1 item from the start
+ mTransitionSet.addTransition(mTransition);
+ }
+
+ @Test
+ public void testOrdering() {
+ assertThat(mTransitionSet.getOrdering(), is(TransitionSet.ORDERING_TOGETHER));
+ assertThat(mTransitionSet.setOrdering(TransitionSet.ORDERING_SEQUENTIAL),
+ is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getOrdering(), is(TransitionSet.ORDERING_SEQUENTIAL));
+ }
+
+ @Test
+ public void testAddAndRemoveTransition() {
+ assertThat(mTransitionSet.getTransitionCount(), is(1));
+ assertThat(mTransitionSet.getTransitionAt(0), is(sameInstance(mTransition)));
+ Transition anotherTransition = new TransitionTest.EmptyTransition();
+ assertThat(mTransitionSet.addTransition(anotherTransition),
+ is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTransitionCount(), is(2));
+ assertThat(mTransitionSet.getTransitionAt(0), is(sameInstance(mTransition)));
+ assertThat(mTransitionSet.getTransitionAt(1), is(sameInstance(anotherTransition)));
+ assertThat(mTransitionSet.removeTransition(mTransition),
+ is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTransitionCount(), is(1));
+ }
+
+ @Test
+ public void testSetDuration() {
+ assertThat(mTransitionSet.setDuration(123), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getDuration(), is(123L));
+ assertThat(mTransition.getDuration(), is(123L));
+ }
+
+ @Test
+ public void testTargetId() {
+ assertThat(mTransitionSet.addTarget(R.id.view0), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTargetIds(), hasItem(R.id.view0));
+ assertThat(mTransitionSet.getTargetIds(), hasSize(1));
+ assertThat(mTransition.getTargetIds(), hasItem(R.id.view0));
+ assertThat(mTransition.getTargetIds(), hasSize(1));
+ assertThat(mTransitionSet.removeTarget(R.id.view0), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTargetIds(), hasSize(0));
+ assertThat(mTransition.getTargetIds(), hasSize(0));
+ }
+
+ @Test
+ public void testTargetView() {
+ final View view = new View(rule.getActivity());
+ assertThat(mTransitionSet.addTarget(view), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTargets(), hasItem(view));
+ assertThat(mTransitionSet.getTargets(), hasSize(1));
+ assertThat(mTransition.getTargets(), hasItem(view));
+ assertThat(mTransition.getTargets(), hasSize(1));
+ assertThat(mTransitionSet.removeTarget(view), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTargets(), hasSize(0));
+ assertThat(mTransition.getTargets(), hasSize(0));
+ }
+
+ @Test
+ public void testTargetName() {
+ assertThat(mTransitionSet.addTarget("abc"), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTargetNames(), hasItem("abc"));
+ assertThat(mTransitionSet.getTargetNames(), hasSize(1));
+ assertThat(mTransition.getTargetNames(), hasItem("abc"));
+ assertThat(mTransition.getTargetNames(), hasSize(1));
+ assertThat(mTransitionSet.removeTarget("abc"), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTargetNames(), hasSize(0));
+ assertThat(mTransition.getTargetNames(), hasSize(0));
+ }
+
+ @Test
+ public void testTargetClass() {
+ assertThat(mTransitionSet.addTarget(View.class), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTargetTypes(), hasItem(View.class));
+ assertThat(mTransitionSet.getTargetTypes(), hasSize(1));
+ assertThat(mTransition.getTargetTypes(), hasItem(View.class));
+ assertThat(mTransition.getTargetTypes(), hasSize(1));
+ assertThat(mTransitionSet.removeTarget(View.class), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTargetTypes(), hasSize(0));
+ assertThat(mTransition.getTargetTypes(), hasSize(0));
+ }
+
+ @Test
+ public void testSetPropagation() {
+ final TransitionPropagation propagation = new SidePropagation();
+ mTransitionSet.setPropagation(propagation);
+ assertThat(mTransitionSet.getPropagation(), is(propagation));
+ assertThat(mTransition.getPropagation(), is(propagation));
+ }
+
+}
diff --git a/androidx/transition/TransitionTest.java b/androidx/transition/TransitionTest.java
new file mode 100644
index 0000000..85602e5
--- /dev/null
+++ b/androidx/transition/TransitionTest.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.graphics.Rect;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.LinearInterpolator;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.view.ViewCompat;
+import androidx.transition.test.R;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+@MediumTest
+public class TransitionTest extends BaseTest {
+
+ private Scene[] mScenes = new Scene[2];
+ private View[] mViews = new View[3];
+
+ @Before
+ public void prepareScenes() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ mScenes[0] = Scene.getSceneForLayout(root, R.layout.support_scene0, activity);
+ mScenes[1] = Scene.getSceneForLayout(root, R.layout.support_scene1, activity);
+ }
+
+ @Test
+ public void testName() {
+ Transition transition = new EmptyTransition();
+ assertThat(transition.getName(),
+ is(equalTo("androidx.transition.TransitionTest$EmptyTransition")));
+ }
+
+ @Test
+ public void testDuration() {
+ Transition transition = new EmptyTransition();
+ long duration = 12345;
+ assertThat(transition.setDuration(duration), is(sameInstance(transition)));
+ assertThat(transition.getDuration(), is(duration));
+ }
+
+ @Test
+ public void testInterpolator() {
+ Transition transition = new EmptyTransition();
+ TimeInterpolator interpolator = new LinearInterpolator();
+ assertThat(transition.setInterpolator(interpolator), is(sameInstance(transition)));
+ assertThat(transition.getInterpolator(), is(interpolator));
+ }
+
+ @Test
+ public void testStartDelay() {
+ Transition transition = new EmptyTransition();
+ long startDelay = 12345;
+ assertThat(transition.setStartDelay(startDelay), is(sameInstance(transition)));
+ assertThat(transition.getStartDelay(), is(startDelay));
+ }
+
+ @Test
+ public void testTargetIds() {
+ Transition transition = new EmptyTransition();
+ assertThat(transition.addTarget(R.id.view0), is(sameInstance(transition)));
+ assertThat(transition.addTarget(R.id.view1), is(sameInstance(transition)));
+ List<Integer> targetIds = transition.getTargetIds();
+ assertThat(targetIds.size(), is(2));
+ assertThat(targetIds, hasItem(R.id.view0));
+ assertThat(targetIds, hasItem(R.id.view1));
+ assertThat(transition.removeTarget(R.id.view0), is(sameInstance(transition)));
+ targetIds = transition.getTargetIds();
+ assertThat(targetIds.size(), is(1));
+ assertThat(targetIds, not(hasItem(R.id.view0)));
+ assertThat(targetIds, hasItem(R.id.view1));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testTargetView() {
+ // Set up views
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ View container = LayoutInflater.from(activity)
+ .inflate(R.layout.support_scene0, root, false);
+ root.addView(container);
+ View view0 = container.findViewById(R.id.view0);
+ View view1 = container.findViewById(R.id.view1);
+ // Test transition targets
+ Transition transition = new EmptyTransition();
+ assertThat(transition.addTarget(view0), is(sameInstance(transition)));
+ assertThat(transition.addTarget(view1), is(sameInstance(transition)));
+ List<View> targets = transition.getTargets();
+ assertThat(targets.size(), is(2));
+ assertThat(targets, hasItem(sameInstance(view0)));
+ assertThat(targets, hasItem(sameInstance(view1)));
+ assertThat(transition.removeTarget(view0), is(sameInstance(transition)));
+ targets = transition.getTargets();
+ assertThat(targets.size(), is(1));
+ assertThat(targets, not(hasItem(sameInstance(view0))));
+ assertThat(targets, hasItem(sameInstance(view1)));
+ }
+
+ @Test
+ public void testTargetName() {
+ Transition transition = new EmptyTransition();
+ assertThat(transition.addTarget("a"), is(sameInstance(transition)));
+ assertThat(transition.addTarget("b"), is(sameInstance(transition)));
+ List<String> targetNames = transition.getTargetNames();
+ assertNotNull(targetNames);
+ assertThat(targetNames.size(), is(2));
+ assertThat(targetNames, hasItem("a"));
+ assertThat(targetNames, hasItem("b"));
+ transition.removeTarget("a");
+ assertThat(targetNames.size(), is(1));
+ assertThat(targetNames, not(hasItem("a")));
+ assertThat(targetNames, hasItem("b"));
+ }
+
+ @Test
+ public void testTargetType() {
+ Transition transition = new EmptyTransition();
+ assertThat(transition.addTarget(Button.class), is(sameInstance(transition)));
+ assertThat(transition.addTarget(ImageView.class), is(sameInstance(transition)));
+ List<Class> targetTypes = transition.getTargetTypes();
+ assertNotNull(targetTypes);
+ assertThat(targetTypes.size(), is(2));
+ assertThat(targetTypes, hasItem(Button.class));
+ assertThat(targetTypes, hasItem(ImageView.class));
+ transition.removeTarget(Button.class);
+ assertThat(targetTypes.size(), is(1));
+ assertThat(targetTypes, not(hasItem(Button.class)));
+ assertThat(targetTypes, hasItem(ImageView.class));
+ }
+
+ @Test
+ public void testExcludeTargetId() throws Throwable {
+ showInitialScene();
+ Transition transition = new EmptyTransition();
+ transition.addTarget(R.id.view0);
+ transition.addTarget(R.id.view1);
+ View view0 = rule.getActivity().findViewById(R.id.view0);
+ View view1 = rule.getActivity().findViewById(R.id.view1);
+ assertThat(transition.isValidTarget(view0), is(true));
+ assertThat(transition.isValidTarget(view1), is(true));
+ transition.excludeTarget(R.id.view0, true);
+ assertThat(transition.isValidTarget(view0), is(false));
+ assertThat(transition.isValidTarget(view1), is(true));
+ }
+
+ @Test
+ public void testExcludeTargetView() throws Throwable {
+ showInitialScene();
+ Transition transition = new EmptyTransition();
+ View view0 = rule.getActivity().findViewById(R.id.view0);
+ View view1 = rule.getActivity().findViewById(R.id.view1);
+ transition.addTarget(view0);
+ transition.addTarget(view1);
+ assertThat(transition.isValidTarget(view0), is(true));
+ assertThat(transition.isValidTarget(view1), is(true));
+ transition.excludeTarget(view0, true);
+ assertThat(transition.isValidTarget(view0), is(false));
+ assertThat(transition.isValidTarget(view1), is(true));
+ }
+
+ @Test
+ public void testExcludeTargetName() throws Throwable {
+ showInitialScene();
+ Transition transition = new EmptyTransition();
+ View view0 = rule.getActivity().findViewById(R.id.view0);
+ View view1 = rule.getActivity().findViewById(R.id.view1);
+ ViewCompat.setTransitionName(view0, "zero");
+ ViewCompat.setTransitionName(view1, "one");
+ transition.addTarget("zero");
+ transition.addTarget("one");
+ assertThat(transition.isValidTarget(view0), is(true));
+ assertThat(transition.isValidTarget(view1), is(true));
+ transition.excludeTarget("zero", true);
+ assertThat(transition.isValidTarget(view0), is(false));
+ assertThat(transition.isValidTarget(view1), is(true));
+ }
+
+ @Test
+ public void testExcludeTargetType() throws Throwable {
+ showInitialScene();
+ Transition transition = new EmptyTransition();
+ FrameLayout container = (FrameLayout) rule.getActivity().findViewById(R.id.container);
+ View view0 = rule.getActivity().findViewById(R.id.view0);
+ transition.addTarget(View.class);
+ assertThat(transition.isValidTarget(container), is(true));
+ assertThat(transition.isValidTarget(view0), is(true));
+ transition.excludeTarget(FrameLayout.class, true);
+ assertThat(transition.isValidTarget(container), is(false));
+ assertThat(transition.isValidTarget(view0), is(true));
+ }
+
+ @Test
+ public void testListener() {
+ Transition transition = new EmptyTransition();
+ Transition.TransitionListener listener = new EmptyTransitionListener();
+ assertThat(transition.addListener(listener), is(sameInstance(transition)));
+ assertThat(transition.removeListener(listener), is(sameInstance(transition)));
+ }
+
+ @Test
+ public void testMatchOrder() throws Throwable {
+ showInitialScene();
+ final Transition transition = new ChangeBounds() {
+ @Nullable
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot,
+ @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) {
+ if (startValues != null && endValues != null) {
+ fail("Match by View ID should be prevented");
+ }
+ return super.createAnimator(sceneRoot, startValues, endValues);
+ }
+ };
+ transition.setDuration(0);
+ // This prevents matches between start and end scenes because they have different set of
+ // View instances. They will be regarded as independent views even though they share the
+ // same View IDs.
+ transition.setMatchOrder(Transition.MATCH_INSTANCE);
+ SyncRunnable enter1 = new SyncRunnable();
+ mScenes[1].setEnterAction(enter1);
+ goToScene(mScenes[1], transition);
+ if (!enter1.await()) {
+ fail("Timed out while waiting for scene change");
+ }
+ }
+
+ @Test
+ public void testExcludedTransitionAnimator() throws Throwable {
+ showInitialScene();
+ final Animator.AnimatorListener animatorListener = mock(Animator.AnimatorListener.class);
+ final DummyTransition transition = new DummyTransition(animatorListener);
+ final SyncTransitionListener transitionListener = new SyncTransitionListener(
+ SyncTransitionListener.EVENT_END);
+ transition.addListener(transitionListener);
+ transition.addTarget(mViews[0]);
+ transition.excludeTarget(mViews[0], true);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(rule.getActivity().getRoot(), transition);
+ mViews[0].setTranslationX(3.f);
+ }
+ });
+ if (!transitionListener.await()) {
+ fail("Timed out waiting for the TransitionListener");
+ }
+ verify(animatorListener, never()).onAnimationStart(any(Animator.class));
+ }
+
+ @Test
+ public void testEpicenter() throws Throwable {
+ final Transition transition = new EmptyTransition();
+ final Transition.EpicenterCallback epicenterCallback = new Transition.EpicenterCallback() {
+ private Rect mRect = new Rect();
+
+ @Override
+ public Rect onGetEpicenter(@NonNull Transition t) {
+ assertThat(t, is(sameInstance(transition)));
+ mRect.set(1, 2, 3, 4);
+ return mRect;
+ }
+ };
+ transition.setEpicenterCallback(epicenterCallback);
+ assertThat(transition.getEpicenterCallback(),
+ is(sameInstance(transition.getEpicenterCallback())));
+ Rect rect = transition.getEpicenter();
+ assertNotNull(rect);
+ assertThat(rect.left, is(1));
+ assertThat(rect.top, is(2));
+ assertThat(rect.right, is(3));
+ assertThat(rect.bottom, is(4));
+ }
+
+ @Test
+ public void testSetPropagation() throws Throwable {
+ final Transition transition = new EmptyTransition();
+ assertThat(transition.getPropagation(), is(nullValue()));
+ final TransitionPropagation propagation = new CircularPropagation();
+ transition.setPropagation(propagation);
+ assertThat(propagation, is(sameInstance(propagation)));
+ }
+
+ @Test
+ public void testIsTransitionRequired() throws Throwable {
+ final EmptyTransition transition = new EmptyTransition();
+ assertThat(transition.isTransitionRequired(null, null), is(false));
+ final TransitionValues start = new TransitionValues();
+ final String propname = "android:transition:dummy";
+ start.values.put(propname, 1);
+ final TransitionValues end = new TransitionValues();
+ end.values.put(propname, 1);
+ assertThat(transition.isTransitionRequired(start, end), is(false));
+ end.values.put(propname, 2);
+ assertThat(transition.isTransitionRequired(start, end), is(true));
+ }
+
+ private void showInitialScene() throws Throwable {
+ SyncRunnable enter0 = new SyncRunnable();
+ mScenes[0].setEnterAction(enter0);
+ AutoTransition transition1 = new AutoTransition();
+ transition1.setDuration(0);
+ goToScene(mScenes[0], transition1);
+ if (!enter0.await()) {
+ fail("Timed out while waiting for scene change");
+ }
+ mViews[0] = rule.getActivity().findViewById(R.id.view0);
+ mViews[1] = rule.getActivity().findViewById(R.id.view1);
+ mViews[2] = rule.getActivity().findViewById(R.id.view2);
+ }
+
+ private void goToScene(final Scene scene, final Transition transition) throws Throwable {
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.go(scene, transition);
+ }
+ });
+ }
+
+ public static class EmptyTransition extends Transition {
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ }
+
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot,
+ @Nullable TransitionValues startValues,
+ @Nullable TransitionValues endValues) {
+ return null;
+ }
+
+ }
+
+ public static class EmptyTransitionListener implements Transition.TransitionListener {
+
+ @Override
+ public void onTransitionStart(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ }
+
+ }
+
+ /**
+ * A dummy transition for monitoring use of its animator by the Transition framework.
+ */
+ private static class DummyTransition extends Transition {
+
+ private final Animator.AnimatorListener mListener;
+
+ DummyTransition(Animator.AnimatorListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ transitionValues.values.put("state", 1);
+ }
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ transitionValues.values.put("state", 2);
+ }
+
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot, TransitionValues startValues,
+ TransitionValues endValues) {
+ if (startValues == null || endValues == null) {
+ return null;
+ }
+ final ObjectAnimator animator = ObjectAnimator
+ .ofFloat(startValues.view, "translationX", 1.f, 2.f);
+ animator.addListener(mListener);
+ return animator;
+ }
+
+ }
+}
diff --git a/androidx/transition/TransitionUtils.java b/androidx/transition/TransitionUtils.java
new file mode 100644
index 0000000..39de677
--- /dev/null
+++ b/androidx/transition/TransitionUtils.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.TypeEvaluator;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+class TransitionUtils {
+
+ private static final int MAX_IMAGE_SIZE = 1024 * 1024;
+
+ /**
+ * Creates a View using the bitmap copy of <code>view</code>. If <code>view</code> is large,
+ * the copy will use a scaled bitmap of the given view.
+ *
+ * @param sceneRoot The ViewGroup in which the view copy will be displayed.
+ * @param view The view to create a copy of.
+ * @param parent The parent of view.
+ */
+ static View copyViewImage(ViewGroup sceneRoot, View view, View parent) {
+ Matrix matrix = new Matrix();
+ matrix.setTranslate(-parent.getScrollX(), -parent.getScrollY());
+ ViewUtils.transformMatrixToGlobal(view, matrix);
+ ViewUtils.transformMatrixToLocal(sceneRoot, matrix);
+ RectF bounds = new RectF(0, 0, view.getWidth(), view.getHeight());
+ matrix.mapRect(bounds);
+ int left = Math.round(bounds.left);
+ int top = Math.round(bounds.top);
+ int right = Math.round(bounds.right);
+ int bottom = Math.round(bounds.bottom);
+
+ ImageView copy = new ImageView(view.getContext());
+ copy.setScaleType(ImageView.ScaleType.CENTER_CROP);
+ Bitmap bitmap = createViewBitmap(view, matrix, bounds);
+ if (bitmap != null) {
+ copy.setImageBitmap(bitmap);
+ }
+ int widthSpec = View.MeasureSpec.makeMeasureSpec(right - left, View.MeasureSpec.EXACTLY);
+ int heightSpec = View.MeasureSpec.makeMeasureSpec(bottom - top, View.MeasureSpec.EXACTLY);
+ copy.measure(widthSpec, heightSpec);
+ copy.layout(left, top, right, bottom);
+ return copy;
+ }
+
+ /**
+ * Creates a Bitmap of the given view, using the Matrix matrix to transform to the local
+ * coordinates. <code>matrix</code> will be modified during the bitmap creation.
+ *
+ * <p>If the bitmap is large, it will be scaled uniformly down to at most 1MB size.</p>
+ *
+ * @param view The view to create a bitmap for.
+ * @param matrix The matrix converting the view local coordinates to the coordinates that
+ * the bitmap will be displayed in. <code>matrix</code> will be modified before
+ * returning.
+ * @param bounds The bounds of the bitmap in the destination coordinate system (where the
+ * view should be presented. Typically, this is matrix.mapRect(viewBounds);
+ * @return A bitmap of the given view or null if bounds has no width or height.
+ */
+ private static Bitmap createViewBitmap(View view, Matrix matrix, RectF bounds) {
+ Bitmap bitmap = null;
+ int bitmapWidth = Math.round(bounds.width());
+ int bitmapHeight = Math.round(bounds.height());
+ if (bitmapWidth > 0 && bitmapHeight > 0) {
+ float scale = Math.min(1f, ((float) MAX_IMAGE_SIZE) / (bitmapWidth * bitmapHeight));
+ bitmapWidth = (int) (bitmapWidth * scale);
+ bitmapHeight = (int) (bitmapHeight * scale);
+ matrix.postTranslate(-bounds.left, -bounds.top);
+ matrix.postScale(scale, scale);
+ bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ canvas.concat(matrix);
+ view.draw(canvas);
+ }
+ return bitmap;
+ }
+
+ static Animator mergeAnimators(Animator animator1, Animator animator2) {
+ if (animator1 == null) {
+ return animator2;
+ } else if (animator2 == null) {
+ return animator1;
+ } else {
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(animator1, animator2);
+ return animatorSet;
+ }
+ }
+
+ static class MatrixEvaluator implements TypeEvaluator<Matrix> {
+
+ final float[] mTempStartValues = new float[9];
+
+ final float[] mTempEndValues = new float[9];
+
+ final Matrix mTempMatrix = new Matrix();
+
+ @Override
+ public Matrix evaluate(float fraction, Matrix startValue, Matrix endValue) {
+ startValue.getValues(mTempStartValues);
+ endValue.getValues(mTempEndValues);
+ for (int i = 0; i < 9; i++) {
+ float diff = mTempEndValues[i] - mTempStartValues[i];
+ mTempEndValues[i] = mTempStartValues[i] + (fraction * diff);
+ }
+ mTempMatrix.setValues(mTempEndValues);
+ return mTempMatrix;
+ }
+
+ }
+
+ private TransitionUtils() {
+ }
+}
diff --git a/androidx/transition/TransitionValues.java b/androidx/transition/TransitionValues.java
new file mode 100644
index 0000000..442bd15
--- /dev/null
+++ b/androidx/transition/TransitionValues.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Data structure which holds cached values for the transition.
+ * The view field is the target which all of the values pertain to.
+ * The values field is a map which holds information for fields
+ * according to names selected by the transitions. These names should
+ * be unique to avoid clobbering values stored by other transitions,
+ * such as the convention project:transition_name:property_name. For
+ * example, the platform might store a property "alpha" in a transition
+ * "Fader" as "android:fader:alpha".
+ *
+ * <p>These values are cached during the
+ * {@link androidx.transition.Transition#captureStartValues(TransitionValues)}
+ * capture} phases of a scene change, once when the start values are captured
+ * and again when the end values are captured. These start/end values are then
+ * passed into the transitions via the
+ * for {@link androidx.transition.Transition#createAnimator(android.view.ViewGroup,
+ * TransitionValues, TransitionValues)} method.</p>
+ */
+public class TransitionValues {
+
+ /**
+ * The set of values tracked by transitions for this scene
+ */
+ public final Map<String, Object> values = new HashMap<>();
+
+ /**
+ * The View with these values
+ */
+ public View view;
+
+ /**
+ * The Transitions that targeted this view.
+ */
+ final ArrayList<Transition> mTargetedTransitions = new ArrayList<>();
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof TransitionValues) {
+ if (view == ((TransitionValues) other).view) {
+ if (values.equals(((TransitionValues) other).values)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * view.hashCode() + values.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ String returnValue = "TransitionValues@" + Integer.toHexString(hashCode()) + ":\n";
+ returnValue += " view = " + view + "\n";
+ returnValue += " values:";
+ for (String s : values.keySet()) {
+ returnValue += " " + s + ": " + values.get(s) + "\n";
+ }
+ return returnValue;
+ }
+
+}
diff --git a/androidx/transition/TransitionValuesMaps.java b/androidx/transition/TransitionValuesMaps.java
new file mode 100644
index 0000000..4db4603
--- /dev/null
+++ b/androidx/transition/TransitionValuesMaps.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.util.SparseArray;
+import android.view.View;
+
+import androidx.collection.ArrayMap;
+import androidx.collection.LongSparseArray;
+
+class TransitionValuesMaps {
+
+ final ArrayMap<View, TransitionValues> mViewValues = new ArrayMap<>();
+
+ final SparseArray<View> mIdValues = new SparseArray<>();
+
+ final LongSparseArray<View> mItemIdValues = new LongSparseArray<>();
+
+ final ArrayMap<String, View> mNameValues = new ArrayMap<>();
+
+}
diff --git a/androidx/transition/TranslationAnimationCreator.java b/androidx/transition/TranslationAnimationCreator.java
new file mode 100644
index 0000000..c3e30e8
--- /dev/null
+++ b/androidx/transition/TranslationAnimationCreator.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.TimeInterpolator;
+import android.view.View;
+
+/**
+ * This class is used by Slide and Explode to create an animator that goes from the start
+ * position to the end position. It takes into account the canceled position so that it
+ * will not blink out or shift suddenly when the transition is interrupted.
+ */
+class TranslationAnimationCreator {
+
+ /**
+ * Creates an animator that can be used for x and/or y translations. When interrupted,
+ * it sets a tag to keep track of the position so that it may be continued from position.
+ *
+ * @param view The view being moved. This may be in the overlay for onDisappear.
+ * @param values The values containing the view in the view hierarchy.
+ * @param viewPosX The x screen coordinate of view
+ * @param viewPosY The y screen coordinate of view
+ * @param startX The start translation x of view
+ * @param startY The start translation y of view
+ * @param endX The end translation x of view
+ * @param endY The end translation y of view
+ * @param interpolator The interpolator to use with this animator.
+ * @return An animator that moves from (startX, startY) to (endX, endY) unless there was
+ * a previous interruption, in which case it moves from the current position to (endX, endY).
+ */
+ static Animator createAnimation(View view, TransitionValues values, int viewPosX, int viewPosY,
+ float startX, float startY, float endX, float endY, TimeInterpolator interpolator) {
+ float terminalX = view.getTranslationX();
+ float terminalY = view.getTranslationY();
+ int[] startPosition = (int[]) values.view.getTag(R.id.transition_position);
+ if (startPosition != null) {
+ startX = startPosition[0] - viewPosX + terminalX;
+ startY = startPosition[1] - viewPosY + terminalY;
+ }
+ // Initial position is at translation startX, startY, so position is offset by that amount
+ int startPosX = viewPosX + Math.round(startX - terminalX);
+ int startPosY = viewPosY + Math.round(startY - terminalY);
+
+ view.setTranslationX(startX);
+ view.setTranslationY(startY);
+ if (startX == endX && startY == endY) {
+ return null;
+ }
+ ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(view,
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_X, startX, endX),
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, startY, endY));
+
+ TransitionPositionListener listener = new TransitionPositionListener(view, values.view,
+ startPosX, startPosY, terminalX, terminalY);
+ anim.addListener(listener);
+ AnimatorUtils.addPauseListener(anim, listener);
+ anim.setInterpolator(interpolator);
+ return anim;
+ }
+
+ private static class TransitionPositionListener extends AnimatorListenerAdapter {
+
+ private final View mViewInHierarchy;
+ private final View mMovingView;
+ private final int mStartX;
+ private final int mStartY;
+ private int[] mTransitionPosition;
+ private float mPausedX;
+ private float mPausedY;
+ private final float mTerminalX;
+ private final float mTerminalY;
+
+ private TransitionPositionListener(View movingView, View viewInHierarchy,
+ int startX, int startY, float terminalX, float terminalY) {
+ mMovingView = movingView;
+ mViewInHierarchy = viewInHierarchy;
+ mStartX = startX - Math.round(mMovingView.getTranslationX());
+ mStartY = startY - Math.round(mMovingView.getTranslationY());
+ mTerminalX = terminalX;
+ mTerminalY = terminalY;
+ mTransitionPosition = (int[]) mViewInHierarchy.getTag(R.id.transition_position);
+ if (mTransitionPosition != null) {
+ mViewInHierarchy.setTag(R.id.transition_position, null);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ if (mTransitionPosition == null) {
+ mTransitionPosition = new int[2];
+ }
+ mTransitionPosition[0] = Math.round(mStartX + mMovingView.getTranslationX());
+ mTransitionPosition[1] = Math.round(mStartY + mMovingView.getTranslationY());
+ mViewInHierarchy.setTag(R.id.transition_position, mTransitionPosition);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ mMovingView.setTranslationX(mTerminalX);
+ mMovingView.setTranslationY(mTerminalY);
+ }
+
+ @Override
+ public void onAnimationPause(Animator animator) {
+ mPausedX = mMovingView.getTranslationX();
+ mPausedY = mMovingView.getTranslationY();
+ mMovingView.setTranslationX(mTerminalX);
+ mMovingView.setTranslationY(mTerminalY);
+ }
+
+ @Override
+ public void onAnimationResume(Animator animator) {
+ mMovingView.setTranslationX(mPausedX);
+ mMovingView.setTranslationY(mPausedY);
+ }
+ }
+
+ private TranslationAnimationCreator() {
+ }
+}
diff --git a/androidx/transition/ViewGroupOverlayApi14.java b/androidx/transition/ViewGroupOverlayApi14.java
new file mode 100644
index 0000000..f1bd80a
--- /dev/null
+++ b/androidx/transition/ViewGroupOverlayApi14.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+
+class ViewGroupOverlayApi14 extends ViewOverlayApi14 implements ViewGroupOverlayImpl {
+
+ ViewGroupOverlayApi14(Context context, ViewGroup hostView, View requestingView) {
+ super(context, hostView, requestingView);
+ }
+
+ static ViewGroupOverlayApi14 createFrom(ViewGroup viewGroup) {
+ return (ViewGroupOverlayApi14) ViewOverlayApi14.createFrom(viewGroup);
+ }
+
+ @Override
+ public void add(@NonNull View view) {
+ mOverlayViewGroup.add(view);
+ }
+
+ @Override
+ public void remove(@NonNull View view) {
+ mOverlayViewGroup.remove(view);
+ }
+
+}
diff --git a/androidx/transition/ViewGroupOverlayApi18.java b/androidx/transition/ViewGroupOverlayApi18.java
new file mode 100644
index 0000000..54e3413
--- /dev/null
+++ b/androidx/transition/ViewGroupOverlayApi18.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroupOverlay;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(18)
+class ViewGroupOverlayApi18 implements ViewGroupOverlayImpl {
+
+ private final ViewGroupOverlay mViewGroupOverlay;
+
+ ViewGroupOverlayApi18(@NonNull ViewGroup group) {
+ mViewGroupOverlay = group.getOverlay();
+ }
+
+ @Override
+ public void add(@NonNull Drawable drawable) {
+ mViewGroupOverlay.add(drawable);
+ }
+
+ @Override
+ public void clear() {
+ mViewGroupOverlay.clear();
+ }
+
+ @Override
+ public void remove(@NonNull Drawable drawable) {
+ mViewGroupOverlay.remove(drawable);
+ }
+
+ @Override
+ public void add(@NonNull View view) {
+ mViewGroupOverlay.add(view);
+ }
+
+ @Override
+ public void remove(@NonNull View view) {
+ mViewGroupOverlay.remove(view);
+ }
+
+}
diff --git a/androidx/transition/ViewGroupOverlayImpl.java b/androidx/transition/ViewGroupOverlayImpl.java
new file mode 100644
index 0000000..be009e3
--- /dev/null
+++ b/androidx/transition/ViewGroupOverlayImpl.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+interface ViewGroupOverlayImpl extends ViewOverlayImpl {
+
+ /**
+ * Adds a View to the overlay. The bounds of the added view should be
+ * relative to the host view. Any view added to the overlay should be
+ * removed when it is no longer needed or no longer visible.
+ *
+ * <p>Views in the overlay are visual-only; they do not receive input
+ * events and do not participate in focus traversal. Overlay views
+ * are intended to be transient, such as might be needed by a temporary
+ * animation effect.</p>
+ *
+ * <p>If the view has a parent, the view will be removed from that parent
+ * before being added to the overlay. Also, if that parent is attached
+ * in the current view hierarchy, the view will be repositioned
+ * such that it is in the same relative location inside the activity. For
+ * example, if the view's current parent lies 100 pixels to the right
+ * and 200 pixels down from the origin of the overlay's
+ * host view, then the view will be offset by (100, 200).</p>
+ *
+ * @param view The View to be added to the overlay. The added view will be
+ * drawn when the overlay is drawn.
+ * @see #remove(View)
+ * @see android.view.ViewOverlay#add(android.graphics.drawable.Drawable)
+ */
+ void add(@NonNull View view);
+
+ /**
+ * Removes the specified View from the overlay.
+ *
+ * @param view The View to be removed from the overlay.
+ * @see #add(View)
+ * @see android.view.ViewOverlay#remove(android.graphics.drawable.Drawable)
+ */
+ void remove(@NonNull View view);
+
+}
diff --git a/androidx/transition/ViewGroupUtils.java b/androidx/transition/ViewGroupUtils.java
new file mode 100644
index 0000000..dee0fa9
--- /dev/null
+++ b/androidx/transition/ViewGroupUtils.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.os.Build;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Compatibility utilities for platform features of {@link ViewGroup}.
+ */
+class ViewGroupUtils {
+
+ /**
+ * Backward-compatible {@link ViewGroup#getOverlay()}.
+ */
+ static ViewGroupOverlayImpl getOverlay(@NonNull ViewGroup group) {
+ if (Build.VERSION.SDK_INT >= 18) {
+ return new ViewGroupOverlayApi18(group);
+ }
+ return ViewGroupOverlayApi14.createFrom(group);
+ }
+
+ /**
+ * Provides access to the hidden ViewGroup#suppressLayout method.
+ */
+ static void suppressLayout(@NonNull ViewGroup group, boolean suppress) {
+ if (Build.VERSION.SDK_INT >= 18) {
+ ViewGroupUtilsApi18.suppressLayout(group, suppress);
+ } else {
+ ViewGroupUtilsApi14.suppressLayout(group, suppress);
+ }
+ }
+
+ private ViewGroupUtils() {
+ }
+}
diff --git a/androidx/transition/ViewGroupUtilsApi14.java b/androidx/transition/ViewGroupUtilsApi14.java
new file mode 100644
index 0000000..32d546a
--- /dev/null
+++ b/androidx/transition/ViewGroupUtilsApi14.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.animation.LayoutTransition;
+import android.util.Log;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+class ViewGroupUtilsApi14 {
+
+ private static final String TAG = "ViewGroupUtilsApi14";
+
+ private static final int LAYOUT_TRANSITION_CHANGING = 4;
+
+ private static LayoutTransition sEmptyLayoutTransition;
+
+ private static Field sLayoutSuppressedField;
+ private static boolean sLayoutSuppressedFieldFetched;
+
+ private static Method sCancelMethod;
+ private static boolean sCancelMethodFetched;
+
+ static void suppressLayout(@NonNull ViewGroup group, boolean suppress) {
+ // Prepare the dummy LayoutTransition
+ if (sEmptyLayoutTransition == null) {
+ sEmptyLayoutTransition = new LayoutTransition() {
+ @Override
+ public boolean isChangingLayout() {
+ return true;
+ }
+ };
+ sEmptyLayoutTransition.setAnimator(LayoutTransition.APPEARING, null);
+ sEmptyLayoutTransition.setAnimator(LayoutTransition.CHANGE_APPEARING, null);
+ sEmptyLayoutTransition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, null);
+ sEmptyLayoutTransition.setAnimator(LayoutTransition.DISAPPEARING, null);
+ sEmptyLayoutTransition.setAnimator(LAYOUT_TRANSITION_CHANGING, null);
+ }
+ if (suppress) {
+ // Save the current LayoutTransition
+ final LayoutTransition layoutTransition = group.getLayoutTransition();
+ if (layoutTransition != null) {
+ if (layoutTransition.isRunning()) {
+ cancelLayoutTransition(layoutTransition);
+ }
+ if (layoutTransition != sEmptyLayoutTransition) {
+ group.setTag(R.id.transition_layout_save, layoutTransition);
+ }
+ }
+ // Suppress the layout
+ group.setLayoutTransition(sEmptyLayoutTransition);
+ } else {
+ // Thaw the layout suppression
+ group.setLayoutTransition(null);
+ // Request layout if necessary
+ if (!sLayoutSuppressedFieldFetched) {
+ try {
+ sLayoutSuppressedField = ViewGroup.class.getDeclaredField("mLayoutSuppressed");
+ sLayoutSuppressedField.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ Log.i(TAG, "Failed to access mLayoutSuppressed field by reflection");
+ }
+ sLayoutSuppressedFieldFetched = true;
+ }
+ boolean layoutSuppressed = false;
+ if (sLayoutSuppressedField != null) {
+ try {
+ layoutSuppressed = sLayoutSuppressedField.getBoolean(group);
+ if (layoutSuppressed) {
+ sLayoutSuppressedField.setBoolean(group, false);
+ }
+ } catch (IllegalAccessException e) {
+ Log.i(TAG, "Failed to get mLayoutSuppressed field by reflection");
+ }
+ }
+ if (layoutSuppressed) {
+ group.requestLayout();
+ }
+ // Restore the saved LayoutTransition
+ final LayoutTransition layoutTransition =
+ (LayoutTransition) group.getTag(R.id.transition_layout_save);
+ if (layoutTransition != null) {
+ group.setTag(R.id.transition_layout_save, null);
+ group.setLayoutTransition(layoutTransition);
+ }
+ }
+ }
+
+ private static void cancelLayoutTransition(LayoutTransition t) {
+ if (!sCancelMethodFetched) {
+ try {
+ sCancelMethod = LayoutTransition.class.getDeclaredMethod("cancel");
+ sCancelMethod.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ Log.i(TAG, "Failed to access cancel method by reflection");
+ }
+ sCancelMethodFetched = true;
+ }
+ if (sCancelMethod != null) {
+ try {
+ sCancelMethod.invoke(t);
+ } catch (IllegalAccessException e) {
+ Log.i(TAG, "Failed to access cancel method by reflection");
+ } catch (InvocationTargetException e) {
+ Log.i(TAG, "Failed to invoke cancel method by reflection");
+ }
+ }
+ }
+
+ private ViewGroupUtilsApi14() {
+ }
+}
diff --git a/androidx/transition/ViewGroupUtilsApi18.java b/androidx/transition/ViewGroupUtilsApi18.java
new file mode 100644
index 0000000..e4d4ffa
--- /dev/null
+++ b/androidx/transition/ViewGroupUtilsApi18.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.util.Log;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+@RequiresApi(18)
+class ViewGroupUtilsApi18 {
+
+ private static final String TAG = "ViewUtilsApi18";
+
+ private static Method sSuppressLayoutMethod;
+ private static boolean sSuppressLayoutMethodFetched;
+
+ static void suppressLayout(@NonNull ViewGroup group, boolean suppress) {
+ fetchSuppressLayoutMethod();
+ if (sSuppressLayoutMethod != null) {
+ try {
+ sSuppressLayoutMethod.invoke(group, suppress);
+ } catch (IllegalAccessException e) {
+ Log.i(TAG, "Failed to invoke suppressLayout method", e);
+ } catch (InvocationTargetException e) {
+ Log.i(TAG, "Error invoking suppressLayout method", e);
+ }
+ }
+ }
+
+ private static void fetchSuppressLayoutMethod() {
+ if (!sSuppressLayoutMethodFetched) {
+ try {
+ sSuppressLayoutMethod = ViewGroup.class.getDeclaredMethod("suppressLayout",
+ boolean.class);
+ sSuppressLayoutMethod.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ Log.i(TAG, "Failed to retrieve suppressLayout method", e);
+ }
+ sSuppressLayoutMethodFetched = true;
+ }
+ }
+
+ private ViewGroupUtilsApi18() {
+ }
+}
diff --git a/androidx/transition/ViewOverlayApi14.java b/androidx/transition/ViewOverlayApi14.java
new file mode 100644
index 0000000..049e685
--- /dev/null
+++ b/androidx/transition/ViewOverlayApi14.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.view.ViewCompat;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+
+class ViewOverlayApi14 implements ViewOverlayImpl {
+
+ /**
+ * The actual container for the drawables (and views, if it's a ViewGroupOverlay).
+ * All of the management and rendering details for the overlay are handled in
+ * OverlayViewGroup.
+ */
+ protected OverlayViewGroup mOverlayViewGroup;
+
+ ViewOverlayApi14(Context context, ViewGroup hostView, View requestingView) {
+ mOverlayViewGroup = new OverlayViewGroup(context, hostView, requestingView, this);
+ }
+
+ static ViewGroup getContentView(View view) {
+ View parent = view;
+ while (parent != null) {
+ if (parent.getId() == android.R.id.content && parent instanceof ViewGroup) {
+ return (ViewGroup) parent;
+ }
+ if (parent.getParent() instanceof ViewGroup) {
+ parent = (ViewGroup) parent.getParent();
+ }
+ }
+ return null;
+ }
+
+ static ViewOverlayApi14 createFrom(View view) {
+ ViewGroup contentView = getContentView(view);
+ if (contentView != null) {
+ final int numChildren = contentView.getChildCount();
+ for (int i = 0; i < numChildren; ++i) {
+ View child = contentView.getChildAt(i);
+ if (child instanceof OverlayViewGroup) {
+ return ((OverlayViewGroup) child).mViewOverlay;
+ }
+ }
+ return new ViewGroupOverlayApi14(contentView.getContext(), contentView, view);
+ }
+ return null;
+ }
+
+ /**
+ * Used internally by View and ViewGroup to handle drawing and invalidation
+ * of the overlay
+ */
+ ViewGroup getOverlayView() {
+ return mOverlayViewGroup;
+ }
+
+ @Override
+ public void add(@NonNull Drawable drawable) {
+ mOverlayViewGroup.add(drawable);
+ }
+
+ @Override
+ public void clear() {
+ mOverlayViewGroup.clear();
+ }
+
+ @Override
+ public void remove(@NonNull Drawable drawable) {
+ mOverlayViewGroup.remove(drawable);
+ }
+
+ boolean isEmpty() {
+ return mOverlayViewGroup.isEmpty();
+ }
+
+
+ /**
+ * OverlayViewGroup is a container that View and ViewGroup use to host
+ * drawables and views added to their overlays ({@code ViewOverlay} and
+ * {@code ViewGroupOverlay}, respectively). Drawables are added to the overlay
+ * via the add/remove methods in ViewOverlay, Views are added/removed via
+ * ViewGroupOverlay. These drawable and view objects are
+ * drawn whenever the view itself is drawn; first the view draws its own
+ * content (and children, if it is a ViewGroup), then it draws its overlay
+ * (if it has one).
+ *
+ * <p>Besides managing and drawing the list of drawables, this class serves
+ * two purposes:
+ * (1) it noops layout calls because children are absolutely positioned and
+ * (2) it forwards all invalidation calls to its host view. The invalidation
+ * redirect is necessary because the overlay is not a child of the host view
+ * and invalidation cannot therefore follow the normal path up through the
+ * parent hierarchy.</p>
+ *
+ * @see View#getOverlay()
+ * @see ViewGroup#getOverlay()
+ */
+ static class OverlayViewGroup extends ViewGroup {
+
+ static Method sInvalidateChildInParentFastMethod;
+
+ static {
+ try {
+ sInvalidateChildInParentFastMethod = ViewGroup.class.getDeclaredMethod(
+ "invalidateChildInParentFast", int.class, int.class, Rect.class);
+ } catch (NoSuchMethodException e) {
+ }
+
+ }
+
+ /**
+ * The View for which this is an overlay. Invalidations of the overlay are redirected to
+ * this host view.
+ */
+ ViewGroup mHostView;
+ View mRequestingView;
+ /**
+ * The set of drawables to draw when the overlay is rendered.
+ */
+ ArrayList<Drawable> mDrawables = null;
+ /**
+ * Reference to the hosting overlay object
+ */
+ ViewOverlayApi14 mViewOverlay;
+
+ OverlayViewGroup(Context context, ViewGroup hostView, View requestingView,
+ ViewOverlayApi14 viewOverlay) {
+ super(context);
+ mHostView = hostView;
+ mRequestingView = requestingView;
+ setRight(hostView.getWidth());
+ setBottom(hostView.getHeight());
+ hostView.addView(this);
+ mViewOverlay = viewOverlay;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ // Intercept and noop all touch events - overlays do not allow touch events
+ return false;
+ }
+
+ public void add(Drawable drawable) {
+ if (mDrawables == null) {
+
+ mDrawables = new ArrayList<>();
+ }
+ if (!mDrawables.contains(drawable)) {
+ // Make each drawable unique in the overlay; can't add it more than once
+ mDrawables.add(drawable);
+ invalidate(drawable.getBounds());
+ drawable.setCallback(this);
+ }
+ }
+
+ public void remove(Drawable drawable) {
+ if (mDrawables != null) {
+ mDrawables.remove(drawable);
+ invalidate(drawable.getBounds());
+ drawable.setCallback(null);
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(@NonNull Drawable who) {
+ return super.verifyDrawable(who) || (mDrawables != null && mDrawables.contains(who));
+ }
+
+ public void add(View child) {
+ if (child.getParent() instanceof ViewGroup) {
+ ViewGroup parent = (ViewGroup) child.getParent();
+ if (parent != mHostView && parent.getParent() != null
+ && ViewCompat.isAttachedToWindow(parent)) {
+ // Moving to different container; figure out how to position child such that
+ // it is in the same location on the screen
+ int[] parentLocation = new int[2];
+ int[] hostViewLocation = new int[2];
+ parent.getLocationOnScreen(parentLocation);
+ mHostView.getLocationOnScreen(hostViewLocation);
+ ViewCompat.offsetLeftAndRight(child, parentLocation[0] - hostViewLocation[0]);
+ ViewCompat.offsetTopAndBottom(child, parentLocation[1] - hostViewLocation[1]);
+ }
+ parent.removeView(child);
+// if (parent.getLayoutTransition() != null) {
+// // LayoutTransition will cause the child to delay removal - cancel it
+// parent.getLayoutTransition().cancel(LayoutTransition.DISAPPEARING);
+// }
+ // fail-safe if view is still attached for any reason
+ if (child.getParent() != null) {
+ parent.removeView(child);
+ }
+ }
+ super.addView(child, getChildCount() - 1);
+ }
+
+ public void remove(View view) {
+ super.removeView(view);
+ if (isEmpty()) {
+ mHostView.removeView(this);
+ }
+ }
+
+ public void clear() {
+ removeAllViews();
+ if (mDrawables != null) {
+ mDrawables.clear();
+ }
+ }
+
+ boolean isEmpty() {
+ return getChildCount() == 0
+ && (mDrawables == null || mDrawables.size() == 0);
+ }
+
+ @Override
+ public void invalidateDrawable(@NonNull Drawable drawable) {
+ invalidate(drawable.getBounds());
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ int[] contentViewLocation = new int[2];
+ int[] hostViewLocation = new int[2];
+ mHostView.getLocationOnScreen(contentViewLocation);
+ mRequestingView.getLocationOnScreen(hostViewLocation);
+ canvas.translate(hostViewLocation[0] - contentViewLocation[0],
+ hostViewLocation[1] - contentViewLocation[1]);
+ canvas.clipRect(
+ new Rect(0, 0, mRequestingView.getWidth(), mRequestingView.getHeight()));
+ super.dispatchDraw(canvas);
+ final int numDrawables = (mDrawables == null) ? 0 : mDrawables.size();
+ for (int i = 0; i < numDrawables; ++i) {
+ mDrawables.get(i).draw(canvas);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ // Noop: children are positioned absolutely
+ }
+
+ /*
+ The following invalidation overrides exist for the purpose of redirecting invalidation to
+ the host view. The overlay is not parented to the host view (since a View cannot be a
+ parent), so the invalidation cannot proceed through the normal parent hierarchy.
+ There is a built-in assumption that the overlay exactly covers the host view, therefore
+ the invalidation rectangles received do not need to be adjusted when forwarded to
+ the host view.
+ */
+
+ private void getOffset(int[] offset) {
+ int[] contentViewLocation = new int[2];
+ int[] hostViewLocation = new int[2];
+ mHostView.getLocationOnScreen(contentViewLocation);
+ mRequestingView.getLocationOnScreen(hostViewLocation);
+ offset[0] = hostViewLocation[0] - contentViewLocation[0];
+ offset[1] = hostViewLocation[1] - contentViewLocation[1];
+ }
+
+ public void invalidateChildFast(View child, final Rect dirty) {
+ if (mHostView != null) {
+ // Note: This is not a "fast" invalidation. Would be nice to instead invalidate
+ // using DisplayList properties and a dirty rect instead of causing a real
+ // invalidation of the host view
+ int left = child.getLeft();
+ int top = child.getTop();
+ int[] offset = new int[2];
+ getOffset(offset);
+ // TODO: implement transforms
+// if (!child.getMatrix().isIdentity()) {
+// child.transformRect(dirty);
+// }
+ dirty.offset(left + offset[0], top + offset[1]);
+ mHostView.invalidate(dirty);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ protected ViewParent invalidateChildInParentFast(int left, int top, Rect dirty) {
+ if (mHostView instanceof ViewGroup && sInvalidateChildInParentFastMethod != null) {
+ try {
+ int[] offset = new int[2];
+ getOffset(offset);
+ sInvalidateChildInParentFastMethod.invoke(mHostView, left, top, dirty);
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+ return null;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
+ if (mHostView != null) {
+ dirty.offset(location[0], location[1]);
+ if (mHostView instanceof ViewGroup) {
+ location[0] = 0;
+ location[1] = 0;
+ int[] offset = new int[2];
+ getOffset(offset);
+ dirty.offset(offset[0], offset[1]);
+ return super.invalidateChildInParent(location, dirty);
+// return ((ViewGroup) mHostView).invalidateChildInParent(location, dirty);
+ } else {
+ invalidate(dirty);
+ }
+ }
+ return null;
+ }
+
+ static class TouchInterceptor extends View {
+ TouchInterceptor(Context context) {
+ super(context);
+ }
+ }
+ }
+
+ private ViewOverlayApi14() {
+ }
+}
diff --git a/androidx/transition/ViewOverlayApi18.java b/androidx/transition/ViewOverlayApi18.java
new file mode 100644
index 0000000..056123d
--- /dev/null
+++ b/androidx/transition/ViewOverlayApi18.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.ViewOverlay;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(18)
+class ViewOverlayApi18 implements ViewOverlayImpl {
+
+ private final ViewOverlay mViewOverlay;
+
+ ViewOverlayApi18(@NonNull View view) {
+ mViewOverlay = view.getOverlay();
+ }
+
+ @Override
+ public void add(@NonNull Drawable drawable) {
+ mViewOverlay.add(drawable);
+ }
+
+ @Override
+ public void clear() {
+ mViewOverlay.clear();
+ }
+
+ @Override
+ public void remove(@NonNull Drawable drawable) {
+ mViewOverlay.remove(drawable);
+ }
+
+}
diff --git a/androidx/transition/ViewOverlayImpl.java b/androidx/transition/ViewOverlayImpl.java
new file mode 100644
index 0000000..355bd06
--- /dev/null
+++ b/androidx/transition/ViewOverlayImpl.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.NonNull;
+
+interface ViewOverlayImpl {
+
+ /**
+ * Adds a Drawable to the overlay. The bounds of the drawable should be relative to
+ * the host view. Any drawable added to the overlay should be removed when it is no longer
+ * needed or no longer visible.
+ *
+ * @param drawable The Drawable to be added to the overlay. This drawable will be
+ * drawn when the view redraws its overlay.
+ * @see #remove(Drawable)
+ */
+ void add(@NonNull Drawable drawable);
+
+ /**
+ * Removes all content from the overlay.
+ */
+ void clear();
+
+ /**
+ * Removes the specified Drawable from the overlay.
+ *
+ * @param drawable The Drawable to be removed from the overlay.
+ * @see #add(Drawable)
+ */
+ void remove(@NonNull Drawable drawable);
+
+}
diff --git a/androidx/transition/ViewUtils.java b/androidx/transition/ViewUtils.java
new file mode 100644
index 0000000..d770ab6
--- /dev/null
+++ b/androidx/transition/ViewUtils.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.Log;
+import android.util.Property;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.view.ViewCompat;
+
+import java.lang.reflect.Field;
+
+/**
+ * Compatibility utilities for platform features of {@link View}.
+ */
+class ViewUtils {
+
+ private static final ViewUtilsBase IMPL;
+ private static final String TAG = "ViewUtils";
+
+ private static Field sViewFlagsField;
+ private static boolean sViewFlagsFieldFetched;
+ private static final int VISIBILITY_MASK = 0x0000000C;
+
+ static {
+ if (Build.VERSION.SDK_INT >= 22) {
+ IMPL = new ViewUtilsApi22();
+ } else if (Build.VERSION.SDK_INT >= 21) {
+ IMPL = new ViewUtilsApi21();
+ } else if (Build.VERSION.SDK_INT >= 19) {
+ IMPL = new ViewUtilsApi19();
+ } else {
+ IMPL = new ViewUtilsBase();
+ }
+ }
+
+ /**
+ * A {@link Property} for animating transitionAlpha value of a View.
+ */
+ static final Property<View, Float> TRANSITION_ALPHA =
+ new Property<View, Float>(Float.class, "translationAlpha") {
+
+ @Override
+ public Float get(View view) {
+ return getTransitionAlpha(view);
+ }
+
+ @Override
+ public void set(View view, Float alpha) {
+ setTransitionAlpha(view, alpha);
+ }
+
+ };
+
+ static final Property<View, Rect> CLIP_BOUNDS =
+ new Property<View, Rect>(Rect.class, "clipBounds") {
+
+ @Override
+ public Rect get(View view) {
+ return ViewCompat.getClipBounds(view);
+ }
+
+ @Override
+ public void set(View view, Rect clipBounds) {
+ ViewCompat.setClipBounds(view, clipBounds);
+ }
+
+ };
+
+ /**
+ * Backward-compatible {@link View#getOverlay()}.
+ */
+ static ViewOverlayImpl getOverlay(@NonNull View view) {
+ if (Build.VERSION.SDK_INT >= 18) {
+ return new ViewOverlayApi18(view);
+ }
+ return ViewOverlayApi14.createFrom(view);
+ }
+
+ /**
+ * Backward-compatible {@link View#getWindowId()}.
+ */
+ static WindowIdImpl getWindowId(@NonNull View view) {
+ if (Build.VERSION.SDK_INT >= 18) {
+ return new WindowIdApi18(view);
+ }
+ return new WindowIdApi14(view.getWindowToken());
+ }
+
+ static void setTransitionAlpha(@NonNull View view, float alpha) {
+ IMPL.setTransitionAlpha(view, alpha);
+ }
+
+ static float getTransitionAlpha(@NonNull View view) {
+ return IMPL.getTransitionAlpha(view);
+ }
+
+ /**
+ * This method needs to be called before an animation using {@link #setTransitionAlpha(View,
+ * float)} in order to make its behavior backward-compatible.
+ */
+ static void saveNonTransitionAlpha(@NonNull View view) {
+ IMPL.saveNonTransitionAlpha(view);
+ }
+
+ /**
+ * This method needs to be called after an animation using
+ * {@link #setTransitionAlpha(View, float)} if {@link #saveNonTransitionAlpha(View)} has been
+ * called.
+ */
+ static void clearNonTransitionAlpha(@NonNull View view) {
+ IMPL.clearNonTransitionAlpha(view);
+ }
+
+ /**
+ * Copy of a hidden platform method, View#setTransitionVisibility.
+ *
+ * <p>Change the visibility of the View without triggering any other changes. This is
+ * important for transitions, where visibility changes should not adjust focus or
+ * trigger a new layout. This is only used when the visibility has already been changed
+ * and we need a transient value during an animation. When the animation completes,
+ * the original visibility value is always restored.</p>
+ *
+ * @param view The target view.
+ * @param visibility One of {@link View#VISIBLE}, {@link View#INVISIBLE}, or
+ * {@link View#GONE}.
+ */
+ static void setTransitionVisibility(@NonNull View view, int visibility) {
+ fetchViewFlagsField();
+ if (sViewFlagsField != null) {
+ try {
+ int viewFlags = sViewFlagsField.getInt(view);
+ sViewFlagsField.setInt(view, (viewFlags & ~VISIBILITY_MASK) | visibility);
+ } catch (IllegalAccessException e) {
+ // Do nothing
+ }
+ }
+ }
+
+ /**
+ * Modifies the input matrix such that it maps view-local coordinates to
+ * on-screen coordinates.
+ *
+ * <p>On API Level 21 and above, this includes transformation matrix applied to {@code
+ * ViewRootImpl}, but not on older platforms. This difference is balanced out by the
+ * implementation difference in other related platform APIs and their backport, such as
+ * GhostView.</p>
+ *
+ * @param view target view
+ * @param matrix input matrix to modify
+ */
+ static void transformMatrixToGlobal(@NonNull View view, @NonNull Matrix matrix) {
+ IMPL.transformMatrixToGlobal(view, matrix);
+ }
+
+ /**
+ * Modifies the input matrix such that it maps on-screen coordinates to
+ * view-local coordinates.
+ *
+ * <p>On API Level 21 and above, this includes transformation matrix applied to {@code
+ * ViewRootImpl}, but not on older platforms. This difference is balanced out by the
+ * implementation difference in other related platform APIs and their backport, such as
+ * GhostView.</p>
+ *
+ * @param view target view
+ * @param matrix input matrix to modify
+ */
+ static void transformMatrixToLocal(@NonNull View view, @NonNull Matrix matrix) {
+ IMPL.transformMatrixToLocal(view, matrix);
+ }
+
+ /**
+ * Sets the transformation matrix for animation.
+ *
+ * @param v The view
+ * @param m The matrix
+ */
+ static void setAnimationMatrix(@NonNull View v, @Nullable Matrix m) {
+ IMPL.setAnimationMatrix(v, m);
+ }
+
+ /**
+ * Assign a size and position to this view.
+ *
+ * @param left Left position, relative to parent
+ * @param top Top position, relative to parent
+ * @param right Right position, relative to parent
+ * @param bottom Bottom position, relative to parent
+ */
+ static void setLeftTopRightBottom(@NonNull View v, int left, int top, int right, int bottom) {
+ IMPL.setLeftTopRightBottom(v, left, top, right, bottom);
+ }
+
+ private static void fetchViewFlagsField() {
+ if (!sViewFlagsFieldFetched) {
+ try {
+ sViewFlagsField = View.class.getDeclaredField("mViewFlags");
+ sViewFlagsField.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ Log.i(TAG, "fetchViewFlagsField: ");
+ }
+ sViewFlagsFieldFetched = true;
+ }
+ }
+
+ private ViewUtils() {
+ }
+}
diff --git a/androidx/transition/ViewUtilsApi19.java b/androidx/transition/ViewUtilsApi19.java
new file mode 100644
index 0000000..ff60431
--- /dev/null
+++ b/androidx/transition/ViewUtilsApi19.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+@RequiresApi(19)
+class ViewUtilsApi19 extends ViewUtilsBase {
+
+ private static final String TAG = "ViewUtilsApi19";
+
+ private static Method sSetTransitionAlphaMethod;
+ private static boolean sSetTransitionAlphaMethodFetched;
+ private static Method sGetTransitionAlphaMethod;
+ private static boolean sGetTransitionAlphaMethodFetched;
+
+ @Override
+ public void setTransitionAlpha(@NonNull View view, float alpha) {
+ fetchSetTransitionAlphaMethod();
+ if (sSetTransitionAlphaMethod != null) {
+ try {
+ sSetTransitionAlphaMethod.invoke(view, alpha);
+ } catch (IllegalAccessException e) {
+ // Do nothing
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ }
+ } else {
+ view.setAlpha(alpha);
+ }
+ }
+
+ @Override
+ public float getTransitionAlpha(@NonNull View view) {
+ fetchGetTransitionAlphaMethod();
+ if (sGetTransitionAlphaMethod != null) {
+ try {
+ return (Float) sGetTransitionAlphaMethod.invoke(view);
+ } catch (IllegalAccessException e) {
+ // Do nothing
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ }
+ }
+ return super.getTransitionAlpha(view);
+ }
+
+ @Override
+ public void saveNonTransitionAlpha(@NonNull View view) {
+ // Do nothing
+ }
+
+ @Override
+ public void clearNonTransitionAlpha(@NonNull View view) {
+ // Do nothing
+ }
+
+ private void fetchSetTransitionAlphaMethod() {
+ if (!sSetTransitionAlphaMethodFetched) {
+ try {
+ sSetTransitionAlphaMethod = View.class.getDeclaredMethod("setTransitionAlpha",
+ float.class);
+ sSetTransitionAlphaMethod.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ Log.i(TAG, "Failed to retrieve setTransitionAlpha method", e);
+ }
+ sSetTransitionAlphaMethodFetched = true;
+ }
+ }
+
+ private void fetchGetTransitionAlphaMethod() {
+ if (!sGetTransitionAlphaMethodFetched) {
+ try {
+ sGetTransitionAlphaMethod = View.class.getDeclaredMethod("getTransitionAlpha");
+ sGetTransitionAlphaMethod.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ Log.i(TAG, "Failed to retrieve getTransitionAlpha method", e);
+ }
+ sGetTransitionAlphaMethodFetched = true;
+ }
+ }
+
+}
diff --git a/androidx/transition/ViewUtilsApi21.java b/androidx/transition/ViewUtilsApi21.java
new file mode 100644
index 0000000..14301d2
--- /dev/null
+++ b/androidx/transition/ViewUtilsApi21.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.graphics.Matrix;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+@RequiresApi(21)
+class ViewUtilsApi21 extends ViewUtilsApi19 {
+
+ private static final String TAG = "ViewUtilsApi21";
+
+ private static Method sTransformMatrixToGlobalMethod;
+ private static boolean sTransformMatrixToGlobalMethodFetched;
+ private static Method sTransformMatrixToLocalMethod;
+ private static boolean sTransformMatrixToLocalMethodFetched;
+ private static Method sSetAnimationMatrixMethod;
+ private static boolean sSetAnimationMatrixMethodFetched;
+
+ @Override
+ public void transformMatrixToGlobal(@NonNull View view, @NonNull Matrix matrix) {
+ fetchTransformMatrixToGlobalMethod();
+ if (sTransformMatrixToGlobalMethod != null) {
+ try {
+ sTransformMatrixToGlobalMethod.invoke(view, matrix);
+ } catch (IllegalAccessException e) {
+ // Do nothing
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ }
+ }
+ }
+
+ @Override
+ public void transformMatrixToLocal(@NonNull View view, @NonNull Matrix matrix) {
+ fetchTransformMatrixToLocalMethod();
+ if (sTransformMatrixToLocalMethod != null) {
+ try {
+ sTransformMatrixToLocalMethod.invoke(view, matrix);
+ } catch (IllegalAccessException e) {
+ // Do nothing
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ }
+ }
+ }
+
+ @Override
+ public void setAnimationMatrix(@NonNull View view, Matrix matrix) {
+ fetchSetAnimationMatrix();
+ if (sSetAnimationMatrixMethod != null) {
+ try {
+ sSetAnimationMatrixMethod.invoke(view, matrix);
+ } catch (InvocationTargetException e) {
+ // Do nothing
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e.getCause());
+ }
+ }
+ }
+
+ private void fetchTransformMatrixToGlobalMethod() {
+ if (!sTransformMatrixToGlobalMethodFetched) {
+ try {
+ sTransformMatrixToGlobalMethod = View.class.getDeclaredMethod(
+ "transformMatrixToGlobal", Matrix.class);
+ sTransformMatrixToGlobalMethod.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ Log.i(TAG, "Failed to retrieve transformMatrixToGlobal method", e);
+ }
+ sTransformMatrixToGlobalMethodFetched = true;
+ }
+ }
+
+ private void fetchTransformMatrixToLocalMethod() {
+ if (!sTransformMatrixToLocalMethodFetched) {
+ try {
+ sTransformMatrixToLocalMethod = View.class.getDeclaredMethod(
+ "transformMatrixToLocal", Matrix.class);
+ sTransformMatrixToLocalMethod.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ Log.i(TAG, "Failed to retrieve transformMatrixToLocal method", e);
+ }
+ sTransformMatrixToLocalMethodFetched = true;
+ }
+ }
+
+ private void fetchSetAnimationMatrix() {
+ if (!sSetAnimationMatrixMethodFetched) {
+ try {
+ sSetAnimationMatrixMethod = View.class.getDeclaredMethod(
+ "setAnimationMatrix", Matrix.class);
+ sSetAnimationMatrixMethod.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ Log.i(TAG, "Failed to retrieve setAnimationMatrix method", e);
+ }
+ sSetAnimationMatrixMethodFetched = true;
+ }
+ }
+
+}
diff --git a/androidx/transition/ViewUtilsApi22.java b/androidx/transition/ViewUtilsApi22.java
new file mode 100644
index 0000000..f8dd2a0
--- /dev/null
+++ b/androidx/transition/ViewUtilsApi22.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.annotation.SuppressLint;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.RequiresApi;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+@RequiresApi(22)
+class ViewUtilsApi22 extends ViewUtilsApi21 {
+
+ private static final String TAG = "ViewUtilsApi22";
+
+ private static Method sSetLeftTopRightBottomMethod;
+ private static boolean sSetLeftTopRightBottomMethodFetched;
+
+ @Override
+ public void setLeftTopRightBottom(View v, int left, int top, int right, int bottom) {
+ fetchSetLeftTopRightBottomMethod();
+ if (sSetLeftTopRightBottomMethod != null) {
+ try {
+ sSetLeftTopRightBottomMethod.invoke(v, left, top, right, bottom);
+ } catch (IllegalAccessException e) {
+ // Do nothing
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ }
+ }
+ }
+
+ @SuppressLint("PrivateApi")
+ private void fetchSetLeftTopRightBottomMethod() {
+ if (!sSetLeftTopRightBottomMethodFetched) {
+ try {
+ sSetLeftTopRightBottomMethod = View.class.getDeclaredMethod("setLeftTopRightBottom",
+ int.class, int.class, int.class, int.class);
+ sSetLeftTopRightBottomMethod.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ Log.i(TAG, "Failed to retrieve setLeftTopRightBottom method", e);
+ }
+ sSetLeftTopRightBottomMethodFetched = true;
+ }
+ }
+
+}
+
diff --git a/androidx/transition/ViewUtilsBase.java b/androidx/transition/ViewUtilsBase.java
new file mode 100644
index 0000000..c3dad8f
--- /dev/null
+++ b/androidx/transition/ViewUtilsBase.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.graphics.Matrix;
+import android.view.View;
+import android.view.ViewParent;
+
+import androidx.annotation.NonNull;
+
+class ViewUtilsBase {
+
+ private float[] mMatrixValues;
+
+ public void setTransitionAlpha(@NonNull View view, float alpha) {
+ Float savedAlpha = (Float) view.getTag(R.id.save_non_transition_alpha);
+ if (savedAlpha != null) {
+ view.setAlpha(savedAlpha * alpha);
+ } else {
+ view.setAlpha(alpha);
+ }
+ }
+
+ public float getTransitionAlpha(@NonNull View view) {
+ Float savedAlpha = (Float) view.getTag(R.id.save_non_transition_alpha);
+ if (savedAlpha != null) {
+ return view.getAlpha() / savedAlpha;
+ } else {
+ return view.getAlpha();
+ }
+ }
+
+ public void saveNonTransitionAlpha(@NonNull View view) {
+ if (view.getTag(R.id.save_non_transition_alpha) == null) {
+ view.setTag(R.id.save_non_transition_alpha, view.getAlpha());
+ }
+ }
+
+ public void clearNonTransitionAlpha(@NonNull View view) {
+ // We don't clear the saved value when the view is hidden; that's the situation we are
+ // saving this value for.
+ if (view.getVisibility() == View.VISIBLE) {
+ view.setTag(R.id.save_non_transition_alpha, null);
+ }
+ }
+
+ public void transformMatrixToGlobal(@NonNull View view, @NonNull Matrix matrix) {
+ final ViewParent parent = view.getParent();
+ if (parent instanceof View) {
+ final View vp = (View) parent;
+ transformMatrixToGlobal(vp, matrix);
+ matrix.preTranslate(-vp.getScrollX(), -vp.getScrollY());
+ }
+ matrix.preTranslate(view.getLeft(), view.getTop());
+ final Matrix vm = view.getMatrix();
+ if (!vm.isIdentity()) {
+ matrix.preConcat(vm);
+ }
+ }
+
+ public void transformMatrixToLocal(@NonNull View view, @NonNull Matrix matrix) {
+ final ViewParent parent = view.getParent();
+ if (parent instanceof View) {
+ final View vp = (View) parent;
+ transformMatrixToLocal(vp, matrix);
+ matrix.postTranslate(vp.getScrollX(), vp.getScrollY());
+ }
+ matrix.postTranslate(view.getLeft(), view.getTop());
+ final Matrix vm = view.getMatrix();
+ if (!vm.isIdentity()) {
+ final Matrix inverted = new Matrix();
+ if (vm.invert(inverted)) {
+ matrix.postConcat(inverted);
+ }
+ }
+ }
+
+ public void setAnimationMatrix(@NonNull View view, Matrix matrix) {
+ if (matrix == null || matrix.isIdentity()) {
+ view.setPivotX(view.getWidth() / 2);
+ view.setPivotY(view.getHeight() / 2);
+ view.setTranslationX(0);
+ view.setTranslationY(0);
+ view.setScaleX(1);
+ view.setScaleY(1);
+ view.setRotation(0);
+ } else {
+ float[] values = mMatrixValues;
+ if (values == null) {
+ mMatrixValues = values = new float[9];
+ }
+ matrix.getValues(values);
+ final float sin = values[Matrix.MSKEW_Y];
+ final float cos = (float) Math.sqrt(1 - sin * sin)
+ * (values[Matrix.MSCALE_X] < 0 ? -1 : 1);
+ final float rotation = (float) Math.toDegrees(Math.atan2(sin, cos));
+ final float scaleX = values[Matrix.MSCALE_X] / cos;
+ final float scaleY = values[Matrix.MSCALE_Y] / cos;
+ final float dx = values[Matrix.MTRANS_X];
+ final float dy = values[Matrix.MTRANS_Y];
+ view.setPivotX(0);
+ view.setPivotY(0);
+ view.setTranslationX(dx);
+ view.setTranslationY(dy);
+ view.setRotation(rotation);
+ view.setScaleX(scaleX);
+ view.setScaleY(scaleY);
+ }
+ }
+
+ public void setLeftTopRightBottom(View v, int left, int top, int right, int bottom) {
+ v.setLeft(left);
+ v.setTop(top);
+ v.setRight(right);
+ v.setBottom(bottom);
+ }
+
+}
diff --git a/androidx/transition/Visibility.java b/androidx/transition/Visibility.java
new file mode 100644
index 0000000..f26a547
--- /dev/null
+++ b/androidx/transition/Visibility.java
@@ -0,0 +1,574 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.content.res.TypedArrayUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This transition tracks changes to the visibility of target views in the
+ * start and end scenes. Visibility is determined not just by the
+ * {@link View#setVisibility(int)} state of views, but also whether
+ * views exist in the current view hierarchy. The class is intended to be a
+ * utility for subclasses such as {@link Fade}, which use this visibility
+ * information to determine the specific animations to run when visibility
+ * changes occur. Subclasses should implement one or both of the methods
+ * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)},
+ * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)} or
+ * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)},
+ * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}.
+ */
+public abstract class Visibility extends Transition {
+
+ static final String PROPNAME_VISIBILITY = "android:visibility:visibility";
+ private static final String PROPNAME_PARENT = "android:visibility:parent";
+ private static final String PROPNAME_SCREEN_LOCATION = "android:visibility:screenLocation";
+
+ /**
+ * Mode used in {@link #setMode(int)} to make the transition
+ * operate on targets that are appearing. Maybe be combined with
+ * {@link #MODE_OUT} to target Visibility changes both in and out.
+ */
+ public static final int MODE_IN = 0x1;
+
+ /**
+ * Mode used in {@link #setMode(int)} to make the transition
+ * operate on targets that are disappearing. Maybe be combined with
+ * {@link #MODE_IN} to target Visibility changes both in and out.
+ */
+ public static final int MODE_OUT = 0x2;
+
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef(flag = true, value = {MODE_IN, MODE_OUT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Mode {
+ }
+
+ private static final String[] sTransitionProperties = {
+ PROPNAME_VISIBILITY,
+ PROPNAME_PARENT,
+ };
+
+ private static class VisibilityInfo {
+ boolean mVisibilityChange;
+ boolean mFadeIn;
+ int mStartVisibility;
+ int mEndVisibility;
+ ViewGroup mStartParent;
+ ViewGroup mEndParent;
+ }
+
+ private int mMode = MODE_IN | MODE_OUT;
+
+ public Visibility() {
+ }
+
+ public Visibility(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = context.obtainStyledAttributes(attrs, Styleable.VISIBILITY_TRANSITION);
+ @Mode
+ int mode = TypedArrayUtils.getNamedInt(a, (XmlResourceParser) attrs,
+ "transitionVisibilityMode",
+ Styleable.VisibilityTransition.TRANSITION_VISIBILITY_MODE, 0);
+ a.recycle();
+ if (mode != 0) {
+ setMode(mode);
+ }
+ }
+
+ /**
+ * Changes the transition to support appearing and/or disappearing Views, depending
+ * on <code>mode</code>.
+ *
+ * @param mode The behavior supported by this transition, a combination of
+ * {@link #MODE_IN} and {@link #MODE_OUT}.
+ */
+ public void setMode(@Mode int mode) {
+ if ((mode & ~(MODE_IN | MODE_OUT)) != 0) {
+ throw new IllegalArgumentException("Only MODE_IN and MODE_OUT flags are allowed");
+ }
+ mMode = mode;
+ }
+
+ /**
+ * Returns whether appearing and/or disappearing Views are supported.
+ *
+ * @return whether appearing and/or disappearing Views are supported. A combination of
+ * {@link #MODE_IN} and {@link #MODE_OUT}.
+ */
+ @Mode
+ public int getMode() {
+ return mMode;
+ }
+
+ @Nullable
+ @Override
+ public String[] getTransitionProperties() {
+ return sTransitionProperties;
+ }
+
+ private void captureValues(TransitionValues transitionValues) {
+ int visibility = transitionValues.view.getVisibility();
+ transitionValues.values.put(PROPNAME_VISIBILITY, visibility);
+ transitionValues.values.put(PROPNAME_PARENT, transitionValues.view.getParent());
+ int[] loc = new int[2];
+ transitionValues.view.getLocationOnScreen(loc);
+ transitionValues.values.put(PROPNAME_SCREEN_LOCATION, loc);
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ captureValues(transitionValues);
+ }
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ captureValues(transitionValues);
+ }
+
+ /**
+ * Returns whether the view is 'visible' according to the given values
+ * object. This is determined by testing the same properties in the values
+ * object that are used to determine whether the object is appearing or
+ * disappearing in the {@link
+ * Transition#createAnimator(ViewGroup, TransitionValues, TransitionValues)}
+ * method. This method can be called by, for example, subclasses that want
+ * to know whether the object is visible in the same way that Visibility
+ * determines it for the actual animation.
+ *
+ * @param values The TransitionValues object that holds the information by
+ * which visibility is determined.
+ * @return True if the view reference by <code>values</code> is visible,
+ * false otherwise.
+ */
+ public boolean isVisible(TransitionValues values) {
+ if (values == null) {
+ return false;
+ }
+ int visibility = (Integer) values.values.get(PROPNAME_VISIBILITY);
+ View parent = (View) values.values.get(PROPNAME_PARENT);
+
+ return visibility == View.VISIBLE && parent != null;
+ }
+
+ private VisibilityInfo getVisibilityChangeInfo(TransitionValues startValues,
+ TransitionValues endValues) {
+ final VisibilityInfo visInfo = new VisibilityInfo();
+ visInfo.mVisibilityChange = false;
+ visInfo.mFadeIn = false;
+ if (startValues != null && startValues.values.containsKey(PROPNAME_VISIBILITY)) {
+ visInfo.mStartVisibility = (Integer) startValues.values.get(PROPNAME_VISIBILITY);
+ visInfo.mStartParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT);
+ } else {
+ visInfo.mStartVisibility = -1;
+ visInfo.mStartParent = null;
+ }
+ if (endValues != null && endValues.values.containsKey(PROPNAME_VISIBILITY)) {
+ visInfo.mEndVisibility = (Integer) endValues.values.get(PROPNAME_VISIBILITY);
+ visInfo.mEndParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT);
+ } else {
+ visInfo.mEndVisibility = -1;
+ visInfo.mEndParent = null;
+ }
+ if (startValues != null && endValues != null) {
+ if (visInfo.mStartVisibility == visInfo.mEndVisibility
+ && visInfo.mStartParent == visInfo.mEndParent) {
+ return visInfo;
+ } else {
+ if (visInfo.mStartVisibility != visInfo.mEndVisibility) {
+ if (visInfo.mStartVisibility == View.VISIBLE) {
+ visInfo.mFadeIn = false;
+ visInfo.mVisibilityChange = true;
+ } else if (visInfo.mEndVisibility == View.VISIBLE) {
+ visInfo.mFadeIn = true;
+ visInfo.mVisibilityChange = true;
+ }
+ // no visibilityChange if going between INVISIBLE and GONE
+ } else /* if (visInfo.mStartParent != visInfo.mEndParent) */ {
+ if (visInfo.mEndParent == null) {
+ visInfo.mFadeIn = false;
+ visInfo.mVisibilityChange = true;
+ } else if (visInfo.mStartParent == null) {
+ visInfo.mFadeIn = true;
+ visInfo.mVisibilityChange = true;
+ }
+ }
+ }
+ } else if (startValues == null && visInfo.mEndVisibility == View.VISIBLE) {
+ visInfo.mFadeIn = true;
+ visInfo.mVisibilityChange = true;
+ } else if (endValues == null && visInfo.mStartVisibility == View.VISIBLE) {
+ visInfo.mFadeIn = false;
+ visInfo.mVisibilityChange = true;
+ }
+ return visInfo;
+ }
+
+ @Nullable
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot,
+ @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) {
+ VisibilityInfo visInfo = getVisibilityChangeInfo(startValues, endValues);
+ if (visInfo.mVisibilityChange
+ && (visInfo.mStartParent != null || visInfo.mEndParent != null)) {
+ if (visInfo.mFadeIn) {
+ return onAppear(sceneRoot, startValues, visInfo.mStartVisibility,
+ endValues, visInfo.mEndVisibility);
+ } else {
+ return onDisappear(sceneRoot, startValues, visInfo.mStartVisibility,
+ endValues, visInfo.mEndVisibility
+ );
+ }
+ }
+ return null;
+ }
+
+ /**
+ * The default implementation of this method does nothing. Subclasses
+ * should override if they need to create an Animator when targets appear.
+ * The method should only be called by the Visibility class; it is
+ * not intended to be called from external classes.
+ *
+ * @param sceneRoot The root of the transition hierarchy
+ * @param startValues The target values in the start scene
+ * @param startVisibility The target visibility in the start scene
+ * @param endValues The target values in the end scene
+ * @param endVisibility The target visibility in the end scene
+ * @return An Animator to be started at the appropriate time in the
+ * overall transition for this scene change. A null value means no animation
+ * should be run.
+ */
+ @SuppressWarnings("UnusedParameters")
+ public Animator onAppear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility,
+ TransitionValues endValues, int endVisibility) {
+ if ((mMode & MODE_IN) != MODE_IN || endValues == null) {
+ return null;
+ }
+ if (startValues == null) {
+ View endParent = (View) endValues.view.getParent();
+ TransitionValues startParentValues = getMatchedTransitionValues(endParent,
+ false);
+ TransitionValues endParentValues = getTransitionValues(endParent, false);
+ VisibilityInfo parentVisibilityInfo =
+ getVisibilityChangeInfo(startParentValues, endParentValues);
+ if (parentVisibilityInfo.mVisibilityChange) {
+ return null;
+ }
+ }
+ return onAppear(sceneRoot, endValues.view, startValues, endValues);
+ }
+
+ /**
+ * The default implementation of this method returns a null Animator. Subclasses should
+ * override this method to make targets appear with the desired transition. The
+ * method should only be called from
+ * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}.
+ *
+ * @param sceneRoot The root of the transition hierarchy
+ * @param view The View to make appear. This will be in the target scene's View
+ * hierarchy
+ * and
+ * will be VISIBLE.
+ * @param startValues The target values in the start scene
+ * @param endValues The target values in the end scene
+ * @return An Animator to be started at the appropriate time in the
+ * overall transition for this scene change. A null value means no animation
+ * should be run.
+ */
+ public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
+ TransitionValues endValues) {
+ return null;
+ }
+
+ /**
+ * The default implementation of this method does nothing. Subclasses
+ * should override if they need to create an Animator when targets disappear.
+ * The method should only be called by the Visibility class; it is
+ * not intended to be called from external classes.
+ *
+ * @param sceneRoot The root of the transition hierarchy
+ * @param startValues The target values in the start scene
+ * @param startVisibility The target visibility in the start scene
+ * @param endValues The target values in the end scene
+ * @param endVisibility The target visibility in the end scene
+ * @return An Animator to be started at the appropriate time in the
+ * overall transition for this scene change. A null value means no animation
+ * should be run.
+ */
+ @SuppressWarnings("UnusedParameters")
+ public Animator onDisappear(ViewGroup sceneRoot, TransitionValues startValues,
+ int startVisibility, TransitionValues endValues, int endVisibility) {
+ if ((mMode & MODE_OUT) != MODE_OUT) {
+ return null;
+ }
+
+ View startView = (startValues != null) ? startValues.view : null;
+ View endView = (endValues != null) ? endValues.view : null;
+ View overlayView = null;
+ View viewToKeep = null;
+ if (endView == null || endView.getParent() == null) {
+ if (endView != null) {
+ // endView was removed from its parent - add it to the overlay
+ overlayView = endView;
+ } else if (startView != null) {
+ // endView does not exist. Use startView only under certain
+ // conditions, because placing a view in an overlay necessitates
+ // it being removed from its current parent
+ if (startView.getParent() == null) {
+ // no parent - safe to use
+ overlayView = startView;
+ } else if (startView.getParent() instanceof View) {
+ View startParent = (View) startView.getParent();
+ TransitionValues startParentValues = getTransitionValues(startParent, true);
+ TransitionValues endParentValues = getMatchedTransitionValues(startParent,
+ true);
+ VisibilityInfo parentVisibilityInfo =
+ getVisibilityChangeInfo(startParentValues, endParentValues);
+ if (!parentVisibilityInfo.mVisibilityChange) {
+ overlayView = TransitionUtils.copyViewImage(sceneRoot, startView,
+ startParent);
+ } else if (startParent.getParent() == null) {
+ int id = startParent.getId();
+ if (id != View.NO_ID && sceneRoot.findViewById(id) != null
+ && mCanRemoveViews) {
+ // no parent, but its parent is unparented but the parent
+ // hierarchy has been replaced by a new hierarchy with the same id
+ // and it is safe to un-parent startView
+ overlayView = startView;
+ }
+ }
+ }
+ }
+ } else {
+ // visibility change
+ if (endVisibility == View.INVISIBLE) {
+ viewToKeep = endView;
+ } else {
+ // Becoming GONE
+ if (startView == endView) {
+ viewToKeep = endView;
+ } else {
+ overlayView = startView;
+ }
+ }
+ }
+ final int finalVisibility = endVisibility;
+
+ if (overlayView != null && startValues != null) {
+ // TODO: Need to do this for general case of adding to overlay
+ int[] screenLoc = (int[]) startValues.values.get(PROPNAME_SCREEN_LOCATION);
+ int screenX = screenLoc[0];
+ int screenY = screenLoc[1];
+ int[] loc = new int[2];
+ sceneRoot.getLocationOnScreen(loc);
+ overlayView.offsetLeftAndRight((screenX - loc[0]) - overlayView.getLeft());
+ overlayView.offsetTopAndBottom((screenY - loc[1]) - overlayView.getTop());
+ final ViewGroupOverlayImpl overlay = ViewGroupUtils.getOverlay(sceneRoot);
+ overlay.add(overlayView);
+ Animator animator = onDisappear(sceneRoot, overlayView, startValues, endValues);
+ if (animator == null) {
+ overlay.remove(overlayView);
+ } else {
+ final View finalOverlayView = overlayView;
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ overlay.remove(finalOverlayView);
+ }
+ });
+ }
+ return animator;
+ }
+
+ if (viewToKeep != null) {
+ int originalVisibility = viewToKeep.getVisibility();
+ ViewUtils.setTransitionVisibility(viewToKeep, View.VISIBLE);
+ Animator animator = onDisappear(sceneRoot, viewToKeep, startValues, endValues);
+ if (animator != null) {
+ DisappearListener disappearListener = new DisappearListener(viewToKeep,
+ finalVisibility, true);
+ animator.addListener(disappearListener);
+ AnimatorUtils.addPauseListener(animator, disappearListener);
+ addListener(disappearListener);
+ } else {
+ ViewUtils.setTransitionVisibility(viewToKeep, originalVisibility);
+ }
+ return animator;
+ }
+ return null;
+ }
+
+ /**
+ * The default implementation of this method returns a null Animator. Subclasses should
+ * override this method to make targets disappear with the desired transition. The
+ * method should only be called from
+ * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)}.
+ *
+ * @param sceneRoot The root of the transition hierarchy
+ * @param view The View to make disappear. This will be in the target scene's View
+ * hierarchy or in an {@link android.view.ViewGroupOverlay} and will be
+ * VISIBLE.
+ * @param startValues The target values in the start scene
+ * @param endValues The target values in the end scene
+ * @return An Animator to be started at the appropriate time in the
+ * overall transition for this scene change. A null value means no animation
+ * should be run.
+ */
+ public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues,
+ TransitionValues endValues) {
+ return null;
+ }
+
+ @Override
+ public boolean isTransitionRequired(TransitionValues startValues, TransitionValues newValues) {
+ if (startValues == null && newValues == null) {
+ return false;
+ }
+ if (startValues != null && newValues != null
+ && newValues.values.containsKey(PROPNAME_VISIBILITY)
+ != startValues.values.containsKey(PROPNAME_VISIBILITY)) {
+ // The transition wasn't targeted in either the start or end, so it couldn't
+ // have changed.
+ return false;
+ }
+ VisibilityInfo changeInfo = getVisibilityChangeInfo(startValues, newValues);
+ return changeInfo.mVisibilityChange && (changeInfo.mStartVisibility == View.VISIBLE
+ || changeInfo.mEndVisibility == View.VISIBLE);
+ }
+
+ private static class DisappearListener extends AnimatorListenerAdapter
+ implements TransitionListener, AnimatorUtils.AnimatorPauseListenerCompat {
+
+ private final View mView;
+ private final int mFinalVisibility;
+ private final ViewGroup mParent;
+ private final boolean mSuppressLayout;
+
+ private boolean mLayoutSuppressed;
+ boolean mCanceled = false;
+
+ DisappearListener(View view, int finalVisibility, boolean suppressLayout) {
+ mView = view;
+ mFinalVisibility = finalVisibility;
+ mParent = (ViewGroup) view.getParent();
+ mSuppressLayout = suppressLayout;
+ // Prevent a layout from including mView in its calculation.
+ suppressLayout(true);
+ }
+
+ // This overrides both AnimatorListenerAdapter and
+ // AnimatorUtilsApi14.AnimatorPauseListenerCompat
+ @Override
+ public void onAnimationPause(Animator animation) {
+ if (!mCanceled) {
+ ViewUtils.setTransitionVisibility(mView, mFinalVisibility);
+ }
+ }
+
+ // This overrides both AnimatorListenerAdapter and
+ // AnimatorUtilsApi14.AnimatorPauseListenerCompat
+ @Override
+ public void onAnimationResume(Animator animation) {
+ if (!mCanceled) {
+ ViewUtils.setTransitionVisibility(mView, View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCanceled = true;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ hideViewWhenNotCanceled();
+ }
+
+ @Override
+ public void onTransitionStart(@NonNull Transition transition) {
+ // Do nothing
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ hideViewWhenNotCanceled();
+ transition.removeListener(this);
+ }
+
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ suppressLayout(false);
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ suppressLayout(true);
+ }
+
+ private void hideViewWhenNotCanceled() {
+ if (!mCanceled) {
+ // Recreate the parent's display list in case it includes mView.
+ ViewUtils.setTransitionVisibility(mView, mFinalVisibility);
+ if (mParent != null) {
+ mParent.invalidate();
+ }
+ }
+ // Layout is allowed now that the View is in its final state
+ suppressLayout(false);
+ }
+
+ private void suppressLayout(boolean suppress) {
+ if (mSuppressLayout && mLayoutSuppressed != suppress && mParent != null) {
+ mLayoutSuppressed = suppress;
+ ViewGroupUtils.suppressLayout(mParent, suppress);
+ }
+ }
+ }
+
+ // TODO: Implement API 23; isTransitionRequired
+
+}
diff --git a/androidx/transition/VisibilityPropagation.java b/androidx/transition/VisibilityPropagation.java
new file mode 100644
index 0000000..fd424c3
--- /dev/null
+++ b/androidx/transition/VisibilityPropagation.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2017 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 androidx.transition;
+
+import android.view.View;
+
+/**
+ * Base class for <code>TransitionPropagation</code>s that care about
+ * View Visibility and the center position of the View.
+ */
+public abstract class VisibilityPropagation extends TransitionPropagation {
+
+ /**
+ * The property key used for {@link android.view.View#getVisibility()}.
+ */
+ private static final String PROPNAME_VISIBILITY = "android:visibilityPropagation:visibility";
+
+ /**
+ * The property key used for the center of the View in screen coordinates. This is an
+ * int[2] with the index 0 taking the x coordinate and index 1 taking the y coordinate.
+ */
+ private static final String PROPNAME_VIEW_CENTER = "android:visibilityPropagation:center";
+
+ private static final String[] VISIBILITY_PROPAGATION_VALUES = {
+ PROPNAME_VISIBILITY,
+ PROPNAME_VIEW_CENTER,
+ };
+
+ @Override
+ public void captureValues(TransitionValues values) {
+ View view = values.view;
+ Integer visibility = (Integer) values.values.get(Visibility.PROPNAME_VISIBILITY);
+ if (visibility == null) {
+ visibility = view.getVisibility();
+ }
+ values.values.put(PROPNAME_VISIBILITY, visibility);
+ int[] loc = new int[2];
+ view.getLocationOnScreen(loc);
+ loc[0] += Math.round(view.getTranslationX());
+ loc[0] += view.getWidth() / 2;
+ loc[1] += Math.round(view.getTranslationY());
+ loc[1] += view.getHeight() / 2;
+ values.values.put(PROPNAME_VIEW_CENTER, loc);
+ }
+
+ @Override
+ public String[] getPropagationProperties() {
+ return VISIBILITY_PROPAGATION_VALUES;
+ }
+
+ /**
+ * Returns {@link android.view.View#getVisibility()} for the View at the time the values
+ * were captured.
+ * @param values The TransitionValues captured at the start or end of the Transition.
+ * @return {@link android.view.View#getVisibility()} for the View at the time the values
+ * were captured.
+ */
+ public int getViewVisibility(TransitionValues values) {
+ if (values == null) {
+ return View.GONE;
+ }
+ Integer visibility = (Integer) values.values.get(PROPNAME_VISIBILITY);
+ if (visibility == null) {
+ return View.GONE;
+ }
+ return visibility;
+ }
+
+ /**
+ * Returns the View's center x coordinate, relative to the screen, at the time the values
+ * were captured.
+ * @param values The TransitionValues captured at the start or end of the Transition.
+ * @return the View's center x coordinate, relative to the screen, at the time the values
+ * were captured.
+ */
+ public int getViewX(TransitionValues values) {
+ return getViewCoordinate(values, 0);
+ }
+
+ /**
+ * Returns the View's center y coordinate, relative to the screen, at the time the values
+ * were captured.
+ * @param values The TransitionValues captured at the start or end of the Transition.
+ * @return the View's center y coordinate, relative to the screen, at the time the values
+ * were captured.
+ */
+ public int getViewY(TransitionValues values) {
+ return getViewCoordinate(values, 1);
+ }
+
+ private static int getViewCoordinate(TransitionValues values, int coordinateIndex) {
+ if (values == null) {
+ return -1;
+ }
+
+ int[] coordinates = (int[]) values.values.get(PROPNAME_VIEW_CENTER);
+ if (coordinates == null) {
+ return -1;
+ }
+
+ return coordinates[coordinateIndex];
+ }
+
+}
diff --git a/androidx/transition/VisibilityTest.java b/androidx/transition/VisibilityTest.java
new file mode 100644
index 0000000..8c7a2fd
--- /dev/null
+++ b/androidx/transition/VisibilityTest.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+@MediumTest
+public class VisibilityTest extends BaseTest {
+
+ private View mView;
+ private ViewGroup mRoot;
+
+ @UiThreadTest
+ @Before
+ public void setUp() {
+ mRoot = rule.getActivity().getRoot();
+ mView = new View(rule.getActivity());
+ mRoot.addView(mView, new ViewGroup.LayoutParams(100, 100));
+ }
+
+ @Test
+ public void testMode() {
+ final CustomVisibility visibility = new CustomVisibility();
+ assertThat(visibility.getMode(), is(Visibility.MODE_IN | Visibility.MODE_OUT));
+ visibility.setMode(Visibility.MODE_IN);
+ assertThat(visibility.getMode(), is(Visibility.MODE_IN));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testCustomVisibility() {
+ final CustomVisibility visibility = new CustomVisibility();
+ assertThat(visibility.getName(), is(equalTo(CustomVisibility.class.getName())));
+ assertNotNull(visibility.getTransitionProperties());
+
+ // Capture start values
+ mView.setScaleX(0.5f);
+ final TransitionValues startValues = new TransitionValues();
+ startValues.view = mView;
+ visibility.captureStartValues(startValues);
+ assertThat((float) startValues.values.get(CustomVisibility.PROPNAME_SCALE_X), is(0.5f));
+
+ // Hide the view and capture end values
+ mView.setVisibility(View.GONE);
+ final TransitionValues endValues = new TransitionValues();
+ endValues.view = mView;
+ visibility.captureEndValues(endValues);
+
+ // This should invoke onDisappear, not onAppear
+ ObjectAnimator animator = (ObjectAnimator) visibility
+ .createAnimator(mRoot, startValues, endValues);
+ assertNotNull(animator);
+ assertThat(animator.getPropertyName(), is(equalTo("scaleX")));
+
+ // Jump to the end of the animation
+ animator.end();
+
+ // This value confirms that onDisappear, not onAppear, was called
+ assertThat((float) animator.getAnimatedValue(), is(0.25f));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testCustomVisibility2() {
+ final CustomVisibility2 visibility = new CustomVisibility2();
+ final TransitionValues startValues = new TransitionValues();
+ startValues.view = mView;
+ visibility.captureStartValues(startValues);
+ mView.setVisibility(View.GONE);
+ final TransitionValues endValues = new TransitionValues();
+ endValues.view = mView;
+ visibility.captureEndValues(endValues);
+ ObjectAnimator animator = (ObjectAnimator) visibility
+ .createAnimator(mRoot, startValues, endValues);
+ assertNotNull(animator);
+
+ // Jump to the end of the animation
+ animator.end();
+
+ // This value confirms that onDisappear, not onAppear, was called
+ assertThat((float) animator.getAnimatedValue(), is(0.25f));
+ }
+
+ /**
+ * A custom {@link Visibility} with 5-arg onAppear/Disappear
+ */
+ public static class CustomVisibility extends Visibility {
+
+ static final String PROPNAME_SCALE_X = "customVisibility:scaleX";
+
+ private static String[] sTransitionProperties;
+
+ @Nullable
+ @Override
+ public String[] getTransitionProperties() {
+ if (sTransitionProperties == null) {
+ String[] properties = super.getTransitionProperties();
+ if (properties != null) {
+ sTransitionProperties = Arrays.copyOf(properties, properties.length + 1);
+ } else {
+ sTransitionProperties = new String[1];
+ }
+ sTransitionProperties[sTransitionProperties.length - 1] = PROPNAME_SCALE_X;
+ }
+ return sTransitionProperties;
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ super.captureStartValues(transitionValues);
+ transitionValues.values.put(PROPNAME_SCALE_X, transitionValues.view.getScaleX());
+ }
+
+ @Override
+ public Animator onAppear(ViewGroup sceneRoot, TransitionValues startValues,
+ int startVisibility, TransitionValues endValues, int endVisibility) {
+ if (startValues == null) {
+ return null;
+ }
+ float startScaleX = (float) startValues.values.get(PROPNAME_SCALE_X);
+ return ObjectAnimator.ofFloat(startValues.view, "scaleX", startScaleX, 0.75f);
+ }
+
+ @Override
+ public Animator onDisappear(ViewGroup sceneRoot, TransitionValues startValues,
+ int startVisibility, TransitionValues endValues, int endVisibility) {
+ if (startValues == null) {
+ return null;
+ }
+ float startScaleX = (float) startValues.values.get(PROPNAME_SCALE_X);
+ return ObjectAnimator.ofFloat(startValues.view, "scaleX", startScaleX, 0.25f);
+ }
+
+ }
+
+ /**
+ * A custom {@link Visibility} with 4-arg onAppear/Disappear
+ */
+ public static class CustomVisibility2 extends Visibility {
+
+ static final String PROPNAME_SCALE_X = "customVisibility:scaleX";
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ super.captureStartValues(transitionValues);
+ transitionValues.values.put(PROPNAME_SCALE_X, transitionValues.view.getScaleX());
+ }
+
+ @Override
+ public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
+ TransitionValues endValues) {
+ float startScaleX = startValues == null ? 0.25f :
+ (float) startValues.values.get(PROPNAME_SCALE_X);
+ return ObjectAnimator.ofFloat(view, "scaleX", startScaleX, 0.75f);
+ }
+
+ @Override
+ public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues,
+ TransitionValues endValues) {
+ if (startValues == null) {
+ return null;
+ }
+ float startScaleX = (float) startValues.values.get(PROPNAME_SCALE_X);
+ return ObjectAnimator.ofFloat(view, "scaleX", startScaleX, 0.25f);
+ }
+
+ }
+
+}
diff --git a/androidx/transition/WindowIdApi14.java b/androidx/transition/WindowIdApi14.java
new file mode 100644
index 0000000..6a9231e
--- /dev/null
+++ b/androidx/transition/WindowIdApi14.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.os.IBinder;
+
+class WindowIdApi14 implements WindowIdImpl {
+
+ private final IBinder mToken;
+
+ WindowIdApi14(IBinder token) {
+ mToken = token;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof WindowIdApi14 && ((WindowIdApi14) o).mToken.equals(this.mToken);
+ }
+
+ @Override
+ public int hashCode() {
+ return mToken.hashCode();
+ }
+}
diff --git a/androidx/transition/WindowIdApi18.java b/androidx/transition/WindowIdApi18.java
new file mode 100644
index 0000000..cceaab9
--- /dev/null
+++ b/androidx/transition/WindowIdApi18.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.view.View;
+import android.view.WindowId;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(18)
+class WindowIdApi18 implements WindowIdImpl {
+
+ private final WindowId mWindowId;
+
+ WindowIdApi18(@NonNull View view) {
+ mWindowId = view.getWindowId();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof WindowIdApi18 && ((WindowIdApi18) o).mWindowId.equals(mWindowId);
+ }
+
+ @Override
+ public int hashCode() {
+ return mWindowId.hashCode();
+ }
+}
diff --git a/androidx/transition/WindowIdImpl.java b/androidx/transition/WindowIdImpl.java
new file mode 100644
index 0000000..ca57588
--- /dev/null
+++ b/androidx/transition/WindowIdImpl.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+interface WindowIdImpl {
+}