diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java b/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java index e4da2655..90fc2034 100755 --- a/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java @@ -87,6 +87,7 @@ public abstract class Controller { private final ArrayList onRouterSetListeners = new ArrayList<>(); private final List childBackstack = new LinkedList<>(); private WeakReference destroyedView; + private boolean recreateViewOnConfigChange; private final OnControllerPushedListener onControllerPushedListener = new OnControllerPushedListener() { @Override @@ -600,6 +601,33 @@ public void setRetainViewMode(@NonNull RetainViewMode retainViewMode) { } } + /** + * Returns whether this controller should recreate its view when configuration changes. + * + * @see #setRecreateViewOnConfigChange(boolean) + */ + public boolean isRecreateViewOnConfigChange() { + return recreateViewOnConfigChange; + } + + /** + * Sets whether this Controller should recreate its view when configuration changes. This is + * typically necessary when the Activity has specified in the manifest that it will handle + * configuration changes such as an orientation change. + * + * For this method to have any effect, the Activity should set a value for {@code android:configChanges} + * in the manifest. Otherwise, the Activity will be destroyed and recreated, and there will be + * no need to explicitly recreate the view outside of that. + * + * If the top Controller has set this to {@code true} when a configuration change happens, that + * Controller's current view will be removed and a recreated view will be added to the parent. + * For Controllers that are lower in the stack, those Controllers' view will only be recreated + * if the Controller also has a {@link RetainViewMode} of {@link RetainViewMode#RETAIN_DETACH}. + */ + public void setRecreateViewOnConfigChange(boolean recreateViewOnConfigChange) { + this.recreateViewOnConfigChange = recreateViewOnConfigChange; + } + /** * Returns the {@link ControllerChangeHandler} that should be used for pushing this Controller, or null * if the handler from the {@link RouterTransaction} should be used instead. @@ -876,6 +904,21 @@ final View inflate(@NonNull ViewGroup parent) { removeViewReference(); } + return inflateAfterCheckingView(parent); + } + + final View reinflate(@NonNull ViewGroup parent) { + if (view != null) { + detach(view, true); + removeViewReference(); + } + + view = null; + + return inflateAfterCheckingView(parent); + } + + private View inflateAfterCheckingView(@NonNull ViewGroup parent) { if (view == null) { List listeners = new ArrayList<>(lifecycleListeners); for (LifecycleListener lifecycleListener : listeners) { diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/Router.java b/conductor/src/main/java/com/bluelinelabs/conductor/Router.java index f4774845..35c2f8c2 100755 --- a/conductor/src/main/java/com/bluelinelabs/conductor/Router.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/Router.java @@ -2,6 +2,7 @@ import android.app.Activity; import android.content.Intent; +import android.content.res.Configuration; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -22,6 +23,8 @@ import java.util.Iterator; import java.util.List; +import static com.bluelinelabs.conductor.Controller.RetainViewMode.RETAIN_DETACH; + /** * A Router implements navigation and backstack handling for {@link Controller}s. Router objects are attached * to Activity/containing ViewGroup pairs. Routers do not directly render or push Views to the container ViewGroup, @@ -570,6 +573,40 @@ public final boolean onOptionsItemSelected(@NonNull MenuItem item) { return false; } + /** + * @see Controller#setRecreateViewOnConfigChange(boolean) + */ + public final void onConfigurationChanged(Configuration newConfig) { + if (backstack.isEmpty()) { + return; + } + + RouterTransaction topTransaction = backstack.peek(); + Controller topController = topTransaction.controller; + if (topController.isRecreateViewOnConfigChange()) { + View currentView = topController.getView(); + if (currentView != null) { + container.removeView(currentView); + } + + View newView = topController.reinflate(container); + container.addView(newView); + } + + Iterator backstackIterator = backstack.iterator(); + while (backstackIterator.hasNext()) { + RouterTransaction transaction = backstackIterator.next(); + if (transaction == topTransaction) { + continue; + } + + Controller controller = transaction.controller; + if (controller.isRecreateViewOnConfigChange() && controller.getRetainViewMode() == RETAIN_DETACH) { + controller.reinflate(container); + } + } + } + private void popToTransaction(@NonNull RouterTransaction transaction, @Nullable ControllerChangeHandler changeHandler) { RouterTransaction topTransaction = backstack.peek(); List poppedTransactions = backstack.popTo(transaction); diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/internal/LifecycleHandler.java b/conductor/src/main/java/com/bluelinelabs/conductor/internal/LifecycleHandler.java index 456b2ec6..946f01e3 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/internal/LifecycleHandler.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/internal/LifecycleHandler.java @@ -6,6 +6,7 @@ import android.app.Fragment; import android.content.Context; import android.content.Intent; +import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.os.Parcel; @@ -261,6 +262,15 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + for (Router router : routerMap.values()) { + router.onConfigurationChanged(newConfig); + } + } + public void registerForActivityResult(@NonNull String instanceId, int requestCode) { activityRequestMap.put(requestCode, instanceId); } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/RouterTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/RouterTests.java index 4195b340..19cd8d73 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/RouterTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/RouterTests.java @@ -1,5 +1,7 @@ package com.bluelinelabs.conductor; +import android.view.View; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -318,4 +320,87 @@ public void testReplaceTopControllerWithNoRemoveViewOnPush() { Assert.assertTrue(newTopTransaction.controller.isAttached()); } + @Test + public void testRecreateViewsOnConfigChange_notRecreatesTopControllerView() { + RouterTransaction rootTransaction = RouterTransaction.with(new TestController()); + RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false)); + + List backstack = new ArrayList<>(); + backstack.add(rootTransaction); + backstack.add(topTransaction); + + router.setBackstack(backstack, null); + + View originalTopView = topTransaction.controller.getView(); + router.onConfigurationChanged(null); + View newTopView = topTransaction.controller.getView(); + + Assert.assertNotNull(originalTopView); + Assert.assertNotNull(newTopView); + Assert.assertEquals(originalTopView, newTopView); + } + + @Test + public void testRecreateViewsOnConfigChange_recreatesTopControllerView() { + RouterTransaction rootTransaction = RouterTransaction.with(new TestController()); + RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false)); + topTransaction.controller.setRecreateViewOnConfigChange(true); + + List backstack = new ArrayList<>(); + backstack.add(rootTransaction); + backstack.add(topTransaction); + + router.setBackstack(backstack, null); + + View originalTopView = topTransaction.controller.getView(); + router.onConfigurationChanged(null); + View newTopView = topTransaction.controller.getView(); + + Assert.assertNotNull(originalTopView); + Assert.assertNotNull(newTopView); + Assert.assertNotEquals(originalTopView, newTopView); + } + + @Test + public void testRecreateViewsOnConfigChange_notRecreatesLowerControllerView() { + RouterTransaction rootTransaction = RouterTransaction.with(new TestController()); + RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false)); + + List backstack = new ArrayList<>(); + backstack.add(rootTransaction); + backstack.add(topTransaction); + + router.setBackstack(backstack, null); + + View originalRootView = rootTransaction.controller.getView(); + router.onConfigurationChanged(null); + View newRootView = rootTransaction.controller.getView(); + + Assert.assertNotNull(originalRootView); + Assert.assertNotNull(newRootView); + Assert.assertEquals(originalRootView, newRootView); + } + + @Test + public void testRecreateViewsOnConfigChange_recreatesLowerControllerView() { + RouterTransaction rootTransaction = RouterTransaction.with(new TestController()); + RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false)); + rootTransaction.controller.setRecreateViewOnConfigChange(true); + rootTransaction.controller.setRetainViewMode(Controller.RetainViewMode.RETAIN_DETACH); + + List backstack = new ArrayList<>(); + backstack.add(rootTransaction); + backstack.add(topTransaction); + + router.setBackstack(backstack, null); + + View originalRootView = rootTransaction.controller.getView(); + router.onConfigurationChanged(null); + View newRootView = rootTransaction.controller.getView(); + + Assert.assertNotNull(originalRootView); + Assert.assertNotNull(newRootView); + Assert.assertNotEquals(originalRootView, newRootView); + } + }