/* * 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.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.zhuinden.simplestack.StateChange; import com.zhuinden.simplestack.StateChanger; import com.zhuinden.simplestack.navigator.changehandlers.NoOpViewChangeHandler; /** * A default state changer that handles view changes, and allows an optional external state changer (which is executed before the view change). * * For the default behavior to work, all keys must implement {@link StateKey}, which specifies a layout, and a {@link ViewChangeHandler}. * * But if {@link LayoutInflationStrategy} and {@link GetViewChangeHandlerStrategy} are re-defined, then this is no longer necessary. */ public final class DefaultStateChanger implements StateChanger { private static class NoOpStateChanger implements StateChanger { @Override public void handleStateChange(StateChange stateChange, Callback completionCallback) { completionCallback.stateChangeComplete(); } } private static class NoOpViewChangeStartListener implements ViewChangeStartListener { @Override public void handleViewChangeStart(@NonNull StateChange stateChange, @NonNull ViewGroup container, @Nullable View previousView, @NonNull View newView, @NonNull Callback startCallback) { startCallback.startViewChange(); } } private static class NoOpViewChangeCompletionListener implements ViewChangeCompletionListener { @Override public void handleViewChangeComplete(@NonNull StateChange stateChange, @NonNull ViewGroup container, @Nullable View previousView, @NonNull View newView, @NonNull Callback completionCallback) { completionCallback.viewChangeComplete(); } } private static class DefaultLayoutInflationStrategy implements LayoutInflationStrategy { @Override public void inflateLayout(StateChange stateChange, Object key, Context context, ViewGroup container, Callback callback) { final View newView = LayoutInflater.from(context).inflate(((StateKey) key).layout(), container, false); callback.layoutInflationComplete(newView); } } private static class NavigatorStatePersistenceStrategy implements StatePersistenceStrategy { @Override public void persistViewToState(@NonNull Object previousKey, @NonNull View previousView) { Navigator.persistViewToState(previousView); } @Override public void restoreViewFromState(@NonNull Object newKey, @NonNull View newView) { Navigator.restoreViewFromState(newView); } } private static class DefaultGetPreviousViewStrategy implements GetPreviousViewStrategy { @Nullable @Override public View getPreviousView(@NonNull ViewGroup container, @NonNull StateChange stateChange, @Nullable Object previousKey) { return container.getChildAt(0); } } private static class DefaultContextCreationStrategy implements ContextCreationStrategy { @NonNull @Override public Context createContext(@NonNull Context baseContext, @NonNull Object newKey, @NonNull ViewGroup container, @NonNull StateChange stateChange) { return stateChange.createContext(baseContext, newKey); } } private static class DefaultGetViewChangeHandlerStrategy implements GetViewChangeHandlerStrategy { @Override public ViewChangeHandler getViewChangeHandler(@NonNull StateChange stateChange, @NonNull ViewGroup container, @NonNull Object previousKey, @NonNull Object newKey, @NonNull View previousView, @NonNull View newView, int direction) { ViewChangeHandler viewChangeHandler; if(direction == StateChange.FORWARD) { viewChangeHandler = ((StateKey) newKey).viewChangeHandler(); } else if(previousKey != null && direction == StateChange.BACKWARD) { viewChangeHandler = ((StateKey) previousKey).viewChangeHandler(); } else { viewChangeHandler = NO_OP_VIEW_CHANGE_HANDLER; } return viewChangeHandler; } } /** * Allows the possibility of listening to when the view change is to start, after the views are inflated and state is restored, but before view change is started. */ public interface ViewChangeStartListener { /** * Notifies the {@link DefaultStateChanger} that the view change start listener completed its callback. */ public interface Callback { void startViewChange(); } /** * Called when a view change is to be started. * * @param stateChange the state change * @param container the container * @param previousView the previous view * @param newView the new view * @param startCallback the start callback that must be called once the view change callback is completed by the listener */ void handleViewChangeStart(@NonNull StateChange stateChange, @NonNull ViewGroup container, @Nullable View previousView, @NonNull View newView, @NonNull ViewChangeStartListener.Callback startCallback); } /** * Allows the possibility of listening to when the view change is completed. */ public interface ViewChangeCompletionListener { /** * Notifies the {@link DefaultStateChanger} that the view change completion listener completed its callback. */ public interface Callback { void viewChangeComplete(); } /** * Called when a view change is completed. * * @param stateChange the state change * @param container the container * @param previousView the previous view * @param newView the new view * @param completionCallback the completion callback that must be called once the view change callback is completed by the listener */ void handleViewChangeComplete(@NonNull StateChange stateChange, @NonNull ViewGroup container, @Nullable View previousView, @NonNull View newView, @NonNull Callback completionCallback); } /** * Allows the possibility of creating a custom context. */ public interface ContextCreationStrategy { /** * Creates the context used by layout inflation. * * @param baseContext the base context * @param newKey the new key * @param stateChange the state change * @return the new context */ @NonNull Context createContext(@NonNull Context baseContext, @NonNull Object newKey, @NonNull ViewGroup container, @NonNull StateChange stateChange); } /** * Allows specifying a custom way to obtain the view change handler for the view change. */ public interface GetViewChangeHandlerStrategy { /** * Gets the view change handler used for the view change, between the given keys and with specified direction. * * @param stateChange the state change * @param container the container * @param previousKey the previous key * @param newKey the new key * @param previousView the previous view * @param newView the new view * @param direction the direction * @return the view change handler */ ViewChangeHandler getViewChangeHandler(@NonNull StateChange stateChange, @NonNull ViewGroup container, @NonNull Object previousKey, @NonNull Object newKey, @NonNull View previousView, @NonNull View newView, int direction); } /** * Allows defining a custom way of determining what the previous view is. */ public interface GetPreviousViewStrategy { /** * Gets the previous view from the container. * * @param container the container * @param stateChange the state change * @param previousKey the previous key * @return the previous view */ @Nullable View getPreviousView(@NonNull ViewGroup container, @NonNull StateChange stateChange, @Nullable Object previousKey); } /** * Allows the possibility of using a custom layout inflation strategy for inflating the new view. */ public interface LayoutInflationStrategy { /** * This callback must be called to provide the inflated view. */ public interface Callback { void layoutInflationComplete(View view); } /** * This method needs to inflate the new view, preferably using the provided context. * @param stateChange the state change * @param key the new key this view is inflated for * @param context the context the layout inflater is originally acquired from * @param container the container * @param callback the inflation callback that must be called when layout inflation is complete */ void inflateLayout(StateChange stateChange, Object key, Context context, ViewGroup container, Callback callback); } /** * Allows replacing the default Navigator-based view state persistence with a custom one. */ public interface StatePersistenceStrategy { /** * Persists the previous active view's view state. * * @param previousKey the previous key * @param previousView the previous view */ void persistViewToState(@NonNull Object previousKey, @NonNull View previousView); /** * Restores the new active view's view state. * * @param newKey the new key * @param newView the new view */ void restoreViewFromState(@NonNull Object newKey, @NonNull View newView); } private static final NoOpViewChangeHandler NO_OP_VIEW_CHANGE_HANDLER = new NoOpViewChangeHandler(); private Context baseContext; private ViewGroup container; private StateChanger externalStateChanger; private ViewChangeStartListener viewChangeStartListener; private ViewChangeCompletionListener viewChangeCompletionListener; private LayoutInflationStrategy layoutInflationStrategy; private StatePersistenceStrategy statePersistenceStrategy; private GetViewChangeHandlerStrategy getViewChangeHandlerStrategy; private GetPreviousViewStrategy getPreviousViewStrategy; private ContextCreationStrategy contextCreationStrategy; /** * Used to configure the instance of the {@link DefaultStateChanger}. * * Allows setting an external state changer, which is executed before the view change. * Also allows setting a {@link ViewChangeCompletionListener} which is executed after the view change. */ public static class Configurer { StateChanger externalStateChanger = null; ViewChangeStartListener viewChangeStartListener = null; ViewChangeCompletionListener viewChangeCompletionListener = null; LayoutInflationStrategy layoutInflationStrategy = null; StatePersistenceStrategy statePersistenceStrategy = null; GetPreviousViewStrategy getPreviousViewStrategy = null; ContextCreationStrategy contextCreationStrategy = null; GetViewChangeHandlerStrategy getViewChangeHandlerStrategy = null; private Configurer() { } /** * Sets the external state changer. It is executed before the view change. * * @param stateChanger the state changer * @return the configurer */ public Configurer setExternalStateChanger(@NonNull StateChanger stateChanger) { if(stateChanger == null) { throw new NullPointerException("If set, external state changer cannot be null!"); } this.externalStateChanger = stateChanger; return this; } /** * Sets the {@link ViewChangeStartListener}. It is executed before the view change. * * @param viewChangeStartListener the view change start listener * @return the configurer */ public Configurer setViewChangeStartListener(@NonNull ViewChangeStartListener viewChangeStartListener) { if(viewChangeStartListener == null) { throw new NullPointerException("If set, view change start listener cannot be null!"); } this.viewChangeStartListener = viewChangeStartListener; return this; } /** * Sets the {@link ViewChangeCompletionListener}. It is executed after the view change. * * @param viewChangeCompletionListener the view change completion listener * @return the configurer */ public Configurer setViewChangeCompletionListener(@NonNull ViewChangeCompletionListener viewChangeCompletionListener) { if(viewChangeCompletionListener == null) { throw new NullPointerException("If set, view change completion listener cannot be null!"); } this.viewChangeCompletionListener = viewChangeCompletionListener; return this; } /** * Sets the {@link StatePersistenceStrategy}. It is used to persist and restore the view's state. * * @param statePersistenceStrategy the state persistence strategy * @return the configurer */ public Configurer setStatePersistenceStrategy(@NonNull StatePersistenceStrategy statePersistenceStrategy) { if(statePersistenceStrategy == null) { throw new NullPointerException("If set, state persistence strategy cannot be null!"); } this.statePersistenceStrategy = statePersistenceStrategy; return this; } /** * Sets the {@link LayoutInflationStrategy}. It is used to inflate the new view before a view change. * * @param layoutInflationStrategy the layout inflation strategy * @return the configurer */ public Configurer setLayoutInflationStrategy(@NonNull LayoutInflationStrategy layoutInflationStrategy) { if(layoutInflationStrategy == null) { throw new NullPointerException("If set, layout inflation strategy cannot be null!"); } this.layoutInflationStrategy = layoutInflationStrategy; return this; } /** * Sets the {@link GetPreviousViewStrategy}. It is used to obtain the previous view from the container. * * @param getPreviousViewStrategy the previous view strategy * @return the configurer */ public Configurer setGetPreviousViewStrategy(GetPreviousViewStrategy getPreviousViewStrategy) { if(getPreviousViewStrategy == null) { throw new NullPointerException("If set, get previous view strategy cannot be null!"); } this.getPreviousViewStrategy = getPreviousViewStrategy; return this; } /** * Sets the {@link ContextCreationStrategy}. It is used to create the new context for the new view. * * @param contextCreationStrategy the create context strategy * @return the configurer */ public Configurer setContextCreationStrategy(ContextCreationStrategy contextCreationStrategy) { if(contextCreationStrategy == null) { throw new NullPointerException("If set, create context strategy cannot be null!"); } this.contextCreationStrategy = contextCreationStrategy; return this; } /** * Sets the {@link GetViewChangeHandlerStrategy}. It is used to obtain the view change handler for a view change. * * @param getViewChangeHandlerStrategy the get view change handler strategy * @return the configurer */ public Configurer setGetViewChangeHandlerStrategy(GetViewChangeHandlerStrategy getViewChangeHandlerStrategy) { if(getViewChangeHandlerStrategy == null) { throw new NullPointerException("If set, get view change handler strategy cannot be null!"); } this.getViewChangeHandlerStrategy = getViewChangeHandlerStrategy; return this; } /** * Creates the {@link DefaultStateChanger} with the specified parameters. * * @param baseContext the base context used to inflate the views * @param container the container into which views are added and removed from * @return the new {@link DefaultStateChanger} */ public DefaultStateChanger create(Context baseContext, ViewGroup container) { return new DefaultStateChanger(baseContext, container, externalStateChanger, viewChangeStartListener, viewChangeCompletionListener, layoutInflationStrategy, statePersistenceStrategy, getPreviousViewStrategy, contextCreationStrategy, getViewChangeHandlerStrategy); } } /** * Factory method to create a configured {@link DefaultStateChanger}. * You can set an external state changer which is executed before the view change, and a {@link ViewChangeCompletionListener} that is executed after view change. * * @return the {@link Configurer} */ public static Configurer configure() { return new Configurer(); } /** * Factory method to create the {@link DefaultStateChanger} with default configuration. * * To add additional configuration such as external state changer or {@link ViewChangeCompletionListener}, use the {@link DefaultStateChanger#configure()} method. * * @param baseContext the base context used to inflate views * @param container the container into which views are added to or removed from * @return the state changer */ public static DefaultStateChanger create(Context baseContext, ViewGroup container) { return new DefaultStateChanger(baseContext, container, null, null, null, null, null, null, null, null); } DefaultStateChanger(@NonNull Context baseContext, @NonNull ViewGroup container, @Nullable StateChanger externalStateChanger, ViewChangeStartListener viewChangeStartListener, @Nullable ViewChangeCompletionListener viewChangeCompletionListener, @Nullable LayoutInflationStrategy layoutInflationStrategy, @Nullable StatePersistenceStrategy statePersistenceStrategy, @Nullable GetPreviousViewStrategy getPreviousViewStrategy, @Nullable ContextCreationStrategy contextCreationStrategy, GetViewChangeHandlerStrategy getViewChangeHandlerStrategy) { if(baseContext == null) { throw new NullPointerException("baseContext cannot be null"); } if(container == null) { throw new NullPointerException("container cannot be null"); } this.baseContext = baseContext; this.container = container; if(externalStateChanger == null) { externalStateChanger = new NoOpStateChanger(); } this.externalStateChanger = externalStateChanger; if(viewChangeStartListener == null) { viewChangeStartListener = new NoOpViewChangeStartListener(); } this.viewChangeStartListener = viewChangeStartListener; if(viewChangeCompletionListener == null) { viewChangeCompletionListener = new NoOpViewChangeCompletionListener(); } this.viewChangeCompletionListener = viewChangeCompletionListener; if(layoutInflationStrategy == null) { layoutInflationStrategy = new DefaultLayoutInflationStrategy(); } this.layoutInflationStrategy = layoutInflationStrategy; if(statePersistenceStrategy == null) { statePersistenceStrategy = new NavigatorStatePersistenceStrategy(); } this.statePersistenceStrategy = statePersistenceStrategy; if(getPreviousViewStrategy == null) { getPreviousViewStrategy = new DefaultGetPreviousViewStrategy(); } this.getPreviousViewStrategy = getPreviousViewStrategy; if(contextCreationStrategy == null) { contextCreationStrategy = new DefaultContextCreationStrategy(); } this.contextCreationStrategy = contextCreationStrategy; if(getViewChangeHandlerStrategy == null) { getViewChangeHandlerStrategy = new DefaultGetViewChangeHandlerStrategy(); } this.getViewChangeHandlerStrategy = getViewChangeHandlerStrategy; } private void finishStateChange(StateChange stateChange, ViewGroup container, View previousView, View newView, final Callback completionCallback) { viewChangeCompletionListener.handleViewChangeComplete(stateChange, container, previousView, newView, new ViewChangeCompletionListener.Callback() { @Override public void viewChangeComplete() { completionCallback.stateChangeComplete(); } }); } @Override public final void handleStateChange(final StateChange stateChange, final Callback completionCallback) { externalStateChanger.handleStateChange(stateChange, new Callback() { @Override public void stateChangeComplete() { if(stateChange.topNewState().equals(stateChange.topPreviousState())) { completionCallback.stateChangeComplete(); return; } performViewChange(stateChange.topPreviousState(), stateChange.topNewState(), stateChange, completionCallback); } }); } /** * Handles the view change using the provided parameters. The direction is specified by the direction in the state change. * * @param previousKey the previous key * @param newKey the new key * @param stateChange the state change * @param completionCallback the completion callback */ public final void performViewChange(Object previousKey, Object newKey, final StateChange stateChange, final Callback completionCallback) { performViewChange(previousKey, newKey, stateChange, stateChange.getDirection(), completionCallback); } /** * Handles the view change using the provided parameters. The direction is also manually provided. * * @param previousKey the previous key * @param newKey the new key * @param stateChange the state change * @param direction the direction * @param completionCallback the completion callback */ public void performViewChange(final Object previousKey, final Object newKey, final StateChange stateChange, final int direction, final Callback completionCallback) { final View previousView = getPreviousViewStrategy.getPreviousView(container, stateChange, previousKey); if(previousView != null && previousKey != null) { statePersistenceStrategy.persistViewToState(previousKey, previousView); } Context newContext = contextCreationStrategy.createContext(stateChange.createContext(baseContext, newKey), newKey, container, stateChange); layoutInflationStrategy.inflateLayout(stateChange, newKey, newContext, container, new LayoutInflationStrategy.Callback() { @Override public void layoutInflationComplete(final View newView) { statePersistenceStrategy.restoreViewFromState(newKey, newView); viewChangeStartListener.handleViewChangeStart(stateChange, container, previousView, newView, new ViewChangeStartListener.Callback() { @Override public void startViewChange() { if(previousView == null) { container.addView(newView); finishStateChange(stateChange, container, previousView, newView, completionCallback); } else { final ViewChangeHandler viewChangeHandler = getViewChangeHandlerStrategy.getViewChangeHandler( stateChange, container, previousKey, newKey, previousView, newView, direction); viewChangeHandler.performViewChange(container, previousView, newView, direction, new ViewChangeHandler.CompletionCallback() { @Override public void onCompleted() { finishStateChange(stateChange, container, previousView, newView, completionCallback); } }); } } }); } }); } }