Compare commits

...

13 Commits

Author SHA1 Message Date
Eric Kuck 6d4b5a5ef6 Version bump 2016-07-12 13:20:30 -05:00
Eric Kuck ae27c5e453 Another test for #86 2016-07-11 16:30:31 -05:00
Eric Kuck e4d23a7c74 Added some tests around #86 2016-07-11 16:24:07 -05:00
Eric Kuck 778cdcfd58 Controllers now properly persist their retain view modes 2016-07-11 13:37:38 -05:00
Eric Kuck 5e730947aa Corrected logic for first fix on #86 2016-07-11 13:37:22 -05:00
Eric Kuck 71b10c7365 Fixed back handling on child controllers demo 2016-07-08 15:08:05 -05:00
Eric Kuck 8c323b9613 Fixes issue with demo app trying to reuse change handlers 2016-07-08 14:59:19 -05:00
Eric Kuck af45aae110 Fixes 2nd issue associated with #86 2016-07-08 13:33:47 -05:00
Hannes Dorfmann d4c7e5791e Added oss sonatype snapshot url (#90) 2016-07-08 08:23:30 -05:00
Eric Kuck dc4c3a9709 Fixes at least one cause of #86 2016-07-07 20:59:37 -05:00
Eric Kuck ef84fbd547 AnimatorChangeHandlers are now properly cancelable 2016-07-07 18:32:04 -05:00
Eric Kuck 431569763e First pass at a fix for #85 2016-07-07 17:43:26 -05:00
Mykhailo Shchurov 7b5bab3681 Fixed typo in readme (#83) 2016-06-30 07:25:21 -05:00
12 changed files with 373 additions and 42 deletions
-1
View File
@@ -15,7 +15,6 @@ jdk:
branches:
only
- v2
- develop
- master
+18 -7
View File
@@ -20,22 +20,33 @@ Conductor is architecture-agnostic and does not try to force any design decision
## Installation
```gradle
compile 'com.bluelinelabs:conductor:2.0.0'
compile 'com.bluelinelabs:conductor:2.0.1'
// If you want the components that go along with
// Android's support libraries (currently just a PagerAdapter):
compile 'com.bluelinelabs:conductor-support:2.0.0'
compile 'com.bluelinelabs:conductor-support:2.0.1'
// If you want RxJava/RxAndroid lifecycle support:
compile 'com.bluelinelabs:conductor-rxlifecycle:2.0.0'
compile 'com.bluelinelabs:conductor-rxlifecycle:2.0.1'
```
SNAPSHOT:
```gradle
compile 'com.bluelinelabs:conductor:2.0.1-SNAPSHOT'
compile 'com.bluelinelabs:conductor-support:2.0.1-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle:2.0.1-SNAPSHOT'
compile 'com.bluelinelabs:conductor:2.0.2-SNAPSHOT'
compile 'com.bluelinelabs:conductor-support:2.0.2-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle:2.0.2-SNAPSHOT'
```
You also have to add the url to the snapshot repository:
```gradle
allprojects {
repositories {
...
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
}
```
## Components to Know
@@ -114,7 +125,7 @@ The lifecycle of a Controller is significantly simpler to understand than that o
`ControllerChangeHandler` can be subclassed in order to perform different functions when changing between two `Controllers`. Two convenience `ControllerChangeHandler` subclasses are included to cover most basic needs: `AnimatorChangeHandler`, which will use an `Animator` object to transition between two views, and `TransitionChangeHandler`, which will use Lollipop's `Transition` framework for transitioning between views.
### Child Routers & Controllers
`getChildController` can be called on a `Controller` in order to get a nested `Router` into which child `Controller`s can be pushed. This enables creating advanced layouts, such as Master/Detail.
`getChildRouter` can be called on a `Controller` in order to get a nested `Router` into which child `Controller`s can be pushed. This enables creating advanced layouts, such as Master/Detail.
### RxJava Lifecycle
If the RxLifecycle dependency has been added, there is an `RxController` available that can be used along with the standard [RxLifecycle library](https://github.com/trello/RxLifecycle). There is also a `ControllerLifecycleProvider` available if you do not wish to use this subclass.
@@ -54,6 +54,7 @@ public abstract class Controller {
private static final String KEY_OVERRIDDEN_POP_HANDLER = "Controller.overriddenPopHandler";
private static final String KEY_VIEW_STATE_HIERARCHY = "Controller.viewState.hierarchy";
private static final String KEY_VIEW_STATE_BUNDLE = "Controller.viewState.bundle";
private static final String KEY_RETAIN_VIEW_MODE = "Controller.retainViewMode";
private final Bundle args;
@@ -692,6 +693,8 @@ public abstract class Controller {
final void activityResumed(Activity activity) {
if (!attached && view != null && viewIsAttached) {
attach(view);
} else if (attached) {
needsAttach = false;
}
onActivityResumed(activity);
@@ -734,7 +737,7 @@ public abstract class Controller {
}
}
private void detach(@NonNull View view, boolean forceViewRefRemoval) {
void detach(@NonNull View view, boolean forceViewRefRemoval) {
for (ControllerHostedRouter router : childRouters) {
router.prepareForHostDetach();
}
@@ -908,6 +911,7 @@ public abstract class Controller {
if (containerView != null && containerView instanceof ViewGroup) {
childRouter.setHost(this, (ViewGroup)containerView);
monitorChildRouter(childRouter);
childRouter.rebindIfNeeded();
}
}
@@ -932,6 +936,7 @@ public abstract class Controller {
outState.putString(KEY_TARGET_INSTANCE_ID, targetInstanceId);
outState.putStringArrayList(KEY_REQUESTED_PERMISSIONS, requestedPermissions);
outState.putBoolean(KEY_NEEDS_ATTACH, needsAttach || attached);
outState.putInt(KEY_RETAIN_VIEW_MODE, retainViewMode.ordinal());
if (overriddenPushHandler != null) {
outState.putBundle(KEY_OVERRIDDEN_PUSH_HANDLER, overriddenPushHandler.toBundle());
@@ -968,6 +973,7 @@ public abstract class Controller {
overriddenPushHandler = ControllerChangeHandler.fromBundle(savedInstanceState.getBundle(KEY_OVERRIDDEN_PUSH_HANDLER));
overriddenPopHandler = ControllerChangeHandler.fromBundle(savedInstanceState.getBundle(KEY_OVERRIDDEN_POP_HANDLER));
needsAttach = savedInstanceState.getBoolean(KEY_NEEDS_ATTACH);
retainViewMode = RetainViewMode.values()[savedInstanceState.getInt(KEY_RETAIN_VIEW_MODE, 0)];
List<Bundle> childBundles = savedInstanceState.getParcelableArrayList(KEY_CHILD_ROUTERS);
for (Bundle childBundle : childBundles) {
@@ -1036,28 +1042,19 @@ public abstract class Controller {
}
final void createOptionsMenu(Menu menu, MenuInflater inflater) {
if (attached) {
if (hasOptionsMenu && !optionsMenuHidden) {
onCreateOptionsMenu(menu, inflater);
}
if (attached && hasOptionsMenu && !optionsMenuHidden) {
onCreateOptionsMenu(menu, inflater);
}
}
final void prepareOptionsMenu(Menu menu) {
if (attached) {
if (hasOptionsMenu && !optionsMenuHidden) {
onPrepareOptionsMenu(menu);
}
if (attached && hasOptionsMenu && !optionsMenuHidden) {
onPrepareOptionsMenu(menu);
}
}
final boolean optionsItemSelected(MenuItem item) {
if (attached) {
if (hasOptionsMenu && !optionsMenuHidden && onOptionsItemSelected(item)) {
return true;
}
}
return false;
return attached && hasOptionsMenu && !optionsMenuHidden && onOptionsItemSelected(item);
}
private void monitorChildRouter(ControllerHostedRouter childRouter) {
@@ -1137,6 +1134,7 @@ public abstract class Controller {
public void onSaveViewState(@NonNull Controller controller, @NonNull Bundle outState) { }
public void onRestoreViewState(@NonNull Controller controller, @NonNull Bundle savedViewState) { }
}
}
@@ -138,7 +138,6 @@ public abstract class ControllerChangeHandler {
handler.performChange(container, fromView, toView, isPush, new ControllerChangeCompletedListener() {
@Override
public void onChangeCompleted() {
if (from != null) {
from.changeEnded(handler, fromChangeType);
}
@@ -12,7 +12,7 @@ import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListen
import java.util.ArrayList;
import java.util.List;
public class ControllerHostedRouter extends Router {
class ControllerHostedRouter extends Router {
private final String KEY_HOST_ID = "ControllerHostedRouter.hostId";
private final String KEY_TAG = "ControllerHostedRouter.tag";
@@ -22,14 +22,14 @@ public class ControllerHostedRouter extends Router {
@IdRes private int hostId;
private String tag;
public ControllerHostedRouter() { }
ControllerHostedRouter() { }
public ControllerHostedRouter(int hostId, String tag) {
ControllerHostedRouter(int hostId, String tag) {
this.hostId = hostId;
this.tag = tag;
}
public final void setHost(@NonNull Controller controller, @NonNull ViewGroup container) {
final void setHost(@NonNull Controller controller, @NonNull ViewGroup container) {
if (hostController != controller || this.container != container) {
removeHost();
@@ -42,17 +42,28 @@ public class ControllerHostedRouter extends Router {
}
}
public final void removeHost() {
final void removeHost() {
if (container != null && container instanceof ControllerChangeListener) {
removeChangeListener((ControllerChangeListener)container);
}
for (Controller controller : destroyingControllers) {
if (controller.getView() != null) {
controller.detach(controller.getView(), true);
}
}
for (RouterTransaction transaction : backStack) {
if (transaction.controller.getView() != null) {
transaction.controller.detach(transaction.controller.getView(), true);
}
}
prepareForContainerRemoval();
hostController = null;
container = null;
}
public final void setDetachFrozen(boolean frozen) {
final void setDetachFrozen(boolean frozen) {
for (RouterTransaction transaction : backStack) {
transaction.controller.setDetachFrozen(frozen);
}
@@ -153,11 +164,11 @@ public class ControllerHostedRouter extends Router {
controller.setParentController(hostController);
}
public int getHostId() {
int getHostId() {
return hostId;
}
public String getTag() {
String getTag() {
return tag;
}
@@ -31,7 +31,7 @@ public abstract class Router {
protected final Backstack backStack = new Backstack();
private final List<ControllerChangeListener> changeListeners = new ArrayList<>();
private final List<Controller> destroyingControllers = new ArrayList<>();
final List<Controller> destroyingControllers = new ArrayList<>();
private boolean popsLastView = false;
@@ -124,7 +124,7 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
}
}
private void performAnimation(@NonNull final ViewGroup container, final View from, View to, final boolean isPush, final boolean toAddedToContainer, @NonNull final ControllerChangeCompletedListener changeListener) {
private void performAnimation(@NonNull final ViewGroup container, final View from, final View to, final boolean isPush, final boolean toAddedToContainer, @NonNull final ControllerChangeCompletedListener changeListener) {
if (canceled) {
changeListener.onChangeCompleted();
return;
@@ -144,7 +144,7 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
@Override
public void onAnimationEnd(Animator animation) {
if (from != null && (!isPush || removesFromViewOnPush)) {
if (from != null && (!isPush || removesFromViewOnPush) && !canceled) {
container.removeView(from);
}
@@ -0,0 +1,306 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeCompletedListener;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.util.ActivityController;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ReattachCaseTests {
private ActivityController<TestActivity> activityController;
private Router router;
public void createActivityController(Bundle savedInstanceState) {
activityController = Robolectric.buildActivity(TestActivity.class).create(savedInstanceState).start();
@IdRes int containerId = 4;
FrameLayout routerContainer = new FrameLayout(activityController.get());
routerContainer.setId(containerId);
router = Conductor.attachRouter(activityController.get(), routerContainer, savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Before
public void setup() {
createActivityController(null);
}
@Test
public void testNeedsAttachingOnPauseAndOrientation() {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(getPushHandler())
.popChangeHandler(getPopHandler()));
Assert.assertTrue(controllerA.isAttached());
Assert.assertFalse(controllerB.isAttached());
sleepWakeDevice();
Assert.assertTrue(controllerA.isAttached());
Assert.assertFalse(controllerB.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(getPushHandler())
.popChangeHandler(getPopHandler()));
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
rotateDevice();
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
}
@Test
public void testChildNeedsAttachOnPauseAndOrientation() {
final TestController controllerA = new TestController();
final TestController childController = new TestController();
final TestController controllerB = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(getPushHandler())
.popChangeHandler(getPopHandler()));
Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID), null);
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(getPushHandler())
.popChangeHandler(getPopHandler()));
Assert.assertTrue(controllerA.isAttached());
Assert.assertTrue(childController.isAttached());
Assert.assertFalse(controllerB.isAttached());
sleepWakeDevice();
Assert.assertTrue(controllerA.isAttached());
Assert.assertTrue(childController.isAttached());
Assert.assertFalse(controllerB.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(getPushHandler())
.popChangeHandler(getPopHandler()));
Assert.assertFalse(controllerA.isAttached());
Assert.assertFalse(childController.isAttached());
Assert.assertTrue(controllerB.isAttached());
rotateDevice();
Assert.assertFalse(controllerA.isAttached());
Assert.assertFalse(childController.isAttached());
Assert.assertTrue(childController.getNeedsAttach());
Assert.assertTrue(controllerB.isAttached());
}
@Test
public void testChildHandleBackOnOrientation() {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
final TestController childController = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(getPushHandler())
.popChangeHandler(getPopHandler()));
Assert.assertTrue(controllerA.isAttached());
Assert.assertFalse(controllerB.isAttached());
Assert.assertFalse(childController.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(getPushHandler())
.popChangeHandler(getPopHandler()));
Router childRouter = controllerB.getChildRouter((ViewGroup)controllerB.getView().findViewById(TestController.VIEW_ID), null);
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(getPushHandler())
.popChangeHandler(getPopHandler()));
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertTrue(childController.isAttached());
rotateDevice();
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertTrue(childController.isAttached());
router.handleBack();
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertFalse(childController.isAttached());
router.handleBack();
Assert.assertTrue(controllerA.isAttached());
Assert.assertFalse(controllerB.isAttached());
Assert.assertFalse(childController.isAttached());
}
// Attempt to test https://github.com/bluelinelabs/Conductor/issues/86#issuecomment-231381271
@Test
public void testReusedChildRouterHandleBackOnOrientation() {
TestController controllerA = new TestController();
TestController controllerB = new TestController();
TestController childController = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(getPushHandler())
.popChangeHandler(getPopHandler()));
Assert.assertTrue(controllerA.isAttached());
Assert.assertFalse(controllerB.isAttached());
Assert.assertFalse(childController.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(getPushHandler())
.popChangeHandler(getPopHandler()));
Router childRouter = controllerB.getChildRouter((ViewGroup)controllerB.getView().findViewById(TestController.VIEW_ID), null);
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(getPushHandler())
.popChangeHandler(getPopHandler()));
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertTrue(childController.isAttached());
router.handleBack();
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertFalse(childController.isAttached());
childController = new TestController();
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(getPushHandler())
.popChangeHandler(getPopHandler()));
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertTrue(childController.isAttached());
rotateDevice();
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertTrue(childController.isAttached());
router.handleBack();
childController = new TestController();
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(getPushHandler())
.popChangeHandler(getPopHandler()));
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertTrue(childController.isAttached());
router.handleBack();
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertFalse(childController.isAttached());
router.handleBack();
Assert.assertTrue(controllerA.isAttached());
Assert.assertFalse(controllerB.isAttached());
Assert.assertFalse(childController.isAttached());
}
private void sleepWakeDevice() {
activityController.saveInstanceState(new Bundle()).pause();
activityController.resume();
}
private void rotateDevice() {
@IdRes int containerId = 4;
FrameLayout routerContainer = new FrameLayout(activityController.get());
routerContainer.setId(containerId);
activityController.get().isChangingConfigurations = true;
activityController.get().recreate();
router.rebindIfNeeded();
}
private ChangeHandler getPushHandler() {
return new ChangeHandler(new ChangeHandlerListener() {
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, @NonNull ControllerChangeCompletedListener changeListener) {
container.addView(to);
ViewUtils.setAttached(to, true);
if (from != null) {
container.removeView(from);
ViewUtils.setAttached(from, false);
}
changeListener.onChangeCompleted();
}
});
}
private ChangeHandler getPopHandler() {
return new ChangeHandler(new ChangeHandlerListener() {
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, @NonNull ControllerChangeCompletedListener changeListener) {
container.removeView(from);
ViewUtils.setAttached(from, false);
changeListener.onChangeCompleted();
if (to != null) {
container.addView(to);
ViewUtils.setAttached(to, true);
}
}
});
}
interface ChangeHandlerListener {
void performChange(@NonNull ViewGroup container, View from, View to, @NonNull ControllerChangeCompletedListener changeListener);
}
public static class ChangeHandler extends ControllerChangeHandler {
private ChangeHandlerListener listener;
public ChangeHandler() { }
public ChangeHandler(ChangeHandlerListener listener) {
this.listener = listener;
}
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
listener.performChange(container, from, to, changeListener);
}
}
}
@@ -9,7 +9,7 @@ import java.util.List;
public class ViewUtils {
static void setAttached(View view, boolean attached) {
public static void setAttached(View view, boolean attached) {
Object listenerInfo = ReflectionHelpers.callInstanceMethod(view, "getListenerInfo");
List<OnAttachStateChangeListener> listeners = ReflectionHelpers.getField(listenerInfo, "mOnAttachStateChangeListeners");
@@ -71,11 +71,19 @@ public class ParentController extends BaseController {
@Override
public boolean handleBack() {
if (!finishing) {
int childControllers = 0;
for (Router childRouter : getChildRouters()) {
if (childRouter.hasRootController()) {
childControllers++;
}
}
if (childControllers != NUMBER_OF_CHILDREN || finishing) {
return true;
} else {
finishing = true;
return super.handleBack();
}
return true;
}
@Override
@@ -140,11 +140,10 @@ public class TransitionDemoController extends BaseController {
public static RouterTransaction getRouterTransaction(int index, Controller fromController) {
TransitionDemoController toController = new TransitionDemoController(index);
ControllerChangeHandler changeHandler = toController.getChangeHandler(fromController);
return RouterTransaction.with(toController)
.pushChangeHandler(changeHandler)
.popChangeHandler(changeHandler);
.pushChangeHandler(toController.getChangeHandler(fromController))
.popChangeHandler(toController.getChangeHandler(fromController));
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
VERSION_NAME=2.0.1-SNAPSHOT
VERSION_NAME=2.0.2-SNAPSHOT
VERSION_CODE=2
GROUP=com.bluelinelabs