From 90b7add15305d54409a14b512ff51366ba65fa90 Mon Sep 17 00:00:00 2001 From: Chris Hanson Date: Wed, 21 Dec 2016 16:48:56 -0800 Subject: [PATCH] Recreate views on config changes This is typically necessary when the Activity has specified in the manifest that it will handle configuration changes such as an orientation change. If the top Controller.setRecreateViewOnConfigChange(true) has been called before 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 RetainViewMode of RetainViewMode#RETAIN_DETACH. --- .../bluelinelabs/conductor/Controller.java | 43 +++++++++ .../com/bluelinelabs/conductor/Router.java | 35 ++++++++ .../conductor/internal/LifecycleHandler.java | 10 +++ .../bluelinelabs/conductor/RouterTests.java | 88 +++++++++++++++++++ 4 files changed, 176 insertions(+) diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java b/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java index e8b34d18..1f774664 100755 --- a/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java @@ -88,6 +88,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 @@ -601,6 +602,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. @@ -878,6 +906,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 cc8c9b1c..d6c6aaeb 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; @@ -564,6 +565,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); + } + + for (RouterTransaction transaction : backstack) { + if (transaction == topTransaction) { + continue; + } + + Controller controller = transaction.controller; + if (controller.isRecreateViewOnConfigChange() + && controller.getRetainViewMode() == Controller.RetainViewMode.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 eae0661a..a6e3e518 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 com.bluelinelabs.conductor.util.ActivityProxy; import com.bluelinelabs.conductor.util.ListUtils; import com.bluelinelabs.conductor.util.MockChangeHandler; @@ -11,10 +13,13 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import java.util.ArrayList; import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -312,4 +317,87 @@ public void testReplaceTopControllerWithNoRemoveViewOnPush() { assertTrue(newTopTransaction.controller.isAttached()); } + @Test + public void testRecreateViewsOnConfigChange_notRecreatesTopControllerView() { + RouterTransaction rootTransaction = RouterTransaction.with(new TestController()); + RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler()); + + 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(); + + assertNotNull(originalTopView); + assertNotNull(newTopView); + assertEquals(originalTopView, newTopView); + } + + @Test + public void testRecreateViewsOnConfigChange_recreatesTopControllerView() { + RouterTransaction rootTransaction = RouterTransaction.with(new TestController()); + RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler()); + 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(); + + assertNotNull(originalTopView); + assertNotNull(newTopView); + assertNotEquals(originalTopView, newTopView); + } + + @Test + public void testRecreateViewsOnConfigChange_notRecreatesLowerControllerView() { + RouterTransaction rootTransaction = RouterTransaction.with(new TestController()); + RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler()); + + 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(); + + assertNotNull(originalRootView); + assertNotNull(newRootView); + assertEquals(originalRootView, newRootView); + } + + @Test + public void testRecreateViewsOnConfigChange_recreatesLowerControllerView() { + RouterTransaction rootTransaction = RouterTransaction.with(new TestController()); + RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler()); + 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(); + + assertNotNull(originalRootView); + assertNotNull(newRootView); + assertNotEquals(originalRootView, newRootView); + } + }