blob: 5c57d84d7305756b6aba73f4d44c2e6d74303f78 [file] [log] [blame]
/*
* 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<View> 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<View> 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> {
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;
}
}
}