feat: overhaul and streamline Android page navigation transitions (#7925)

* feat: remove Animators and replace with Transitions

* fix: handle disappearing nested fragments for tabs.

Extract TabFragmentImplementation in tab-navigation base for both tabs and bottom navigation

* chore: bump webpack cycles counter

* feat(android-widgets): add androidx.transition:transition as dependency

* chore: fix typescript errors

* fix(frame-android): child already has a parent. Replace removeView with removeAllViews

* fix(tests): wait for fragment before isAdded() check

* fix(bottom-navigation): prevent changeTab logic when fragment manager is destroyed

* chore: apply PR comments changes
This commit is contained in:
Alexander Djenkov
2019-10-15 18:19:47 +03:00
committed by GitHub
parent dc6540269f
commit 08e23bcc3b
20 changed files with 834 additions and 574 deletions

View File

@@ -0,0 +1,135 @@
package org.nativescript.widgets;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.transition.Transition;
import androidx.transition.TransitionListenerAdapter;
import androidx.transition.TransitionValues;
import androidx.transition.Visibility;
import java.util.ArrayList;
public class CustomTransition extends Visibility {
private boolean resetOnTransitionEnd;
private AnimatorSet animatorSet;
private AnimatorSet immediateAnimatorSet;
private String transitionName;
public CustomTransition(AnimatorSet animatorSet, String transitionName) {
this.animatorSet = animatorSet;
this.transitionName = transitionName;
}
@Nullable
@Override
public Animator onAppear(@NonNull ViewGroup sceneRoot, @NonNull final View view, @Nullable TransitionValues startValues,
@Nullable TransitionValues endValues) {
if (endValues == null || view == null || this.animatorSet == null) {
return null;
}
return this.setAnimatorsTarget(this.animatorSet, view);
}
@Override
public Animator onDisappear(@NonNull ViewGroup sceneRoot, @NonNull final View view, @Nullable TransitionValues startValues,
@Nullable TransitionValues endValues) {
if (startValues == null || view == null || this.animatorSet == null) {
return null;
}
return this.setAnimatorsTarget(this.animatorSet, view);
}
public void setResetOnTransitionEnd(boolean resetOnTransitionEnd) {
this.resetOnTransitionEnd = resetOnTransitionEnd;
}
public String getTransitionName(){
return this.transitionName;
}
private Animator setAnimatorsTarget(AnimatorSet animatorSet, final View view) {
ArrayList<Animator> animatorsList = animatorSet.getChildAnimations();
boolean resetOnTransitionEnd = this.resetOnTransitionEnd;
for (int i = 0; i < animatorsList.size(); i++) {
animatorsList.get(i).setTarget(view);
}
// Reset animation to its initial state to prevent mirrorered effect
if (this.resetOnTransitionEnd) {
this.immediateAnimatorSet = this.animatorSet.clone();
}
// Switching to hardware layer during transition to improve animation performance
CustomAnimatorListener listener = new CustomAnimatorListener(view);
animatorSet.addListener(listener);
this.addListener(new CustomTransitionListenerAdapter(this));
return this.animatorSet;
}
private class ReverseInterpolator implements Interpolator {
@Override
public float getInterpolation(float paramFloat) {
return Math.abs(paramFloat - 1f);
}
}
private class CustomTransitionListenerAdapter extends TransitionListenerAdapter {
private CustomTransition customTransition;
CustomTransitionListenerAdapter(CustomTransition transition) {
this.customTransition = transition;
}
@Override
public void onTransitionEnd(@NonNull Transition transition) {
if (this.customTransition.resetOnTransitionEnd) {
this.customTransition.immediateAnimatorSet.setDuration(0);
this.customTransition.immediateAnimatorSet.setInterpolator(new ReverseInterpolator());
this.customTransition.immediateAnimatorSet.start();
this.customTransition.setResetOnTransitionEnd(false);
}
this.customTransition.immediateAnimatorSet = null;
this.customTransition = null;
transition.removeListener(this);
}
}
private static class CustomAnimatorListener extends AnimatorListenerAdapter {
private final View mView;
private boolean mLayerTypeChanged = false;
CustomAnimatorListener(View view) {
mView = view;
}
@Override
public void onAnimationStart(Animator animation) {
if (ViewCompat.hasOverlappingRendering(mView)
&& mView.getLayerType() == View.LAYER_TYPE_NONE) {
mLayerTypeChanged = true;
mView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (mLayerTypeChanged) {
mView.setLayerType(View.LAYER_TYPE_NONE, null);
}
}
}
}

View File

@@ -4,27 +4,6 @@ import android.animation.Animator;
import androidx.fragment.app.Fragment;
public abstract class FragmentBase extends Fragment {
@Override
public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
// [nested frames / fragments] apply dummy animator to the nested fragment with
// the same duration as the exit animator of the removing parent fragment to work around
// https://code.google.com/p/android/issues/detail?id=55228 (child fragments disappear
// when parent fragment is removed as all children are first removed from parent)
if (!enter) {
Fragment removingParentFragment = this.getRemovingParentFragment();
if (removingParentFragment != null) {
Animator parentAnimator = removingParentFragment.onCreateAnimator(transit, enter, AnimatorHelper.exitFakeResourceId);
if (parentAnimator != null) {
long duration = AnimatorHelper.getTotalDuration(parentAnimator);
return AnimatorHelper.createDummyAnimator(duration);
}
}
}
return super.onCreateAnimator(transit, enter, nextAnim);
}
public Fragment getRemovingParentFragment() {
Fragment parentFragment = this.getParentFragment();
while (parentFragment != null && !parentFragment.isRemoving()) {
@@ -33,4 +12,4 @@ public abstract class FragmentBase extends Fragment {
return parentFragment;
}
}
}