Skip to content

feat: overhaul and streamline Android page navigation transitions #7925

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Oct 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/app/ui/tab-view/tab-view-root-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function waitUntilTabViewReady(page: Page, action: Function) {
action();

if (isAndroid) {
TKUnit.waitUntilReady(() => page.frame._currentEntry.fragment.isAdded());
TKUnit.waitUntilReady(() => page.frame._currentEntry.fragment && page.frame._currentEntry.fragment.isAdded());
} else {
TKUnit.waitUntilReady(() => page.isLoaded);
}
Expand Down
2 changes: 1 addition & 1 deletion tests/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const { NativeScriptWorkerPlugin } = require("nativescript-worker-loader/NativeS
const TerserPlugin = require("terser-webpack-plugin");
const hashSalt = Date.now().toString();

const ANDROID_MAX_CYCLES = 68;
const ANDROID_MAX_CYCLES = 66;
const IOS_MAX_CYCLES = 39;
let numCyclesDetected = 0;

Expand Down
1 change: 1 addition & 0 deletions tns-core-modules-widgets/android/widgets/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ dependencies {
def androidxVersion = computeAndroidXVersion()
implementation 'androidx.viewpager:viewpager:' + androidxVersion
implementation 'androidx.fragment:fragment:' + androidxVersion
implementation 'androidx.transition:transition:' + androidxVersion
} else {
println 'Using support library'
implementation 'com.android.support:support-v4:' + computeSupportVersion()
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,6 @@
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()) {
Expand All @@ -33,4 +12,4 @@ public Fragment getRemovingParentFragment() {

return parentFragment;
}
}
}
69 changes: 64 additions & 5 deletions tns-core-modules/ui/bottom-navigation/bottom-navigation.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const ownerSymbol = Symbol("_owner");
let TabFragment: any;
let BottomNavigationBar: any;
let AttachStateChangeListener: any;
let appResources: android.content.res.Resources;

function makeFragmentName(viewId: number, id: number): string {
return "android:bottomnavigation:" + viewId + ":" + id;
Expand All @@ -55,8 +56,9 @@ function initializeNativeClasses() {
}

class TabFragmentImplementation extends org.nativescript.widgets.FragmentBase {
private tab: BottomNavigation;
private owner: BottomNavigation;
private index: number;
private backgroundBitmap: android.graphics.Bitmap = null;

constructor() {
super();
Expand All @@ -77,18 +79,61 @@ function initializeNativeClasses() {
public onCreate(savedInstanceState: android.os.Bundle): void {
super.onCreate(savedInstanceState);
const args = this.getArguments();
this.tab = getTabById(args.getInt(TABID));
this.owner = getTabById(args.getInt(TABID));
this.index = args.getInt(INDEX);
if (!this.tab) {
if (!this.owner) {
throw new Error(`Cannot find BottomNavigation`);
}
}

public onCreateView(inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle): android.view.View {
const tabItem = this.tab.items[this.index];
const tabItem = this.owner.items[this.index];

return tabItem.nativeViewProtected;
}

public onDestroyView() {
const hasRemovingParent = this.getRemovingParentFragment();

// Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments.
// TODO: Consider removing it when update to androidx.fragment:1.2.0
if (hasRemovingParent && this.owner.selectedIndex === this.index) {
const bitmapDrawable = new android.graphics.drawable.BitmapDrawable(appResources, this.backgroundBitmap);
this.owner._originalBackground = this.owner.backgroundColor || new Color("White");
this.owner.nativeViewProtected.setBackgroundDrawable(bitmapDrawable);
this.backgroundBitmap = null;
}

super.onDestroyView();
}

public onPause(): void {
const hasRemovingParent = this.getRemovingParentFragment();

// Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments.
// TODO: Consider removing it when update to androidx.fragment:1.2.0
if (hasRemovingParent && this.owner.selectedIndex === this.index) {
this.backgroundBitmap = this.loadBitmapFromView(this.owner.nativeViewProtected);
}

super.onPause();
}

private loadBitmapFromView(view: android.view.View): android.graphics.Bitmap {
// Another way to get view bitmap. Test performance vs setDrawingCacheEnabled
// const width = view.getWidth();
// const height = view.getHeight();
// const bitmap = android.graphics.Bitmap.createBitmap(width, height, android.graphics.Bitmap.Config.ARGB_8888);
// const canvas = new android.graphics.Canvas(bitmap);
// view.layout(0, 0, width, height);
// view.draw(canvas);

view.setDrawingCacheEnabled(true);
const bitmap = android.graphics.Bitmap.createBitmap(view.getDrawingCache());
view.setDrawingCacheEnabled(false);

return bitmap;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this return null? Would new android.graphics.drawable.BitmapDrawable(appResources, null) throw an exception?

}
}

class BottomNavigationBarImplementation extends org.nativescript.widgets.BottomNavigationBar {
Expand Down Expand Up @@ -168,6 +213,7 @@ function initializeNativeClasses() {
TabFragment = TabFragmentImplementation;
BottomNavigationBar = BottomNavigationBarImplementation;
AttachStateChangeListener = new AttachListener();
appResources = application.android.context.getResources();
}

function setElevation(bottomNavigationBar: org.nativescript.widgets.BottomNavigationBar) {
Expand Down Expand Up @@ -196,6 +242,7 @@ export class BottomNavigation extends TabNavigationBase {
private _currentFragment: androidx.fragment.app.Fragment;
private _currentTransaction: androidx.fragment.app.FragmentTransaction;
private _attachedToWindow = false;
public _originalBackground: any;

constructor() {
super();
Expand Down Expand Up @@ -320,6 +367,12 @@ export class BottomNavigation extends TabNavigationBase {
public onLoaded(): void {
super.onLoaded();

if (this._originalBackground) {
this.backgroundColor = null;
this.backgroundColor = this._originalBackground;
this._originalBackground = null;
}

if (this.tabStrip) {
this.setTabStripItems(this.tabStrip.items);
} else {
Expand All @@ -334,8 +387,14 @@ export class BottomNavigation extends TabNavigationBase {

_onAttachedToWindow(): void {
super._onAttachedToWindow();

this._attachedToWindow = true;

// _onAttachedToWindow called from OS again after it was detach
// TODO: Consider testing and removing it when update to androidx.fragment:1.2.0
if (this._manager && this._manager.isDestroyed()) {
return;
}

this.changeTab(this.selectedIndex);
}

Expand Down
2 changes: 1 addition & 1 deletion tns-core-modules/ui/core/view/view-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
public static showingModallyEvent = "showingModally";

protected _closeModalCallback: Function;

public _manager: any;
public _modalParent: ViewCommon;
private _modalContext: any;
private _modal: ViewCommon;
Expand Down
2 changes: 1 addition & 1 deletion tns-core-modules/ui/core/view/view.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,12 +272,12 @@ export class View extends ViewCommon {
public static androidBackPressedEvent = androidBackPressedEvent;

public _dialogFragment: androidx.fragment.app.DialogFragment;
public _manager: androidx.fragment.app.FragmentManager;
private _isClickable: boolean;
private touchListenerIsSet: boolean;
private touchListener: android.view.View.OnTouchListener;
private layoutChangeListenerIsSet: boolean;
private layoutChangeListener: android.view.View.OnLayoutChangeListener;
private _manager: androidx.fragment.app.FragmentManager;
private _rootManager: androidx.fragment.app.FragmentManager;

nativeViewProtected: android.view.View;
Expand Down
5 changes: 5 additions & 0 deletions tns-core-modules/ui/core/view/view.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,11 @@ export abstract class View extends ViewBase {
* @private
*/
_gestureObservers: any;
/**
* @private
* androidx.fragment.app.FragmentManager
*/
_manager: any;
/**
* @private
*/
Expand Down
4 changes: 4 additions & 0 deletions tns-core-modules/ui/frame/fragment.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ class FragmentClass extends org.nativescript.widgets.FragmentBase {
this._callbacks.onStop(this, super.onStop);
}

public onPause(): void {
this._callbacks.onPause(this, super.onStop);
}

public onCreate(savedInstanceState: android.os.Bundle) {
if (!this._callbacks) {
setFragmentCallbacks(this);
Expand Down
Loading