[go: nahoru, domu]

Add ChangeTransform

ChangeTransformTest is based on the CTS test.

Bug: 34722322
Test: ChangeTransformTest on 15, 18, 19, 21, 25
Change-Id: Ic851f4fe15cf0f30ab999fb44fdf978134b326d5
diff --git a/api/current.txt b/api/current.txt
index ec5c076..8c63a7b 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -1894,6 +1894,17 @@
     method public void captureStartValues(android.support.transition.TransitionValues);
   }
 
+  public class ChangeTransform extends android.support.transition.Transition {
+    ctor public ChangeTransform();
+    ctor public ChangeTransform(android.content.Context, android.util.AttributeSet);
+    method public void captureEndValues(android.support.transition.TransitionValues);
+    method public void captureStartValues(android.support.transition.TransitionValues);
+    method public boolean getReparent();
+    method public boolean getReparentWithOverlay();
+    method public void setReparent(boolean);
+    method public void setReparentWithOverlay(boolean);
+  }
+
   public class CircularPropagation extends android.support.transition.VisibilityPropagation {
     ctor public CircularPropagation();
     method public long getStartDelay(android.view.ViewGroup, android.support.transition.Transition, android.support.transition.TransitionValues, android.support.transition.TransitionValues);
diff --git a/samples/SupportTransitionDemos/AndroidManifest.xml b/samples/SupportTransitionDemos/AndroidManifest.xml
index 6a08d2c..e48e85f 100644
--- a/samples/SupportTransitionDemos/AndroidManifest.xml
+++ b/samples/SupportTransitionDemos/AndroidManifest.xml
@@ -90,5 +90,14 @@
                 <category android:name="com.example.android.support.transition.SAMPLE_CODE" />
             </intent-filter>
         </activity>
+
+        <activity android:name=".widget.ChangeTransformUsage"
+                  android:label="@string/changeTransform"
+                  android:theme="@style/Theme.Transition">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.example.android.support.transition.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
     </application>
 </manifest>
diff --git a/samples/SupportTransitionDemos/res/layout/change_transform.xml b/samples/SupportTransitionDemos/res/layout/change_transform.xml
new file mode 100644
index 0000000..35909e3
--- /dev/null
+++ b/samples/SupportTransitionDemos/res/layout/change_transform.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/root"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <Button
+        android:id="@+id/toggle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_margin="16dp"
+        android:text="@string/toggle"/>
+
+    <FrameLayout
+        android:id="@+id/container_1"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:background="#BBDEFB"/>
+
+    <FrameLayout
+        android:id="@+id/container_2"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:background="#FFCC80"/>
+
+</LinearLayout>
diff --git a/samples/SupportTransitionDemos/res/layout/red_square.xml b/samples/SupportTransitionDemos/res/layout/red_square.xml
new file mode 100644
index 0000000..550b2c6
--- /dev/null
+++ b/samples/SupportTransitionDemos/res/layout/red_square.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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.
+-->
+<View
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/red_square"
+    android:layout_width="64dp"
+    android:layout_height="64dp"
+    android:layout_gravity="center"
+    android:background="#f00"/>
diff --git a/samples/SupportTransitionDemos/res/values/strings.xml b/samples/SupportTransitionDemos/res/values/strings.xml
index 73ed883..5f70d5a 100644
--- a/samples/SupportTransitionDemos/res/values/strings.xml
+++ b/samples/SupportTransitionDemos/res/values/strings.xml
@@ -22,6 +22,7 @@
     <string name="arcMotion">Arc Motion</string>
     <string name="explode">Explode</string>
     <string name="clipBounds">Change Clip Bounds</string>
+    <string name="changeTransform">Change Transform</string>
     <string name="toggle">Toggle</string>
     <string name="begin">Begin</string>
     <string name="hello_world">Hello, world!</string>
diff --git a/samples/SupportTransitionDemos/src/com/example/android/support/transition/widget/ChangeTransformUsage.java b/samples/SupportTransitionDemos/src/com/example/android/support/transition/widget/ChangeTransformUsage.java
new file mode 100644
index 0000000..b84c3417
--- /dev/null
+++ b/samples/SupportTransitionDemos/src/com/example/android/support/transition/widget/ChangeTransformUsage.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 com.example.android.support.transition.widget;
+
+import android.os.Bundle;
+import android.support.transition.ChangeTransform;
+import android.support.transition.TransitionManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import com.example.android.support.transition.R;
+
+/**
+ * This demonstrates basic usage of the ChangeTransform Transition.
+ */
+public class ChangeTransformUsage extends TransitionUsageBase {
+
+    private LinearLayout mRoot;
+    private FrameLayout mContainer1;
+    private FrameLayout mContainer2;
+    private ChangeTransform mChangeTransform;
+
+    @Override
+    int getLayoutResId() {
+        return R.layout.change_transform;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mChangeTransform = new ChangeTransform();
+        mRoot = (LinearLayout) findViewById(R.id.root);
+        mContainer1 = (FrameLayout) findViewById(R.id.container_1);
+        mContainer2 = (FrameLayout) findViewById(R.id.container_2);
+        findViewById(R.id.toggle).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                TransitionManager.beginDelayedTransition(mRoot, mChangeTransform);
+                toggle();
+            }
+        });
+        showRedSquare(mContainer1);
+    }
+
+    void toggle() {
+        if (mContainer2.getChildCount() > 0) {
+            mContainer2.removeAllViews();
+            showRedSquare(mContainer1);
+        } else {
+            mContainer1.removeAllViews();
+            showRedSquare(mContainer2);
+            mContainer2.getChildAt(0).setRotation(45);
+        }
+    }
+
+    private void showRedSquare(FrameLayout container) {
+        final View view = LayoutInflater.from(this)
+                .inflate(R.layout.red_square, container, false);
+        container.addView(view);
+    }
+
+}
diff --git a/transition/api14/android/support/transition/GhostViewApi14.java b/transition/api14/android/support/transition/GhostViewApi14.java
new file mode 100644
index 0000000..c164a4c
--- /dev/null
+++ b/transition/api14/android/support/transition/GhostViewApi14.java
@@ -0,0 +1,197 @@
+/*
+ * 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 android.support.transition;
+
+import android.annotation.SuppressLint;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import android.support.v4.view.ViewCompat;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+
+/**
+ * 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.
+ */
+@RequiresApi(14)
+@SuppressLint("ViewConstructor")
+class GhostViewApi14 extends View implements GhostViewImpl {
+
+    static class Creator implements GhostViewImpl.Creator {
+
+        @Override
+        public GhostViewImpl addGhost(View view, ViewGroup viewGroup, Matrix matrix) {
+            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;
+        }
+
+        @Override
+        public 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) parent).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);
+        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/transition/api14/android/support/transition/ViewOverlayApi14.java b/transition/api14/android/support/transition/ViewOverlayApi14.java
index c45da98..7e45b21 100644
--- a/transition/api14/android/support/transition/ViewOverlayApi14.java
+++ b/transition/api14/android/support/transition/ViewOverlayApi14.java
@@ -107,8 +107,8 @@
 
     /**
      * OverlayViewGroup is a container that View and ViewGroup use to host
-     * drawables and views added to their overlays  ({@link ViewOverlay} and
-     * {@link ViewGroupOverlay}, respectively). Drawables are added to the overlay
+     * 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
diff --git a/transition/api14/android/support/transition/ViewUtilsApi14.java b/transition/api14/android/support/transition/ViewUtilsApi14.java
index a8ea8db..c4de8a6 100644
--- a/transition/api14/android/support/transition/ViewUtilsApi14.java
+++ b/transition/api14/android/support/transition/ViewUtilsApi14.java
@@ -16,13 +16,17 @@
 
 package android.support.transition;
 
+import android.graphics.Matrix;
 import android.support.annotation.NonNull;
 import android.support.annotation.RequiresApi;
 import android.view.View;
+import android.view.ViewParent;
 
 @RequiresApi(14)
 class ViewUtilsApi14 implements ViewUtilsImpl {
 
+    private float[] mMatrixValues;
+
     @Override
     public ViewOverlayImpl getOverlay(@NonNull View view) {
         return ViewOverlayApi14.createFrom(view);
@@ -43,4 +47,71 @@
         return view.getAlpha();
     }
 
+    @Override
+    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);
+        }
+    }
+
+    @Override
+    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);
+            }
+        }
+    }
+
+    @Override
+    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);
+        }
+    }
+
 }
diff --git a/transition/api21/android/support/transition/GhostViewApi21.java b/transition/api21/android/support/transition/GhostViewApi21.java
new file mode 100644
index 0000000..ace0016
--- /dev/null
+++ b/transition/api21/android/support/transition/GhostViewApi21.java
@@ -0,0 +1,130 @@
+/*
+ * 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 android.support.transition;
+
+import android.graphics.Matrix;
+import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+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 class Creator implements GhostViewImpl.Creator {
+
+        @Override
+        public 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;
+        }
+
+        @Override
+        public 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/transition/api21/android/support/transition/ViewUtilsApi21.java b/transition/api21/android/support/transition/ViewUtilsApi21.java
new file mode 100644
index 0000000..c403235
--- /dev/null
+++ b/transition/api21/android/support/transition/ViewUtilsApi21.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 android.support.transition;
+
+import android.graphics.Matrix;
+import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import android.util.Log;
+import android.view.View;
+
+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/transition/base/android/support/transition/GhostViewImpl.java b/transition/base/android/support/transition/GhostViewImpl.java
new file mode 100644
index 0000000..037b5731
--- /dev/null
+++ b/transition/base/android/support/transition/GhostViewImpl.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 android.support.transition;
+
+import android.graphics.Matrix;
+import android.support.annotation.RequiresApi;
+import android.view.View;
+import android.view.ViewGroup;
+
+@RequiresApi(14)
+interface GhostViewImpl {
+
+    interface Creator {
+
+        GhostViewImpl addGhost(View view, ViewGroup viewGroup, Matrix matrix);
+
+        void removeGhost(View view);
+
+    }
+
+    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/transition/base/android/support/transition/ViewUtilsImpl.java b/transition/base/android/support/transition/ViewUtilsImpl.java
index 68bf350..e19985f 100644
--- a/transition/base/android/support/transition/ViewUtilsImpl.java
+++ b/transition/base/android/support/transition/ViewUtilsImpl.java
@@ -16,6 +16,7 @@
 
 package android.support.transition;
 
+import android.graphics.Matrix;
 import android.support.annotation.NonNull;
 import android.support.annotation.RequiresApi;
 import android.view.View;
@@ -31,4 +32,11 @@
 
     float getTransitionAlpha(@NonNull View view);
 
+    void transformMatrixToGlobal(@NonNull View view, @NonNull Matrix matrix);
+
+    void transformMatrixToLocal(@NonNull View view, @NonNull Matrix matrix);
+
+    void setAnimationMatrix(@NonNull View view, Matrix matrix);
+
+
 }
diff --git a/transition/res/values/ids.xml b/transition/res/values/ids.xml
index 5fc44dc..f3f2323 100644
--- a/transition/res/values/ids.xml
+++ b/transition/res/values/ids.xml
@@ -19,4 +19,7 @@
     <item name="transition_current_scene" type="id"/>
     <item name="transition_layout_save" type="id"/>
     <item name="transition_position" type="id"/>
+    <item name="transition_transform" type="id"/>
+    <item name="parent_matrix" type="id"/>
+    <item name="ghost_view" type="id"/>
 </resources>
diff --git a/transition/src/android/support/transition/ChangeTransform.java b/transition/src/android/support/transition/ChangeTransform.java
new file mode 100644
index 0000000..e3d0d22
--- /dev/null
+++ b/transition/src/android/support/transition/ChangeTransform.java
@@ -0,0 +1,490 @@
+/*
+ * 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 android.support.transition;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Matrix;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.v4.content.res.TypedArrayUtils;
+import android.support.v4.view.ViewCompat;
+import android.util.AttributeSet;
+import android.util.Property;
+import android.view.View;
+import android.view.ViewGroup;
+
+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,
+    };
+
+    private static final Property<View, Matrix> ANIMATION_MATRIX_PROPERTY =
+            new Property<View, Matrix>(Matrix.class, "animationMatrix") {
+                @Override
+                public Matrix get(View view) {
+                    return null;
+                }
+
+                @Override
+                public void set(View view, Matrix matrix) {
+                    ViewUtils.setAnimationMatrix(view, matrix);
+                }
+            };
+
+    /**
+     * 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);
+
+        ObjectAnimator animator = ObjectAnimator.ofObject(view, ANIMATION_MATRIX_PROPERTY,
+                new TransitionUtils.MatrixEvaluator(), startMatrix, endMatrix);
+
+        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);
+                    }
+                }
+                ANIMATION_MATRIX_PROPERTY.set(view, null);
+                transforms.restore(view);
+            }
+
+            @Override
+            public void onAnimationPause(Animator animation) {
+                ValueAnimator animator = (ValueAnimator) animation;
+                Matrix currentMatrix = (Matrix) animator.getAnimatedValue();
+                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;
+        }
+    }
+
+    private static class GhostListener extends Transition.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);
+        }
+
+    }
+
+}
diff --git a/transition/src/android/support/transition/GhostViewUtils.java b/transition/src/android/support/transition/GhostViewUtils.java
new file mode 100644
index 0000000..66f01c3
--- /dev/null
+++ b/transition/src/android/support/transition/GhostViewUtils.java
@@ -0,0 +1,44 @@
+/*
+ * 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 android.support.transition;
+
+import android.graphics.Matrix;
+import android.os.Build;
+import android.view.View;
+import android.view.ViewGroup;
+
+class GhostViewUtils {
+
+    private static final GhostViewImpl.Creator CREATOR;
+
+    static {
+        if (Build.VERSION.SDK_INT >= 21) {
+            CREATOR = new GhostViewApi21.Creator();
+        } else {
+            CREATOR = new GhostViewApi14.Creator();
+        }
+    }
+
+    static GhostViewImpl addGhost(View view, ViewGroup viewGroup, Matrix matrix) {
+        return CREATOR.addGhost(view, viewGroup, matrix);
+    }
+
+    static void removeGhost(View view) {
+        CREATOR.removeGhost(view);
+    }
+
+}
diff --git a/transition/src/android/support/transition/MatrixUtils.java b/transition/src/android/support/transition/MatrixUtils.java
new file mode 100644
index 0000000..1d9be29
--- /dev/null
+++ b/transition/src/android/support/transition/MatrixUtils.java
@@ -0,0 +1,207 @@
+/*
+ * 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 android.support.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();
+        }
+
+    };
+
+}
diff --git a/transition/src/android/support/transition/Styleable.java b/transition/src/android/support/transition/Styleable.java
index 8f1bc9a..f200c66 100644
--- a/transition/src/android/support/transition/Styleable.java
+++ b/transition/src/android/support/transition/Styleable.java
@@ -106,6 +106,19 @@
     }
 
     @StyleableRes
+    int[] CHANGE_TRANSFORM = {
+            android.R.attr.reparent,
+            android.R.attr.reparentWithOverlay,
+    };
+
+    interface ChangeTransform {
+        @StyleableRes
+        int REPARENT = 0;
+        @StyleableRes
+        int REPARENT_WITH_OVERLAY = 1;
+    }
+
+    @StyleableRes
     int[] TRANSITION_SET = {
             android.R.attr.transitionOrdering,
     };
diff --git a/transition/src/android/support/transition/TransitionInflater.java b/transition/src/android/support/transition/TransitionInflater.java
index e1da808..1a43458 100644
--- a/transition/src/android/support/transition/TransitionInflater.java
+++ b/transition/src/android/support/transition/TransitionInflater.java
@@ -136,6 +136,8 @@
                 transition = new ChangeBounds(mContext, attrs);
             } else if ("explode".equals(name)) {
                 transition = new Explode(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)) {
diff --git a/transition/src/android/support/transition/TransitionUtils.java b/transition/src/android/support/transition/TransitionUtils.java
index e3770b8..bd9851c 100644
--- a/transition/src/android/support/transition/TransitionUtils.java
+++ b/transition/src/android/support/transition/TransitionUtils.java
@@ -18,6 +18,7 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
+import android.animation.TypeEvaluator;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Matrix;
@@ -107,4 +108,26 @@
         }
     }
 
+    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;
+        }
+
+    }
+
 }
diff --git a/transition/src/android/support/transition/ViewUtils.java b/transition/src/android/support/transition/ViewUtils.java
index ac170cc..0bf0d61 100644
--- a/transition/src/android/support/transition/ViewUtils.java
+++ b/transition/src/android/support/transition/ViewUtils.java
@@ -23,7 +23,6 @@
 import android.support.v4.view.ViewCompat;
 import android.util.Property;
 import android.view.View;
-import android.view.ViewParent;
 
 /**
  * Compatibility utilities for platform features of {@link View}.
@@ -33,7 +32,9 @@
     private static final ViewUtilsImpl IMPL;
 
     static {
-        if (Build.VERSION.SDK_INT >= 19) {
+        if (Build.VERSION.SDK_INT >= 21) {
+            IMPL = new ViewUtilsApi21();
+        } else if (Build.VERSION.SDK_INT >= 19) {
             IMPL = new ViewUtilsApi19();
         } else if (Build.VERSION.SDK_INT >= 18) {
             IMPL = new ViewUtilsApi18();
@@ -101,45 +102,42 @@
      * Modifies the input matrix such that it maps view-local coordinates to
      * on-screen coordinates.
      *
-     * @param view target view
+     * <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) {
-        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);
-        }
+        IMPL.transformMatrixToGlobal(view, matrix);
     }
 
     /**
      * Modifies the input matrix such that it maps on-screen coordinates to
      * view-local coordinates.
      *
-     * @param view target view
+     * <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) {
-        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);
-            }
-        }
+        IMPL.transformMatrixToLocal(view, matrix);
+    }
+
+    /**
+     * Sets the transformation matrix for animation.
+     *
+     * @param v The view
+     * @param m The matrix
+     */
+    static void setAnimationMatrix(@NonNull View v, @NonNull Matrix m) {
+        IMPL.setAnimationMatrix(v, m);
     }
 
 }
