Compare commits

...

8 Commits

28 changed files with 837 additions and 217 deletions
+8 -8
View File
@@ -20,26 +20,26 @@ Conductor is architecture-agnostic and does not try to force any design decision
## Installation
```gradle
compile 'com.bluelinelabs:conductor:2.0.7'
compile 'com.bluelinelabs:conductor:2.1.0'
// If you want the components that go along with
// Android's support libraries (currently just a PagerAdapter):
compile 'com.bluelinelabs:conductor-support:2.0.7'
compile 'com.bluelinelabs:conductor-support:2.1.0'
// If you want RxJava lifecycle support:
compile 'com.bluelinelabs:conductor-rxlifecycle:2.0.7'
compile 'com.bluelinelabs:conductor-rxlifecycle:2.1.0'
// If you want RxJava2 lifecycle support:
compile 'com.bluelinelabs:conductor-rxlifecycle2:2.0.7'
compile 'com.bluelinelabs:conductor-rxlifecycle2:2.1.0'
```
SNAPSHOT:
```gradle
compile 'com.bluelinelabs:conductor:2.0.6-SNAPSHOT'
compile 'com.bluelinelabs:conductor-support:2.0.6-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle:2.0.6-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle2:2.0.6-SNAPSHOT'
compile 'com.bluelinelabs:conductor:2.1.1-SNAPSHOT'
compile 'com.bluelinelabs:conductor-support:2.1.1-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle:2.1.1-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle2:2.1.1-SNAPSHOT'
```
You also have to add the url to the snapshot repository:
@@ -13,8 +13,12 @@ import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
/**
* @deprecated Use RouterPagerAdapter instead! This implementation was too limited and had too many
* gotchas associated with it.
*
* An adapter for ViewPagers that will handle adding and removing Controllers
*/
@Deprecated
public abstract class ControllerPagerAdapter extends PagerAdapter {
private static final String KEY_SAVED_PAGES = "ControllerPagerAdapter.savedStates";
@@ -53,12 +57,17 @@ public abstract class ControllerPagerAdapter extends PagerAdapter {
}
}
final Controller controller;
if (!router.hasRootController()) {
Controller controller = getItem(position);
controller = getItem(position);
router.setRoot(RouterTransaction.with(controller).tag(name));
visiblePageIds.put(position, controller.getInstanceId());
} else {
router.rebindIfNeeded();
controller = router.getControllerWithTag(name);
}
if (controller != null) {
visiblePageIds.put(position, controller.getInstanceId());
}
return router.getControllerWithTag(name);
@@ -119,7 +128,8 @@ public abstract class ControllerPagerAdapter extends PagerAdapter {
}
/**
* Returns the already instantiated Controller in the specified position, if available.
* Returns the already instantiated Controller in the specified position or {@code null} if
* this position does not yet have a controller.
*/
@Nullable
public Controller getController(int position) {
@@ -139,4 +149,4 @@ public abstract class ControllerPagerAdapter extends PagerAdapter {
return viewId + ":" + id;
}
}
}
@@ -101,7 +101,8 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
}
/**
* Returns the already instantiated Router in the specified position, if available.
* Returns the already instantiated Router in the specified position or {@code null} if there
* is no router associated with this position.
*/
@Nullable
public Router getRouter(int position) {
@@ -9,12 +9,14 @@ import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.internal.LifecycleHandler;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
import java.util.List;
public class ActivityHostedRouter extends Router {
private LifecycleHandler lifecycleHandler;
private final TransactionIndexer transactionIndexer = new TransactionIndexer();
public final void setHost(@NonNull LifecycleHandler lifecycleHandler, @NonNull ViewGroup container) {
if (this.lifecycleHandler != lifecycleHandler || this.container != container) {
@@ -31,6 +33,20 @@ public class ActivityHostedRouter extends Router {
}
}
@Override
public void saveInstanceState(@NonNull Bundle outState) {
super.saveInstanceState(outState);
transactionIndexer.saveInstanceState(outState);
}
@Override
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
super.restoreInstanceState(savedInstanceState);
transactionIndexer.restoreInstanceState(savedInstanceState);
}
@Override @Nullable
public Activity getActivity() {
return lifecycleHandler != null ? lifecycleHandler.getLifecycleActivity() : null;
@@ -98,4 +114,9 @@ public class ActivityHostedRouter extends Router {
Router getRootRouter() {
return this;
}
@Override @Nullable
TransactionIndexer getTransactionIndexer() {
return transactionIndexer;
}
}
@@ -20,7 +20,6 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Router.OnControllerPushedListener;
import com.bluelinelabs.conductor.internal.ClassUtils;
import com.bluelinelabs.conductor.internal.RouterRequiringFunc;
import com.bluelinelabs.conductor.internal.ViewAttachHandler;
@@ -30,7 +29,8 @@ import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
@@ -45,7 +45,6 @@ public abstract class Controller {
private static final String KEY_CLASS_NAME = "Controller.className";
private static final String KEY_VIEW_STATE = "Controller.viewState";
private static final String KEY_CHILD_ROUTERS = "Controller.childRouters";
private static final String KEY_CHILD_BACKSTACK = "Controller.childBackstack";
private static final String KEY_SAVED_STATE = "Controller.savedState";
private static final String KEY_INSTANCE_ID = "Controller.instanceId";
private static final String KEY_TARGET_INSTANCE_ID = "Controller.target.instanceId";
@@ -86,16 +85,8 @@ public abstract class Controller {
private final List<LifecycleListener> lifecycleListeners = new ArrayList<>();
private final ArrayList<String> requestedPermissions = new ArrayList<>();
private final ArrayList<RouterRequiringFunc> onRouterSetListeners = new ArrayList<>();
private final List<Controller> childBackstack = new LinkedList<>();
private WeakReference<View> destroyedView;
private final OnControllerPushedListener onControllerPushedListener = new OnControllerPushedListener() {
@Override
public void onControllerPushed(Controller controller) {
onChildControllerPushed(controller);
}
};
@NonNull
static Controller newInstance(@NonNull Bundle bundle) {
final String className = bundle.getString(KEY_CLASS_NAME);
@@ -183,7 +174,7 @@ public abstract class Controller {
* the same container unless you have a great reason to do so (ex: ViewPagers).
*
* @param container The ViewGroup that hosts the child Router
* @param tag The router's tag
* @param tag The router's tag or {@code null} if none is needed
*/
@NonNull
public final Router getChildRouter(@NonNull ViewGroup container, @Nullable String tag) {
@@ -195,10 +186,12 @@ public abstract class Controller {
* Retrieves the child {@link Router} for the given container/tag combination. Note that multiple
* routers should not exist in the same container unless a lot of care is taken to maintain order
* between them. Avoid using the same container unless you have a great reason to do so (ex: ViewPagers).
* The only time this method will return {@code null} is when the child router does not exist prior
* to calling this method and the createIfNeeded parameter is set to false.
*
* @param container The ViewGroup that hosts the child Router
* @param tag The router's tag
* @param createIfNeeded If true, a router will be created if one does not yet exist. Else false will be returned in this case.
* @param tag The router's tag or {@code null} if none is needed
* @param createIfNeeded If true, a router will be created if one does not yet exist. Else {@code null} will be returned in this case.
*/
@Nullable
public final Router getChildRouter(@NonNull ViewGroup container, @Nullable String tag, boolean createIfNeeded) {
@@ -215,19 +208,23 @@ public abstract class Controller {
if (childRouter == null) {
if (createIfNeeded) {
childRouter = new ControllerHostedRouter(container.getId(), tag);
monitorChildRouter(childRouter);
childRouter.setHost(this, container);
childRouters.add(childRouter);
}
} else if (!childRouter.hasHost()) {
childRouter.setHost(this, container);
monitorChildRouter(childRouter);
childRouter.rebindIfNeeded();
}
return childRouter;
}
/**
* Removes a child {@link Router} from this Controller. When removed, all Controllers currently managed by
* the {@link Router} will be destroyed.
*
* @param childRouter The router to be removed
*/
public final void removeChildRouter(@NonNull Router childRouter) {
if ((childRouter instanceof ControllerHostedRouter) && childRouters.remove(childRouter)) {
childRouter.destroy(true);
@@ -256,7 +253,8 @@ public abstract class Controller {
}
/**
* Return this Controller's View, if available.
* Return this Controller's View or {@code null} if it has not yet been created or has been
* destroyed.
*/
@Nullable
public final View getView() {
@@ -264,15 +262,17 @@ public abstract class Controller {
}
/**
* Returns the host Activity of this Controller's {@link Router}
* Returns the host Activity of this Controller's {@link Router} or {@code null} if this
* Controller has not yet been attached to an Activity or if the Activity has been destroyed.
*/
@Nullable
public final Activity getActivity() {
return router.getActivity();
return router != null ? router.getActivity() : null;
}
/**
* Returns the Resources from the host Activity
* Returns the Resources from the host Activity or {@code null} if this Controller has not
* yet been attached to an Activity or if the Activity has been destroyed.
*/
@Nullable
public final Resources getResources() {
@@ -281,7 +281,8 @@ public abstract class Controller {
}
/**
* Returns the Application Context derived from the host Activity
* Returns the Application Context derived from the host Activity or {@code null} if this Controller
* has not yet been attached to an Activity or if the Activity has been destroyed.
*/
@Nullable
public final Context getApplicationContext() {
@@ -290,7 +291,8 @@ public abstract class Controller {
}
/**
* Returns this Controller's parent Controller if it is a child Controller.
* Returns this Controller's parent Controller if it is a child Controller or {@code null} if
* it has no parent.
*/
@Nullable
public final Controller getParentController() {
@@ -307,10 +309,10 @@ public abstract class Controller {
}
/**
* Returns the Controller with the given instance id, if available.
* May return the controller itself or a matching descendant
* Returns the Controller with the given instance id or {@code null} if no such Controller
* exists. May return the Controller itself or a matching descendant
*
* @param instanceId The instance ID being searched for
* @return The matching Controller, if one exists
*/
@Nullable
final Controller findController(@NonNull String instanceId) {
@@ -355,7 +357,8 @@ public abstract class Controller {
}
/**
* Returns the target Controller that was set with the {@link #setTargetController(Controller)} method
* Returns the target Controller that was set with the {@link #setTargetController(Controller)}
* method or {@code null} if this Controller has no target.
*
* @return This Controller's target
*/
@@ -552,8 +555,22 @@ public abstract class Controller {
* @return True if this Controller has consumed the back button press, otherwise false
*/
public boolean handleBack() {
for (int i = childBackstack.size() - 1; i >= 0; i--) {
Controller childController = childBackstack.get(i);
List<RouterTransaction> childTransactions = new ArrayList<>();
for (ControllerHostedRouter childRouter : childRouters) {
childTransactions.addAll(childRouter.getBackstack());
}
Collections.sort(childTransactions, new Comparator<RouterTransaction>() {
@Override
public int compare(RouterTransaction o1, RouterTransaction o2) {
return o2.transactionIndex - o1.transactionIndex;
}
});
for (RouterTransaction transaction : childTransactions) {
Controller childController = transaction.controller;
if (childController.isAttached() && childController.getRouter().handleBack()) {
return true;
}
@@ -744,6 +761,10 @@ public abstract class Controller {
}
final void activityStarted(@NonNull Activity activity) {
if (viewAttachHandler != null) {
viewAttachHandler.onActivityStarted();
}
onActivityStarted(activity);
}
@@ -763,12 +784,15 @@ public abstract class Controller {
}
final void activityStopped(@NonNull Activity activity) {
if (viewAttachHandler != null) {
viewAttachHandler.onActivityStopped();
}
onActivityStopped(activity);
}
final void activityDestroyed(boolean isChangingConfigurations) {
if (isChangingConfigurations) {
detach(view, true);
detach(view, true, false);
} else {
destroy(true);
}
@@ -802,14 +826,14 @@ public abstract class Controller {
}
}
void detach(@NonNull View view, boolean forceViewRefRemoval) {
void detach(@NonNull View view, boolean forceViewRefRemoval, boolean blockViewRefRemoval) {
if (!attachedToUnownedParent) {
for (ControllerHostedRouter router : childRouters) {
router.prepareForHostDetach();
}
}
final boolean removeViewRef = forceViewRefRemoval || retainViewMode == RetainViewMode.RELEASE_DETACH || isBeingDestroyed;
final boolean removeViewRef = !blockViewRefRemoval && (forceViewRefRemoval || retainViewMode == RetainViewMode.RELEASE_DETACH || isBeingDestroyed);
if (attached) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
@@ -874,7 +898,7 @@ public abstract class Controller {
final View inflate(@NonNull ViewGroup parent) {
if (view != null && view.getParent() != null && view.getParent() != parent) {
detach(view, true);
detach(view, true, false);
removeViewReference();
}
@@ -898,21 +922,26 @@ public abstract class Controller {
viewAttachHandler = new ViewAttachHandler(new ViewAttachListener() {
@Override
public void onAttached(View v) {
if (v == view) {
viewIsAttached = true;
viewWasDetached = false;
}
attach(v);
public void onAttached() {
viewIsAttached = true;
viewWasDetached = false;
attach(view);
}
@Override
public void onDetached(View v) {
public void onDetached(boolean fromActivityStop) {
viewIsAttached = false;
viewWasDetached = true;
if (!isDetachFrozen) {
detach(v, false);
detach(view, false, fromActivityStop);
}
}
@Override
public void onViewDetachAfterStop() {
if (!isDetachFrozen) {
detach(view, false, false);
}
}
});
@@ -931,7 +960,6 @@ public abstract class Controller {
if (containerView != null && containerView instanceof ViewGroup) {
childRouter.setHost(this, (ViewGroup)containerView);
monitorChildRouter(childRouter);
childRouter.rebindIfNeeded();
}
}
@@ -976,7 +1004,7 @@ public abstract class Controller {
if (!attached) {
removeViewReference();
} else if (removeViews) {
detach(view, true);
detach(view, true, false);
}
}
@@ -1043,12 +1071,6 @@ public abstract class Controller {
}
outState.putParcelableArrayList(KEY_CHILD_ROUTERS, childBundles);
ArrayList<String> childBackstack = new ArrayList<>();
for (Controller controller : this.childBackstack) {
childBackstack.add(controller.instanceId);
}
outState.putStringArrayList(KEY_CHILD_BACKSTACK, childBackstack);
Bundle savedState = new Bundle();
onSaveInstanceState(savedState);
@@ -1080,18 +1102,9 @@ public abstract class Controller {
for (Bundle childBundle : childBundles) {
ControllerHostedRouter childRouter = new ControllerHostedRouter();
childRouter.restoreInstanceState(childBundle);
monitorChildRouter(childRouter);
childRouters.add(childRouter);
}
List<String> childBackstackIds = savedInstanceState.getStringArrayList(KEY_CHILD_BACKSTACK);
for (String controllerId : childBackstackIds) {
Controller childController = findController(controllerId);
if (childController != null) {
childBackstack.add(childController);
}
}
this.savedInstanceState = savedInstanceState.getBundle(KEY_SAVED_STATE);
performOnRestoreInstanceState();
}
@@ -1156,7 +1169,7 @@ public abstract class Controller {
}
if (!frozen && view != null && viewWasDetached) {
detach(view, false);
detach(view, false, false);
}
}
}
@@ -1177,22 +1190,6 @@ public abstract class Controller {
return attached && hasOptionsMenu && !optionsMenuHidden && onOptionsItemSelected(item);
}
private void monitorChildRouter(@NonNull ControllerHostedRouter childRouter) {
childRouter.setOnControllerPushedListener(onControllerPushedListener);
}
private void onChildControllerPushed(@NonNull Controller controller) {
if (!childBackstack.contains(controller)) {
childBackstack.add(controller);
controller.addLifecycleListener(new LifecycleListener() {
@Override
public void postDestroy(@NonNull Controller controller) {
childBackstack.remove(controller);
}
});
}
}
final void setParentController(@Nullable Controller controller) {
parentController = controller;
}
@@ -10,7 +10,6 @@ import android.view.ViewParent;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import com.bluelinelabs.conductor.internal.ClassUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -34,8 +33,8 @@ public abstract class ControllerChangeHandler {
* Responsible for swapping Views from one Controller to another.
*
* @param container The container these Views are hosted in.
* @param from The previous View in the container, if any.
* @param to The next View that should be put in the container, if any.
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop.
* @param changeListener This listener must be called when any transitions or animations are completed.
*/
@@ -63,8 +62,9 @@ public abstract class ControllerChangeHandler {
* Will be called on change handlers that push a controller if the controller being pushed is
* popped before it has completed.
*
* @param newHandler the change handler that has caused this push to be aborted
* @param newTop the controller that will now be at the top of the backstack
* @param newHandler The change handler that has caused this push to be aborted
* @param newTop The Controller that will now be at the top of the backstack or {@code null}
* if there will be no new Controller at the top
*/
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) { }
@@ -137,7 +137,7 @@ public abstract class ControllerChangeHandler {
return false;
}
public static void abortPush(@NonNull Controller toAbort, @Nullable Controller newController, @NonNull ControllerChangeHandler newChangeHandler) {
static void abortPush(@NonNull Controller toAbort, @Nullable Controller newController, @NonNull ControllerChangeHandler newChangeHandler) {
ControllerChangeHandler handlerForPush = inProgressPushHandlers.get(toAbort.getInstanceId());
if (handlerForPush != null) {
handlerForPush.onAbortPush(newChangeHandler, newController);
@@ -145,11 +145,7 @@ public abstract class ControllerChangeHandler {
}
}
public static void executeChange(@Nullable final Controller to, @Nullable final Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler inHandler) {
executeChange(to, from, isPush, container, inHandler, new ArrayList<ControllerChangeListener>());
}
public static void executeChange(@Nullable final Controller to, @Nullable final Controller from, final boolean isPush, @Nullable final ViewGroup container, @Nullable final ControllerChangeHandler inHandler, @NonNull final List<ControllerChangeListener> listeners) {
static void executeChange(@Nullable final Controller to, @Nullable final Controller from, final boolean isPush, @Nullable final ViewGroup container, @Nullable final ControllerChangeHandler inHandler, @NonNull final List<ControllerChangeListener> listeners) {
if (isPush && to != null && to.isDestroyed()) {
throw new IllegalStateException("Trying to push a controller that has already been destroyed. (" + to.getClass().getSimpleName() + ")");
}
@@ -240,8 +236,8 @@ public abstract class ControllerChangeHandler {
/**
* Called when a {@link ControllerChangeHandler} has started changing {@link Controller}s
*
* @param to The new Controller
* @param from The old Controller
* @param to The new Controller or {@code null} if no Controller is being transitioned to
* @param from The old Controller or {@code null} if there was no Controller before this transition
* @param isPush True if this is a push operation, or false if it's a pop.
* @param container The containing ViewGroup
* @param handler The change handler being used.
@@ -251,9 +247,9 @@ public abstract class ControllerChangeHandler {
/**
* Called when a {@link ControllerChangeHandler} has completed changing {@link Controller}s
*
* @param to The new Controller
* @param from The old Controller
* @param isPush True if this was a push operation, or false if it's a pop.
* @param to The new Controller or {@code null} if no Controller is being transitioned to
* @param from The old Controller or {@code null} if there was no Controller before this transition
* @param isPush True if this was a push operation, or false if it's a pop
* @param container The containing ViewGroup
* @param handler The change handler that was used.
*/
@@ -9,6 +9,7 @@ import android.support.annotation.Nullable;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
import java.util.ArrayList;
import java.util.List;
@@ -51,12 +52,12 @@ class ControllerHostedRouter extends Router {
final List<Controller> controllersToDestroy = new ArrayList<>(destroyingControllers);
for (Controller controller : controllersToDestroy) {
if (controller.getView() != null) {
controller.detach(controller.getView(), true);
controller.detach(controller.getView(), true, false);
}
}
for (RouterTransaction transaction : backstack) {
if (transaction.controller.getView() != null) {
transaction.controller.detach(transaction.controller.getView(), true);
transaction.controller.detach(transaction.controller.getView(), true, false);
}
}
@@ -197,4 +198,9 @@ class ControllerHostedRouter extends Router {
return this;
}
}
@Override @Nullable
TransactionIndexer getTransactionIndexer() {
return getRootRouter().getTransactionIndexer();
}
}
@@ -45,7 +45,7 @@ abstract public class RestoreViewOnCreateController extends Controller {
* This Controller's view should NOT be added in this method. It is simply passed in
* so that valid LayoutParams can be used during inflation.
* @param savedViewState A bundle for the view's state, which would have been created in {@link #onSaveViewState(View, Bundle)},
* or null if no saved state exists.
* or {@code null} if no saved state exists.
*/
@NonNull
protected abstract View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState);
@@ -16,6 +16,7 @@ import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import com.bluelinelabs.conductor.internal.NoOpControllerChangeHandler;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
import java.util.ArrayList;
import java.util.Collections;
@@ -33,7 +34,6 @@ public abstract class Router {
private static final String KEY_POPS_LAST_VIEW = "Router.popsLastView";
protected final Backstack backstack = new Backstack();
private OnControllerPushedListener onControllerPushedListener;
private final List<ControllerChangeListener> changeListeners = new ArrayList<>();
final List<Controller> destroyingControllers = new ArrayList<>();
@@ -42,7 +42,8 @@ public abstract class Router {
ViewGroup container;
/**
* Returns this Router's host Activity
* Returns this Router's host Activity or {@code null} if it has either not yet been attached to
* an Activity or if the Activity has been destroyed.
*/
@Nullable
public abstract Activity getActivity();
@@ -52,8 +53,8 @@ public abstract class Router {
* of the controller that called startActivityForResult is not known.
*
* @param requestCode The Activity's onActivityResult requestCode
* @param resultCode The Activity's onActivityResult resultCode
* @param data The Activity's onActivityResult data
* @param resultCode The Activity's onActivityResult resultCode
* @param data The Activity's onActivityResult data
*/
public abstract void onActivityResult(int requestCode, int resultCode, @Nullable Intent data);
@@ -61,9 +62,9 @@ public abstract class Router {
* This should be called by the host Activity when its onRequestPermissionsResult method is called. The call will be forwarded
* to the {@link Controller} with the instanceId passed in.
*
* @param instanceId The instanceId of the Controller to which this result should be forwarded
* @param requestCode The Activity's onRequestPermissionsResult requestCode
* @param permissions The Activity's onRequestPermissionsResult permissions
* @param instanceId The instanceId of the Controller to which this result should be forwarded
* @param requestCode The Activity's onRequestPermissionsResult requestCode
* @param permissions The Activity's onRequestPermissionsResult permissions
* @param grantResults The Activity's onRequestPermissionsResult grantResults
*/
public void onRequestPermissionsResult(@NonNull String instanceId, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
@@ -172,7 +173,7 @@ public abstract class Router {
final boolean newHandlerRemovesViews = handler == null || handler.removesFromViewOnPush();
if (!oldHandlerRemovedViews && newHandlerRemovesViews) {
for (RouterTransaction visibleTransaction : getVisibleTransactions(backstack.iterator())) {
performControllerChange(null, visibleTransaction.controller, true, handler);
performControllerChange(null, visibleTransaction, true, handler);
}
}
}
@@ -188,11 +189,10 @@ public abstract class Router {
void destroy(boolean popViews) {
popsLastView = true;
List<RouterTransaction> poppedControllers = backstack.popAll();
trackDestroyingControllers(poppedControllers);
if (popViews && poppedControllers.size() > 0) {
trackDestroyingControllers(poppedControllers);
performControllerChange(null, poppedControllers.get(0).controller, false, poppedControllers.get(0).popChangeHandler());
performControllerChange(null, poppedControllers.get(0), false, poppedControllers.get(0).popChangeHandler());
}
}
@@ -252,7 +252,7 @@ public abstract class Router {
/**
* Pops all {@link Controller}s until the {@link Controller} with the passed tag is at the top
*
* @param tag The tag being popped to
* @param tag The tag being popped to
* @param changeHandler The {@link ControllerChangeHandler} to handle this transaction
* @return Whether or not the {@link Controller} with the passed tag is now at the top
*/
@@ -281,10 +281,10 @@ public abstract class Router {
}
/**
* Returns the hosted Controller with the given instance id, if available.
* Returns the hosted Controller with the given instance id or {@code null} if no such
* Controller exists in this Router.
*
* @param instanceId The instance ID being searched for
* @return The matching Controller, if one exists
*/
@Nullable
public Controller getControllerWithInstanceId(@NonNull String instanceId) {
@@ -298,10 +298,10 @@ public abstract class Router {
}
/**
* Returns the hosted Controller that was pushed with the given tag, if available.
* Returns the hosted Controller that was pushed with the given tag or {@code null} if no
* such Controller exists in this Router.
*
* @param tag The tag being searched for
* @return The matching Controller, if one exists
*/
@Nullable
public Controller getControllerWithTag(@NonNull String tag) {
@@ -337,7 +337,7 @@ public abstract class Router {
* Sets the backstack, transitioning from the current top controller to the top of the new stack (if different)
* using the passed {@link ControllerChangeHandler}
*
* @param newBackstack The new backstack
* @param newBackstack The new backstack
* @param changeHandler An optional change handler to be used to handle the root view of transition
*/
@UiThread
@@ -345,6 +345,7 @@ public abstract class Router {
List<RouterTransaction> oldVisibleTransactions = getVisibleTransactions(backstack.iterator());
removeAllExceptVisibleAndUnowned();
ensureOrderedTransactionIndices(newBackstack);
backstack.setBackstack(newBackstack);
for (RouterTransaction transaction : backstack) {
@@ -358,19 +359,19 @@ public abstract class Router {
boolean visibleTransactionsChanged = !backstacksAreEqual(newVisibleTransactions, oldVisibleTransactions);
if (visibleTransactionsChanged) {
Controller rootController = oldVisibleTransactions.size() > 0 ? oldVisibleTransactions.get(0).controller : null;
performControllerChange(newVisibleTransactions.get(0).controller, rootController, true, changeHandler);
RouterTransaction rootTransaction = oldVisibleTransactions.size() > 0 ? oldVisibleTransactions.get(0) : null;
performControllerChange(newVisibleTransactions.get(0), rootTransaction, true, changeHandler);
for (int i = oldVisibleTransactions.size() - 1; i > 0; i--) {
RouterTransaction transaction = oldVisibleTransactions.get(i);
ControllerChangeHandler localHandler = changeHandler != null ? changeHandler.copy() : new SimpleSwapChangeHandler();
localHandler.setForceRemoveViewOnPush(true);
performControllerChange(null, transaction.controller, true, localHandler);
performControllerChange(null, transaction, true, localHandler);
}
for (int i = 1; i < newVisibleTransactions.size(); i++) {
RouterTransaction transaction = newVisibleTransactions.get(i);
performControllerChange(transaction.controller, newVisibleTransactions.get(i - 1).controller, true, transaction.pushChangeHandler());
performControllerChange(transaction, newVisibleTransactions.get(i - 1), true, transaction.pushChangeHandler());
}
}
@@ -379,12 +380,6 @@ public abstract class Router {
transaction.controller.setRouter(this);
}
}
if (onControllerPushedListener != null) {
for (RouterTransaction transaction : newBackstack) {
onControllerPushedListener.onControllerPushed(transaction.controller);
}
}
}
/**
@@ -424,7 +419,7 @@ public abstract class Router {
RouterTransaction transaction = backstackIterator.next();
if (transaction.controller.getNeedsAttach()) {
performControllerChange(transaction.controller, null, true, new SimpleSwapChangeHandler(false));
performControllerChange(transaction, null, true, new SimpleSwapChangeHandler(false));
}
}
}
@@ -575,14 +570,10 @@ public abstract class Router {
changeHandler = topTransaction.popChangeHandler();
}
performControllerChange(backstack.peek().controller, topTransaction.controller, false, changeHandler);
performControllerChange(backstack.peek(), topTransaction, false, changeHandler);
}
}
final void setOnControllerPushedListener(OnControllerPushedListener listener) {
onControllerPushedListener = listener;
}
void prepareForContainerRemoval() {
if (container != null) {
container.setOnHierarchyChangeListener(null);
@@ -626,30 +617,27 @@ public abstract class Router {
changeHandler = null;
}
performControllerChange(to, from, isPush, changeHandler);
}
private void performControllerChange(@Nullable final RouterTransaction to, @Nullable final RouterTransaction from, boolean isPush, @Nullable ControllerChangeHandler changeHandler) {
Controller toController = to != null ? to.controller : null;
Controller fromController = from != null ? from.controller : null;
performControllerChange(toController, fromController, isPush, changeHandler);
}
private void performControllerChange(@Nullable final Controller to, @Nullable final Controller from, boolean isPush, @Nullable ControllerChangeHandler changeHandler) {
if (to != null) {
setControllerRouter(to);
to.ensureValidIndex(getTransactionIndexer());
setControllerRouter(toController);
} else if (backstack.size() == 0 && !popsLastView) {
// We're emptying out the backstack. Views get weird if you transition them out, so just no-op it. The hosting
// Activity should be handling this by finishing or at least hiding this view.
changeHandler = new NoOpControllerChangeHandler();
}
ControllerChangeHandler.executeChange(to, from, isPush, container, changeHandler, changeListeners);
ControllerChangeHandler.executeChange(toController, fromController, isPush, container, changeHandler, changeListeners);
}
private void pushToBackstack(@NonNull RouterTransaction entry) {
backstack.push(entry);
if (onControllerPushedListener != null) {
onControllerPushedListener.onControllerPushed(entry.controller);
}
}
private void trackDestroyingController(@NonNull RouterTransaction transaction) {
@@ -695,6 +683,22 @@ public abstract class Router {
}
}
// Swap around transaction indicies to ensure they don't get thrown out of order by the
// developer rearranging the backstack at runtime.
private void ensureOrderedTransactionIndices(List<RouterTransaction> backstack) {
List<Integer> indices = new ArrayList<>();
for (RouterTransaction transaction : backstack) {
transaction.ensureValidIndex(getTransactionIndexer());
indices.add(transaction.transactionIndex);
}
Collections.sort(indices);
for (int i = 0; i < backstack.size(); i++) {
backstack.get(i).transactionIndex = indices.get(i);
}
}
private void addRouterViewsToList(@NonNull Router router, @NonNull List<View> list) {
for (Controller controller : router.getControllers()) {
if (controller.getView() != null) {
@@ -750,8 +754,6 @@ public abstract class Router {
abstract boolean hasHost();
@NonNull abstract List<Router> getSiblingRouters();
@NonNull abstract Router getRootRouter();
@Nullable abstract TransactionIndexer getTransactionIndexer();
interface OnControllerPushedListener {
void onControllerPushed(Controller controller);
}
}
@@ -4,15 +4,20 @@ import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
/**
* Metadata used for adding {@link Controller}s to a {@link Router}.
*/
public class RouterTransaction {
private static int INVALID_INDEX = -1;
private static final String KEY_VIEW_CONTROLLER_BUNDLE = "RouterTransaction.controller.bundle";
private static final String KEY_PUSH_TRANSITION = "RouterTransaction.pushControllerChangeHandler";
private static final String KEY_POP_TRANSITION = "RouterTransaction.popControllerChangeHandler";
private static final String KEY_TAG = "RouterTransaction.tag";
private static final String KEY_INDEX = "RouterTransaction.transactionIndex";
private static final String KEY_ATTACHED_TO_ROUTER = "RouterTransaction.attachedToRouter";
@NonNull final Controller controller;
@@ -21,6 +26,7 @@ public class RouterTransaction {
private ControllerChangeHandler pushControllerChangeHandler;
private ControllerChangeHandler popControllerChangeHandler;
private boolean attachedToRouter;
int transactionIndex = INVALID_INDEX;
@NonNull
public static RouterTransaction with(@NonNull Controller controller) {
@@ -36,6 +42,7 @@ public class RouterTransaction {
pushControllerChangeHandler = ControllerChangeHandler.fromBundle(bundle.getBundle(KEY_PUSH_TRANSITION));
popControllerChangeHandler = ControllerChangeHandler.fromBundle(bundle.getBundle(KEY_POP_TRANSITION));
tag = bundle.getString(KEY_TAG);
transactionIndex = bundle.getInt(KEY_INDEX);
attachedToRouter = bundle.getBoolean(KEY_ATTACHED_TO_ROUTER);
}
@@ -101,6 +108,15 @@ public class RouterTransaction {
}
}
void ensureValidIndex(@Nullable TransactionIndexer indexer) {
if (indexer == null) {
throw new RuntimeException();
}
if (transactionIndex == INVALID_INDEX && indexer != null) {
transactionIndex = indexer.nextIndex();
}
}
/**
* Used to serialize this transaction into a Bundle
*/
@@ -118,6 +134,7 @@ public class RouterTransaction {
}
bundle.putString(KEY_TAG, tag);
bundle.putInt(KEY_INDEX, transactionIndex);
bundle.putBoolean(KEY_ATTACHED_TO_ROUTER, attachedToRouter);
return bundle;
@@ -94,8 +94,8 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
* Should be overridden to return the Animator to use while replacing Views.
*
* @param container The container these Views are hosted in.
* @param from The previous View in the container, if any.
* @param to The next View that should be put in the container, if any.
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop.
* @param toAddedToContainer True if the "to" view was added to the container as a part of this ChangeHandler. False if it was already in the hierarchy.
*/
@@ -150,7 +150,7 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
if (animatorListener != null) {
animator.removeListener(animatorListener);
}
animator.end();
animator.cancel();
animator = null;
}
}
@@ -32,8 +32,9 @@ public class FadeChangeHandler extends AnimatorChangeHandler {
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animator = new AnimatorSet();
if (to != null && toAddedToContainer) {
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, 0, 1));
if (to != null) {
float start = toAddedToContainer ? 0 : to.getAlpha();
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1));
}
if (from != null) {
@@ -45,7 +45,9 @@ public class HorizontalChangeHandler extends AnimatorChangeHandler {
animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, from.getWidth()));
}
if (to != null) {
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, -to.getWidth(), 0));
// Allow this to have a nice transition when coming off an aborted push animation
float fromLeft = from != null ? from.getX() : 0;
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, fromLeft - to.getWidth(), 0));
}
}
@@ -24,10 +24,10 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
/**
* Should be overridden to return the Transition to use while replacing Views.
*
* @param container The container these Views are hosted in.
* @param from The previous View in the container, if any.
* @param to The next View that should be put in the container, if any.
* @param isPush True if this is a push transaction, false if it's a pop.
* @param container The container these Views are hosted in
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop
*/
@NonNull
protected abstract Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
@@ -0,0 +1,24 @@
package com.bluelinelabs.conductor.internal;
import android.os.Bundle;
import android.support.annotation.NonNull;
public class TransactionIndexer {
private static final String KEY_INDEX = "TransactionIndexer.currentIndex";
private int currentIndex;
public int nextIndex() {
return ++currentIndex;
}
public void saveInstanceState(@NonNull Bundle outState) {
outState.putInt(KEY_INDEX, currentIndex);
}
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
currentIndex = savedInstanceState.getInt(KEY_INDEX);
}
}
@@ -4,66 +4,107 @@ import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
public class ViewAttachHandler {
public class ViewAttachHandler implements OnAttachStateChangeListener {
private enum ReportedState {
VIEW_DETACHED,
ACTIVITY_STOPPED,
ATTACHED
}
public interface ViewAttachListener {
void onAttached(View view);
void onDetached(View view);
void onAttached();
void onDetached(boolean fromActivityStop);
void onViewDetachAfterStop();
}
private interface ChildAttachListener {
void onAttached();
}
private boolean rootAttached = false;
boolean childrenAttached = false;
private boolean activityStopped = false;
private ReportedState reportedState = ReportedState.VIEW_DETACHED;
private ViewAttachListener attachListener;
private OnAttachStateChangeListener rootOnAttachStateChangeListener = new OnAttachStateChangeListener() {
boolean rootAttached = false;
boolean childrenAttached = false;
@Override
public void onViewAttachedToWindow(final View v) {
if (rootAttached) {
return;
}
rootAttached = true;
listenForDeepestChildAttach(v, new ChildAttachListener() {
@Override
public void onAttached() {
childrenAttached = true;
attachListener.onAttached(v);
}
});
}
@Override
public void onViewDetachedFromWindow(View v) {
rootAttached = false;
if (childrenAttached) {
childrenAttached = false;
attachListener.onDetached(v);
}
}
};
private OnAttachStateChangeListener childOnAttachStateChangeListener;
public ViewAttachHandler(ViewAttachListener attachListener) {
this.attachListener = attachListener;
}
@Override
public void onViewAttachedToWindow(final View v) {
if (rootAttached) {
return;
}
rootAttached = true;
listenForDeepestChildAttach(v, new ChildAttachListener() {
@Override
public void onAttached() {
childrenAttached = true;
reportAttached();
}
});
}
@Override
public void onViewDetachedFromWindow(View v) {
rootAttached = false;
if (childrenAttached) {
childrenAttached = false;
reportDetached();
}
}
public void listenForAttach(final View view) {
view.addOnAttachStateChangeListener(rootOnAttachStateChangeListener);
view.addOnAttachStateChangeListener(this);
}
public void unregisterAttachListener(View view) {
view.removeOnAttachStateChangeListener(rootOnAttachStateChangeListener);
view.removeOnAttachStateChangeListener(this);
if (childOnAttachStateChangeListener != null && view instanceof ViewGroup) {
findDeepestChild((ViewGroup)view).removeOnAttachStateChangeListener(childOnAttachStateChangeListener);
}
}
public void onActivityStarted() {
activityStopped = false;
reportAttached();
}
public void onActivityStopped() {
activityStopped = true;
reportDetached();
}
void reportAttached() {
if (rootAttached && childrenAttached && !activityStopped && reportedState != ReportedState.ATTACHED) {
reportedState = ReportedState.ATTACHED;
attachListener.onAttached();
}
}
void reportDetached() {
boolean wasDetachedForActivity = reportedState == ReportedState.ACTIVITY_STOPPED;
boolean isDetachedForActivity = rootAttached;
if (isDetachedForActivity) {
reportedState = ReportedState.ACTIVITY_STOPPED;
} else {
reportedState = ReportedState.VIEW_DETACHED;
}
if (wasDetachedForActivity && !isDetachedForActivity) {
attachListener.onViewDetachAfterStop();
} else {
attachListener.onDetached(isDetachedForActivity);
}
}
void listenForDeepestChildAttach(final View view, final ChildAttachListener attachListener) {
if (!(view instanceof ViewGroup)) {
attachListener.onAttached();
@@ -0,0 +1,268 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.ListUtils;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
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.assertNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ControllerLifecycleActivityReferenceTests {
private Router router;
private ActivityProxy activityProxy;
public void createActivityController(Bundle savedInstanceState, boolean includeStartAndResume) {
activityProxy = new ActivityProxy().create(savedInstanceState);
if (includeStartAndResume) {
activityProxy.start().resume();
}
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
router.setPopsLastView(true);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Before
public void setup() {
createActivityController(null, true);
}
@Test
public void testSingleControllerActivityOnPush() {
Controller controller = new TestController();
assertNull(controller.getActivity());
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
controller.addLifecycleListener(listener);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertEquals(ListUtils.listOf(true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(), listener.postDetachReferences);
assertEquals(ListUtils.listOf(), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(), listener.postDestroyReferences);
}
@Test
public void testChildControllerActivityOnPush() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
assertNull(child.getActivity());
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertEquals(ListUtils.listOf(true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(), listener.postDetachReferences);
assertEquals(ListUtils.listOf(), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(), listener.postDestroyReferences);
}
@Test
public void testSingleControllerActivityOnPop() {
Controller controller = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
controller.addLifecycleListener(listener);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
router.popCurrentController();
assertEquals(ListUtils.listOf(true, true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(true), listener.postDetachReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyReferences);
}
@Test
public void testChildControllerActivityOnPop() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
childRouter.popCurrentController();
assertEquals(ListUtils.listOf(true, true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(true), listener.postDetachReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyReferences);
}
@Test
public void testChildControllerActivityOnParentPop() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
router.popCurrentController();
assertEquals(ListUtils.listOf(true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(true), listener.postDetachReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyReferences);
}
@Test
public void testSingleControllerActivityOnDestroy() {
Controller controller = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
controller.addLifecycleListener(listener);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
activityProxy.pause().stop(false).destroy();
assertEquals(ListUtils.listOf(true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(true), listener.postDetachReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyReferences);
}
@Test
public void testChildControllerActivityOnDestroy() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
activityProxy.pause().stop(false).destroy();
assertEquals(ListUtils.listOf(true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(true), listener.postDetachReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyReferences);
}
static class ActivityReferencingLifecycleListener extends Controller.LifecycleListener {
List<Boolean> changeEndReferences = new ArrayList<>();
List<Boolean> postCreateViewReferences = new ArrayList<>();
List<Boolean> postAttachReferences = new ArrayList<>();
List<Boolean> postDetachReferences = new ArrayList<>();
List<Boolean> postDestroyViewReferences = new ArrayList<>();
List<Boolean> postDestroyReferences = new ArrayList<>();
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
changeEndReferences.add(controller.getActivity() != null);
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
postCreateViewReferences.add(controller.getActivity() != null);
}
@Override
public void postAttach(@NonNull Controller controller, @NonNull View view) {
postAttachReferences.add(controller.getActivity() != null);
}
@Override
public void postDetach(@NonNull Controller controller, @NonNull View view) {
postDetachReferences.add(controller.getActivity() != null);
}
@Override
public void postDestroyView(@NonNull Controller controller) {
postDestroyViewReferences.add(controller.getActivity() != null);
}
@Override
public void postDestroy(@NonNull Controller controller) {
postDestroyReferences.add(controller.getActivity() != null);
}
}
}
@@ -11,6 +11,7 @@ import com.bluelinelabs.conductor.util.CallState;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.MockChangeHandler.ChangeHandlerListener;
import com.bluelinelabs.conductor.util.TestController;
import com.bluelinelabs.conductor.util.ViewUtils;
import org.junit.Before;
import org.junit.Test;
@@ -19,11 +20,12 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ControllerLifecycleTests {
public class ControllerLifecycleCallbacksTests {
private Router router;
@@ -71,6 +73,37 @@ public class ControllerLifecycleTests {
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleWithActivityStop() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller)));
assertCalls(expectedCallState, controller);
activityProxy.getActivity().isDestroying = true;
activityProxy.pause();
assertCalls(expectedCallState, controller);
activityProxy.stop(false);
expectedCallState.detachCalls++;
assertCalls(expectedCallState, controller);
assertNotNull(controller.getView());
ViewUtils.reportAttached(controller.getView(), false);
expectedCallState.saveViewStateCalls++;
expectedCallState.destroyViewCalls++;
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleWithActivityDestroy() {
TestController controller = new TestController();
@@ -89,7 +122,7 @@ public class ControllerLifecycleTests {
assertCalls(expectedCallState, controller);
activityProxy.stop();
activityProxy.stop(true);
expectedCallState.saveViewStateCalls++;
expectedCallState.detachCalls++;
@@ -128,7 +161,7 @@ public class ControllerLifecycleTests {
activityProxy.pause();
assertCalls(expectedCallState, controller);
activityProxy.stop();
activityProxy.stop(true);
expectedCallState.detachCalls++;
expectedCallState.destroyViewCalls++;
assertCalls(expectedCallState, controller);
@@ -21,6 +21,7 @@ import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@@ -362,6 +363,48 @@ public class ControllerTests {
assertNull(child2.getParentController());
}
@Test
public void testRestoredChildRouterBackstack() {
TestController parent = new TestController();
router.pushController(RouterTransaction.with(parent));
ViewUtils.reportAttached(parent.getView(), true);
RouterTransaction childTransaction1 = RouterTransaction.with(new TestController());
RouterTransaction childTransaction2 = RouterTransaction.with(new TestController());
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1));
childRouter.setPopsLastView(true);
childRouter.setRoot(childTransaction1);
childRouter.pushController(childTransaction2);
Bundle savedState = new Bundle();
childRouter.saveInstanceState(savedState);
parent.removeChildRouter(childRouter);
childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1));
assertEquals(0, childRouter.getBackstackSize());
childRouter.restoreInstanceState(savedState);
childRouter.rebindIfNeeded();
assertEquals(2, childRouter.getBackstackSize());
RouterTransaction restoredChildTransaction1 = childRouter.getBackstack().get(0);
RouterTransaction restoredChildTransaction2 = childRouter.getBackstack().get(1);
assertEquals(childTransaction1.transactionIndex, restoredChildTransaction1.transactionIndex);
assertEquals(childTransaction1.controller.getInstanceId(), restoredChildTransaction1.controller.getInstanceId());
assertEquals(childTransaction2.transactionIndex, restoredChildTransaction2.transactionIndex);
assertEquals(childTransaction2.controller.getInstanceId(), restoredChildTransaction2.controller.getInstanceId());
assertTrue(parent.handleBack());
assertEquals(1, childRouter.getBackstackSize());
assertEquals(restoredChildTransaction1, childRouter.getBackstack().get(0));
assertTrue(parent.handleBack());
assertEquals(0, childRouter.getBackstackSize());
}
private void assertCalls(CallState callState, TestController controller) {
assertEquals("Expected call counts and controller call counts do not match.", callState, controller.currentCallState);
}
@@ -1,5 +1,7 @@
package com.bluelinelabs.conductor;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.ListUtils;
import com.bluelinelabs.conductor.util.MockChangeHandler;
@@ -312,4 +314,61 @@ public class RouterTests {
assertTrue(newTopTransaction.controller.isAttached());
}
@Test
public void testRearrangeTransactionBackstack() {
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = ListUtils.listOf(transaction1, transaction2);
router.setBackstack(backstack, null);
assertEquals(1, transaction1.transactionIndex);
assertEquals(2, transaction2.transactionIndex);
backstack = ListUtils.listOf(transaction2, transaction1);
router.setBackstack(backstack, null);
assertEquals(1, transaction2.transactionIndex);
assertEquals(2, transaction1.transactionIndex);
router.handleBack();
assertEquals(1, router.getBackstackSize());
assertEquals(transaction2, router.getBackstack().get(0));
router.handleBack();
assertEquals(0, router.getBackstackSize());
}
@Test
public void testChildRouterRearrangeTransactionBackstack() {
Controller parent = new TestController();
router.setRoot(RouterTransaction.with(parent));
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1));
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = ListUtils.listOf(transaction1, transaction2);
childRouter.setBackstack(backstack, null);
assertEquals(2, transaction1.transactionIndex);
assertEquals(3, transaction2.transactionIndex);
backstack = ListUtils.listOf(transaction2, transaction1);
childRouter.setBackstack(backstack, null);
assertEquals(2, transaction2.transactionIndex);
assertEquals(3, transaction1.transactionIndex);
childRouter.handleBack();
assertEquals(1, childRouter.getBackstackSize());
assertEquals(transaction2, childRouter.getBackstack().get(0));
childRouter.handleBack();
assertEquals(0, childRouter.getBackstackSize());
}
}
@@ -40,55 +40,107 @@ public class ViewAttachHandlerTests {
assertEquals(0, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(2, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStopped();
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStarted();
assertEquals(3, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
}
@Test
public void testSimpleViewGroupAttachDetach() {
View view = new LinearLayout(activity);
View view = new View(activity);
viewAttachHandler.listenForAttach(view);
assertEquals(0, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(2, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStopped();
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStarted();
assertEquals(3, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
}
@Test
@@ -100,50 +152,85 @@ public class ViewAttachHandlerTests {
assertEquals(0, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true, false);
assertEquals(0, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(child, true, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true, false);
ViewUtils.reportAttached(child, true, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(child, true, false);
assertEquals(2, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStopped();
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false, false);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true, false);
ViewUtils.reportAttached(child, true, false);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStarted();
assertEquals(3, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
}
private static class CountingViewAttachListener implements ViewAttachListener {
int attaches;
int detaches;
int detachAfterStops;
@Override
public void onAttached(View view) {
public void onAttached() {
attaches++;
}
@Override
public void onDetached(View view) {
public void onDetached(boolean fromActivityStop) {
detaches++;
}
@Override
public void onViewDetachAfterStop() {
detachAfterStops++;
}
}
}
@@ -45,14 +45,19 @@ public class ActivityProxy {
return this;
}
public ActivityProxy stop() {
public ActivityProxy stop(boolean detachView) {
activityController.stop();
view.setAttached(false);
if (detachView) {
view.setAttached(false);
}
return this;
}
public ActivityProxy destroy() {
activityController.destroy();
view.setAttached(false);
return this;
}
@@ -33,11 +33,11 @@ public class TestController extends Controller {
FrameLayout view = new AttachFakingFrameLayout(inflater.getContext());
view.setId(VIEW_ID);
FrameLayout childContainer1 = new FrameLayout(inflater.getContext());
FrameLayout childContainer1 = new AttachFakingFrameLayout(inflater.getContext());
childContainer1.setId(CHILD_VIEW_ID_1);
view.addView(childContainer1);
FrameLayout childContainer2 = new FrameLayout(inflater.getContext());
FrameLayout childContainer2 = new AttachFakingFrameLayout(inflater.getContext());
childContainer2.setId(CHILD_VIEW_ID_2);
view.addView(childContainer2);
@@ -22,8 +22,9 @@ public class CircularRevealChangeHandlerCompat extends CircularRevealChangeHandl
return super.getAnimator(container, from, to, isPush, toAddedToContainer);
} else {
AnimatorSet animator = new AnimatorSet();
if (to != null && toAddedToContainer) {
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, 0, 1));
if (to != null) {
float start = toAddedToContainer ? 0 : to.getAlpha();
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1));
}
if (from != null) {
@@ -18,8 +18,9 @@ public class ScaleFadeChangeHandler extends AnimatorChangeHandler {
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animator = new AnimatorSet();
if (to != null && toAddedToContainer) {
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, 0, 1));
if (to != null) {
float start = toAddedToContainer ? 0 : to.getAlpha();
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1));
}
if (from != null) {
@@ -8,9 +8,11 @@ import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.support.ControllerPagerAdapter;
import com.bluelinelabs.conductor.support.RouterPagerAdapter;
import java.util.Locale;
@@ -23,13 +25,16 @@ public class PagerController extends BaseController {
@BindView(R.id.tab_layout) TabLayout tabLayout;
@BindView(R.id.view_pager) ViewPager viewPager;
private final ControllerPagerAdapter pagerAdapter;
private final RouterPagerAdapter pagerAdapter;
public PagerController() {
pagerAdapter = new ControllerPagerAdapter(this, false) {
pagerAdapter = new RouterPagerAdapter(this) {
@Override
public Controller getItem(int position) {
return new ChildController(String.format(Locale.getDefault(), "Child #%d (Swipe to see more)", position), PAGE_COLORS[position], true);
public void configureRouter(Router router, int position) {
if (!router.hasRootController()) {
Controller page = new ChildController(String.format(Locale.getDefault(), "Child #%d (Swipe to see more)", position), PAGE_COLORS[position], true);
router.setRoot(RouterTransaction.with(page));
}
}
@Override
Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 65 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
VERSION_NAME=2.0.8-SNAPSHOT
VERSION_NAME=2.1.1-SNAPSHOT
VERSION_CODE=2
GROUP=com.bluelinelabs