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) {