diff --git a/transition/tests/res/layout/scene5.xml b/transition/tests/res/layout/scene5.xml
new file mode 100644
index 0000000..11e876d
--- /dev/null
+++ b/transition/tests/res/layout/scene5.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:transitionName="holder"
+                android:id="@+id/holder">
+    <TextView
+        android:id="@+id/text"
+        android:text="@string/longText"
+        android:layout_width="100dp"
+        android:layout_height="100dp"/>
+</RelativeLayout>
diff --git a/transition/tests/res/layout/scene9.xml b/transition/tests/res/layout/scene9.xml
new file mode 100644
index 0000000..fd779c2
--- /dev/null
+++ b/transition/tests/res/layout/scene9.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:transitionName="holder"
+                android:id="@+id/holder">
+    <FrameLayout
+        android:layout_marginTop="50dp"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+        <TextView
+            android:id="@+id/text"
+            android:text="@string/longText"
+            android:layout_width="100dp"
+            android:layout_height="100dp"/>
+    </FrameLayout>
+</RelativeLayout>
diff --git a/transition/tests/res/transition/change_transform.xml b/transition/tests/res/transition/change_transform.xml
new file mode 100644
index 0000000..be692fb
--- /dev/null
+++ b/transition/tests/res/transition/change_transform.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<changeTransform
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:reparent="false"
+    android:reparentWithOverlay="false"/>
diff --git a/transition/tests/res/values/strings.xml b/transition/tests/res/values/strings.xml
index 741c335..aa87c7f 100644
--- a/transition/tests/res/values/strings.xml
+++ b/transition/tests/res/values/strings.xml
@@ -16,4 +16,5 @@
 -->
 <resources>
     <string name="hello">Hello</string>
+    <string name="longText">Gummi bears sugar plum pudding. Carrot cake chupa chups lollipop brownie candy canes carrot cake. Chocolate cake dragée chocolate danish halvah brownie. Cake jelly-o danish jelly beans carrot cake toffee jelly-o. Danish sesame snaps soufflé chocolate cupcake jujubes pudding pudding candy canes. Sesame snaps sweet chupa chups marzipan tart. Dessert brownie marzipan powder. Biscuit sugar plum soufflé topping cheesecake. Jelly-o ice cream candy canes tart. Brownie ice cream cake. Chocolate bar cake tart powder. Cookie candy canes marzipan donut jelly beans cheesecake marzipan. Carrot cake dragée cupcake liquorice tiramisu chocolate cake powder macaroon. Liquorice sugar plum powder dessert jelly.</string>
 </resources>
diff --git a/transition/tests/src/android/support/transition/BaseTransitionTest.java b/transition/tests/src/android/support/transition/BaseTransitionTest.java
index 81a9526..5d39d94 100644
--- a/transition/tests/src/android/support/transition/BaseTransitionTest.java
+++ b/transition/tests/src/android/support/transition/BaseTransitionTest.java
@@ -75,6 +75,20 @@
         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));
     }
diff --git a/transition/tests/src/android/support/transition/ChangeTransformTest.java b/transition/tests/src/android/support/transition/ChangeTransformTest.java
new file mode 100644
index 0000000..3e543aa
--- /dev/null
+++ b/transition/tests/src/android/support/transition/ChangeTransformTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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 android.support.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.support.transition.test.R;
+import android.view.View;
+
+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/transition/tests/src/android/support/transition/TransitionInflaterTest.java b/transition/tests/src/android/support/transition/TransitionInflaterTest.java
index bc53dc8..b87ee415 100644
--- a/transition/tests/src/android/support/transition/TransitionInflaterTest.java
+++ b/transition/tests/src/android/support/transition/TransitionInflaterTest.java
@@ -16,6 +16,8 @@
 
 package android.support.transition;
 
+import static junit.framework.TestCase.assertFalse;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
@@ -52,6 +54,7 @@
         // TODO: Add more Transition types
         verifyFadeProperties(inflater.inflateTransition(R.transition.fade));
         verifyExplodeProperties(inflater.inflateTransition(R.transition.explode));
+        verifyChangeTransformProperties(inflater.inflateTransition(R.transition.change_transform));
         verifyChangeClipBoundsProperties(
                 inflater.inflateTransition(R.transition.change_clip_bounds));
         verifyAutoTransitionProperties(inflater.inflateTransition(R.transition.auto_transition));
@@ -77,6 +80,13 @@
         assertEquals(Visibility.MODE_IN, visibility.getMode());
     }
 
+    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);
     }