Add animations to LeanbackPreferenceFragment

The prefscreen-to-prefscreen animations used to be handled by the StackedLayout,
but now we can do them with fragment transitions.

b/22179367

Change-Id: I2141cc4701016b6f4743b94c2242053abb67b07b
diff --git a/v17/preference-leanback/Android.mk b/v17/preference-leanback/Android.mk
index 05a7a54..c3bc803 100644
--- a/v17/preference-leanback/Android.mk
+++ b/v17/preference-leanback/Android.mk
@@ -30,10 +30,20 @@
         frameworks/support/v17/leanback/res \
         $(LOCAL_PATH)/res
 LOCAL_AAPT_FLAGS := \
-	--auto-add-overlay
+        --auto-add-overlay
 LOCAL_JAR_EXCLUDE_FILES := none
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
+# -----------------------------------------------------------------------
+
+#  A helper sub-library that makes direct use of API 21.
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-v17-preference-leanback-api21
+LOCAL_SDK_VERSION := 21
+LOCAL_SRC_FILES := $(call all-java-files-under, api21)
+LOCAL_JAVA_LIBRARIES := android-support-v17-preference-leanback-res
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
 # Here is the final static library that apps can link against.
 # The R class is automatically excluded from the generated library.
 # Applications that use this library must specify LOCAL_RESOURCE_DIR
@@ -42,7 +52,8 @@
 LOCAL_MODULE := android-support-v17-preference-leanback
 LOCAL_SDK_VERSION := 17
 LOCAL_SRC_FILES := $(call all-java-files-under,src)
-# LOCAL_STATIC_JAVA_LIBRARIES :=
+LOCAL_STATIC_JAVA_LIBRARIES := \
+        android-support-v17-preference-leanback-api21
 LOCAL_JAVA_LIBRARIES := \
         android-support-v4 \
         android-support-v7-appcompat \
