Skip to content

Commit

Permalink
Bottom tabs attach mode (#4633)
Browse files Browse the repository at this point in the history
This pr adds support for changing tab initialisation mode. Currently bottom tabs are attached together and consequently, their corresponding react root views are created and rendered. This adds lots of stress on the js thread and hiders app start time.

To mitigate this issue, this pr adds support for three modes
* `together` (default, current behaviour) - all tabs are loaded together, adding unnecessary load on the js thread as we're loading invisible views, which leads to increased app start time.
* `afterInitialTab` - Initial tab is loaded first. After it is rendered, other tabs are loaded as well. This should shave a few hunderdish ms from app start time. Since other tabs are loaded after the initial tab is visible, the ui might be unresponsive for a few hunderdish ms as multiple root views (which are typically complex) are being created and attached to hierarchy at once.
* `onSwitchToTab` - initial tab is loaded. Other tabs are loaded when switching to them (tab click or programmatically). While this won't stress js thread after initial tab is rendered, there will be a flicker when entering a tab for the first time as it's ui isn't ready.
  • Loading branch information
guyca authored Jan 24, 2019
1 parent 57d8ff7 commit 740ad3c
Show file tree
Hide file tree
Showing 29 changed files with 547 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public static BottomTabsOptions parse(JSONObject json) {
options.elevation = FractionParser.parse(json, "elevation");
options.testId = TextParser.parse(json, "testID");
options.titleDisplayMode = TitleDisplayMode.fromString(json.optString("titleDisplayMode"));
options.tabsAttachMode = TabsAttachMode.fromString(json.optString("tabsAttachMode"));

return options;
}
Expand All @@ -47,6 +48,7 @@ public static BottomTabsOptions parse(JSONObject json) {
public Text currentTabId = new NullText();
public Text testId = new NullText();
public TitleDisplayMode titleDisplayMode = TitleDisplayMode.UNDEFINED;
public TabsAttachMode tabsAttachMode = TabsAttachMode.UNDEFINED;

void mergeWith(final BottomTabsOptions other) {
if (other.currentTabId.hasValue()) currentTabId = other.currentTabId;
Expand All @@ -58,6 +60,7 @@ void mergeWith(final BottomTabsOptions other) {
if (other.backgroundColor.hasValue()) backgroundColor = other.backgroundColor;
if (other.testId.hasValue()) testId = other.testId;
if (other.titleDisplayMode.hasValue()) titleDisplayMode = other.titleDisplayMode;
if (other.tabsAttachMode.hasValue()) tabsAttachMode = other.tabsAttachMode;
}

void mergeWithDefault(final BottomTabsOptions defaultOptions) {
Expand All @@ -69,6 +72,7 @@ void mergeWithDefault(final BottomTabsOptions defaultOptions) {
if (!elevation.hasValue()) elevation = defaultOptions.elevation;
if (!backgroundColor.hasValue()) backgroundColor = defaultOptions.backgroundColor;
if (!titleDisplayMode.hasValue()) titleDisplayMode = defaultOptions.titleDisplayMode;
if (!tabsAttachMode.hasValue()) tabsAttachMode = defaultOptions.tabsAttachMode;
}

public void clearOneTimeOptions() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
import com.reactnativenavigation.viewcontrollers.ComponentViewController;
import com.reactnativenavigation.viewcontrollers.ViewController;
import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsAttacher;
import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsController;
import com.reactnativenavigation.viewcontrollers.externalcomponent.ExternalComponentCreator;
import com.reactnativenavigation.viewcontrollers.externalcomponent.ExternalComponentViewController;
Expand Down Expand Up @@ -193,6 +194,7 @@ private ViewController createBottomTabs(LayoutNode node) {
for (int i = 0; i < node.children.size(); i++) {
tabs.add(create(node.children.get(i)));
}
BottomTabsPresenter bottomTabsPresenter = new BottomTabsPresenter(tabs, defaultOptions);
return new BottomTabsController(activity,
tabs,
childRegistry,
Expand All @@ -201,7 +203,8 @@ private ViewController createBottomTabs(LayoutNode node) {
node.id,
parse(typefaceManager, node.getOptions()),
new Presenter(activity, defaultOptions),
new BottomTabsPresenter(tabs, defaultOptions),
new BottomTabsAttacher(tabs, bottomTabsPresenter),
bottomTabsPresenter,
new BottomTabPresenter(activity, tabs, new ImageLoader(), defaultOptions));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.reactnativenavigation.parse;

public enum TabsAttachMode {
TOGETHER,
AFTER_INITIAL_TAB,
ON_SWITCH_TO_TAB,
UNDEFINED;

public static TabsAttachMode fromString(String mode) {
switch (mode) {
case "together":
return TOGETHER;
case "afterInitialTab":
return AFTER_INITIAL_TAB;
case "onSwitchToTab":
return ON_SWITCH_TO_TAB;
default:
return UNDEFINED;
}
}

public boolean hasValue() {
return this != UNDEFINED;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class OverlayManager {

public void show(ViewGroup overlaysContainer, ViewController overlay, CommandListener listener) {
overlayRegistry.put(overlay.getId(), overlay);
overlay.setOnAppearedListener(() -> listener.onSuccess(overlay.getId()));
overlay.addOnAppearedListener(() -> listener.onSuccess(overlay.getId()));
overlaysContainer.addView(overlay.getView());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,27 @@ public static <T> List<T> merge(@Nullable Collection<T> a, @Nullable Collection<
}

public static <T> void forEach(@Nullable Collection<T> items, Apply<T> apply) {
if (items != null) forEach(new ArrayList(items), 0, apply);
}

public static <T> void forEach(@Nullable T[] items, Apply<T> apply) {
if (items == null) return;
for (T item : items) {
apply.on(item);
}
}

public static <T> void forEach(@Nullable List<T> items, Apply<T> apply) {
forEach(items, 0, apply);
}

public static <T> void forEach(@Nullable List<T> items, int startIndex, Apply<T> apply) {
if (items == null) return;
for (int i = startIndex; i < items.size(); i++) {
apply.on(items.get(i));
}
}

public static @Nullable <T> T first(@Nullable Collection<T> items, Filter<T> by) {
if (isNullOrEmpty(items)) return null;
for (T item : items) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@
import com.reactnativenavigation.views.Renderable;
import com.reactnativenavigation.views.element.Element;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static com.reactnativenavigation.utils.CollectionUtils.forEach;

public abstract class ViewController<T extends ViewGroup> implements ViewTreeObserver.OnGlobalLayoutListener, ViewGroup.OnHierarchyChangeListener {

private Runnable onAppearedListener;
private final List<Runnable> onAppearedListeners = new ArrayList();
private boolean appearEventPosted;
private boolean isFirstLayout = true;
private Bool waitForRender = new NullBool();
Expand All @@ -47,7 +50,7 @@ public interface ViewVisibilityListener {
boolean onViewDisappear(View view);
}

protected Options initialOptions;
public Options initialOptions;
public Options options;

private final Activity activity;
Expand Down Expand Up @@ -77,8 +80,12 @@ public void setWaitForRender(Bool waitForRender) {
this.waitForRender = waitForRender;
}

public void setOnAppearedListener(Runnable onAppearedListener) {
this.onAppearedListener = onAppearedListener;
public void addOnAppearedListener(Runnable onAppearedListener) {
onAppearedListeners.add(onAppearedListener);
}

public void removeOnAppearedListener(Runnable onAppearedListener) {
onAppearedListeners.remove(onAppearedListener);
}

protected abstract T createView();
Expand Down Expand Up @@ -199,11 +206,11 @@ public void onViewAppeared() {
parentController.clearOptions();
if (getView() instanceof Component) parentController.applyChildOptions(options, (Component) getView());
});
if (onAppearedListener != null && !appearEventPosted) {
if (!onAppearedListeners.isEmpty() && !appearEventPosted) {
appearEventPosted = true;
UiThread.post(() -> {
onAppearedListener.run();
onAppearedListener = null;
forEach(onAppearedListeners, Runnable::run);
onAppearedListeners.clear();
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.reactnativenavigation.viewcontrollers.bottomtabs;

import android.view.*;

import com.reactnativenavigation.parse.*;
import com.reactnativenavigation.presentation.*;
import com.reactnativenavigation.viewcontrollers.*;

import java.util.*;

import static com.reactnativenavigation.utils.CollectionUtils.filter;
import static com.reactnativenavigation.utils.CollectionUtils.forEach;

public class AfterInitialTab extends AttachMode {
private final Runnable attachOtherTabs;

public AfterInitialTab(ViewGroup parent, List<ViewController> tabs, BottomTabsPresenter presenter, Options resolved) {
super(parent, tabs, presenter, resolved);
attachOtherTabs = () -> forEach(otherTabs(), this::attach);
}

@Override
public void attach() {
initialTab.addOnAppearedListener(attachOtherTabs);
attach(initialTab);
}

@Override
public void destroy() {
initialTab.removeOnAppearedListener(attachOtherTabs);
}

private List<ViewController> otherTabs() {
return filter(tabs, t -> t != initialTab);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.reactnativenavigation.viewcontrollers.bottomtabs;

import android.support.annotation.*;
import android.view.*;
import android.widget.*;

import com.reactnativenavigation.parse.*;
import com.reactnativenavigation.presentation.*;
import com.reactnativenavigation.viewcontrollers.*;

import java.util.*;

import static android.view.ViewGroup.LayoutParams.*;

public abstract class AttachMode {
protected final ViewGroup parent;
protected final BottomTabsPresenter presenter;
protected final List<ViewController> tabs;
final ViewController initialTab;
private final Options resolved;


public static AttachMode get(ViewGroup parent, List<ViewController> tabs, BottomTabsPresenter presenter, Options resolved) {
switch (resolved.bottomTabsOptions.tabsAttachMode) {
case AFTER_INITIAL_TAB:
return new AfterInitialTab(parent, tabs, presenter, resolved);
case ON_SWITCH_TO_TAB:
return new OnSwitchToTab(parent, tabs, presenter, resolved);
case UNDEFINED:
case TOGETHER:
default:
return new Together(parent, tabs, presenter, resolved);
}
}

AttachMode(ViewGroup parent, List<ViewController> tabs, BottomTabsPresenter presenter, Options resolved) {
this.parent = parent;
this.tabs = tabs;
this.presenter = presenter;
this.resolved = resolved;
initialTab = tabs.get(resolved.bottomTabsOptions.currentTabIndex.get(0));
}

public abstract void attach();

public void destroy() {

}

public void onTabSelected(ViewController tab) {

}

@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
public void attach(ViewController tab) {
ViewGroup view = tab.getView();
view.setLayoutParams(new RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
presenter.applyLayoutParamsOptions(resolved, tabs.indexOf(tab));
view.setVisibility(tab == initialTab ? View.VISIBLE : View.INVISIBLE);
parent.addView(view);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.reactnativenavigation.viewcontrollers.bottomtabs;

import android.support.annotation.VisibleForTesting;
import android.view.ViewGroup;

import com.reactnativenavigation.parse.Options;
import com.reactnativenavigation.presentation.BottomTabsPresenter;
import com.reactnativenavigation.viewcontrollers.ViewController;

import java.util.List;

public class BottomTabsAttacher {
private final List<ViewController> tabs;
private final BottomTabsPresenter presenter;
@VisibleForTesting
AttachMode attachStrategy;

public BottomTabsAttacher(List<ViewController> tabs, BottomTabsPresenter presenter) {
this.tabs = tabs;
this.presenter = presenter;
}

void init(ViewGroup parent, Options resolved) {
attachStrategy = AttachMode.get(parent, tabs, presenter, resolved);
}

void attach() {
attachStrategy.attach();
}

public void destroy() {
attachStrategy.destroy();
}

void onTabSelected(ViewController tab) {
attachStrategy.onTabSelected(tab);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,16 @@ public class BottomTabsController extends ParentController implements AHBottomNa
private List<ViewController> tabs;
private EventEmitter eventEmitter;
private ImageLoader imageLoader;
private final BottomTabsAttacher tabsAttacher;
private BottomTabsPresenter presenter;
private BottomTabPresenter tabPresenter;

public BottomTabsController(Activity activity, List<ViewController> tabs, ChildControllersRegistry childRegistry, EventEmitter eventEmitter, ImageLoader imageLoader, String id, Options initialOptions, Presenter presenter, BottomTabsPresenter bottomTabsPresenter, BottomTabPresenter bottomTabPresenter) {
public BottomTabsController(Activity activity, List<ViewController> tabs, ChildControllersRegistry childRegistry, EventEmitter eventEmitter, ImageLoader imageLoader, String id, Options initialOptions, Presenter presenter, BottomTabsAttacher tabsAttacher, BottomTabsPresenter bottomTabsPresenter, BottomTabPresenter bottomTabPresenter) {
super(activity, childRegistry, id, presenter, initialOptions);
this.tabs = tabs;
this.eventEmitter = eventEmitter;
this.imageLoader = imageLoader;
this.tabsAttacher = tabsAttacher;
this.presenter = bottomTabsPresenter;
this.tabPresenter = bottomTabPresenter;
forEach(tabs, (tab) -> tab.setParentController(this));
Expand All @@ -63,14 +65,15 @@ public void setDefaultOptions(Options defaultOptions) {
protected ViewGroup createView() {
RelativeLayout root = new RelativeLayout(getActivity());
bottomTabs = createBottomTabs();
tabsAttacher.init(root, resolveCurrentOptions());
presenter.bindView(bottomTabs, this);
tabPresenter.bindView(bottomTabs);
bottomTabs.setOnTabSelectedListener(this);
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT);
lp.addRule(ALIGN_PARENT_BOTTOM);
root.addView(bottomTabs, lp);
bottomTabs.addItems(createTabs());
attachTabs(root);
tabsAttacher.attach();
return root;
}

Expand Down Expand Up @@ -155,17 +158,6 @@ private List<AHBottomNavigationItem> createTabs() {
});
}

private void attachTabs(RelativeLayout root) {
for (int i = 0; i < tabs.size(); i++) {
ViewGroup tab = tabs.get(i).getView();
tab.setLayoutParams(new RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
Options options = resolveCurrentOptions();
presenter.applyLayoutParamsOptions(options, i);
if (i != 0) tab.setVisibility(View.INVISIBLE);
root.addView(tab);
}
}

public int getSelectedIndex() {
return bottomTabs.getCurrentItem();
}
Expand All @@ -176,8 +168,15 @@ public Collection<ViewController> getChildControllers() {
return tabs;
}

@Override
public void destroy() {
tabsAttacher.destroy();
super.destroy();
}

@Override
public void selectTab(final int newIndex) {
tabsAttacher.onTabSelected(tabs.get(newIndex));
getCurrentView().setVisibility(View.INVISIBLE);
bottomTabs.setCurrentItem(newIndex, false);
getCurrentView().setVisibility(View.VISIBLE);
Expand Down
Loading

0 comments on commit 740ad3c

Please sign in to comment.