From 5c3366b51db364097fe753ac1c045223506f7f1e 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 | 37 ++++++++ .../conductor/internal/LifecycleHandler.java | 10 +++ .../bluelinelabs/conductor/RouterTests.java | 85 +++++++++++++++++++ 4 files changed, 175 insertions(+) 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); + } + }