diff --git a/v17/preference-leanback/api21/android/support/v17/preference/LeanbackPreferenceFragmentTransitionHelperApi21.java b/v17/preference-leanback/api21/android/support/v17/preference/LeanbackPreferenceFragmentTransitionHelperApi21.java
new file mode 100644
index 0000000..2002a54
--- /dev/null
+++ b/v17/preference-leanback/api21/android/support/v17/preference/LeanbackPreferenceFragmentTransitionHelperApi21.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2015 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.v17.preference;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.app.Fragment;
+import android.graphics.Path;
+import android.transition.Fade;
+import android.transition.Transition;
+import android.transition.TransitionValues;
+import android.transition.Visibility;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+
+/**
+ * @hide
+ */
+public class LeanbackPreferenceFragmentTransitionHelperApi21 {
+
+    public static void addTransitions(Fragment f) {
+        final Transition transitionStartEdge = new FadeAndShortSlideTransition(Gravity.START);
+        final Transition transitionEndEdge = new FadeAndShortSlideTransition(Gravity.END);
+
+        f.setEnterTransition(transitionEndEdge);
+        f.setExitTransition(transitionStartEdge);
+        f.setReenterTransition(transitionStartEdge);
+        f.setReturnTransition(transitionEndEdge);
+    }
+
+    private static class FadeAndShortSlideTransition extends Visibility {
+
+        private static final TimeInterpolator sDecelerate = new DecelerateInterpolator();
+//        private static final TimeInterpolator sAccelerate = new AccelerateInterpolator();
+        private static final String PROPNAME_SCREEN_POSITION =
+                "android:fadeAndShortSlideTransition:screenPosition";
+
+        private CalculateSlide mSlideCalculator = sCalculateEnd;
+        private Visibility mFade = new Fade();
+
+        private interface CalculateSlide {
+
+            /** Returns the translation value for view when it goes out of the scene */
+            float getGoneX(ViewGroup sceneRoot, View view);
+        }
+
+        private static final CalculateSlide sCalculateStart = new CalculateSlide() {
+            @Override
+            public float getGoneX(ViewGroup sceneRoot, View view) {
+                final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+                final float x;
+                if (isRtl) {
+                    x = view.getTranslationX() + sceneRoot.getWidth() / 4;
+                } else {
+                    x = view.getTranslationX() - sceneRoot.getWidth() / 4;
+                }
+                return x;
+            }
+        };
+
+        private static final CalculateSlide sCalculateEnd = new CalculateSlide() {
+            @Override
+            public float getGoneX(ViewGroup sceneRoot, View view) {
+                final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+                final float x;
+                if (isRtl) {
+                    x = view.getTranslationX() - sceneRoot.getWidth() / 4;
+                } else {
+                    x = view.getTranslationX() + sceneRoot.getWidth() / 4;
+                }
+                return x;
+            }
+        };
+
+        public FadeAndShortSlideTransition(int slideEdge) {
+            setSlideEdge(slideEdge);
+        }
+
+        @Override
+        public void setEpicenterCallback(EpicenterCallback epicenterCallback) {
+            super.setEpicenterCallback(epicenterCallback);
+            mFade.setEpicenterCallback(epicenterCallback);
+        }
+
+        private void captureValues(TransitionValues transitionValues) {
+            View view = transitionValues.view;
+            int[] position = new int[2];
+            view.getLocationOnScreen(position);
+            transitionValues.values.put(PROPNAME_SCREEN_POSITION, position[0]);
+        }
+
+        @Override
+        public void captureStartValues(TransitionValues transitionValues) {
+            super.captureStartValues(transitionValues);
+            mFade.captureStartValues(transitionValues);
+            captureValues(transitionValues);
+        }
+
+        @Override
+        public void captureEndValues(TransitionValues transitionValues) {
+            super.captureEndValues(transitionValues);
+            mFade.captureEndValues(transitionValues);
+            captureValues(transitionValues);
+        }
+
+        public void setSlideEdge(int slideEdge) {
+            switch (slideEdge) {
+                case Gravity.START:
+                    mSlideCalculator = sCalculateStart;
+                    break;
+                case Gravity.END:
+                    mSlideCalculator = sCalculateEnd;
+                    break;
+                default:
+                    throw new IllegalArgumentException("Invalid slide direction");
+            }
+//            SidePropagation propagation = new SidePropagation();
+//            propagation.setSide(slideEdge);
+//            setPropagation(propagation);
+        }
+
+        @Override
+        public Animator onAppear(ViewGroup sceneRoot, View view,
+                TransitionValues startValues, TransitionValues endValues) {
+            if (endValues == null) {
+                return null;
+            }
+            Integer position = (Integer) endValues.values.get(PROPNAME_SCREEN_POSITION);
+            float endX = view.getTranslationX();
+            float startX = mSlideCalculator.getGoneX(sceneRoot, view);
+            final Animator slideAnimator = TranslationAnimationCreator
+                    .createAnimation(view, endValues, position,
+                            startX, endX, sDecelerate, this);
+            final AnimatorSet set = new AnimatorSet();
+            set.play(slideAnimator)
+                    .with(mFade.onAppear(sceneRoot, view, startValues, endValues));
+
+            return set;
+        }
+
+        @Override
+        public Animator onDisappear(ViewGroup sceneRoot, View view,
+                TransitionValues startValues, TransitionValues endValues) {
+            if (startValues == null) {
+                return null;
+            }
+            Integer position = (Integer) startValues.values.get(PROPNAME_SCREEN_POSITION);
+            float startX = view.getTranslationX();
+            float endX = mSlideCalculator.getGoneX(sceneRoot, view);
+            final Animator slideAnimator = TranslationAnimationCreator
+                    .createAnimation(view, startValues, position,
+                            startX, endX, sDecelerate /*sAccelerate*/, this);
+            final AnimatorSet set = new AnimatorSet();
+            set.play(slideAnimator)
+                    .with(mFade.onDisappear(sceneRoot, view, startValues, endValues));
+
+            return set;
+        }
+
+        @Override
+        public Transition addListener(TransitionListener listener) {
+            mFade.addListener(listener);
+            return super.addListener(listener);
+        }
+
+        @Override
+        public Transition removeListener(TransitionListener listener) {
+            mFade.removeListener(listener);
+            return super.removeListener(listener);
+        }
+
+        @Override
+        public Transition clone() {
+            FadeAndShortSlideTransition clone = null;
+            clone = (FadeAndShortSlideTransition) super.clone();
+            clone.mFade = (Visibility) mFade.clone();
+            return clone;
+        }
+    }
+
+    /**
+     * 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.
+     */
+    private static 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 startX The start translation x of view
+         * @param endX The end translation x 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,
+                float startX, float endX, TimeInterpolator interpolator,
+                Transition transition) {
+            float terminalX = view.getTranslationX();
+            Integer startPosition = (Integer) values.view.getTag(R.id.transitionPosition);
+            if (startPosition != null) {
+                startX = startPosition - viewPosX + terminalX;
+            }
+            // Initial position is at translation startX, startY, so position is offset by that
+            // amount
+            int startPosX = viewPosX + Math.round(startX - terminalX);
+
+            view.setTranslationX(startX);
+            if (startX == endX) {
+                return null;
+            }
+            Path path = new Path();
+            path.moveTo(startX, 0);
+            path.lineTo(endX, 0);
+            ObjectAnimator anim =
+                    ObjectAnimator.ofFloat(view, View.TRANSLATION_X, View.TRANSLATION_Y, path);
+
+            TransitionPositionListener listener = new TransitionPositionListener(view, values.view,
+                    startPosX, terminalX);
+            transition.addListener(listener);
+            anim.addListener(listener);
+            anim.addPauseListener(listener);
+            anim.setInterpolator(interpolator);
+            return anim;
+        }
+
+        private static class TransitionPositionListener extends AnimatorListenerAdapter implements
+                Transition.TransitionListener {
+
+            private final View mViewInHierarchy;
+            private final View mMovingView;
+            private final int mStartX;
+            private Integer mTransitionPosition;
+            private float mPausedX;
+            private final float mTerminalX;
+
+            private TransitionPositionListener(View movingView, View viewInHierarchy,
+                    int startX, float terminalX) {
+                mMovingView = movingView;
+                mViewInHierarchy = viewInHierarchy;
+                mStartX = startX - Math.round(mMovingView.getTranslationX());
+                mTerminalX = terminalX;
+                mTransitionPosition = (Integer) mViewInHierarchy.getTag(R.id.transitionPosition);
+                if (mTransitionPosition != null) {
+                    mViewInHierarchy.setTag(R.id.transitionPosition, null);
+                }
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mTransitionPosition = Math.round(mStartX + mMovingView.getTranslationX());
+                mViewInHierarchy.setTag(R.id.transitionPosition, mTransitionPosition);
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animator) {
+            }
+
+            @Override
+            public void onAnimationPause(Animator animator) {
+                mPausedX = mMovingView.getTranslationX();
+                mMovingView.setTranslationX(mTerminalX);
+            }
+
+            @Override
+            public void onAnimationResume(Animator animator) {
+                mMovingView.setTranslationX(mPausedX);
+            }
+
+            @Override
+            public void onTransitionStart(Transition transition) {
+            }
+
+            @Override
+            public void onTransitionEnd(Transition transition) {
+                mMovingView.setTranslationX(mTerminalX);
+            }
+
+            @Override
+            public void onTransitionCancel(Transition transition) {
+            }
+
+            @Override
+            public void onTransitionPause(Transition transition) {
+            }
+
+            @Override
+            public void onTransitionResume(Transition transition) {
+            }
+        }
+
+    }
+
+}
diff --git a/v17/preference-leanback/res/layout/leanback_preference_fragment.xml b/v17/preference-leanback/res/layout/leanback_preference_fragment.xml
index 3279a42..d119c2d 100644
--- a/v17/preference-leanback/res/layout/leanback_preference_fragment.xml
+++ b/v17/preference-leanback/res/layout/leanback_preference_fragment.xml
@@ -21,20 +21,29 @@
     android:layout_height="match_parent"
     android:background="@color/lb_preference_decor_list_background"
     android:orientation="vertical"
