package com.wealthfront.magellan; import android.app.Activity; import android.app.Dialog; import android.content.Context; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.ColorRes; import android.support.annotation.StringRes; import android.support.annotation.VisibleForTesting; import android.util.SparseArray; import android.view.Menu; import android.view.ViewGroup; import static com.wealthfront.magellan.Preconditions.checkState; /** * Screens are where your logic lives (you can think of it as a Presenter in the MVP pattern, or a Controller * in the MVC pattern). * <p> * <b>Screens survive rotation.</b> * <p> * Here is a basic implementation of a Screen: * <pre> <code> * public class HomeScreen extends Screen<HomeView> { * {@literal @}Override * protected HomeView createView(Context context) { * return new HomeView(context); * } * {@literal @}Override * protected void onShow(Context context) { * getView().displaySomeData(); * } * } * </code> </pre> */ public abstract class Screen<V extends ViewGroup & ScreenView> implements BackHandler { public static final int DEFAULT_ACTION_BAR_COLOR_RES = 0; private static final String VIEW_STATE = "com.wealthfront.navigation.Screen.viewState"; private Activity activity; private Navigator navigator; private V view; private DialogCreator dialogCreator; private boolean dialogIsShowing; private Dialog dialog; private SparseArray<Parcelable> viewState; /** * @return the View associated with this Screen or null if we are not in between {@link #onShow(Context)} and\ * {@link #onHide(Context)}. */ public final V getView() { return view; } /** * @return the Activity associated with this Screen or null if we are not in between {@link #onShow(Context)} and\ * {@link #onHide(Context)}. */ public final Activity getActivity() { return activity; } /** * @return the Navigator associated with this Screen. */ public final Navigator getNavigator() { return navigator; } public final Dialog getDialog() { return dialog; } final void restore(Bundle savedInstanceState) { if (viewState == null && savedInstanceState != null) { viewState = savedInstanceState.getSparseParcelableArray(VIEW_STATE + hashCode()); } } final V recreateView(Activity activity, Navigator navigator) { this.activity = activity; this.navigator = navigator; view = createView(activity); // noinspection unchecked view.setScreen(this); if (viewState != null) { view.restoreHierarchyState(viewState); } return view; } final void save(Bundle outState) { saveViewState(); if (viewState != null) { outState.putSparseParcelableArray(VIEW_STATE + hashCode(), viewState); } viewState = null; } final void destroyView() { saveViewState(); activity = null; view = null; } private void saveViewState() { if (view != null) { viewState = new SparseArray<>(); view.saveHierarchyState(viewState); } } final void createDialog() { if (dialogCreator != null && dialogIsShowing) { dialog = dialogCreator.createDialog(activity); dialog.show(); } } final void destroyDialog() { if (dialog != null) { dialogIsShowing = dialog.isShowing(); dialog.setOnDismissListener(null); dialog.dismiss(); dialog = null; } } /** * @return true if we should show the ActionBar, false otherwise (true by default). */ protected boolean shouldShowActionBar() { return true; } /** * @return true if we should animate the ActionBar, false otherwise (true by default). */ protected boolean shouldAnimateActionBar() { return true; } public String getTitle(Context context) { return ""; } /** * @return the color of the ActionBar (invalid by default). */ @ColorRes protected int getActionBarColorRes() { return DEFAULT_ACTION_BAR_COLOR_RES; } protected void onRestore(Bundle savedInstanceState) {} /** * The only mandatory method to implement in a Screen. <b>Must</b> create and return a new instance of the View * to be displayed for this Screen. */ protected abstract V createView(Context context); /** * Override this method to dynamically change the menu. */ protected void onUpdateMenu(Menu menu) {} /** * Called when the Activity is resumed and when the Screen is shown. */ protected void onResume(Context context) {} /** * Called when the Screen in shown (including on rotation). */ protected void onShow(Context context) {} protected void onSave(Bundle outState) {} /** * Called when the Activity is paused and when the Screen is hidden. */ protected void onPause(Context context) {} /** * Called when the Screen is hidden (including on rotation). */ protected void onHide(Context context) {} /** * Finish the Activity, and therefore quit the app in a Single Activity Architecture. */ protected final boolean quit() { if (getActivity() != null) { getActivity().finish(); } return true; } protected final void setTitle(@StringRes int titleResId) { activity.setTitle(titleResId); } protected final void setTitle(CharSequence title) { activity.setTitle(title); } /** * Display a {@link Dialog} using a {@link DialogCreator}. The dialog will be automatically recreated and redisplayed * on rotation. */ protected final void showDialog(DialogCreator dialogCreator) { this.dialogCreator = dialogCreator; this.dialogIsShowing = true; createDialog(); } /** * Override this method to implement a custom behavior one back pressed. * * @return true if the method consumed the back event, false otherwise. */ @Override public boolean handleBack() { return false; } /** * @return a String representation of the Screen to be used for logging purposes. Return the Simple name of the class * by default. */ @Override public String toString() { return getClass().getSimpleName(); } @VisibleForTesting public final void setView(V view) { this.view = view; } @VisibleForTesting public final void setActivity(Activity activity) { this.activity = activity; } @VisibleForTesting public final void setNavigator(Navigator navigator) { this.navigator = navigator; } protected final void checkOnCreateNotYetCalled(String reason) { checkState(activity == null, reason); } }