From b8676157304280e5d06bee9776d6901447fb6f0d Mon Sep 17 00:00:00 2001 From: Guy Carmeli Date: Sun, 17 Dec 2017 11:31:14 +0200 Subject: [PATCH 1/2] Further work on TopTabs and some refactoring * OptionsPresenter receives views as params * Remove TopBar and style logic from ViewControllers, these are the OptionsPresenters concern * StackAnimator isn't coupled to LinearLayout * Convert Runnable to lambda * ContainerLayout now implements ReactContainer interface * Enable desugaring in playground app --- lib/android/app/build.gradle | 4 ++ .../anim/StackAnimator.java | 40 +++++------ .../parse/LayoutFactory.java | 22 +++++-- .../parse/LayoutNode.java | 10 +-- .../presentation/OptionsPresenter.java | 46 +++++++------ .../com/reactnativenavigation/utils/Task.java | 5 ++ .../ContainerViewController.java | 63 +++++------------- .../viewcontrollers/Navigator.java | 41 ++++-------- .../viewcontrollers/StackController.java | 2 +- .../viewcontrollers/ViewController.java | 42 +++++++++--- .../toptabs/TopTabController.java | 66 +++++++++++++++++++ .../toptabs/TopTabsAdapter.java | 21 +++++- .../toptabs/TopTabsController.java | 25 +++++-- .../toptabs/TopTabsViewPager.java | 41 ++++-------- .../views/Container.java | 9 ++- .../views/ContainerLayout.java | 29 ++++---- .../views/ReactContainer.java | 6 ++ .../reactnativenavigation/views/TopTab.java | 14 ++-- .../views/TopTabsLayout.java | 18 ++++- .../mocks/TestContainerLayout.java | 25 ++++--- .../viewcontrollers/OptionsApplyingTest.java | 14 ++-- lib/src/commands/LayoutTreeParser.js | 6 +- lib/src/commands/LayoutTypes.js | 2 +- playground/android/app/build.gradle | 5 ++ .../src/containers/TopTabOptionsScreen.js | 52 +++++++++++++++ playground/src/containers/TopTabScreen.js | 20 ++++++ playground/src/containers/WelcomeScreen.js | 5 +- playground/src/containers/index.js | 2 + 28 files changed, 411 insertions(+), 224 deletions(-) create mode 100644 lib/android/app/src/main/java/com/reactnativenavigation/utils/Task.java create mode 100644 lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabController.java create mode 100644 lib/android/app/src/main/java/com/reactnativenavigation/views/ReactContainer.java create mode 100644 playground/src/containers/TopTabOptionsScreen.js diff --git a/lib/android/app/build.gradle b/lib/android/app/build.gradle index d26508c4aa4..4e203fcb717 100644 --- a/lib/android/app/build.gradle +++ b/lib/android/app/build.gradle @@ -42,6 +42,10 @@ android { } } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } allprojects { p -> diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/anim/StackAnimator.java b/lib/android/app/src/main/java/com/reactnativenavigation/anim/StackAnimator.java index 752cafaae5e..2b13b11eecf 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/anim/StackAnimator.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/anim/StackAnimator.java @@ -8,10 +8,10 @@ import android.support.annotation.Nullable; import android.util.DisplayMetrics; import android.view.View; +import android.view.ViewGroup; import android.view.WindowManager; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; -import android.widget.LinearLayout; import com.reactnativenavigation.views.TopBar; @@ -116,15 +116,12 @@ public void animateShowTopBar(final TopBar topBar, final View container) { ValueAnimator containerHeightAnim = ValueAnimator.ofInt(container.getMeasuredHeight(), container.getMeasuredHeight() - topBar.getMeasuredHeight()); containerHeightAnim.setInterpolator(DECELERATE_INTERPOLATOR); containerHeightAnim.setDuration(DURATION_TOPBAR); - containerHeightAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator valueAnimator) { - int val = (Integer) valueAnimator.getAnimatedValue(); - LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) container.getLayoutParams(); - layoutParams.height = val; - container.setLayoutParams(layoutParams); - } - }); + containerHeightAnim.addUpdateListener(valueAnimator -> { + int val = (Integer) valueAnimator.getAnimatedValue(); + ViewGroup.LayoutParams layoutParams = container.getLayoutParams(); + layoutParams.height = val; + container.setLayoutParams(layoutParams); + }); ObjectAnimator containerTransitionAnim = ObjectAnimator.ofFloat(container, View.TRANSLATION_Y, -1 * topBar.getMeasuredHeight(), 0); containerTransitionAnim.setInterpolator(DECELERATE_INTERPOLATOR); containerTransitionAnim.setDuration(DURATION_TOPBAR); @@ -142,8 +139,8 @@ public void onAnimationStart(Animator animation) { @Override public void onAnimationEnd(Animator animation) { - LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) container.getLayoutParams(); - layoutParams.height = LinearLayout.LayoutParams.MATCH_PARENT; + ViewGroup.LayoutParams layoutParams = container.getLayoutParams(); + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; container.setLayoutParams(layoutParams); } @@ -165,15 +162,12 @@ public void animateHideTopBar(final TopBar topBar, final View container) { ValueAnimator containerHeightAnim = ValueAnimator.ofInt(container.getMeasuredHeight(), container.getMeasuredHeight() + topBar.getMeasuredHeight()); containerHeightAnim.setInterpolator(ACCELERATE_INTERPOLATOR); containerHeightAnim.setDuration(DURATION_TOPBAR); - containerHeightAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator valueAnimator) { - int val = (Integer) valueAnimator.getAnimatedValue(); - LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) container.getLayoutParams(); - layoutParams.height = val; - container.setLayoutParams(layoutParams); - } - }); + containerHeightAnim.addUpdateListener(valueAnimator -> { + int val = (Integer) valueAnimator.getAnimatedValue(); + ViewGroup.LayoutParams layoutParams = container.getLayoutParams(); + layoutParams.height = val; + container.setLayoutParams(layoutParams); + }); ObjectAnimator containerTransitionAnim = ObjectAnimator.ofFloat(container, View.TRANSLATION_Y, 0, -1 * topBar.getMeasuredHeight()); containerTransitionAnim.setInterpolator(ACCELERATE_INTERPOLATOR); containerTransitionAnim.setDuration(DURATION_TOPBAR); @@ -190,8 +184,8 @@ public void onAnimationStart(Animator animation) { @Override public void onAnimationEnd(Animator animation) { - LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) container.getLayoutParams(); - layoutParams.height = LinearLayout.LayoutParams.MATCH_PARENT; + ViewGroup.LayoutParams layoutParams = container.getLayoutParams(); + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; container.setLayoutParams(layoutParams); container.setTranslationY(0); diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java b/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java index 78e48e162dd..0d938b4e5a8 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java @@ -10,9 +10,9 @@ import com.reactnativenavigation.viewcontrollers.StackController; import com.reactnativenavigation.viewcontrollers.ViewController; import com.reactnativenavigation.viewcontrollers.overlay.DialogViewController; +import com.reactnativenavigation.viewcontrollers.toptabs.TopTabController; import com.reactnativenavigation.viewcontrollers.toptabs.TopTabsController; import com.reactnativenavigation.views.ContainerViewCreator; -import com.reactnativenavigation.views.TopTabCreator; import java.util.ArrayList; import java.util.List; @@ -47,10 +47,10 @@ public ViewController create(final LayoutNode node) { return createSideMenuRight(node); case CustomDialog: return createDialogContainer(node); - case TopTabsContainer: - return createTopTabsContainer(node); + case TopTabs: + return createTopTabs(node); case TopTab: - return createContainer(node, new TopTabCreator(reactInstanceManager)); + return createTopTab(node); default: throw new IllegalArgumentException("Invalid node type: " + node.type); } @@ -126,11 +126,23 @@ private ViewController createDialogContainer(LayoutNode node) { return new DialogViewController(activity, id, name, creator); } - private ViewController createTopTabsContainer(LayoutNode node) { + private ViewController createTopTabs(LayoutNode node) { final List tabs = new ArrayList<>(); for (LayoutNode child : node.children) { tabs.add(create(child)); } return new TopTabsController(activity, node.id, tabs); } + + private ViewController createTopTab(LayoutNode node) { + String id = node.id; + String name = node.data.optString("name"); + NavigationOptions navigationOptions = NavigationOptions.parse(node.data.optJSONObject("navigationOptions"), defaultOptions); + return new TopTabController(activity, + id, + name, + reactInstanceManager, + navigationOptions + ); + } } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutNode.java b/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutNode.java index 2283361c762..8ffabd77a96 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutNode.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutNode.java @@ -15,7 +15,7 @@ public enum Type { SideMenuLeft, SideMenuRight, CustomDialog, - TopTabsContainer, + TopTabs, TopTab } @@ -23,13 +23,13 @@ public enum Type { public final Type type; public final JSONObject data; - public final List children; + final List children; - public LayoutNode(String id, Type type) { - this(id, type, new JSONObject(), new ArrayList()); + LayoutNode(String id, Type type) { + this(id, type, new JSONObject(), new ArrayList<>()); } - public LayoutNode(String id, Type type, JSONObject data, List children) { + LayoutNode(String id, Type type, JSONObject data, List children) { this.id = id; this.type = type; this.data = data; diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/presentation/OptionsPresenter.java b/lib/android/app/src/main/java/com/reactnativenavigation/presentation/OptionsPresenter.java index 1d61b6ad180..e116e9f1c24 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/presentation/OptionsPresenter.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/presentation/OptionsPresenter.java @@ -7,33 +7,33 @@ import com.reactnativenavigation.parse.TopBarOptions; import com.reactnativenavigation.parse.TopTabsOptions; import com.reactnativenavigation.utils.TypefaceLoader; -import com.reactnativenavigation.viewcontrollers.ContainerViewController; -import com.reactnativenavigation.views.ContainerLayout; +import com.reactnativenavigation.views.TopBar; public class OptionsPresenter { - private ContainerViewController controller; private final StackAnimator animator; + private View contentView; + private TopBar topBar; - public OptionsPresenter(ContainerViewController controller) { - this.controller = controller; - animator = new StackAnimator(controller.getActivity()); - } + public OptionsPresenter(TopBar topBar, View contentView) { + this.topBar = topBar; + this.contentView = contentView; + animator = new StackAnimator(topBar.getContext()); + } public void applyOptions(NavigationOptions options) { - if (controller != null && controller.getTopBar() != null) { - applyTopBarOptions(options.topBarOptions); - applyTopTabsOptions(options.topTabsOptions); - } + applyTopBarOptions(options.topBarOptions); + applyTopTabsOptions(options.topTabsOptions); } private void applyTopBarOptions(TopBarOptions options) { - controller.setTitle(options.title); - controller.setBackgroundColor(options.backgroundColor); - controller.setTitleTextColor(options.textColor); - controller.setTitleFontSize(options.textFontSize); + topBar.setTitle(options.title); + topBar.setBackgroundColor(options.backgroundColor); + topBar.setTitleTextColor(options.textColor); + topBar.setTitleFontSize(options.textFontSize); + TypefaceLoader typefaceLoader = new TypefaceLoader(); - controller.getTopBar().setTitleTypeface(typefaceLoader.getTypeFace(controller.getActivity(), options.textFontFamily)); + topBar.setTitleTypeface(typefaceLoader.getTypeFace(topBar.getContext(), options.textFontFamily)); if (options.hidden == NavigationOptions.BooleanOptions.True) { hideTopBar(options.animateHide); } @@ -43,26 +43,24 @@ private void applyTopBarOptions(TopBarOptions options) { } private void showTopBar(NavigationOptions.BooleanOptions animated) { - if (controller.getTopBar().getVisibility() == View.VISIBLE) { + if (topBar.getVisibility() == View.VISIBLE) { return; } if (animated == NavigationOptions.BooleanOptions.True) { - ContainerLayout topBarContainerView = (ContainerLayout) controller.getContainerView(); - animator.animateShowTopBar(controller.getTopBar(), topBarContainerView.getReactView().asView()); + animator.animateShowTopBar(topBar, contentView); } else { - controller.getTopBar().setVisibility(View.VISIBLE); + topBar.setVisibility(View.VISIBLE); } } private void hideTopBar(NavigationOptions.BooleanOptions animated) { - if (controller.getTopBar().getVisibility() == View.GONE) { + if (topBar.getVisibility() == View.GONE) { return; } if (animated == NavigationOptions.BooleanOptions.True) { - ContainerLayout topBarContainerView = (ContainerLayout) controller.getContainerView(); - animator.animateHideTopBar(controller.getTopBar(), topBarContainerView.getReactView().asView()); + animator.animateHideTopBar(topBar, contentView); } else { - controller.getTopBar().setVisibility(View.GONE); + topBar.setVisibility(View.GONE); } } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/utils/Task.java b/lib/android/app/src/main/java/com/reactnativenavigation/utils/Task.java new file mode 100644 index 00000000000..4cb997e5409 --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/utils/Task.java @@ -0,0 +1,5 @@ +package com.reactnativenavigation.utils; + +public interface Task { + void run(T param); +} \ No newline at end of file diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ContainerViewController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ContainerViewController.java index 57359a7e3b2..3c78e44d342 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ContainerViewController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ContainerViewController.java @@ -2,12 +2,12 @@ import android.app.Activity; import android.support.annotation.NonNull; +import android.support.annotation.RestrictTo; import android.view.View; import com.reactnativenavigation.parse.NavigationOptions; import com.reactnativenavigation.presentation.NavigationOptionsListener; -import com.reactnativenavigation.presentation.OptionsPresenter; -import com.reactnativenavigation.views.Container; +import com.reactnativenavigation.views.ReactContainer; import com.reactnativenavigation.views.TopBar; public class ContainerViewController extends ViewController implements NavigationOptionsListener { @@ -35,25 +35,7 @@ public interface IReactView { private final ReactViewCreator viewCreator; private NavigationOptions navigationOptions; - private IReactView containerView; - - private TopBar topBar; - - public void setTitle(String title) { - topBar.setTitle(title); - } - - public void setBackgroundColor(int backgroundColor) { - topBar.setBackgroundColor(backgroundColor); - } - - public void setTitleTextColor(int textColor) { - topBar.setTitleTextColor(textColor); - } - - public void setTitleFontSize(float textFontSize) { - topBar.setTitleFontSize(textFontSize); - } + private ReactContainer container; public ContainerViewController(final Activity activity, final String id, @@ -66,62 +48,51 @@ public ContainerViewController(final Activity activity, this.navigationOptions = initialNavigationOptions; } + @RestrictTo(RestrictTo.Scope.TESTS) + TopBar getTopBar() { + return container.getTopBar(); + } + @Override public void destroy() { super.destroy(); - if (containerView != null) containerView.destroy(); - containerView = null; + if (container != null) container.destroy(); + container = null; } @Override public void onViewAppeared() { super.onViewAppeared(); ensureViewIsCreated(); - applyOptions(); - containerView.sendContainerStart(); + container.applyOptions(navigationOptions); + container.sendContainerStart(); } @Override public void onViewDisappear() { super.onViewDisappear(); - containerView.sendContainerStop(); + container.sendContainerStop(); } @Override protected boolean isViewShown() { - return super.isViewShown() && containerView.isReady(); + return super.isViewShown() && container.isReady(); } @NonNull @Override protected View createView() { - containerView = viewCreator.create(getActivity(), getId(), containerName); - if (containerView instanceof Container) { - topBar = ((Container) containerView).getTopBar(); - } - return containerView.asView(); + container = (ReactContainer) viewCreator.create(getActivity(), getId(), containerName); + return container.asView(); } @Override public void mergeNavigationOptions(NavigationOptions options) { navigationOptions.mergeWith(options); - applyOptions(); + container.applyOptions(options); } NavigationOptions getNavigationOptions() { return navigationOptions; } - - private void applyOptions() { - OptionsPresenter presenter = new OptionsPresenter(this); - presenter.applyOptions(navigationOptions); - } - - public TopBar getTopBar() { - return topBar; - } - - public IReactView getContainerView() { - return containerView; - } } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/Navigator.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/Navigator.java index 2e5ac811e0c..bc6fb754748 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/Navigator.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/Navigator.java @@ -53,7 +53,7 @@ public void destroy() { * Navigation methods */ - public void setRoot(final ViewController viewController) { + void setRoot(final ViewController viewController) { setRoot(viewController, null); } @@ -94,72 +94,53 @@ public void push(final String fromId, final ViewController viewController) { public void push(final String fromId, final ViewController viewController, Promise promise) { ViewController from = findControllerById(fromId); if (from != null) { - StackController parentStackController = from.getParentStackController(); - if (parentStackController != null) { - parentStackController.push(viewController, promise); - } + from.performOnParentStack(stack -> stack.push(viewController, promise)); } } - public void pop(final String fromId) { + void pop(final String fromId) { pop(fromId, null); } - public void pop(final String fromId, Promise promise) { + void pop(final String fromId, Promise promise) { ViewController from = findControllerById(fromId); if (from != null) { - StackController parentStackController = from.getParentStackController(); - if (parentStackController != null) { - parentStackController.pop(promise); - } + from.performOnParentStack(stack -> stack.pop(promise)); } } - public void popSpecific(final String id) { + void popSpecific(final String id) { popSpecific(id, null); } public void popSpecific(final String id, Promise promise) { ViewController from = findControllerById(id); if (from != null) { - StackController parentStackController = from.getParentStackController(); - if (parentStackController != null) { - parentStackController.popSpecific(from, promise); - } else { - rejectPromise(promise); - } + from.performOnParentStack(stack -> stack.popSpecific(from, promise), () -> rejectPromise(promise)); } else { rejectPromise(promise); } } - public void popToRoot(final String id) { + void popToRoot(final String id) { popToRoot(id, null); } public void popToRoot(final String id, Promise promise) { ViewController from = findControllerById(id); if (from != null) { - StackController parentStackController = from.getParentStackController(); - if (parentStackController != null) { - parentStackController.popToRoot(promise); - } + from.performOnParentStack(stack -> stack.popToRoot(promise)); } } - public void popTo(final String containerId) { + void popTo(final String containerId) { popTo(containerId, null); } public void popTo(final String containerId, Promise promise) { ViewController target = findControllerById(containerId); if (target != null) { - StackController parentStackController = target.getParentStackController(); - if (parentStackController != null) { - parentStackController.popTo(target, promise); - } else { - rejectPromise(promise); - } + target.performOnParentStack(stack -> stack.popTo(target, promise), () -> rejectPromise(promise)); } else { rejectPromise(promise); } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/StackController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/StackController.java index e5908a9e7aa..ee7599adef0 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/StackController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/StackController.java @@ -33,7 +33,7 @@ public void push(final ViewController child) { public void push(final ViewController child, final Promise promise) { final ViewController previousTop = peek(); - child.setParentStackController(this); + child.setParentController(this); stack.push(child.getId(), child); View enteringView = child.getView(); getView().addView(enteringView); diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java index c72d7878c03..26e8f2845f6 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java @@ -8,8 +8,10 @@ import android.view.ViewManager; import android.view.ViewTreeObserver; +import com.reactnativenavigation.parse.NavigationOptions; import com.reactnativenavigation.utils.CompatUtils; import com.reactnativenavigation.utils.StringUtils; +import com.reactnativenavigation.utils.Task; public abstract class ViewController implements ViewTreeObserver.OnGlobalLayoutListener { @@ -17,7 +19,7 @@ public abstract class ViewController implements ViewTreeObserver.OnGlobalLayoutL private final String id; private View view; - private StackController parentStackController; + private ParentController parentController; private boolean isShown = false; public ViewController(Activity activity, String id) { @@ -39,15 +41,33 @@ public Activity getActivity() { return activity; } + protected ViewController getParentController() { + return parentController; + } + @Nullable - StackController getParentStackController() { - return parentStackController; + ParentController getParentStackController() { + return parentController; } - void setParentStackController(final StackController parentStackController) { - this.parentStackController = parentStackController; + public void setParentController(final ParentController parentController) { + this.parentController = parentController; } + void performOnParentStack(Task task) { + if (parentController instanceof StackController) { + task.run((StackController) parentController); + } + } + + void performOnParentStack(Task accept, Runnable reject) { + if (parentController instanceof StackController) { + accept.run((StackController) parentController); + } else { + reject.run(); + } + } + @NonNull public View getView() { if (view == null) { @@ -72,12 +92,16 @@ public ViewController findControllerById(String id) { } public void onViewAppeared() { - // - } + isShown = true; + } public void onViewDisappear() { - // - } + isShown = false; + } + + public void applyOptions(NavigationOptions options) { + + } public void destroy() { if (isShown) { diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabController.java new file mode 100644 index 00000000000..7b48e218f3e --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabController.java @@ -0,0 +1,66 @@ +package com.reactnativenavigation.viewcontrollers.toptabs; + +import android.app.Activity; +import android.view.View; + +import com.facebook.react.ReactInstanceManager; +import com.reactnativenavigation.parse.NavigationOptions; +import com.reactnativenavigation.presentation.NavigationOptionsListener; +import com.reactnativenavigation.react.ReactContainerViewCreator; +import com.reactnativenavigation.viewcontrollers.ViewController; +import com.reactnativenavigation.views.TopTab; + +public class TopTabController extends ViewController implements NavigationOptionsListener { + + private final String containerName; + private final ReactInstanceManager instanceManager; + private final NavigationOptions options; + private TopTab topTab; + private boolean isSelectedTab; + + public TopTabController(Activity activity, String id, String name, ReactInstanceManager instanceManager, NavigationOptions initialOptions) { + super(activity, id); + this.containerName = name; + this.instanceManager = instanceManager; + this.options = initialOptions; + } + + @Override + public void onViewAppeared() { + super.onViewAppeared(); + isSelectedTab = true; + getParentController().applyOptions(options); + } + + @Override + public void onViewDisappear() { + super.onViewDisappear(); + isSelectedTab = false; + } + + @Override + protected boolean isViewShown() { + return super.isViewShown() && isSelectedTab; + } + + @Override + protected View createView() { + topTab = new TopTab( + getActivity(), + new ReactContainerViewCreator(instanceManager).create(getActivity(), getId(), containerName) + ); + return topTab; + } + + @Override + public void destroy() { + super.destroy(); + if (topTab != null) topTab.destroy(); + topTab = null; + } + + @Override + public void mergeNavigationOptions(NavigationOptions options) { + this.options.mergeWith(options); + } +} diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsAdapter.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsAdapter.java index e7cb0ba6bc6..88583deabf9 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsAdapter.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsAdapter.java @@ -1,6 +1,7 @@ package com.reactnativenavigation.viewcontrollers.toptabs; import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; import android.view.View; import android.view.ViewGroup; @@ -8,8 +9,9 @@ import java.util.List; -public class TopTabsAdapter extends PagerAdapter { +public class TopTabsAdapter extends PagerAdapter implements ViewPager.OnPageChangeListener { private List tabs; + private int currentPage = 0; TopTabsAdapter(List tabs) { this.tabs = tabs; @@ -29,4 +31,21 @@ public boolean isViewFromObject(View view, Object object) { public Object instantiateItem(ViewGroup container, int position) { return tabs.get(position).getView(); } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + + } + + @Override + public void onPageSelected(int position) { + tabs.get(currentPage).onViewDisappear(); + tabs.get(position).onViewAppeared(); + currentPage = position; + } + + @Override + public void onPageScrollStateChanged(int state) { + + } } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsController.java index 4aadf4c8c48..d4826788ac3 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsController.java @@ -4,6 +4,8 @@ import android.support.annotation.NonNull; import android.view.ViewGroup; +import com.reactnativenavigation.parse.NavigationOptions; +import com.reactnativenavigation.presentation.NavigationOptionsListener; import com.reactnativenavigation.viewcontrollers.ParentController; import com.reactnativenavigation.viewcontrollers.ViewController; import com.reactnativenavigation.views.TopTabsLayout; @@ -11,19 +13,24 @@ import java.util.Collection; import java.util.List; -public class TopTabsController extends ParentController { +public class TopTabsController extends ParentController implements NavigationOptionsListener { private List tabs; + private TopTabsLayout topTabsLayout; public TopTabsController(Activity activity, String id, List tabs) { super(activity, id); this.tabs = tabs; + for (ViewController tab : tabs) { + tab.setParentController(this); + } } @NonNull @Override protected ViewGroup createView() { - return new TopTabsLayout(getActivity(), tabs); + topTabsLayout = new TopTabsLayout(getActivity(), tabs); + return topTabsLayout; } @NonNull @@ -33,7 +40,17 @@ public Collection getChildControllers() { } @Override - protected boolean isViewShown() { - return super.isViewShown(); + public void onViewAppeared() { + tabs.get(0).onViewAppeared(); + } + + @Override + public void applyOptions(NavigationOptions options) { + topTabsLayout.applyOptions(options); + } + + @Override + public void mergeNavigationOptions(NavigationOptions options) { + } } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsViewPager.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsViewPager.java index 6d624604fe9..4e224fe9634 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsViewPager.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsViewPager.java @@ -3,10 +3,8 @@ import android.annotation.SuppressLint; import android.content.Context; import android.support.v4.view.ViewPager; -import android.view.View; import android.view.ViewGroup; -import com.reactnativenavigation.viewcontrollers.ContainerViewController; import com.reactnativenavigation.viewcontrollers.ViewController; import java.util.List; @@ -14,46 +12,29 @@ import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; @SuppressLint("ViewConstructor") -public class TopTabsViewPager extends ViewPager implements ContainerViewController.IReactView { +public class TopTabsViewPager extends ViewPager { private static final int OFFSCREEN_PAGE_LIMIT = 99; - private final List tabs; + private List tabs; public TopTabsViewPager(Context context, List tabs) { super(context); this.tabs = tabs; - init(tabs); + init(); } - private void init(List tabs) { + private void init() { setOffscreenPageLimit(OFFSCREEN_PAGE_LIMIT); for (ViewController tab : tabs) { addView(tab.getView(), new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } - setAdapter(new TopTabsAdapter(tabs)); + TopTabsAdapter adapter = new TopTabsAdapter(tabs); + setAdapter(adapter); + addOnPageChangeListener(adapter); } - @Override - public boolean isReady() { - return false; - } - - @Override - public View asView() { - return null; - } - - @Override public void destroy() { - - } - - @Override - public void sendContainerStart() { - - } - - @Override - public void sendContainerStop() { - + for (ViewController tab : tabs) { + tab.destroy(); + } } -} +} \ No newline at end of file diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/views/Container.java b/lib/android/app/src/main/java/com/reactnativenavigation/views/Container.java index baa045015f6..d6254185e7c 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/views/Container.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/views/Container.java @@ -1,7 +1,12 @@ package com.reactnativenavigation.views; -import com.reactnativenavigation.viewcontrollers.ContainerViewController; +import android.support.annotation.RestrictTo; -public interface Container extends ContainerViewController.IReactView { +import com.reactnativenavigation.parse.NavigationOptions; + +public interface Container { + void applyOptions(NavigationOptions options); + + @RestrictTo(RestrictTo.Scope.TESTS) TopBar getTopBar(); } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/views/ContainerLayout.java b/lib/android/app/src/main/java/com/reactnativenavigation/views/ContainerLayout.java index f3fc96056c8..9111859db7a 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/views/ContainerLayout.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/views/ContainerLayout.java @@ -1,21 +1,28 @@ package com.reactnativenavigation.views; +import android.annotation.SuppressLint; import android.content.Context; +import android.support.annotation.RestrictTo; import android.view.View; import android.widget.LinearLayout; +import com.reactnativenavigation.parse.NavigationOptions; +import com.reactnativenavigation.presentation.OptionsPresenter; import com.reactnativenavigation.viewcontrollers.ContainerViewController.IReactView; -public class ContainerLayout extends LinearLayout implements Container { +@SuppressLint("ViewConstructor") +public class ContainerLayout extends LinearLayout implements ReactContainer { private TopBar topBar; private IReactView reactView; + private final OptionsPresenter optionsPresenter; public ContainerLayout(Context context, IReactView reactView) { super(context); this.topBar = new TopBar(context); this.reactView = reactView; - initViews(); + optionsPresenter = new OptionsPresenter(topBar, this); + initViews(); } private void initViews() { @@ -24,10 +31,6 @@ private void initViews() { addView(reactView.asView()); } - public ContainerLayout(Context context) { - super(context); - } - @Override public boolean isReady() { return reactView.isReady(); @@ -54,11 +57,13 @@ public void sendContainerStop() { } @Override - public TopBar getTopBar() { - return topBar; - } + public void applyOptions(NavigationOptions options) { + optionsPresenter.applyOptions(options); + } - public IReactView getReactView() { - return reactView; - } + @Override + @RestrictTo(RestrictTo.Scope.TESTS) + public TopBar getTopBar() { + return topBar; + } } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/views/ReactContainer.java b/lib/android/app/src/main/java/com/reactnativenavigation/views/ReactContainer.java new file mode 100644 index 00000000000..97c64dba31b --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/views/ReactContainer.java @@ -0,0 +1,6 @@ +package com.reactnativenavigation.views; + +import com.reactnativenavigation.viewcontrollers.ContainerViewController; + +public interface ReactContainer extends Container, ContainerViewController.IReactView { +} diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/views/TopTab.java b/lib/android/app/src/main/java/com/reactnativenavigation/views/TopTab.java index 2ce4c918df8..168915da501 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/views/TopTab.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/views/TopTab.java @@ -1,27 +1,21 @@ package com.reactnativenavigation.views; +import android.annotation.SuppressLint; import android.content.Context; import android.view.View; import android.widget.FrameLayout; import com.reactnativenavigation.viewcontrollers.ContainerViewController.IReactView; +@SuppressLint("ViewConstructor") public class TopTab extends FrameLayout implements IReactView { - private IReactView reactView; + private final IReactView reactView; public TopTab(Context context, IReactView reactView) { super(context); this.reactView = reactView; - initViews(); - } - - private void initViews() { - addView(reactView.asView()); - } - - public TopTab(Context context) { - super(context); + addView(reactView.asView()); } @Override diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/views/TopTabsLayout.java b/lib/android/app/src/main/java/com/reactnativenavigation/views/TopTabsLayout.java index 0dd53c512ca..6e231e3e1e3 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/views/TopTabsLayout.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/views/TopTabsLayout.java @@ -2,23 +2,28 @@ import android.annotation.SuppressLint; import android.content.Context; +import android.support.annotation.RestrictTo; import android.widget.LinearLayout; +import com.reactnativenavigation.parse.NavigationOptions; +import com.reactnativenavigation.presentation.OptionsPresenter; import com.reactnativenavigation.viewcontrollers.ViewController; import com.reactnativenavigation.viewcontrollers.toptabs.TopTabsViewPager; import java.util.List; @SuppressLint("ViewConstructor") -public class TopTabsLayout extends LinearLayout { +public class TopTabsLayout extends LinearLayout implements Container { private TopBar topBar; private TopTabsViewPager viewPager; + private final OptionsPresenter optionsPresenter; public TopTabsLayout(Context context, List tabs) { super(context); topBar = new TopBar(context); viewPager = new TopTabsViewPager(context, tabs); + optionsPresenter = new OptionsPresenter(topBar, this); initViews(); } @@ -27,4 +32,15 @@ private void initViews() { addView(topBar); addView(viewPager); } + + @Override + public void applyOptions(NavigationOptions options) { + optionsPresenter.applyOptions(options); + } + + @Override + @RestrictTo(RestrictTo.Scope.TESTS) + public TopBar getTopBar() { + return topBar; + } } diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/mocks/TestContainerLayout.java b/lib/android/app/src/test/java/com/reactnativenavigation/mocks/TestContainerLayout.java index 5dd3c19fa53..4d78a037c86 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/mocks/TestContainerLayout.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/mocks/TestContainerLayout.java @@ -3,18 +3,25 @@ import android.content.Context; import android.view.View; -import com.reactnativenavigation.viewcontrollers.ContainerViewController; -import com.reactnativenavigation.views.Container; +import com.reactnativenavigation.parse.NavigationOptions; +import com.reactnativenavigation.presentation.OptionsPresenter; +import com.reactnativenavigation.views.ReactContainer; import com.reactnativenavigation.views.TopBar; -public class TestContainerLayout extends View implements ContainerViewController.IReactView, Container { +public class TestContainerLayout extends View implements ReactContainer { - private TopBar topBar; + private final TopBar topBar; + private final OptionsPresenter optionsPresenter; - public TestContainerLayout(final Context context) { + public TestContainerLayout(final Context context) { super(context); - topBar = new TopBar(context); - } + topBar = new TopBar(context); + optionsPresenter = new OptionsPresenter(topBar, this); + } + + public TopBar getTopBar() { + return topBar; + } @Override public boolean isReady() { @@ -39,7 +46,7 @@ public void sendContainerStop() { } @Override - public TopBar getTopBar() { - return topBar; + public void applyOptions(NavigationOptions options) { + optionsPresenter.applyOptions(options); } } diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/OptionsApplyingTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/OptionsApplyingTest.java index a72688def9a..679766e9913 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/OptionsApplyingTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/OptionsApplyingTest.java @@ -26,13 +26,13 @@ public void beforeEach() { activity = newActivity(); initialNavigationOptions = new NavigationOptions(); view = spy(new TestContainerLayout(activity)); - uut = new ContainerViewController(activity, "containerId1", "containerName", - new ContainerViewController.ReactViewCreator() { - @Override - public ContainerViewController.IReactView create(final Activity activity1, final String containerId, final String containerName) { - return view; - } - }, initialNavigationOptions); + uut = new ContainerViewController(activity, + "containerId1", + "containerName", + (activity1, containerId, containerName) -> view, + initialNavigationOptions + ); + uut.ensureViewIsCreated(); } @Test diff --git a/lib/src/commands/LayoutTreeParser.js b/lib/src/commands/LayoutTreeParser.js index fc34890cfb3..8dcd13743af 100644 --- a/lib/src/commands/LayoutTreeParser.js +++ b/lib/src/commands/LayoutTreeParser.js @@ -13,7 +13,7 @@ class LayoutTreeParser { return this._createTabs(simpleJsonApi.bottomTabs); } if (simpleJsonApi.topTabs) { - return this._createTopTabsContainer(simpleJsonApi.topTabs); + return this._createTopTabs(simpleJsonApi.topTabs); } if (simpleJsonApi.name) { return this._createContainer(simpleJsonApi); @@ -36,9 +36,9 @@ class LayoutTreeParser { }; } - _createTopTabsContainer(topTabs) { + _createTopTabs(topTabs) { return { - type: LayoutTypes.TopTabsContainer, + type: LayoutTypes.TopTabs, children: _.map(topTabs, (t) => this._createTopTab(t.container)) }; } diff --git a/lib/src/commands/LayoutTypes.js b/lib/src/commands/LayoutTypes.js index 126d1fe2ed5..1a8091b75e2 100644 --- a/lib/src/commands/LayoutTypes.js +++ b/lib/src/commands/LayoutTypes.js @@ -7,6 +7,6 @@ module.exports = { SideMenuLeft: 'SideMenuLeft', SideMenuRight: 'SideMenuRight', CustomDialog: 'CustomDialog', - TopTabsContainer: 'TopTabsContainer', + TopTabs: 'TopTabs', TopTab: 'TopTab' }; diff --git a/playground/android/app/build.gradle b/playground/android/app/build.gradle index 9eb4a6347d8..5fb89679151 100644 --- a/playground/android/app/build.gradle +++ b/playground/android/app/build.gradle @@ -11,6 +11,11 @@ android { compileSdkVersion 25 buildToolsVersion "27.0.1" + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { applicationId "com.reactnativenavigation.playground" minSdkVersion 19 diff --git a/playground/src/containers/TopTabOptionsScreen.js b/playground/src/containers/TopTabOptionsScreen.js new file mode 100644 index 00000000000..a2a513651d3 --- /dev/null +++ b/playground/src/containers/TopTabOptionsScreen.js @@ -0,0 +1,52 @@ +const React = require('react'); +const { PureComponent } = require('react'); + +const { View, Text } = require('react-native'); + +class TopTabOptionsScreen extends PureComponent { + static get navigationOptions() { + return { + topBar: { + title: 'Tab 1', + textColor: 'black', + textFontSize: 16, + textFontFamily: 'HelveticaNeue-Italic' + } + }; + } + + render() { + return ( + + {this.props.text || 'Top Tab Screen'} + {`this.props.containerId = ${this.props.containerId}`} + + ); + } +} + +const styles = { + root: { + flexGrow: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#f5fcff' + }, + h1: { + fontSize: 24, + textAlign: 'center', + margin: 10 + }, + h2: { + fontSize: 12, + textAlign: 'center', + margin: 10 + }, + footer: { + fontSize: 10, + color: '#888', + marginTop: 10 + } +}; + +module.exports = TopTabOptionsScreen; diff --git a/playground/src/containers/TopTabScreen.js b/playground/src/containers/TopTabScreen.js index a98d95ff2a2..fdab0210bfc 100644 --- a/playground/src/containers/TopTabScreen.js +++ b/playground/src/containers/TopTabScreen.js @@ -1,8 +1,28 @@ const React = require('react'); const { PureComponent } = require('react'); const { View, Text } = require('react-native'); +const Navigation = require('react-native-navigation'); class TopTabScreen extends PureComponent { + static get navigationOptions() { + return { + topBar: { + textColor: 'black', + textFontSize: 16, + textFontFamily: 'HelveticaNeue-Italic' + } + }; + } + + constructor(props) { + super(props); + Navigation.setOptions(this.props.containerId, { + topBar: { + title: this.props.title + } + }); + } + render() { return ( diff --git a/playground/src/containers/WelcomeScreen.js b/playground/src/containers/WelcomeScreen.js index 2bc20109a94..3d69c97e9bb 100644 --- a/playground/src/containers/WelcomeScreen.js +++ b/playground/src/containers/WelcomeScreen.js @@ -152,8 +152,9 @@ class WelcomeScreen extends Component { topTabs: [ { container: { - name: 'navigation.playground.TopTabScreen', + name: 'navigation.playground.TopTabOptionsScreen', passProps: { + title: 'Tab 1', text: 'This is top tab 1' } } @@ -162,6 +163,7 @@ class WelcomeScreen extends Component { container: { name: 'navigation.playground.TopTabScreen', passProps: { + title: 'Tab 2', text: 'This is top tab 2' } } @@ -170,6 +172,7 @@ class WelcomeScreen extends Component { container: { name: 'navigation.playground.TopTabScreen', passProps: { + title: 'Tab 3', text: 'This is top tab 3' } } diff --git a/playground/src/containers/index.js b/playground/src/containers/index.js index 6fa72f06909..afca15c56e4 100644 --- a/playground/src/containers/index.js +++ b/playground/src/containers/index.js @@ -14,6 +14,7 @@ const CustomDialog = require('./CustomDialog'); const BandHandlerScreen = require('./BackHandlerScreen'); const SideMenuScreen = require('./SideMenuScreen'); const TopTabScreen = require('./TopTabScreen'); +const TopTabOptionsScreen = require('./TopTabOptionsScreen'); function registerContainers() { Navigation.registerContainer(`navigation.playground.CustomTransitionDestination`, () => CustomTransitionDestination); @@ -31,6 +32,7 @@ function registerContainers() { Navigation.registerContainer('navigation.playground.BackHandlerScreen', () => BandHandlerScreen); Navigation.registerContainer('navigation.playground.SideMenuScreen', () => SideMenuScreen); Navigation.registerContainer('navigation.playground.TopTabScreen', () => TopTabScreen); + Navigation.registerContainer('navigation.playground.TopTabOptionsScreen', () => TopTabOptionsScreen); } module.exports = { From 301b05409b5ea9bd296740e8f9692f835c80a9be Mon Sep 17 00:00:00 2001 From: Guy Carmeli Date: Sun, 17 Dec 2017 15:49:13 +0200 Subject: [PATCH 2/2] TopTabs unit tests --- .../parse/LayoutFactory.java | 12 +- .../viewcontrollers/ParentController.java | 2 +- .../viewcontrollers/ViewController.java | 4 +- .../toptabs/TopTabController.java | 20 ++- .../toptabs/TopTabsAdapter.java | 6 +- .../toptabs/TopTabsController.java | 17 ++- .../toptabs/TopTabsViewPager.java | 4 +- .../views/TopTabsLayout.java | 22 +++- .../mocks/TopTabLayoutMock.java | 38 ++++++ .../TopTabsViewControllerTest.java | 122 ++++++++++++++++++ lib/src/commands/LayoutTreeParser.test.js | 2 +- 11 files changed, 221 insertions(+), 28 deletions(-) create mode 100644 lib/android/app/src/test/java/com/reactnativenavigation/mocks/TopTabLayoutMock.java create mode 100644 lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopTabsViewControllerTest.java diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java b/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java index 0d938b4e5a8..de1372adcb3 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java @@ -32,7 +32,7 @@ public LayoutFactory(Activity activity, final ReactInstanceManager reactInstance public ViewController create(final LayoutNode node) { switch (node.type) { case Container: - return createContainer(node, new ContainerViewCreator(reactInstanceManager)); + return createContainer(node); case ContainerStack: return createContainerStack(node); case BottomTabs: @@ -89,14 +89,14 @@ private ViewController createSideMenuRight(LayoutNode node) { return create(node.children.get(0)); } - private ViewController createContainer(LayoutNode node, ContainerViewController.ReactViewCreator reactViewCreator) { + private ViewController createContainer(LayoutNode node) { String id = node.id; String name = node.data.optString("name"); NavigationOptions navigationOptions = NavigationOptions.parse(node.data.optJSONObject("navigationOptions"), defaultOptions); return new ContainerViewController(activity, id, name, - reactViewCreator, + new ContainerViewCreator(reactInstanceManager), navigationOptions ); } @@ -127,9 +127,9 @@ private ViewController createDialogContainer(LayoutNode node) { } private ViewController createTopTabs(LayoutNode node) { - final List tabs = new ArrayList<>(); + final List tabs = new ArrayList<>(); for (LayoutNode child : node.children) { - tabs.add(create(child)); + tabs.add((TopTabController) create(child)); } return new TopTabsController(activity, node.id, tabs); } @@ -141,7 +141,7 @@ private ViewController createTopTab(LayoutNode node) { return new TopTabController(activity, id, name, - reactInstanceManager, + new ReactContainerViewCreator(reactInstanceManager), navigationOptions ); } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ParentController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ParentController.java index 43a02c28da6..ce79e62a72b 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ParentController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ParentController.java @@ -24,7 +24,7 @@ public ViewGroup getView() { protected abstract ViewGroup createView(); @NonNull - public abstract Collection getChildControllers(); + public abstract Collection getChildControllers(); @Nullable @Override diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java index 26e8f2845f6..74c41c38e4a 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java @@ -3,6 +3,7 @@ import android.app.Activity; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.view.View; import android.view.ViewGroup; import android.view.ViewManager; @@ -29,7 +30,8 @@ public ViewController(Activity activity, String id) { protected abstract View createView(); - void ensureViewIsCreated() { + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + public void ensureViewIsCreated() { getView(); } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabController.java index 7b48e218f3e..8ec38f06284 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabController.java @@ -3,25 +3,24 @@ import android.app.Activity; import android.view.View; -import com.facebook.react.ReactInstanceManager; import com.reactnativenavigation.parse.NavigationOptions; import com.reactnativenavigation.presentation.NavigationOptionsListener; -import com.reactnativenavigation.react.ReactContainerViewCreator; +import com.reactnativenavigation.viewcontrollers.ContainerViewController; import com.reactnativenavigation.viewcontrollers.ViewController; import com.reactnativenavigation.views.TopTab; public class TopTabController extends ViewController implements NavigationOptionsListener { private final String containerName; - private final ReactInstanceManager instanceManager; + private ContainerViewController.ReactViewCreator viewCreator; private final NavigationOptions options; private TopTab topTab; private boolean isSelectedTab; - public TopTabController(Activity activity, String id, String name, ReactInstanceManager instanceManager, NavigationOptions initialOptions) { + public TopTabController(Activity activity, String id, String name, ContainerViewController.ReactViewCreator viewCreator, NavigationOptions initialOptions) { super(activity, id); this.containerName = name; - this.instanceManager = instanceManager; + this.viewCreator = viewCreator; this.options = initialOptions; } @@ -29,6 +28,12 @@ public TopTabController(Activity activity, String id, String name, ReactInstance public void onViewAppeared() { super.onViewAppeared(); isSelectedTab = true; + applyOptions(options); + topTab.sendContainerStart(); + } + + @Override + public void applyOptions(NavigationOptions options) { getParentController().applyOptions(options); } @@ -36,6 +41,7 @@ public void onViewAppeared() { public void onViewDisappear() { super.onViewDisappear(); isSelectedTab = false; + topTab.sendContainerStop(); } @Override @@ -44,10 +50,10 @@ protected boolean isViewShown() { } @Override - protected View createView() { + public View createView() { topTab = new TopTab( getActivity(), - new ReactContainerViewCreator(instanceManager).create(getActivity(), getId(), containerName) + viewCreator.create(getActivity(), getId(), containerName) ); return topTab; } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsAdapter.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsAdapter.java index 88583deabf9..8eaaca1b708 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsAdapter.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsAdapter.java @@ -5,15 +5,13 @@ import android.view.View; import android.view.ViewGroup; -import com.reactnativenavigation.viewcontrollers.ViewController; - import java.util.List; public class TopTabsAdapter extends PagerAdapter implements ViewPager.OnPageChangeListener { - private List tabs; + private List tabs; private int currentPage = 0; - TopTabsAdapter(List tabs) { + TopTabsAdapter(List tabs) { this.tabs = tabs; } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsController.java index d4826788ac3..38eb35f4232 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsController.java @@ -15,10 +15,10 @@ public class TopTabsController extends ParentController implements NavigationOptionsListener { - private List tabs; + private List tabs; private TopTabsLayout topTabsLayout; - public TopTabsController(Activity activity, String id, List tabs) { + public TopTabsController(Activity activity, String id, List tabs) { super(activity, id); this.tabs = tabs; for (ViewController tab : tabs) { @@ -35,13 +35,18 @@ protected ViewGroup createView() { @NonNull @Override - public Collection getChildControllers() { + public Collection getChildControllers() { return tabs; } @Override public void onViewAppeared() { - tabs.get(0).onViewAppeared(); + topTabsLayout.performOnCurrentTab(TopTabController::onViewAppeared); + } + + @Override + public void onViewDisappear() { + topTabsLayout.performOnCurrentTab(TopTabController::onViewDisappear); } @Override @@ -53,4 +58,8 @@ public void applyOptions(NavigationOptions options) { public void mergeNavigationOptions(NavigationOptions options) { } + + public void switchToTab(int index) { + topTabsLayout.switchToTab(index); + } } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsViewPager.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsViewPager.java index 4e224fe9634..83f612a4d08 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsViewPager.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsViewPager.java @@ -14,9 +14,9 @@ @SuppressLint("ViewConstructor") public class TopTabsViewPager extends ViewPager { private static final int OFFSCREEN_PAGE_LIMIT = 99; - private List tabs; + private List tabs; - public TopTabsViewPager(Context context, List tabs) { + public TopTabsViewPager(Context context, List tabs) { super(context); this.tabs = tabs; init(); diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/views/TopTabsLayout.java b/lib/android/app/src/main/java/com/reactnativenavigation/views/TopTabsLayout.java index 6e231e3e1e3..bc1147a226f 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/views/TopTabsLayout.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/views/TopTabsLayout.java @@ -3,11 +3,13 @@ import android.annotation.SuppressLint; import android.content.Context; import android.support.annotation.RestrictTo; +import android.support.v4.view.ViewPager; import android.widget.LinearLayout; import com.reactnativenavigation.parse.NavigationOptions; import com.reactnativenavigation.presentation.OptionsPresenter; -import com.reactnativenavigation.viewcontrollers.ViewController; +import com.reactnativenavigation.utils.Task; +import com.reactnativenavigation.viewcontrollers.toptabs.TopTabController; import com.reactnativenavigation.viewcontrollers.toptabs.TopTabsViewPager; import java.util.List; @@ -16,12 +18,14 @@ public class TopTabsLayout extends LinearLayout implements Container { private TopBar topBar; + private List tabs; private TopTabsViewPager viewPager; private final OptionsPresenter optionsPresenter; - public TopTabsLayout(Context context, List tabs) { + public TopTabsLayout(Context context, List tabs) { super(context); topBar = new TopBar(context); + this.tabs = tabs; viewPager = new TopTabsViewPager(context, tabs); optionsPresenter = new OptionsPresenter(topBar, this); initViews(); @@ -43,4 +47,18 @@ public void applyOptions(NavigationOptions options) { public TopBar getTopBar() { return topBar; } + + @RestrictTo(RestrictTo.Scope.TESTS) + public ViewPager getViewPager() { + return viewPager; + } + + + public void performOnCurrentTab(Task task) { + task.run(tabs.get(viewPager.getCurrentItem())); + } + + public void switchToTab(int index) { + viewPager.setCurrentItem(index); + } } diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/mocks/TopTabLayoutMock.java b/lib/android/app/src/test/java/com/reactnativenavigation/mocks/TopTabLayoutMock.java new file mode 100644 index 00000000000..4a4def2bbb2 --- /dev/null +++ b/lib/android/app/src/test/java/com/reactnativenavigation/mocks/TopTabLayoutMock.java @@ -0,0 +1,38 @@ +package com.reactnativenavigation.mocks; + +import android.content.Context; +import android.view.View; + +import com.reactnativenavigation.viewcontrollers.ContainerViewController; + +public class TopTabLayoutMock extends View implements ContainerViewController.IReactView { + + public TopTabLayoutMock(Context context) { + super(context); + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public View asView() { + return this; + } + + @Override + public void destroy() { + + } + + @Override + public void sendContainerStart() { + + } + + @Override + public void sendContainerStop() { + + } +} diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopTabsViewControllerTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopTabsViewControllerTest.java new file mode 100644 index 00000000000..5aa7e96ff23 --- /dev/null +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopTabsViewControllerTest.java @@ -0,0 +1,122 @@ +package com.reactnativenavigation.viewcontrollers; + +import android.app.Activity; +import android.view.ViewGroup; + +import com.reactnativenavigation.BaseTest; +import com.reactnativenavigation.mocks.TopTabLayoutMock; +import com.reactnativenavigation.parse.NavigationOptions; +import com.reactnativenavigation.viewcontrollers.ContainerViewController.IReactView; +import com.reactnativenavigation.viewcontrollers.toptabs.TopTabController; +import com.reactnativenavigation.viewcontrollers.toptabs.TopTabsController; +import com.reactnativenavigation.views.TopTabsLayout; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class TopTabsViewControllerTest extends BaseTest { + + private static final int SIZE = 2; + + private TopTabsController uut; + private List tabs = new ArrayList<>(SIZE); + private List tabControllers = new ArrayList<>(SIZE); + private List tabOptions = new ArrayList<>(SIZE); + + @Override + public void beforeEach() { + super.beforeEach(); + tabControllers.clear(); + tabs.clear(); + Activity activity = newActivity(); + createTabs(activity); + uut = new TopTabsController(activity, "containerId", tabControllers); + } + + private void createTabs(Activity activity) { + for (int i = 0; i < SIZE; i++) { + TopTabLayoutMock reactView = spy(new TopTabLayoutMock(activity)); + tabs.add(reactView); + tabOptions.add(new NavigationOptions()); + tabControllers.add(spy(new TopTabController(activity, + "idTab" + i, + "tab" + i, + (activity1, containerId, containerName) -> reactView, + tabOptions.get(i)) + )); + } + } + + @Test + public void createsViewFromContainerViewCreator() throws Exception { + uut.ensureViewIsCreated(); + for (int i = 0; i < SIZE; i++) { + verify(tabControllers.get(i), times(1)).createView(); + } + } + + @Test + public void containerViewDestroyedOnDestroy() throws Exception { + uut.ensureViewIsCreated(); + TopTabsLayout topTabs = (TopTabsLayout) uut.getView(); + for (int i = 0; i < SIZE; i++) { + verify(tab(topTabs, i), times(0)).destroy(); + } + uut.destroy(); + for (ViewController tabController : tabControllers) { + verify(tabController, times(1)).destroy(); + } + } + + @Test + public void lifecycleMethodsSentWhenSelectedTabChanges() throws Exception { + uut.ensureViewIsCreated(); + TopTabLayoutMock initialTab = tabs.get(0); + TopTabLayoutMock selectedTab = tabs.get(1); + uut.onViewAppeared(); + uut.switchToTab(1); + verify(initialTab, times(1)).sendContainerStop(); + verify(selectedTab, times(1)).sendContainerStart(); + verify(selectedTab, times(0)).sendContainerStop(); + } + + @Test + public void lifecycleMethodsSentWhenSelectedPreviouslySelectedTab() throws Exception { + uut.ensureViewIsCreated(); + uut.onViewAppeared(); + uut.switchToTab(1); + uut.switchToTab(0); + verify(tabs.get(0), times(1)).sendContainerStop(); + verify(tabs.get(0), times(2)).sendContainerStart(); + verify(tabs.get(1), times(1)).sendContainerStart(); + verify(tabs.get(1), times(1)).sendContainerStop(); + } + + @Test + public void setOptionsOfInitialTab() throws Exception { + uut.ensureViewIsCreated(); + uut.onViewAppeared(); + verify(tabControllers.get(0), times(1)).applyOptions(tabOptions.get(0)); + } + + @Test + public void setOptionsWhenTabChanges() throws Exception { + uut.ensureViewIsCreated(); + uut.onViewAppeared(); + verify(tabControllers.get(0), times(1)).applyOptions(tabOptions.get(0)); + uut.switchToTab(1); + verify(tabControllers.get(1), times(1)).applyOptions(tabOptions.get(1)); + uut.switchToTab(0); + verify(tabControllers.get(0), times(2)).applyOptions(tabOptions.get(0)); + } + + private IReactView tab(TopTabsLayout topTabs, final int index) { + return (IReactView) ((ViewGroup) topTabs.getViewPager().getChildAt(index)).getChildAt(0); + } +} diff --git a/lib/src/commands/LayoutTreeParser.test.js b/lib/src/commands/LayoutTreeParser.test.js index c330c9175fb..c22c266e5cf 100644 --- a/lib/src/commands/LayoutTreeParser.test.js +++ b/lib/src/commands/LayoutTreeParser.test.js @@ -289,7 +289,7 @@ describe('LayoutTreeParser', () => { it('parses bottomTabs with side menus', () => { expect(uut.parseFromSimpleJSON(SimpleLayouts.singleScreenWithTopTabs)) .toEqual({ - type: 'TopTabsContainer', + type: 'TopTabs', children: [ { type: 'TopTab',