+    android:transitionGroup="false"
     >
 
-    <TextView android:id="@+id/decor_title"
+    <FrameLayout
+        android:id="@+id/decor_title_container"
         android:layout_width="match_parent"
-        android:layout_height="@dimen/lb_preference_decor_title_text_height"
+        android:layout_height="wrap_content"
         android:background="?attr/defaultBrandColor"
-        android:fontFamily="sans-serif-condensed"
-        android:gravity="center_vertical"
-        android:paddingTop="@dimen/lb_preference_decor_title_padding_top"
-        android:paddingStart="@dimen/lb_preference_decor_title_padding_start"
-        android:paddingEnd="@dimen/lb_preference_decor_title_padding_end"
-        android:singleLine="true"
-        android:textSize="@dimen/lb_preference_decor_title_text_size"
-        android:textColor="?android:attr/textColorPrimary"
-        />
+        android:transitionGroup="false"
+        >
+        <TextView
+            android:id="@+id/decor_title"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/lb_preference_decor_title_text_height"
+            android:fontFamily="sans-serif-condensed"
+            android:gravity="center_vertical"
+            android:paddingTop="@dimen/lb_preference_decor_title_padding_top"
+            android:paddingStart="@dimen/lb_preference_decor_title_padding_start"
+            android:paddingEnd="@dimen/lb_preference_decor_title_padding_end"
+            android:singleLine="true"
+            android:textSize="@dimen/lb_preference_decor_title_text_size"
+            android:textColor="?android:attr/textColorPrimary"
+            />
+    </FrameLayout>
 
 </LinearLayout>
diff --git a/v17/preference-leanback/res/values/ids.xml b/v17/preference-leanback/res/values/ids.xml
new file mode 100644
index 0000000..20c1eda
--- /dev/null
+++ b/v17/preference-leanback/res/values/ids.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2015 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
+  -->
+
+<resources>
+    <item name="transitionPosition" type="id" />
+</resources>
diff --git a/v17/preference-leanback/src/android/support/v17/preference/LeanbackPreferenceFragment.java b/v17/preference-leanback/src/android/support/v17/preference/LeanbackPreferenceFragment.java
index 73ce174..9d744ec 100644
--- a/v17/preference-leanback/src/android/support/v17/preference/LeanbackPreferenceFragment.java
+++ b/v17/preference-leanback/src/android/support/v17/preference/LeanbackPreferenceFragment.java
@@ -16,6 +16,7 @@
 
 package android.support.v17.preference;
 
+import android.os.Build;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -28,6 +29,12 @@
  */
 public abstract class LeanbackPreferenceFragment extends BaseLeanbackPreferenceFragment {
 
+    public LeanbackPreferenceFragment() {
+        if (Build.VERSION.SDK_INT >= 21) {
+            LeanbackPreferenceFragmentTransitionHelperApi21.addTransitions(this);
+        }
+    }
+
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {