/* * Copyright 2017 Gabor Varadi * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhuinden.simplestack.navigator; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.View; import android.view.ViewGroup; import com.zhuinden.simplestack.Backstack; import com.zhuinden.simplestack.BackstackManager; import com.zhuinden.simplestack.DefaultKeyFilter; import com.zhuinden.simplestack.DefaultKeyParceler; import com.zhuinden.simplestack.DefaultStateClearStrategy; import com.zhuinden.simplestack.KeyFilter; import com.zhuinden.simplestack.KeyParceler; import com.zhuinden.simplestack.SavedState; import com.zhuinden.simplestack.StateChanger; import java.util.LinkedList; import java.util.List; /** * Convenience class to hide lifecycle integration using retained fragment. * Essentially, a replacement for BackstackDelegate. * * It can be either configured via {@link Navigator#configure()}, or installed with default settings using {@link Navigator#install(Activity, ViewGroup, List)}. */ @TargetApi(11) public class Navigator { private Navigator() { } /** * A configurer for {@link Navigator}. */ @TargetApi(11) public static class Installer { StateChanger stateChanger; KeyFilter keyFilter = new DefaultKeyFilter(); KeyParceler keyParceler = new DefaultKeyParceler(); BackstackManager.StateClearStrategy stateClearStrategy = new DefaultStateClearStrategy(); boolean isInitializeDeferred = false; boolean shouldPersistContainerChild = true; List<BackstackManager.StateChangeCompletionListener> stateChangeCompletionListeners = new LinkedList<>(); /** * Sets the state changer used by the navigator's backstack. * * If not set, then {@link DefaultStateChanger} is used, which by default behavior requires keys to be {@link StateKey}. * * @param stateChanger if set, cannot be null. * @return the installer */ public Installer setStateChanger(@NonNull StateChanger stateChanger) { if(stateChanger == null) { throw new IllegalArgumentException("If set, StateChanger cannot be null!"); } this.stateChanger = stateChanger; return this; } /** * Sets the key filter for filtering the state keys to be restored after process death. * * @param keyFilter cannot be null if set * @return the installer */ public Installer setKeyFilter(@NonNull KeyFilter keyFilter) { if(keyFilter == null) { throw new IllegalArgumentException("If set, KeyFilter cannot be null!"); } this.keyFilter = keyFilter; return this; } /** * Sets the key parceler for parcelling state keys. * * @param keyParceler cannot be null if set * @return the installer */ public Installer setKeyParceler(@NonNull KeyParceler keyParceler) { if(keyParceler == null) { throw new IllegalArgumentException("If set, KeyParceler cannot be null!"); } this.keyParceler = keyParceler; return this; } /** * Sets the state clear strategy used to clear the stored state in BackstackManager after there are no queued state changes left. * * @param stateClearStrategy if set, it cannot be null * @return the installer */ public Installer setStateClearStrategy(@NonNull BackstackManager.StateClearStrategy stateClearStrategy) { if(stateClearStrategy == null) { throw new IllegalArgumentException("If set, StateClearStrategy cannot be null!"); } this.stateClearStrategy = stateClearStrategy; return this; } /** * Sets if after initialization, the state changer should only be set when {@link Navigator#executeDeferredInitialization(Context)} is called. * Typically needed to setup the backstack for dependency injection module. * * @param isInitializeDeferred if call to executing deferred initialization is needed * @return the installer */ public Installer setDeferredInitialization(boolean isInitializeDeferred) { this.isInitializeDeferred = isInitializeDeferred; return this; } /** * Sets if the {@link BackstackHost} should persist the direct child of the provided container. * * @param shouldPersistContainerChild if the container's first child's state should be persisted * @return the installer */ public Installer setShouldPersistContainerChild(boolean shouldPersistContainerChild) { this.shouldPersistContainerChild = shouldPersistContainerChild; return this; } /** * Adds a {@link BackstackManager.StateChangeCompletionListener}, which will be added to the {@link BackstackManager} when it is initialized. * * @param stateChangeCompletionListener the state change completion listener * @return the installer */ public Installer addStateChangeCompletionListener(@NonNull BackstackManager.StateChangeCompletionListener stateChangeCompletionListener) { if(stateChangeCompletionListener == null) { throw new IllegalArgumentException("If added, state change completion listener cannot be null!"); } this.stateChangeCompletionListeners.add(stateChangeCompletionListener); return this; } /** * Installs the {@link BackstackHost}. * * @param activity the activity * @param container the container * @param initialKeys the initial keys. * @return */ public Backstack install(@NonNull Activity activity, @NonNull ViewGroup container, @NonNull List<Object> initialKeys) { if(stateChanger == null) { stateChanger = DefaultStateChanger.create(activity, container); } return Navigator.install(this, activity, container, initialKeys); } } /** * Creates an {@link Installer} to configure the {@link Navigator}. * * @return the installer */ public static Installer configure() { return new Installer(); } /** * Installs the {@link Navigator} with default parameters. * * This means that {@link DefaultStateChanger} and DefaultStateClearStrategy are used. * * @param activity the activity which will host the backstack * @param container the container in which custom viewgroups are hosted (to save its child's state in onSaveInstanceState()) * @param initialKeys the keys used to initialize the backstack */ public static void install(@NonNull Activity activity, @NonNull ViewGroup container, @NonNull List<Object> initialKeys) { configure().install(activity, container, initialKeys); } private static Backstack install(Installer installer, @NonNull Activity activity, @NonNull ViewGroup container, @NonNull List<Object> initialKeys) { if(activity == null) { throw new IllegalArgumentException("Activity cannot be null!"); } if(container == null) { throw new IllegalArgumentException("State changer cannot be null!"); } if(initialKeys == null || initialKeys.isEmpty()) { throw new IllegalArgumentException("Initial keys cannot be null!"); } BackstackHost backstackHost = findBackstackHost(activity); if(backstackHost == null) { backstackHost = new BackstackHost(); activity.getFragmentManager().beginTransaction().add(backstackHost, "NAVIGATOR_BACKSTACK_HOST").commit(); activity.getFragmentManager().executePendingTransactions(); } backstackHost.stateChanger = installer.stateChanger; backstackHost.keyFilter = installer.keyFilter; backstackHost.keyParceler = installer.keyParceler; backstackHost.stateClearStrategy = installer.stateClearStrategy; backstackHost.stateChangeCompletionListeners = installer.stateChangeCompletionListeners; backstackHost.shouldPersistContainerChild = installer.shouldPersistContainerChild; backstackHost.container = container; backstackHost.initialKeys = initialKeys; return backstackHost.initialize(installer.isInitializeDeferred); } /** * If {@link Installer#setDeferredInitialization(boolean)} was set to true, then this will initialize the backstack using the state changer. * * @param context the context to which an activity belongs that hosts the backstack */ public static void executeDeferredInitialization(Context context) { Activity activity = findActivity(context); BackstackHost backstackHost = findBackstackHost(activity); backstackHost.initialize(false); } /** * Gets the backstack that belongs to the Activity which hosts the backstack. * * @param context the context * @return the backstack */ public static Backstack getBackstack(Context context) { BackstackHost backstackHost = getBackstackHost(context); return backstackHost.getBackstack(); } /** * Delegates back press call to the backstack of the navigator. * * @param context the Context that belongs to an Activity which hosts the backstack. * @return true if a state change was handled or is in progress, false otherwise */ public static boolean onBackPressed(Context context) { return getBackstack(context).goBack(); } /** * A method to return the backstack manager, managed by the {@link BackstackHost}. * Typically not needed. * * @return the managed backstack manager that belongs to the {@link BackstackHost} inside the activity. */ public static BackstackManager getManager(Context context) { BackstackHost backstackHost = getBackstackHost(context); return backstackHost.getBackstackManager(); } /** * Persists the view hierarchy state and optional StateBundle. * * @param view the view (can be Bundleable) */ public static void persistViewToState(@Nullable View view) { if(view != null) { Context context = view.getContext(); BackstackHost backstackHost = getBackstackHost(context); backstackHost.getBackstackManager().persistViewToState(view); } } /** * Restores the view hierarchy state and optional StateBundle. * * @param view the view (can be Bundleable) */ public static void restoreViewFromState(@NonNull View view) { if(view == null) { throw new NullPointerException("You cannot restore state into null view!"); } Context context = view.getContext(); BackstackHost backstackHost = getBackstackHost(context); backstackHost.getBackstackManager().restoreViewFromState(view); } /** * Get the saved state for a given key. * * @param context the context to which an Activity belongs that hosts a backstack * @param key the key * @return the saved state */ public static SavedState getSavedState(@NonNull Context context, @NonNull Object key) { if(context == null) { throw new NullPointerException("context cannot be null"); } if(key == null) { throw new NullPointerException("key cannot be null"); } BackstackHost backstackHost = getBackstackHost(context); return backstackHost.getBackstackManager().getSavedState(key); } private static BackstackHost findBackstackHost(Activity activity) { return (BackstackHost) activity.getFragmentManager().findFragmentByTag("NAVIGATOR_BACKSTACK_HOST"); } private static Activity findActivity(Context context) { if(context instanceof Activity) { return (Activity) context; } else { ContextWrapper contextWrapper = (ContextWrapper) context; Context baseContext = contextWrapper.getBaseContext(); if(baseContext == null) { throw new IllegalStateException("Activity was not found as base context of view!"); } return findActivity(baseContext); } } private static BackstackHost getBackstackHost(Context context) { Activity activity = findActivity(context); return findBackstackHost(activity); } }