/* * 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 com.android.tv.common.ui.setup.animation; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.TimeInterpolator; 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.ViewParent; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; /** * Execute horizontal slide of 1/4 width and fade (to workaround bug 23718734) */ public class FadeAndShortSlide extends Visibility { private static final TimeInterpolator APPEAR_INTERPOLATOR = new DecelerateInterpolator(); private static final TimeInterpolator DISAPPEAR_INTERPOLATOR = new AccelerateInterpolator(); private static final String PROPNAME_SCREEN_POSITION = "android_fadeAndShortSlideTransition_screenPosition"; private static final String PROPNAME_DELAY = "propname_delay"; private static final int DEFAULT_DISTANCE = 200; private static abstract class CalculateSlide { /** Returns the translation value for view when it goes out of the scene */ public abstract float getGoneX(ViewGroup sceneRoot, View view, int[] position, int distance); } private static final CalculateSlide sCalculateStart = new CalculateSlide() { @Override public float getGoneX(ViewGroup sceneRoot, View view, int[] position, int distance) { final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; final float x; if (isRtl) { x = view.getTranslationX() + distance; } else { x = view.getTranslationX() - distance; } return x; } }; private static final CalculateSlide sCalculateEnd = new CalculateSlide() { @Override public float getGoneX(ViewGroup sceneRoot, View view, int[] position, int distance) { final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; final float x; if (isRtl) { x = view.getTranslationX() - distance; } else { x = view.getTranslationX() + distance; } return x; } }; private static final ViewPositionComparator sViewPositionComparator = new ViewPositionComparator(); private int mSlideEdge; private CalculateSlide mSlideCalculator = sCalculateEnd; private Visibility mFade = new Fade(); // TODO: Consider using TransitionPropagation. private int[] mParentIdsForDelay; private int mDistance = DEFAULT_DISTANCE; public FadeAndShortSlide() { this(Gravity.START); } public FadeAndShortSlide(int slideEdge) { this(slideEdge, null); } public FadeAndShortSlide(int slideEdge, int[] parentIdsForDelay) { setSlideEdge(slideEdge); mParentIdsForDelay = parentIdsForDelay; } @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); } private int getDelayOrder(View view, boolean appear) { if (mParentIdsForDelay == null) { return -1; } final View parentForDelay = findParentForDelay(view); if (parentForDelay == null || !(parentForDelay instanceof ViewGroup)) { return -1; } List transitionTargets = new ArrayList<>(); getTransitionTargets((ViewGroup) parentForDelay, transitionTargets); sViewPositionComparator.mParentForDelay = parentForDelay; sViewPositionComparator.mIsLtr = view.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; sViewPositionComparator.mToLeft = sViewPositionComparator.mIsLtr ? mSlideEdge == (appear ? Gravity.END : Gravity.START) : mSlideEdge == (appear ? Gravity.START : Gravity.END); Collections.sort(transitionTargets, sViewPositionComparator); return transitionTargets.indexOf(view); } private View findParentForDelay(View view) { if (isParentForDelay(view.getId())) { return view; } View parent = view; while (parent.getParent() instanceof View) { parent = (View) parent.getParent(); if (isParentForDelay(parent.getId())) { return parent; } } return null; } private boolean isParentForDelay(int viewId) { for (int id : mParentIdsForDelay) { if (id == viewId) { return true; } } return false; } private void getTransitionTargets(ViewGroup parent, List transitionTargets) { int count = parent.getChildCount(); for (int i = 0; i < count; ++i) { View child = parent.getChildAt(i); if (child instanceof ViewGroup && !((ViewGroup) child).isTransitionGroup()) { getTransitionTargets((ViewGroup) child, transitionTargets); } else { transitionTargets.add(child); } } } @Override public void captureStartValues(TransitionValues transitionValues) { super.captureStartValues(transitionValues); mFade.captureStartValues(transitionValues); captureValues(transitionValues); int delayIndex = getDelayOrder(transitionValues.view, false); if (delayIndex > 0) { transitionValues.values.put(PROPNAME_DELAY, delayIndex * SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS); } } @Override public void captureEndValues(TransitionValues transitionValues) { super.captureEndValues(transitionValues); mFade.captureEndValues(transitionValues); captureValues(transitionValues); int delayIndex = getDelayOrder(transitionValues.view, true); if (delayIndex > 0) { transitionValues.values.put(PROPNAME_DELAY, delayIndex * SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS); } } public void setSlideEdge(int slideEdge) { mSlideEdge = slideEdge; switch (slideEdge) { case Gravity.START: mSlideCalculator = sCalculateStart; break; case Gravity.END: mSlideCalculator = sCalculateEnd; break; default: throw new IllegalArgumentException("Invalid slide direction"); } } @Override public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) { if (endValues == null) { return null; } int[] position = (int[]) endValues.values.get(PROPNAME_SCREEN_POSITION); int left = position[0]; float endX = view.getTranslationX(); float startX = mSlideCalculator.getGoneX(sceneRoot, view, position, mDistance); final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view, endValues, left, startX, endX, APPEAR_INTERPOLATOR, this); if (slideAnimator == null) { return null; } mFade.setInterpolator(APPEAR_INTERPOLATOR); final AnimatorSet set = new AnimatorSet(); set.play(slideAnimator).with(mFade.onAppear(sceneRoot, view, startValues, endValues)); Long delay = (Long ) endValues.values.get(PROPNAME_DELAY); if (delay != null) { set.setStartDelay(delay); } return set; } @Override public Animator onDisappear(ViewGroup sceneRoot, final View view, TransitionValues startValues, TransitionValues endValues) { if (startValues == null) { return null; } int[] position = (int[]) startValues.values.get(PROPNAME_SCREEN_POSITION); int left = position[0]; float startX = view.getTranslationX(); float endX = mSlideCalculator.getGoneX(sceneRoot, view, position, mDistance); final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view, startValues, left, startX, endX, DISAPPEAR_INTERPOLATOR, this); if (slideAnimator == null) { // slideAnimator is null if startX == endX return null; } mFade.setInterpolator(DISAPPEAR_INTERPOLATOR); final Animator fadeAnimator = mFade.onDisappear(sceneRoot, view, startValues, endValues); if (fadeAnimator == null) { return null; } fadeAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animator) { fadeAnimator.removeListener(this); view.setAlpha(0.0f); } }); final AnimatorSet set = new AnimatorSet(); set.play(slideAnimator).with(fadeAnimator); Long delay = (Long) startValues.values.get(PROPNAME_DELAY); if (delay != null) { set.setStartDelay(delay); } 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() { FadeAndShortSlide clone = (FadeAndShortSlide) super.clone(); clone.mFade = (Visibility) mFade.clone(); return clone; } @Override public Transition setDuration(long duration) { long scaledDuration = SetupAnimationHelper.applyAnimationTimeScale(duration); mFade.setDuration(scaledDuration); return super.setDuration(scaledDuration); } /** * Sets the moving distance in pixel. */ public void setDistance(int distance) { mDistance = distance; } private static class ViewPositionComparator implements Comparator { View mParentForDelay; boolean mIsLtr; boolean mToLeft; @Override public int compare(View lhs, View rhs) { int start1; int start2; if (mIsLtr) { start1 = getRelativeLeft(lhs, mParentForDelay); start2 = getRelativeLeft(rhs, mParentForDelay); } else { start1 = getRelativeRight(lhs, mParentForDelay); start2 = getRelativeRight(rhs, mParentForDelay); } if (mToLeft) { if (start1 > start2) { return 1; } else if (start1 < start2) { return -1; } } else { if (start1 > start2) { return -1; } else if (start1 < start2) { return 1; } } int top1 = getRelativeTop(lhs, mParentForDelay); int top2 = getRelativeTop(rhs, mParentForDelay); return Integer.compare(top1, top2); } private int getRelativeLeft(View child, View ancestor) { ViewParent parent = child.getParent(); int left = child.getLeft(); while (parent instanceof View) { if (parent == ancestor) { break; } left += ((View) parent).getLeft(); parent = parent.getParent(); } return left; } private int getRelativeRight(View child, View ancestor) { ViewParent parent = child.getParent(); int right = child.getRight(); while (parent instanceof View) { if (parent == ancestor) { break; } right += ((View) parent).getLeft(); parent = parent.getParent(); } return right; } private int getRelativeTop(View child, View ancestor) { ViewParent parent = child.getParent(); int top = child.getTop(); while (parent instanceof View) { if (parent == ancestor) { break; } top += ((View) parent).getTop(); parent = parent.getParent(); } return top; } } }