Compare commits

...

60 Commits

Author SHA1 Message Date
Eric Kuck 638b2ad311 Version bump 2016-11-11 11:10:35 -06:00
Eric Kuck 96e068d348 Fixes a controller's internal backstack when setBackstack is used on a child router 2016-11-11 10:44:37 -06:00
Eric Kuck db359d906b Un-deprecated getChildRouter with a tag. Fixes #160. 2016-11-10 13:34:13 -06:00
Eric Kuck 11185458b3 Fixed nullable annotations for menu callbacks 2016-11-09 16:03:20 -06:00
Eric Kuck 977db6b5bf Merge branch 'develop' of github.com:bluelinelabs/Conductor into develop 2016-11-09 15:32:01 -06:00
Eric Kuck 803c20e093 Added UiThread annotations - closes #145 2016-11-09 15:25:28 -06:00
TMTron 9948cb4652 Navigation Demos: "GO UP" is hidden in "Controller #0" - closes #158 (#159)
* Navigation Demos: "GO UP" is hidden in "Controller #0" - this closes #158

* Multiple Child Routers: "GO UP" is hidden for all - #158
2016-11-09 14:54:27 -06:00
Eric Kuck 07a579b939 Fixed incorrect child router backstack handling if controllers were VERY rapidly added 2016-11-09 11:07:41 -06:00
Eric Kuck 104d96e6e2 Filled out @Nullable and @NonNull annotations throughout the library 2016-11-09 10:09:00 -06:00
Eric Kuck e0f40a9fce Fixes tests 2016-11-02 12:29:58 -05:00
Eric Kuck bc8e0c5b2c Fixes #113 2016-11-02 12:04:53 -05:00
Eric Kuck 2b6e41f895 Fixes #140 2016-10-17 17:32:10 -05:00
Eric Kuck b633523d0e Fixes #138 2016-10-12 12:22:22 -05:00
Eric Kuck c5eb7fc89e Fixes #136 2016-10-12 09:44:26 -05:00
Eric Kuck cf6837a41a Fixes #137 2016-10-12 09:24:29 -05:00
Eric Kuck e297242264 Now handles requesting permissions while now yet fully attached to the host activity 2016-10-10 15:14:06 -05:00
Eric Kuck 550e7e0aa1 Added missing constructor to RestoreViewOnCreateController 2016-10-05 15:16:04 -05:00
Eric Kuck 90f21d99a5 Version bump fix 2016-10-03 14:51:51 -05:00
Eric Kuck 641e0dc43c Version bump 2016-10-03 14:50:38 -05:00
Eric Kuck 91c993b005 Fixes #95 2016-10-03 14:36:41 -05:00
Eric Kuck 39ab4723ff Removes viewState getter and instead adds a controller subclass that can allows access to the saved view state and the time of view creation. 2016-10-03 14:11:00 -05:00
Eric Kuck 3769e706af Fixes #124 2016-10-03 14:05:03 -05:00
Yasuhiro Shimizu 26efe8f062 update to RxLifecycle 0.8.0, RxJava 1.2.0, RxAndroid 1.2.1 (#126) 2016-09-30 21:20:32 -05:00
Eric Kuck 8a890644ee Fixes issues with ControllerChangeHandlers being reused when they should not be. 2016-09-30 18:07:47 -05:00
Eric Kuck b2ffa7f7f6 Fixes #127 2016-09-30 17:38:14 -05:00
Eric Kuck 960b931744 Added a view state getter, which is needed if view state must be obtained at the time of view creation (ex: for Google Maps) 2016-09-30 17:33:54 -05:00
Eric Kuck 47158da05e Fixes tests 2016-09-28 13:16:56 -05:00
Eric Kuck b9c22d267d Fixed issue with no view being attached if orientation changed at the exact same instant as a SimpleSwapChangeHandler being run 2016-09-28 12:34:11 -05:00
Eric Kuck 46e6fac6db Fixed issue with AnimatorChangeHandler crashing if onAnimationEnd is called after onAnimationCancel 2016-09-27 15:57:37 -05:00
Eric Kuck dc68990bff Version bump 2016-09-26 10:32:23 -05:00
Eric Kuck e4f7e9e175 Fixes memory leak in CircularRevealAnimatorChangeHandler (and any other AnimatorChangeHandler that kept a reference to its view) 2016-09-26 09:50:29 -05:00
Eric Kuck 4ab99b68da Fixes #123 2016-09-26 09:34:41 -05:00
Eric Kuck b8bd64e078 Fixed leak in demo app 2016-09-26 09:24:32 -05:00
Eric Kuck 812d1f8911 Fixes issue where controllers that are being animated out, but haven't yet been detached, at the time of an orientation change could be incorrectly attached to the new Activity. 2016-09-24 11:24:13 -05:00
Eric Kuck ac8288fece Fixed issue where views would not detach when rapidly setting the root controller 2016-09-24 00:29:36 -05:00
Eric Kuck d2dd786b72 Fixed issue where setting the root controller twice in a row will cause the first controller's view to remain attached 2016-09-23 17:42:57 -05:00
Eric Kuck 7cf30b820c Travis yml fix 2016-09-06 16:44:45 -05:00
Eric Kuck 1120896438 Fixed some Leak Canary false-positives 2016-09-06 16:12:56 -05:00
Eric Kuck 093238cc52 Should fix #106 (more testing needed) 2016-09-06 16:10:47 -05:00
Eric Kuck c153e29273 Fixes #107 2016-09-06 15:49:30 -05:00
Eric Kuck 2757c7a4b6 Fixes #108 2016-09-06 15:46:25 -05:00
Eric Kuck 46091a7c99 Build tools update 2016-09-06 15:30:02 -05:00
Hannes Struß b0a5da2b82 Remove listeners after animator ends/is canceled (#104) 2016-09-05 10:43:22 -05:00
Adrian Pascu 0026566ba0 Nitpicks (#96)
* [Demo] Remove Butterknife unbinder from MainActivity

* Make deploy_snapshot.sh executable
2016-07-29 15:11:04 -05:00
dimsuz 9f640380bf Document a return value of Router.handleBack() (#93) 2016-07-20 17:49:59 -05:00
Eric Kuck a3faaede61 Fixes #97 and cleans up some tests 2016-07-20 15:41:19 -05:00
Eric Kuck 94d8add220 Fixes #98 2016-07-20 14:56:12 -05:00
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
63 changed files with 2013 additions and 660 deletions
Regular → Executable
View File
+1 -5
View File
@@ -14,14 +14,10 @@ jdk:
- oraclejdk8
branches:
only
- v2
only:
- develop
- master
before_install:
- chmod +x .buildscript/deploy_snapshot.sh
after_success:
- .buildscript/deploy_snapshot.sh
+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.4'
// 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.4'
// If you want RxJava/RxAndroid lifecycle support:
compile 'com.bluelinelabs:conductor-rxlifecycle:2.0.0'
compile 'com.bluelinelabs:conductor-rxlifecycle:2.0.4'
```
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.5-SNAPSHOT'
compile 'com.bluelinelabs:conductor-support:2.0.5-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle:2.0.5-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.
+1 -1
View File
@@ -3,7 +3,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.2'
classpath 'com.android.tools.build:gradle:2.2.2'
}
}
+1
View File
@@ -28,6 +28,7 @@ dependencies {
compile rootProject.ext.rxJava
compile rootProject.ext.rxAndroid
compile rootProject.ext.rxLifecycle
compile rootProject.ext.rxLifecycleAndroid
compile project(':conductor')
}
@@ -1,38 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import rx.Observable;
/**
* Interface used for RxController. Can also be used if writing your own Controller component without subclassing RxController.
*/
public interface ControllerLifecycleProvider {
/**
* @return An observable that will have all {@link com.bluelinelabs.conductor.Controller} lifecycle events
*/
@NonNull
@CheckResult
Observable<ControllerEvent> lifecycle();
/**
* Will bind the source until a specific {@link ControllerEvent} occurs.
*
* @param event The {@link ControllerEvent} that should cause onComplete to be called
* @return A {@link rx.Observable.Transformer} that will call onComplete when the event occurs.
*/
@NonNull
@CheckResult
<T> Observable.Transformer<T, T> bindUntilEvent(@NonNull ControllerEvent event);
/**
* Will bind the source until the next reasonable {@link ControllerEvent} occurs.
* @return A {@link rx.Observable.Transformer} that will call onComplete when the event occurs.
*/
@NonNull
@CheckResult
<T> Observable.Transformer<T, T> bindToLifecycle();
}
@@ -5,6 +5,8 @@ import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.Controller;
import com.trello.rxlifecycle.LifecycleProvider;
import com.trello.rxlifecycle.LifecycleTransformer;
import com.trello.rxlifecycle.RxLifecycle;
import rx.Observable;
@@ -13,7 +15,7 @@ import rx.subjects.BehaviorSubject;
/**
* A base {@link Controller} that can be used to expose lifecycle events using RxJava
*/
public abstract class RxController extends Controller implements ControllerLifecycleProvider {
public abstract class RxController extends Controller implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
@@ -36,14 +38,14 @@ public abstract class RxController extends Controller implements ControllerLifec
@Override
@NonNull
@CheckResult
public final <T> Observable.Transformer<T, T> bindUntilEvent(@NonNull ControllerEvent event) {
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> Observable.Transformer<T, T> bindToLifecycle() {
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
@@ -14,7 +14,7 @@ public class RxControllerLifecycle {
/**
* Binds the given source to a Controller lifecycle. This is the Controller version of
* {@link com.trello.rxlifecycle.RxLifecycle#bindFragment(Observable)}.
* {@link com.trello.rxlifecycle.android.RxLifecycleAndroid#bindFragment(Observable)}.
*
* @param lifecycle the lifecycle sequence of a Controller
* @return a reusable {@link Observable.Transformer} that unsubscribes the source during the Controller lifecycle
@@ -0,0 +1,52 @@
package com.bluelinelabs.conductor.rxlifecycle;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.RestoreViewOnCreateController;
import com.trello.rxlifecycle.LifecycleProvider;
import com.trello.rxlifecycle.LifecycleTransformer;
import com.trello.rxlifecycle.RxLifecycle;
import rx.Observable;
import rx.subjects.BehaviorSubject;
/**
* A base {@link RestoreViewOnCreateController} that can be used to expose lifecycle events using RxJava
*/
public abstract class RxRestoreViewOnCreateController extends RestoreViewOnCreateController implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxRestoreViewOnCreateController() {
this(null);
}
public RxRestoreViewOnCreateController(Bundle args) {
super(args);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.asObservable();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
@@ -4,6 +4,7 @@ import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
@@ -30,13 +31,13 @@ public class ActivityHostedRouter extends Router {
}
}
@Override
@Override @Nullable
public Activity getActivity() {
return lifecycleHandler != null ? lifecycleHandler.getLifecycleActivity() : null;
}
@Override
public void onActivityDestroyed(Activity activity) {
public void onActivityDestroyed(@NonNull Activity activity) {
super.onActivityDestroyed(activity);
lifecycleHandler = null;
}
@@ -49,37 +50,37 @@ public class ActivityHostedRouter extends Router {
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
lifecycleHandler.onActivityResult(requestCode, resultCode, data);
}
@Override
void startActivity(Intent intent) {
void startActivity(@NonNull Intent intent) {
lifecycleHandler.startActivity(intent);
}
@Override
void startActivityForResult(String instanceId, Intent intent, int requestCode) {
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) {
lifecycleHandler.startActivityForResult(instanceId, intent, requestCode);
}
@Override
void startActivityForResult(String instanceId, Intent intent, int requestCode, Bundle options) {
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options) {
lifecycleHandler.startActivityForResult(instanceId, intent, requestCode, options);
}
@Override
void registerForActivityResult(String instanceId, int requestCode) {
void registerForActivityResult(@NonNull String instanceId, int requestCode) {
lifecycleHandler.registerForActivityResult(instanceId, requestCode);
}
@Override
void unregisterForActivityResults(String instanceId) {
void unregisterForActivityResults(@NonNull String instanceId) {
lifecycleHandler.unregisterForActivityResults(instanceId);
}
@Override
void requestPermissions(String instanceId, @NonNull String[] permissions, int requestCode) {
void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
lifecycleHandler.requestPermissions(instanceId, permissions, requestCode);
}
@@ -88,8 +89,13 @@ public class ActivityHostedRouter extends Router {
return lifecycleHandler != null;
}
@Override
@Override @NonNull
List<Router> getSiblingRouters() {
return lifecycleHandler.getRouters();
}
@Override @NonNull
Router getRootRouter() {
return this;
}
}
@@ -1,6 +1,8 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -24,20 +26,23 @@ class Backstack implements Iterable<RouterTransaction> {
return backStack.size();
}
@Nullable
public RouterTransaction root() {
return backStack.size() > 0 ? backStack.getLast() : null;
}
@Override
@Override @NonNull
public Iterator<RouterTransaction> iterator() {
return backStack.iterator();
}
@NonNull
public Iterator<RouterTransaction> reverseIterator() {
return backStack.descendingIterator();
}
public List<RouterTransaction> popTo(RouterTransaction transaction) {
@NonNull
public List<RouterTransaction> popTo(@NonNull RouterTransaction transaction) {
List<RouterTransaction> popped = new ArrayList<>();
if (backStack.contains(transaction)) {
while (backStack.peek() != transaction) {
@@ -50,24 +55,27 @@ class Backstack implements Iterable<RouterTransaction> {
return popped;
}
@NonNull
public RouterTransaction pop() {
RouterTransaction popped = backStack.pop();
popped.controller.destroy();
return popped;
}
@Nullable
public RouterTransaction peek() {
return backStack.peek();
}
public void remove(RouterTransaction transaction) {
public void remove(@NonNull RouterTransaction transaction) {
backStack.removeFirstOccurrence(transaction);
}
public void push(RouterTransaction transaction) {
public void push(@NonNull RouterTransaction transaction) {
backStack.push(transaction);
}
@NonNull
public List<RouterTransaction> popAll() {
List<RouterTransaction> list = new ArrayList<>();
while (!isEmpty()) {
@@ -76,7 +84,7 @@ class Backstack implements Iterable<RouterTransaction> {
return list;
}
public void setBackstack(List<RouterTransaction> backstack) {
public void setBackstack(@NonNull List<RouterTransaction> backstack) {
for (RouterTransaction existingTransaction : backStack) {
boolean contains = false;
for (RouterTransaction newTransaction : backstack) {
@@ -97,7 +105,7 @@ class Backstack implements Iterable<RouterTransaction> {
}
}
public void saveInstanceState(Bundle outState) {
public void saveInstanceState(@NonNull Bundle outState) {
ArrayList<Bundle> entryBundles = new ArrayList<>(backStack.size());
for (RouterTransaction entry : backStack) {
entryBundles.add(entry.saveInstanceState());
@@ -106,7 +114,7 @@ class Backstack implements Iterable<RouterTransaction> {
outState.putParcelableArrayList(KEY_ENTRIES, entryBundles);
}
public void restoreInstanceState(Bundle savedInstanceState) {
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
ArrayList<Bundle> entryBundles = savedInstanceState.getParcelableArrayList(KEY_ENTRIES);
if (entryBundles != null) {
Collections.reverse(entryBundles);
@@ -3,6 +3,8 @@ package com.bluelinelabs.conductor;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewGroup;
@@ -42,12 +44,12 @@ public class ChangeHandlerFrameLayout extends FrameLayout implements ControllerC
}
@Override
public void onChangeStarted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler) {
public void onChangeStarted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) {
inProgressTransactionCount++;
}
@Override
public void onChangeCompleted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler) {
public void onChangeCompleted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) {
inProgressTransactionCount--;
}
@@ -3,6 +3,8 @@ package com.bluelinelabs.conductor;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.internal.LifecycleHandler;
@@ -26,7 +28,8 @@ public final class Conductor {
* for restoring the Router's state if possible.
* @return A fully configured {@link Router} instance for use with this Activity/ViewGroup pair.
*/
public static Router attachRouter(@NonNull Activity activity, @NonNull ViewGroup container, Bundle savedInstanceState) {
@NonNull @UiThread
public static Router attachRouter(@NonNull Activity activity, @NonNull ViewGroup container, @Nullable Bundle savedInstanceState) {
LifecycleHandler lifecycleHandler = LifecycleHandler.install(activity);
Router router = lifecycleHandler.getRouter(container, savedInstanceState);
@@ -10,6 +10,7 @@ import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.SparseArray;
import android.view.LayoutInflater;
@@ -20,16 +21,15 @@ import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.Router.OnControllerPushedListener;
import com.bluelinelabs.conductor.internal.ClassUtils;
import com.bluelinelabs.conductor.internal.RouterRequiringFunc;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
@@ -54,10 +54,11 @@ 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;
private Bundle viewState;
Bundle viewState;
private Bundle savedInstanceState;
private boolean isBeingDestroyed;
private boolean destroyed;
@@ -72,6 +73,7 @@ public abstract class Controller {
private String instanceId;
private String targetInstanceId;
private boolean needsAttach;
private boolean attachedToUnownedParent;
private boolean hasSavedViewState;
private boolean isDetachFrozen;
private ControllerChangeHandler overriddenPushHandler;
@@ -82,21 +84,18 @@ 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 Deque<Controller> childBackstack = new ArrayDeque<>();
private final List<Controller> childBackstack = new LinkedList<>();
private WeakReference<View> destroyedView;
private final ControllerChangeListener childRouterChangeListener = new ControllerChangeListener() {
private final OnControllerPushedListener onControllerPushedListener = new OnControllerPushedListener() {
@Override
public void onChangeStarted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler) {
if (isPush) {
onChildControllerPushed(to);
}
public void onControllerPushed(Controller controller) {
onChildControllerPushed(controller);
}
@Override
public void onChangeCompleted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler) { }
};
static Controller newInstance(Bundle bundle) {
@NonNull
static Controller newInstance(@NonNull Bundle bundle) {
final String className = bundle.getString(KEY_CLASS_NAME);
//noinspection ConstantConditions
Constructor[] constructors = ClassUtils.classForName(className, false).getConstructors();
@@ -130,7 +129,7 @@ public abstract class Controller {
*
* @param args Any arguments that need to be retained.
*/
protected Controller(Bundle args) {
protected Controller(@Nullable Bundle args) {
this.args = args;
instanceId = UUID.randomUUID().toString();
ensureRequiredConstructor();
@@ -159,11 +158,34 @@ public abstract class Controller {
/**
* Returns any arguments that were set in this Controller's constructor
*/
@Nullable
public Bundle getArgs() {
return args;
}
public final Router getChildRouter(@NonNull ViewGroup container, String tag) {
/**
* Retrieves the child {@link Router} for the given container. If no child router for this container
* exists yet, it will be created.
*
* @param container The ViewGroup that hosts the child Router
*/
@NonNull
public final Router getChildRouter(@NonNull ViewGroup container) {
//noinspection deprecation
return getChildRouter(container, null);
}
/**
* Retrieves the child {@link Router} for the given container/tag combination. If no child router for
* this container exists yet, it will be created. 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).
*
* @param container The ViewGroup that hosts the child Router
* @param tag The router's tag
*/
@NonNull
public final Router getChildRouter(@NonNull ViewGroup container, @Nullable String tag) {
@IdRes final int containerId = container.getId();
ControllerHostedRouter childRouter = null;
@@ -218,6 +240,7 @@ public abstract class Controller {
/**
* Return this Controller's View, if available.
*/
@Nullable
public final View getView() {
return view;
}
@@ -225,6 +248,7 @@ public abstract class Controller {
/**
* Returns the host Activity of this Controller's {@link Router}
*/
@Nullable
public final Activity getActivity() {
return router.getActivity();
}
@@ -232,6 +256,7 @@ public abstract class Controller {
/**
* Returns the Resources from the host Activity
*/
@Nullable
public final Resources getResources() {
Activity activity = getActivity();
return activity != null ? activity.getResources() : null;
@@ -240,6 +265,7 @@ public abstract class Controller {
/**
* Returns the Application Context derived from the host Activity
*/
@Nullable
public final Context getApplicationContext() {
Activity activity = getActivity();
return activity != null ? activity.getApplicationContext() : null;
@@ -248,6 +274,7 @@ public abstract class Controller {
/**
* Returns this Controller's parent Controller if it is a child Controller.
*/
@Nullable
public final Controller getParentController() {
return parentController;
}
@@ -256,6 +283,7 @@ public abstract class Controller {
* Returns this Controller's instance ID, which is generated when the instance is created and
* retained across restarts.
*/
@NonNull
public final String getInstanceId() {
return instanceId;
}
@@ -266,7 +294,8 @@ public abstract class Controller {
* @param instanceId The instance ID being searched for
* @return The matching Controller, if one exists
*/
final Controller findController(String instanceId) {
@Nullable
final Controller findController(@NonNull String instanceId) {
if (this.instanceId.equals(instanceId)) {
return this;
}
@@ -283,6 +312,7 @@ public abstract class Controller {
/**
* Returns all of this Controller's child Routers
*/
@NonNull
public final List<Router> getChildRouters() {
List<Router> routers = new ArrayList<>();
for (Router router : childRouters) {
@@ -298,7 +328,7 @@ public abstract class Controller {
*
* @param target The Controller that is the target of this one.
*/
public void setTargetController(Controller target) {
public void setTargetController(@Nullable Controller target) {
if (targetInstanceId != null) {
throw new RuntimeException("Target controller already set. A controller's target may only be set once.");
}
@@ -311,8 +341,12 @@ public abstract class Controller {
*
* @return This Controller's target
*/
@Nullable
public final Controller getTargetController() {
return targetInstanceId != null ? router.getControllerWithInstanceId(targetInstanceId) : null;
if (targetInstanceId != null) {
return router.getRootRouter().getControllerWithInstanceId(targetInstanceId);
}
return null;
}
/**
@@ -321,7 +355,7 @@ public abstract class Controller {
*
* @param view The View to which this Controller should be bound.
*/
protected void onDestroyView(View view) { }
protected void onDestroyView(@NonNull View view) { }
/**
* Called when this Controller begins the process of being swapped in or out of the host view.
@@ -361,22 +395,22 @@ public abstract class Controller {
/**
* Called when this Controller's host Activity is started
*/
protected void onActivityStarted(Activity activity) { }
protected void onActivityStarted(@NonNull Activity activity) { }
/**
* Called when this Controller's host Activity is resumed
*/
protected void onActivityResumed(Activity activity) { }
protected void onActivityResumed(@NonNull Activity activity) { }
/**
* Called when this Controller's host Activity is paused
*/
protected void onActivityPaused(Activity activity) { }
protected void onActivityPaused(@NonNull Activity activity) { }
/**
* Called when this Controller's host Activity is stopped
*/
protected void onActivityStopped(Activity activity) { }
protected void onActivityStopped(@NonNull Activity activity) { }
/**
* Called to save this Controller's View state. As Views can be detached and destroyed as part of the
@@ -415,7 +449,7 @@ public abstract class Controller {
/**
* Calls startActivity(Intent) from this Controller's host Activity.
*/
public final void startActivity(final Intent intent) {
public final void startActivity(@NonNull final Intent intent) {
executeWithRouter(new RouterRequiringFunc() {
@Override public void execute() { router.startActivity(intent); }
});
@@ -424,7 +458,7 @@ public abstract class Controller {
/**
* Calls startActivityForResult(Intent, int) from this Controller's host Activity.
*/
public final void startActivityForResult(final Intent intent, final int requestCode) {
public final void startActivityForResult(@NonNull final Intent intent, final int requestCode) {
executeWithRouter(new RouterRequiringFunc() {
@Override public void execute() { router.startActivityForResult(instanceId, intent, requestCode); }
});
@@ -433,7 +467,7 @@ public abstract class Controller {
/**
* Calls startActivityForResult(Intent, int, Bundle) from this Controller's host Activity.
*/
public final void startActivityForResult(final Intent intent, final int requestCode, final Bundle options) {
public final void startActivityForResult(@NonNull final Intent intent, final int requestCode, @Nullable final Bundle options) {
executeWithRouter(new RouterRequiringFunc() {
@Override public void execute() { router.startActivityForResult(instanceId, intent, requestCode, options); }
});
@@ -459,7 +493,7 @@ public abstract class Controller {
* @param resultCode The resultCode that was returned to the host Activity's onActivityResult method
* @param data The data Intent that was returned to the host Activity's onActivityResult method
*/
public void onActivityResult(int requestCode, int resultCode, Intent data) { }
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { }
/**
* Calls requestPermission(String[], int) from this Controller's host Activity. Results for this request,
@@ -500,9 +534,8 @@ public abstract class Controller {
* @return True if this Controller has consumed the back button press, otherwise false
*/
public boolean handleBack() {
Iterator<Controller> childIterator = childBackstack.descendingIterator();
while (childIterator.hasNext()) {
Controller childController = childIterator.next();
for (int i = childBackstack.size() - 1; i >= 0; i--) {
Controller childController = childBackstack.get(i);
if (childController.isAttached() && childController.getRouter().handleBack()) {
return true;
}
@@ -516,7 +549,7 @@ public abstract class Controller {
*
* @param lifecycleListener The listener
*/
public void addLifecycleListener(LifecycleListener lifecycleListener) {
public void addLifecycleListener(@NonNull LifecycleListener lifecycleListener) {
if (!lifecycleListeners.contains(lifecycleListener)) {
lifecycleListeners.add(lifecycleListener);
}
@@ -527,13 +560,14 @@ public abstract class Controller {
*
* @param lifecycleListener The listener to be removed
*/
public void removeLifecycleListener(LifecycleListener lifecycleListener) {
public void removeLifecycleListener(@NonNull LifecycleListener lifecycleListener) {
lifecycleListeners.remove(lifecycleListener);
}
/**
* Returns this Controller's {@link RetainViewMode}. Defaults to {@link RetainViewMode#RELEASE_DETACH}.
*/
@NonNull
public RetainViewMode getRetainViewMode() {
return retainViewMode;
}
@@ -542,8 +576,8 @@ public abstract class Controller {
* Sets this Controller's {@link RetainViewMode}, which will influence when its view will be released.
* This is useful when a Controller's view hierarchy is expensive to tear down and rebuild.
*/
public void setRetainViewMode(RetainViewMode retainViewMode) {
this.retainViewMode = retainViewMode;
public void setRetainViewMode(@NonNull RetainViewMode retainViewMode) {
this.retainViewMode = retainViewMode != null ? retainViewMode : RetainViewMode.RELEASE_DETACH;
if (this.retainViewMode == RetainViewMode.RELEASE_DETACH && !attached) {
removeViewReference();
}
@@ -553,6 +587,7 @@ public abstract class Controller {
* Returns the {@link ControllerChangeHandler} that should be used for pushing this Controller, or null
* if the handler from the {@link RouterTransaction} should be used instead.
*/
@Nullable
public final ControllerChangeHandler getOverriddenPushHandler() {
return overriddenPushHandler;
}
@@ -561,7 +596,7 @@ public abstract class Controller {
* Overrides the {@link ControllerChangeHandler} that should be used for pushing this Controller. If this is a
* non-null value, it will be used instead of the handler from the {@link RouterTransaction}.
*/
public void overridePushHandler(ControllerChangeHandler overriddenPushHandler) {
public void overridePushHandler(@Nullable ControllerChangeHandler overriddenPushHandler) {
this.overriddenPushHandler = overriddenPushHandler;
}
@@ -569,6 +604,7 @@ public abstract class Controller {
* Returns the {@link ControllerChangeHandler} that should be used for popping this Controller, or null
* if the handler from the {@link RouterTransaction} should be used instead.
*/
@Nullable
public ControllerChangeHandler getOverriddenPopHandler() {
return overriddenPopHandler;
}
@@ -577,7 +613,7 @@ public abstract class Controller {
* Overrides the {@link ControllerChangeHandler} that should be used for popping this Controller. If this is a
* non-null value, it will be used instead of the handler from the {@link RouterTransaction}.
*/
public void overridePopHandler(ControllerChangeHandler overriddenPopHandler) {
public void overridePopHandler(@Nullable ControllerChangeHandler overriddenPopHandler) {
this.overriddenPopHandler = overriddenPopHandler;
}
@@ -621,7 +657,7 @@ public abstract class Controller {
* @param menu The menu into which your options should be placed.
* @param inflater The inflater that can be used to inflate your menu items.
*/
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { }
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { }
/**
* Prepare the screen's options menu to be displayed. This is called directly before showing the
@@ -629,7 +665,7 @@ public abstract class Controller {
*
* @param menu The menu that will be displayed
*/
public void onPrepareOptionsMenu(Menu menu) { }
public void onPrepareOptionsMenu(@NonNull Menu menu) { }
/**
* Called when an option menu item has been selected by the user.
@@ -637,10 +673,14 @@ public abstract class Controller {
* @param item The selected item.
* @return True if this event has been consumed, false if it has not.
*/
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
return false;
}
final void setNeedsAttach() {
needsAttach = true;
}
final void prepareForHostDetach() {
needsAttach = needsAttach || attached;
@@ -685,23 +725,25 @@ public abstract class Controller {
}
}
final void activityStarted(Activity activity) {
final void activityStarted(@NonNull Activity activity) {
onActivityStarted(activity);
}
final void activityResumed(Activity activity) {
final void activityResumed(@NonNull Activity activity) {
if (!attached && view != null && viewIsAttached) {
attach(view);
} else if (attached) {
needsAttach = false;
}
onActivityResumed(activity);
}
final void activityPaused(Activity activity) {
final void activityPaused(@NonNull Activity activity) {
onActivityPaused(activity);
}
final void activityStopped(Activity activity) {
final void activityStopped(@NonNull Activity activity) {
onActivityStopped(activity);
}
@@ -714,9 +756,15 @@ public abstract class Controller {
}
private void attach(@NonNull View view) {
attachedToUnownedParent = router == null || view.getParent() != router.container;
if (attachedToUnownedParent) {
return;
}
hasSavedViewState = false;
for (LifecycleListener lifecycleListener : lifecycleListeners) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preAttach(this, view);
}
@@ -729,20 +777,24 @@ public abstract class Controller {
router.invalidateOptionsMenu();
}
for (LifecycleListener lifecycleListener : lifecycleListeners) {
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postAttach(this, view);
}
}
private void detach(@NonNull View view, boolean forceViewRefRemoval) {
for (ControllerHostedRouter router : childRouters) {
router.prepareForHostDetach();
void detach(@NonNull View view, boolean forceViewRefRemoval) {
if (!attachedToUnownedParent) {
for (ControllerHostedRouter router : childRouters) {
router.prepareForHostDetach();
}
}
final boolean removeViewRef = forceViewRefRemoval || retainViewMode == RetainViewMode.RELEASE_DETACH || isBeingDestroyed;
if (attached) {
for (LifecycleListener lifecycleListener : lifecycleListeners) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preDetach(this, view);
}
@@ -753,7 +805,8 @@ public abstract class Controller {
router.invalidateOptionsMenu();
}
for (LifecycleListener lifecycleListener : lifecycleListeners) {
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postDetach(this, view);
}
}
@@ -769,7 +822,8 @@ public abstract class Controller {
saveViewState(view);
}
for (LifecycleListener lifecycleListener : lifecycleListeners) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preDestroyView(this, view);
}
@@ -777,9 +831,14 @@ public abstract class Controller {
view.removeOnAttachStateChangeListener(onAttachStateChangeListener);
viewIsAttached = false;
if (isBeingDestroyed) {
destroyedView = new WeakReference<>(view);
}
view = null;
for (LifecycleListener lifecycleListener : lifecycleListeners) {
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postDestroyView(this);
}
@@ -800,13 +859,15 @@ public abstract class Controller {
}
if (view == null) {
for (LifecycleListener lifecycleListener : lifecycleListeners) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preCreateView(this);
}
view = onCreateView(LayoutInflater.from(parent.getContext()), parent);
for (LifecycleListener lifecycleListener : lifecycleListeners) {
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postCreateView(this, view);
}
@@ -839,23 +900,21 @@ public abstract class Controller {
return view;
}
final void performDestroy() {
private void performDestroy() {
if (!destroyed) {
for (LifecycleListener lifecycleListener : lifecycleListeners) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preDestroy(this);
}
destroyed = true;
if (router != null) {
router.unregisterForActivityResults(instanceId);
}
onDestroy();
parentController = null;
for (LifecycleListener lifecycleListener : lifecycleListeners) {
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postDestroy(this);
}
}
@@ -865,9 +924,13 @@ public abstract class Controller {
destroy(false);
}
final void destroy(boolean removeViews) {
private void destroy(boolean removeViews) {
isBeingDestroyed = true;
if (router != null) {
router.unregisterForActivityResults(instanceId);
}
for (ControllerHostedRouter childRouter : childRouters) {
childRouter.destroy();
}
@@ -875,11 +938,11 @@ public abstract class Controller {
if (!attached) {
removeViewReference();
} else if (removeViews) {
detach(view, false);
detach(view, true);
}
}
final void saveViewState(@NonNull View view) {
private void saveViewState(@NonNull View view) {
hasSavedViewState = true;
viewState = new Bundle();
@@ -892,12 +955,13 @@ public abstract class Controller {
onSaveViewState(view, stateBundle);
viewState.putBundle(KEY_VIEW_STATE_BUNDLE, stateBundle);
for (LifecycleListener lifecycleListener : lifecycleListeners) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.onSaveViewState(this, viewState);
}
}
final void restoreViewState(@NonNull View view) {
private void restoreViewState(@NonNull View view) {
if (viewState != null) {
view.restoreHierarchyState(viewState.getSparseParcelableArray(KEY_VIEW_STATE_HIERARCHY));
onRestoreViewState(view, viewState.getBundle(KEY_VIEW_STATE_BUNDLE));
@@ -908,12 +972,14 @@ public abstract class Controller {
if (containerView != null && containerView instanceof ViewGroup) {
childRouter.setHost(this, (ViewGroup)containerView);
monitorChildRouter(childRouter);
childRouter.rebindIfNeeded();
}
}
}
for (LifecycleListener lifecycleListener : lifecycleListeners) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.onRestoreViewState(this, viewState);
}
}
@@ -932,6 +998,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());
@@ -951,7 +1018,8 @@ public abstract class Controller {
Bundle savedState = new Bundle();
onSaveInstanceState(savedState);
for (LifecycleListener lifecycleListener : lifecycleListeners) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.onSaveInstanceState(this, savedState);
}
@@ -968,6 +1036,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) {
@@ -985,7 +1054,8 @@ public abstract class Controller {
if (savedInstanceState != null && router != null) {
onRestoreInstanceState(savedInstanceState);
for (LifecycleListener lifecycleListener : lifecycleListeners) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.onRestoreInstanceState(this, savedInstanceState);
}
@@ -993,7 +1063,7 @@ public abstract class Controller {
}
}
final void changeStarted(ControllerChangeHandler changeHandler, ControllerChangeType changeType) {
final void changeStarted(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
if (!changeType.isEnter) {
for (ControllerHostedRouter router : childRouters) {
router.setDetachFrozen(true);
@@ -1002,12 +1072,13 @@ public abstract class Controller {
onChangeStarted(changeHandler, changeType);
for (LifecycleListener lifecycleListener : lifecycleListeners) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.onChangeStart(this, changeHandler, changeType);
}
}
final void changeEnded(ControllerChangeHandler changeHandler, ControllerChangeType changeType) {
final void changeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
if (!changeType.isEnter) {
for (ControllerHostedRouter router : childRouters) {
router.setDetachFrozen(false);
@@ -1016,9 +1087,18 @@ public abstract class Controller {
onChangeEnded(changeHandler, changeType);
for (LifecycleListener lifecycleListener : lifecycleListeners) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.onChangeEnd(this, changeHandler, changeType);
}
if (isBeingDestroyed && !viewIsAttached && !attached && destroyedView != null) {
View view = destroyedView.get();
if (router.container != null && view != null && view.getParent() == router.container) {
router.container.removeView(view);
}
destroyedView = null;
}
}
final void setDetachFrozen(boolean frozen) {
@@ -1035,46 +1115,39 @@ public abstract class Controller {
}
}
final void createOptionsMenu(Menu menu, MenuInflater inflater) {
if (attached) {
if (hasOptionsMenu && !optionsMenuHidden) {
onCreateOptionsMenu(menu, inflater);
}
final void createOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
if (attached && hasOptionsMenu && !optionsMenuHidden) {
onCreateOptionsMenu(menu, inflater);
}
}
final void prepareOptionsMenu(Menu menu) {
if (attached) {
if (hasOptionsMenu && !optionsMenuHidden) {
onPrepareOptionsMenu(menu);
}
final void prepareOptionsMenu(@NonNull Menu menu) {
if (attached && hasOptionsMenu && !optionsMenuHidden) {
onPrepareOptionsMenu(menu);
}
}
final boolean optionsItemSelected(MenuItem item) {
if (attached) {
if (hasOptionsMenu && !optionsMenuHidden && onOptionsItemSelected(item)) {
return true;
}
final boolean optionsItemSelected(@NonNull MenuItem item) {
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);
}
});
}
return false;
}
private void monitorChildRouter(ControllerHostedRouter childRouter) {
childRouter.addChangeListener(childRouterChangeListener);
}
private void onChildControllerPushed(Controller controller) {
childBackstack.add(controller);
controller.addLifecycleListener(new LifecycleListener() {
@Override
public void postDestroy(@NonNull Controller controller) {
childBackstack.remove(controller);
}
});
}
final void setParentController(Controller controller) {
final void setParentController(@Nullable Controller controller) {
parentController = controller;
}
@@ -1085,7 +1158,8 @@ public abstract class Controller {
}
}
private static Constructor getDefaultConstructor(Constructor[] constructors) {
@Nullable
private static Constructor getDefaultConstructor(@NonNull Constructor[] constructors) {
for (Constructor constructor : constructors) {
if (constructor.getParameterTypes().length == 0) {
return constructor;
@@ -1094,7 +1168,8 @@ public abstract class Controller {
return null;
}
private static Constructor getBundleConstructor(Constructor[] constructors) {
@Nullable
private static Constructor getBundleConstructor(@NonNull Constructor[] constructors) {
for (Constructor constructor : constructors) {
if (constructor.getParameterTypes().length == 1 && constructor.getParameterTypes()[0] == Bundle.class) {
return constructor;
@@ -1137,6 +1212,7 @@ public abstract class Controller {
public void onSaveViewState(@NonNull Controller controller, @NonNull Bundle outState) { }
public void onRestoreViewState(@NonNull Controller controller, @NonNull Bundle savedViewState) { }
}
}
@@ -2,8 +2,10 @@ package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import com.bluelinelabs.conductor.internal.ClassUtils;
@@ -25,16 +27,18 @@ public abstract class ControllerChangeHandler {
private static final Map<String, ControllerChangeHandler> inProgressPushHandlers = new HashMap<>();
private boolean forceRemoveViewOnPush;
/**
* 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 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, 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 changeListener This listener must be called when any transitions or animations are completed.
*/
public abstract void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener);
public abstract void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener);
public ControllerChangeHandler() {
ensureDefaultConstructor();
@@ -59,10 +63,17 @@ public abstract class ControllerChangeHandler {
* 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 newTop the controller that will now be at the top of the backstack
*/
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, Controller newTop) { }
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) { }
/**
* Will be called on change handlers that push a controller if the controller being pushed is
* needs to be attached immediately, without any animations or transitions.
*/
public void completeImmediately() { }
@NonNull
final Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putString(KEY_CLASS_NAME, getClass().getName());
@@ -74,6 +85,11 @@ public abstract class ControllerChangeHandler {
return bundle;
}
@NonNull
final ControllerChangeHandler copy() {
return fromBundle(toBundle());
}
private void ensureDefaultConstructor() {
try {
getClass().getConstructor();
@@ -82,7 +98,8 @@ public abstract class ControllerChangeHandler {
}
}
public static ControllerChangeHandler fromBundle(Bundle bundle) {
@Nullable
public static ControllerChangeHandler fromBundle(@Nullable Bundle bundle) {
if (bundle != null) {
String className = bundle.getString(KEY_CLASS_NAME);
ControllerChangeHandler changeHandler = ClassUtils.newInstance(className);
@@ -94,26 +111,44 @@ public abstract class ControllerChangeHandler {
}
}
public static void executeChange(final Controller to, final Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler inHandler) {
static boolean completePushImmediately(@NonNull String controllerInstanceId) {
ControllerChangeHandler changeHandler = inProgressPushHandlers.get(controllerInstanceId);
if (changeHandler != null) {
changeHandler.completeImmediately();
inProgressPushHandlers.remove(controllerInstanceId);
return true;
}
return false;
}
public 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);
inProgressPushHandlers.remove(toAbort.getInstanceId());
}
}
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(final Controller to, final Controller from, final boolean isPush, final ViewGroup container, final ControllerChangeHandler inHandler, @NonNull final List<ControllerChangeListener> listeners) {
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) {
if (container != null) {
final ControllerChangeHandler handler = inHandler != null ? inHandler : new SimpleSwapChangeHandler();
if (isPush) {
if (isPush && to != null) {
inProgressPushHandlers.put(to.getInstanceId(), handler);
} else if (from != null) {
ControllerChangeHandler handlerForPush = inProgressPushHandlers.get(from.getInstanceId());
if (handlerForPush != null) {
handlerForPush.onAbortPush(handler, to);
inProgressPushHandlers.remove(from.getInstanceId());
if (from != null) {
completePushImmediately(from.getInstanceId());
}
} else if (!isPush && from != null) {
abortPush(from, to, handler);
}
for (ControllerChangeListener listener : listeners) {
listener.onChangeStarted(to, from, isPush, container, inHandler);
listener.onChangeStarted(to, from, isPush, container, handler);
}
final ControllerChangeType toChangeType = isPush ? ControllerChangeType.PUSH_ENTER : ControllerChangeType.POP_ENTER;
@@ -138,7 +173,6 @@ public abstract class ControllerChangeHandler {
handler.performChange(container, fromView, toView, isPush, new ControllerChangeCompletedListener() {
@Override
public void onChangeCompleted() {
if (from != null) {
from.changeEnded(handler, fromChangeType);
}
@@ -149,13 +183,28 @@ public abstract class ControllerChangeHandler {
}
for (ControllerChangeListener listener : listeners) {
listener.onChangeCompleted(to, from, isPush, container, inHandler);
listener.onChangeCompleted(to, from, isPush, container, handler);
}
if (handler.forceRemoveViewOnPush && fromView != null) {
ViewParent fromParent = fromView.getParent();
if (fromParent != null && fromParent instanceof ViewGroup) {
((ViewGroup)fromParent).removeView(fromView);
}
}
}
});
}
}
public boolean removesFromViewOnPush() {
return true;
}
public void setForceRemoveViewOnPush(boolean force) {
forceRemoveViewOnPush = force;
}
/**
* A listener interface useful for allowing external classes to be notified of change events.
*/
@@ -163,23 +212,24 @@ 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 isPush True if this is a push operation, or false if it's a pop.
* @param to The new Controller
* @param from The old Controller
* @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.
* @param handler The change handler being used.
*/
void onChangeStarted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler);
void onChangeStarted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler);
/**
* 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
* @param from The old Controller
* @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.
* @param handler The change handler that was used.
*/
void onChangeCompleted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler);
void onChangeCompleted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler);
}
/**
@@ -5,6 +5,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
@@ -12,7 +13,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 +23,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, @Nullable 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,36 +43,54 @@ public class ControllerHostedRouter extends Router {
}
}
public final void removeHost() {
final void removeHost() {
if (container != null && container instanceof ControllerChangeListener) {
removeChangeListener((ControllerChangeListener)container);
}
final List<Controller> controllersToDestroy = new ArrayList<>(destroyingControllers);
for (Controller controller : controllersToDestroy) {
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) {
for (RouterTransaction transaction : backStack) {
final void setDetachFrozen(boolean frozen) {
for (RouterTransaction transaction : backstack) {
transaction.controller.setDetachFrozen(frozen);
}
}
@Override
void destroy() {
setDetachFrozen(false);
super.destroy();
}
@Override @Nullable
public Activity getActivity() {
return hostController != null ? hostController.getActivity() : null;
}
@Override
public void onActivityDestroyed(Activity activity) {
public void onActivityDestroyed(@NonNull Activity activity) {
super.onActivityDestroyed(activity);
removeHost();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().onActivityResult(requestCode, resultCode, data);
}
@@ -85,42 +104,42 @@ public class ControllerHostedRouter extends Router {
}
@Override
void startActivity(Intent intent) {
void startActivity(@NonNull Intent intent) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().startActivity(intent);
}
}
@Override
void startActivityForResult(String instanceId, Intent intent, int requestCode) {
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().startActivityForResult(instanceId, intent, requestCode);
}
}
@Override
void startActivityForResult(String instanceId, Intent intent, int requestCode, Bundle options) {
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().startActivityForResult(instanceId, intent, requestCode, options);
}
}
@Override
void registerForActivityResult(String instanceId, int requestCode) {
void registerForActivityResult(@NonNull String instanceId, int requestCode) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().registerForActivityResult(instanceId, requestCode);
}
}
@Override
void unregisterForActivityResults(String instanceId) {
void unregisterForActivityResults(@NonNull String instanceId) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().unregisterForActivityResults(instanceId);
}
}
@Override
void requestPermissions(String instanceId, String[] permissions, int requestCode) {
void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().requestPermissions(instanceId, permissions, requestCode);
}
@@ -132,7 +151,7 @@ public class ControllerHostedRouter extends Router {
}
@Override
public void saveInstanceState(Bundle outState) {
public void saveInstanceState(@NonNull Bundle outState) {
super.saveInstanceState(outState);
outState.putInt(KEY_HOST_ID, hostId);
@@ -140,7 +159,7 @@ public class ControllerHostedRouter extends Router {
}
@Override
public void restoreInstanceState(Bundle savedInstanceState) {
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
super.restoreInstanceState(savedInstanceState);
hostId = savedInstanceState.getInt(KEY_HOST_ID);
@@ -148,24 +167,34 @@ public class ControllerHostedRouter extends Router {
}
@Override
void setControllerRouter(Controller controller) {
void setControllerRouter(@NonNull Controller controller) {
super.setControllerRouter(controller);
controller.setParentController(hostController);
}
public int getHostId() {
int getHostId() {
return hostId;
}
public String getTag() {
@Nullable
String getTag() {
return tag;
}
@Override
@Override @NonNull
List<Router> getSiblingRouters() {
List<Router> list = new ArrayList<>();
list.addAll(hostController.getChildRouters());
list.addAll(hostController.getRouter().getSiblingRouters());
return list;
}
@Override @NonNull
Router getRootRouter() {
if (hostController != null && hostController.getRouter() != null) {
return hostController.getRouter().getRootRouter();
} else {
return this;
}
}
}
@@ -0,0 +1,53 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
/**
* A simple controller subclass that changes the onCreateView signature to include a saved view state parameter.
* This is necessary for some third party libraries like Google Maps, which require passing in a saved state
* bundle at the time of creation.
*/
abstract public class RestoreViewOnCreateController extends Controller {
/**
* Convenience constructor for use when no arguments are needed.
*/
protected RestoreViewOnCreateController() {
super(null);
}
/**
* Constructor that takes arguments that need to be retained across restarts.
*
* @param args Any arguments that need to be retained.
*/
protected RestoreViewOnCreateController(@Nullable Bundle args) {
super(args);
}
@Override @NonNull
protected final View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return onCreateView(inflater, container, viewState);
}
/**
* Called when the controller is ready to display its view. A valid view must be returned. The standard body
* for this method will be {@code return inflater.inflate(R.layout.my_layout, container, false);}, plus
* any binding and state restoration code.
*
* @param inflater The LayoutInflater that should be used to inflate views
* @param container The parent view that this Controller's view will eventually be attached to.
* 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.
*/
@NonNull
protected abstract View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState);
}
@@ -4,6 +4,8 @@ import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -16,6 +18,7 @@ import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import com.bluelinelabs.conductor.internal.NoOpControllerChangeHandler;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@@ -29,9 +32,10 @@ public abstract class Router {
private static final String KEY_BACKSTACK = "Router.backstack";
private static final String KEY_POPS_LAST_VIEW = "Router.popsLastView";
protected final Backstack backStack = new Backstack();
protected final Backstack backstack = new Backstack();
private OnControllerPushedListener onControllerPushedListener;
private final List<ControllerChangeListener> changeListeners = new ArrayList<>();
private final List<Controller> destroyingControllers = new ArrayList<>();
final List<Controller> destroyingControllers = new ArrayList<>();
private boolean popsLastView = false;
@@ -40,6 +44,7 @@ public abstract class Router {
/**
* Returns this Router's host Activity
*/
@Nullable
public abstract Activity getActivity();
/**
@@ -50,7 +55,7 @@ public abstract class Router {
* @param resultCode The Activity's onActivityResult resultCode
* @param data The Activity's onActivityResult data
*/
public abstract void onActivityResult(int requestCode, int resultCode, Intent data);
public abstract void onActivityResult(int requestCode, int resultCode, @Nullable Intent data);
/**
* This should be called by the host Activity when its onRequestPermissionsResult method is called. The call will be forwarded
@@ -61,7 +66,7 @@ public abstract class Router {
* @param permissions The Activity's onRequestPermissionsResult permissions
* @param grantResults The Activity's onRequestPermissionsResult grantResults
*/
public void onRequestPermissionsResult(String instanceId, int requestCode, String[] permissions, int[] grantResults) {
public void onRequestPermissionsResult(@NonNull String instanceId, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Controller controller = getControllerWithInstanceId(instanceId);
if (controller != null) {
controller.requestPermissionsResult(requestCode, permissions, grantResults);
@@ -71,10 +76,14 @@ public abstract class Router {
/**
* This should be called by the host Activity when its onBackPressed method is called. The call will be forwarded
* to its top {@link Controller}. If that controller doesn't handle it, then it will be popped.
*
* @return Whether or not a back action was handled by the Router
*/
@UiThread
public boolean handleBack() {
if (!backStack.isEmpty()) {
if (backStack.peek().controller.handleBack()) {
if (!backstack.isEmpty()) {
//noinspection ConstantConditions
if (backstack.peek().controller.handleBack()) {
return true;
} else if (popCurrentController()) {
return true;
@@ -89,8 +98,13 @@ public abstract class Router {
*
* @return Whether or not this Router still has controllers remaining on it after popping.
*/
@UiThread
public boolean popCurrentController() {
return popController(backStack.peek().controller);
RouterTransaction transaction = backstack.peek();
if (transaction == null) {
throw new IllegalStateException("Trying to pop the current controller when there are none on the backstack.");
}
return popController(transaction.controller);
}
/**
@@ -99,29 +113,30 @@ public abstract class Router {
* @param controller The controller that should be popped from this Router
* @return Whether or not this Router still has controllers remaining on it after popping.
*/
public boolean popController(Controller controller) {
RouterTransaction topController = backStack.peek();
@UiThread
public boolean popController(@NonNull Controller controller) {
RouterTransaction topController = backstack.peek();
boolean poppingTopController = topController != null && topController.controller == controller;
if (poppingTopController) {
trackDestroyingController(backStack.pop());
trackDestroyingController(backstack.pop());
} else {
for (RouterTransaction transaction : backStack) {
for (RouterTransaction transaction : backstack) {
if (transaction.controller == controller) {
backStack.remove(transaction);
backstack.remove(transaction);
break;
}
}
}
if (poppingTopController) {
performControllerChange(backStack.peek(), topController, false);
performControllerChange(backstack.peek(), topController, false);
}
if (popsLastView) {
return topController != null;
} else {
return !backStack.isEmpty();
return !backstack.isEmpty();
}
}
@@ -131,8 +146,9 @@ public abstract class Router {
* @param transaction The transaction detailing what should be pushed, including the {@link Controller},
* and its push and pop {@link ControllerChangeHandler}, and its tag.
*/
@UiThread
public void pushController(@NonNull RouterTransaction transaction) {
RouterTransaction from = backStack.peek();
RouterTransaction from = backstack.peek();
pushToBackstack(transaction);
performControllerChange(transaction, from, true);
}
@@ -143,19 +159,35 @@ public abstract class Router {
* @param transaction The transaction detailing what should be pushed, including the {@link Controller},
* and its push and pop {@link ControllerChangeHandler}, and its tag.
*/
@UiThread
public void replaceTopController(@NonNull RouterTransaction transaction) {
RouterTransaction topTransaction = backStack.peek();
if (!backStack.isEmpty()) {
trackDestroyingController(backStack.pop());
RouterTransaction topTransaction = backstack.peek();
if (!backstack.isEmpty()) {
trackDestroyingController(backstack.pop());
}
final ControllerChangeHandler handler = transaction.pushChangeHandler();
if (topTransaction != null) {
final boolean oldHandlerRemovedViews = topTransaction.pushChangeHandler() == null || topTransaction.pushChangeHandler().removesFromViewOnPush();
final boolean newHandlerRemovesViews = handler == null || handler.removesFromViewOnPush();
if (!oldHandlerRemovedViews && newHandlerRemovesViews) {
for (RouterTransaction visibleTransaction : getVisibleTransactions(backstack.iterator())) {
performControllerChange(null, visibleTransaction.controller, true, handler != null ? handler.copy() : new SimpleSwapChangeHandler());
}
}
}
pushToBackstack(transaction);
performControllerChange(transaction, topTransaction, true);
if (handler != null) {
handler.setForceRemoveViewOnPush(true);
}
performControllerChange(transaction.pushChangeHandler(handler), topTransaction, true);
}
void destroy() {
popsLastView = true;
List<RouterTransaction> poppedControllers = backStack.popAll();
List<RouterTransaction> poppedControllers = backstack.popAll();
if (poppedControllers.size() > 0) {
trackDestroyingControllers(poppedControllers);
@@ -164,11 +196,16 @@ public abstract class Router {
}
}
public int getContainerId() {
return container != null ? container.getId() : 0;
}
/**
* If set to true, this router will handle back presses by performing a change handler on the last controller and view
* in the stack. This defaults to false so that the developer can either finish its containing Activity or otherwise
* in the stack. This defaults to false so that the developer can either finish its containing Activity or otherwise
* hide its parent view without any strange artifacting.
*/
@NonNull
public Router setPopsLastView(boolean popsLastView) {
this.popsLastView = popsLastView;
return this;
@@ -179,6 +216,7 @@ public abstract class Router {
*
* @return Whether or not any {@link Controller}s were popped in order to get to the root transaction
*/
@UiThread
public boolean popToRoot() {
return popToRoot(null);
}
@@ -189,9 +227,11 @@ public abstract class Router {
* @param changeHandler The {@link ControllerChangeHandler} to handle this transaction
* @return Whether or not any {@link Controller}s were popped in order to get to the root transaction
*/
public boolean popToRoot(ControllerChangeHandler changeHandler) {
if (backStack.size() > 1) {
popToTransaction(backStack.root(), changeHandler);
@UiThread
public boolean popToRoot(@Nullable ControllerChangeHandler changeHandler) {
if (backstack.size() > 1) {
//noinspection ConstantConditions
popToTransaction(backstack.root(), changeHandler);
return true;
} else {
return false;
@@ -204,6 +244,7 @@ public abstract class Router {
* @param tag The tag being popped to
* @return Whether or not any {@link Controller}s were popped in order to get to the transaction with the passed tag
*/
@UiThread
public boolean popToTag(@NonNull String tag) {
return popToTag(tag, null);
}
@@ -215,8 +256,9 @@ public abstract class Router {
* @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
*/
public boolean popToTag(@NonNull String tag, ControllerChangeHandler changeHandler) {
for (RouterTransaction transaction : backStack) {
@UiThread
public boolean popToTag(@NonNull String tag, @Nullable ControllerChangeHandler changeHandler) {
for (RouterTransaction transaction : backstack) {
if (tag.equals(transaction.tag())) {
popToTransaction(transaction, changeHandler);
return true;
@@ -231,14 +273,31 @@ public abstract class Router {
* @param transaction The transaction detailing what should be pushed, including the {@link Controller},
* and its push and pop {@link ControllerChangeHandler}, and its tag.
*/
@UiThread
public void setRoot(@NonNull RouterTransaction transaction) {
RouterTransaction currentTop = backStack.peek();
removeAllExceptTopAndUnowned();
ControllerChangeHandler newHandler = transaction.pushChangeHandler() != null ? transaction.pushChangeHandler() : new SimpleSwapChangeHandler();
trackDestroyingControllers(backStack.popAll());
List<RouterTransaction> visibleTransactions = getVisibleTransactions(backstack.iterator());
RouterTransaction rootTransaction = visibleTransactions.size() > 0 ? visibleTransactions.get(0) : null;
removeAllExceptVisibleAndUnowned();
trackDestroyingControllers(backstack.popAll());
pushToBackstack(transaction);
performControllerChange(transaction, currentTop, true);
for (int i = visibleTransactions.size() - 1; i > 0; i--) {
if (visibleTransactions.get(i).controller.getView() == null) {
ControllerChangeHandler.abortPush(visibleTransactions.get(i).controller, transaction.controller, newHandler);
} else {
performControllerChange(null, visibleTransactions.get(i).controller, true, newHandler);
}
}
if (rootTransaction != null && rootTransaction.controller.getView() == null) {
ControllerChangeHandler.abortPush(rootTransaction.controller, transaction.controller, newHandler);
}
performControllerChange(transaction, rootTransaction, true);
}
/**
@@ -247,8 +306,9 @@ public abstract class Router {
* @param instanceId The instance ID being searched for
* @return The matching Controller, if one exists
*/
public Controller getControllerWithInstanceId(String instanceId) {
for (RouterTransaction transaction : backStack) {
@Nullable
public Controller getControllerWithInstanceId(@NonNull String instanceId) {
for (RouterTransaction transaction : backstack) {
Controller controllerWithId = transaction.controller.findController(instanceId);
if (controllerWithId != null) {
return controllerWithId;
@@ -263,8 +323,9 @@ public abstract class Router {
* @param tag The tag being searched for
* @return The matching Controller, if one exists
*/
public Controller getControllerWithTag(String tag) {
for (RouterTransaction transaction : backStack) {
@Nullable
public Controller getControllerWithTag(@NonNull String tag) {
for (RouterTransaction transaction : backstack) {
if (tag.equals(transaction.tag())) {
return transaction.controller;
}
@@ -276,15 +337,16 @@ public abstract class Router {
* Returns the number of {@link Controller}s currently in the backstack
*/
public int getBackstackSize() {
return backStack.size();
return backstack.size();
}
/**
* Returns the current backstack, ordered from root to most recently pushed.
*/
@NonNull
public List<RouterTransaction> getBackstack() {
List<RouterTransaction> list = new ArrayList<>();
Iterator<RouterTransaction> backstackIterator = backStack.reverseIterator();
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
list.add(backstackIterator.next());
}
@@ -295,20 +357,48 @@ 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 backstack The new backstack
* @param changeHandler An optional change handler to be used to handle the transition
* @param newBackstack The new backstack
* @param changeHandler An optional change handler to be used to handle the root view of transition
*/
public void setBackstack(@NonNull List<RouterTransaction> backstack, ControllerChangeHandler changeHandler) {
RouterTransaction oldTopTransaction = backStack.peek();
Controller oldTop = oldTopTransaction != null ? oldTopTransaction.controller : null;
removeAllExceptTopAndUnowned();
@UiThread
public void setBackstack(@NonNull List<RouterTransaction> newBackstack, @Nullable ControllerChangeHandler changeHandler) {
List<RouterTransaction> oldVisibleTransactions = getVisibleTransactions(backstack.iterator());
if (backstack.size() > 0) {
Controller newTop = backstack.get(backstack.size() - 1).controller;
removeAllExceptVisibleAndUnowned();
if (newTop != oldTop) {
if (newBackstack.size() > 0) {
List<RouterTransaction> reverseNewBackstack = new ArrayList<>(newBackstack);
Collections.reverse(reverseNewBackstack);
List<RouterTransaction> newVisibleTransactions = getVisibleTransactions(reverseNewBackstack.iterator());
boolean visibleTransactionsChanged = newVisibleTransactions.size() != oldVisibleTransactions.size();
if (!visibleTransactionsChanged) {
for (int i = 0; i < oldVisibleTransactions.size(); i++) {
if (oldVisibleTransactions.get(i).controller != newVisibleTransactions.get(i).controller) {
visibleTransactionsChanged = true;
break;
}
}
}
if (visibleTransactionsChanged) {
ControllerChangeHandler handler = changeHandler != null ? changeHandler : new SimpleSwapChangeHandler();
performControllerChange(newTop, oldTop, true, handler);
Controller rootController = oldVisibleTransactions.size() > 0 ? oldVisibleTransactions.get(0).controller : null;
performControllerChange(newVisibleTransactions.get(0).controller, rootController, true, handler.copy());
for (int i = oldVisibleTransactions.size() - 1; i > 0; i--) {
RouterTransaction transaction = oldVisibleTransactions.get(i);
ControllerChangeHandler localHandler = handler.copy();
localHandler.setForceRemoveViewOnPush(true);
performControllerChange(null, transaction.controller, true, localHandler);
}
for (int i = 1; i < newVisibleTransactions.size(); i++) {
RouterTransaction transaction = newVisibleTransactions.get(i);
handler = transaction.pushChangeHandler() != null ? transaction.pushChangeHandler().copy() : new SimpleSwapChangeHandler();
performControllerChange(transaction.controller, newVisibleTransactions.get(i - 1).controller, true, handler);
}
}
}
@@ -316,7 +406,13 @@ public abstract class Router {
transaction.onAttachedToRouter();
}
backStack.setBackstack(backstack);
backstack.setBackstack(newBackstack);
if (onControllerPushedListener != null) {
for (RouterTransaction transaction : newBackstack) {
onControllerPushedListener.onControllerPushed(transaction.controller);
}
}
}
/**
@@ -331,7 +427,7 @@ public abstract class Router {
*
* @param changeListener The listener
*/
public void addChangeListener(ControllerChangeListener changeListener) {
public void addChangeListener(@NonNull ControllerChangeListener changeListener) {
if (!changeListeners.contains(changeListener)) {
changeListeners.add(changeListener);
}
@@ -342,15 +438,16 @@ public abstract class Router {
*
* @param changeListener The listener to be removed
*/
public void removeChangeListener(ControllerChangeListener changeListener) {
public void removeChangeListener(@NonNull ControllerChangeListener changeListener) {
changeListeners.remove(changeListener);
}
/**
* Attaches this Router's existing backstack to its container if one exists.
*/
@UiThread
public void rebindIfNeeded() {
Iterator<RouterTransaction> backstackIterator = backStack.reverseIterator();
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
@@ -360,15 +457,15 @@ public abstract class Router {
}
}
public final void onActivityResult(String instanceId, int requestCode, int resultCode, Intent data) {
public final void onActivityResult(@NonNull String instanceId, int requestCode, int resultCode, @Nullable Intent data) {
Controller controller = getControllerWithInstanceId(instanceId);
if (controller != null) {
controller.onActivityResult(requestCode, resultCode, data);
}
}
public final void onActivityStarted(Activity activity) {
for (RouterTransaction transaction : backStack) {
public final void onActivityStarted(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityStarted(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
@@ -377,8 +474,8 @@ public abstract class Router {
}
}
public final void onActivityResumed(Activity activity) {
for (RouterTransaction transaction : backStack) {
public final void onActivityResumed(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityResumed(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
@@ -387,8 +484,8 @@ public abstract class Router {
}
}
public final void onActivityPaused(Activity activity) {
for (RouterTransaction transaction : backStack) {
public final void onActivityPaused(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityPaused(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
@@ -397,8 +494,8 @@ public abstract class Router {
}
}
public final void onActivityStopped(Activity activity) {
for (RouterTransaction transaction : backStack) {
public final void onActivityStopped(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityStopped(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
@@ -407,11 +504,11 @@ public abstract class Router {
}
}
public void onActivityDestroyed(Activity activity) {
public void onActivityDestroyed(@NonNull Activity activity) {
prepareForContainerRemoval();
changeListeners.clear();
for (RouterTransaction transaction : backStack) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityDestroyed(activity.isChangingConfigurations());
for (Router childRouter : transaction.controller.getChildRouters()) {
@@ -432,34 +529,37 @@ public abstract class Router {
}
public void prepareForHostDetach() {
for (RouterTransaction transaction : backStack) {
for (RouterTransaction transaction : backstack) {
if (ControllerChangeHandler.completePushImmediately(transaction.controller.getInstanceId())) {
transaction.controller.setNeedsAttach();
}
transaction.controller.prepareForHostDetach();
}
}
public void saveInstanceState(Bundle outState) {
public void saveInstanceState(@NonNull Bundle outState) {
prepareForHostDetach();
Bundle backstackState = new Bundle();
backStack.saveInstanceState(backstackState);
backstack.saveInstanceState(backstackState);
outState.putParcelable(KEY_BACKSTACK, backstackState);
outState.putBoolean(KEY_POPS_LAST_VIEW, popsLastView);
}
public void restoreInstanceState(Bundle savedInstanceState) {
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
Bundle backstackBundle = savedInstanceState.getParcelable(KEY_BACKSTACK);
backStack.restoreInstanceState(backstackBundle);
backstack.restoreInstanceState(backstackBundle);
popsLastView = savedInstanceState.getBoolean(KEY_POPS_LAST_VIEW);
Iterator<RouterTransaction> backstackIterator = backStack.reverseIterator();
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
setControllerRouter(backstackIterator.next().controller);
}
}
public final void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
for (RouterTransaction transaction : backStack) {
public final void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
for (RouterTransaction transaction : backstack) {
transaction.controller.createOptionsMenu(menu, inflater);
for (Router childRouter : transaction.controller.getChildRouters()) {
@@ -468,8 +568,8 @@ public abstract class Router {
}
}
public final void onPrepareOptionsMenu(Menu menu) {
for (RouterTransaction transaction : backStack) {
public final void onPrepareOptionsMenu(@NonNull Menu menu) {
for (RouterTransaction transaction : backstack) {
transaction.controller.prepareOptionsMenu(menu);
for (Router childRouter : transaction.controller.getChildRouters()) {
@@ -478,8 +578,8 @@ public abstract class Router {
}
}
public final boolean onOptionsItemSelected(MenuItem item) {
for (RouterTransaction transaction : backStack) {
public final boolean onOptionsItemSelected(@NonNull MenuItem item) {
for (RouterTransaction transaction : backstack) {
if (transaction.controller.optionsItemSelected(item)) {
return true;
}
@@ -493,9 +593,9 @@ public abstract class Router {
return false;
}
private void popToTransaction(@NonNull RouterTransaction transaction, ControllerChangeHandler changeHandler) {
RouterTransaction topTransaction = backStack.peek();
List<RouterTransaction> poppedTransactions = backStack.popTo(transaction);
private void popToTransaction(@NonNull RouterTransaction transaction, @Nullable ControllerChangeHandler changeHandler) {
RouterTransaction topTransaction = backstack.peek();
List<RouterTransaction> poppedTransactions = backstack.popTo(transaction);
trackDestroyingControllers(poppedTransactions);
if (poppedTransactions.size() > 0) {
@@ -503,20 +603,25 @@ public abstract class Router {
changeHandler = topTransaction.popChangeHandler();
}
performControllerChange(backStack.peek().controller, topTransaction.controller, false, changeHandler);
performControllerChange(backstack.peek().controller, topTransaction.controller, false, changeHandler);
}
}
final void setOnControllerPushedListener(OnControllerPushedListener listener) {
onControllerPushedListener = listener;
}
void prepareForContainerRemoval() {
if (container != null) {
container.setOnHierarchyChangeListener(null);
}
}
@NonNull
final List<Controller> getControllers() {
List<Controller> controllers = new ArrayList<>();
Iterator<RouterTransaction> backstackIterator = backStack.reverseIterator();
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
controllers.add(backstackIterator.next().controller);
}
@@ -524,8 +629,9 @@ public abstract class Router {
return controllers;
}
@Nullable
public final Boolean handleRequestedPermission(@NonNull String permission) {
for (RouterTransaction transaction : backStack) {
for (RouterTransaction transaction : backstack) {
if (transaction.controller.didRequestPermission(permission)) {
return transaction.controller.shouldShowRequestPermissionRationale(permission);
}
@@ -533,7 +639,7 @@ public abstract class Router {
return null;
}
private void performControllerChange(RouterTransaction to, RouterTransaction from, boolean isPush) {
private void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush) {
if (isPush && to != null) {
to.onAttachedToRouter();
}
@@ -554,10 +660,10 @@ public abstract class Router {
performControllerChange(toController, fromController, isPush, changeHandler);
}
private void performControllerChange(final Controller to, final Controller from, boolean isPush, @NonNull ControllerChangeHandler changeHandler) {
private void performControllerChange(@Nullable final Controller to, @Nullable final Controller from, boolean isPush, @Nullable ControllerChangeHandler changeHandler) {
if (to != null) {
setControllerRouter(to);
} else if (backStack.size() == 0 && !popsLastView) {
} 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();
@@ -566,11 +672,15 @@ public abstract class Router {
ControllerChangeHandler.executeChange(to, from, isPush, container, changeHandler, changeListeners);
}
void pushToBackstack(@NonNull RouterTransaction entry) {
backStack.push(entry);
private void pushToBackstack(@NonNull RouterTransaction entry) {
backstack.push(entry);
if (onControllerPushedListener != null) {
onControllerPushedListener.onControllerPushed(entry.controller);
}
}
private void trackDestroyingController(RouterTransaction transaction) {
private void trackDestroyingController(@NonNull RouterTransaction transaction) {
if (!transaction.controller.isDestroyed()) {
destroyingControllers.add(transaction.controller);
@@ -583,19 +693,19 @@ public abstract class Router {
}
}
private void trackDestroyingControllers(List<RouterTransaction> transactions) {
private void trackDestroyingControllers(@NonNull List<RouterTransaction> transactions) {
for (RouterTransaction transaction : transactions) {
trackDestroyingController(transaction);
}
}
private void removeAllExceptTopAndUnowned() {
private void removeAllExceptVisibleAndUnowned() {
List<View> views = new ArrayList<>();
RouterTransaction topTransaction = backStack.peek();
final View topView = topTransaction != null ? topTransaction.controller.getView() : null;
if (topView != null) {
views.add(topView);
for (RouterTransaction transaction : getVisibleTransactions(backstack.iterator())) {
if (transaction.controller.getView() != null) {
views.add(transaction.controller.getView());
}
}
for (Router router : getSiblingRouters()) {
@@ -613,7 +723,7 @@ public abstract class Router {
}
}
private void addRouterViewsToList(Router router, List<View> list) {
private void addRouterViewsToList(@NonNull Router router, @NonNull List<View> list) {
for (Controller controller : router.getControllers()) {
if (controller.getView() != null) {
list.add(controller.getView());
@@ -625,17 +735,37 @@ public abstract class Router {
}
}
void setControllerRouter(Controller controller) {
private List<RouterTransaction> getVisibleTransactions(@NonNull Iterator<RouterTransaction> backstackIterator) {
List<RouterTransaction> transactions = new ArrayList<>();
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
transactions.add(transaction);
if (transaction.pushChangeHandler() == null || transaction.pushChangeHandler().removesFromViewOnPush()) {
break;
}
}
Collections.reverse(transactions);
return transactions;
}
void setControllerRouter(@NonNull Controller controller) {
controller.setRouter(this);
}
abstract void invalidateOptionsMenu();
abstract void startActivity(Intent intent);
abstract void startActivityForResult(String instanceId, Intent intent, int requestCode);
abstract void startActivityForResult(String instanceId, Intent intent, int requestCode, Bundle options);
abstract void registerForActivityResult(String instanceId, int requestCode);
abstract void unregisterForActivityResults(String instanceId);
abstract void requestPermissions(String instanceId, String[] permissions, int requestCode);
abstract void startActivity(@NonNull Intent intent);
abstract void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode);
abstract void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options);
abstract void registerForActivityResult(@NonNull String instanceId, int requestCode);
abstract void unregisterForActivityResults(@NonNull String instanceId);
abstract void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode);
abstract boolean hasHost();
abstract List<Router> getSiblingRouters();
@NonNull abstract List<Router> getSiblingRouters();
@NonNull abstract Router getRootRouter();
interface OnControllerPushedListener {
void onControllerPushed(Controller controller);
}
}
@@ -2,6 +2,7 @@ package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/**
* Metadata used for adding {@link Controller}s to a {@link Router}.
@@ -21,6 +22,7 @@ public class RouterTransaction {
private ControllerChangeHandler popControllerChangeHandler;
private boolean attachedToRouter;
@NonNull
public static RouterTransaction with(@NonNull Controller controller) {
return new RouterTransaction(controller);
}
@@ -41,15 +43,18 @@ public class RouterTransaction {
attachedToRouter = true;
}
@NonNull
public Controller controller() {
return controller;
}
@Nullable
public String tag() {
return tag;
}
public RouterTransaction tag(String tag) {
@NonNull
public RouterTransaction tag(@Nullable String tag) {
if (!attachedToRouter) {
this.tag = tag;
return this;
@@ -58,6 +63,7 @@ public class RouterTransaction {
}
}
@Nullable
public ControllerChangeHandler pushChangeHandler() {
ControllerChangeHandler handler = controller.getOverriddenPushHandler();
if (handler == null) {
@@ -66,7 +72,8 @@ public class RouterTransaction {
return handler;
}
public RouterTransaction pushChangeHandler(ControllerChangeHandler handler) {
@NonNull
public RouterTransaction pushChangeHandler(@Nullable ControllerChangeHandler handler) {
if (!attachedToRouter) {
pushControllerChangeHandler = handler;
return this;
@@ -75,6 +82,7 @@ public class RouterTransaction {
}
}
@Nullable
public ControllerChangeHandler popChangeHandler() {
ControllerChangeHandler handler = controller.getOverriddenPopHandler();
if (handler == null) {
@@ -83,7 +91,8 @@ public class RouterTransaction {
return handler;
}
public RouterTransaction popChangeHandler(ControllerChangeHandler handler) {
@NonNull
public RouterTransaction popChangeHandler(@Nullable ControllerChangeHandler handler) {
if (!attachedToRouter) {
popControllerChangeHandler = handler;
return this;
@@ -95,6 +104,7 @@ public class RouterTransaction {
/**
* Used to serialize this transaction into a Bundle
*/
@NonNull
public Bundle saveInstanceState() {
Bundle bundle = new Bundle();
@@ -1,9 +1,11 @@
package com.bluelinelabs.conductor.changehandler;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
@@ -24,6 +26,8 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
private long animationDuration;
private boolean removesFromViewOnPush;
private boolean canceled;
private boolean needsImmediateCompletion;
private boolean completed;
private Animator animator;
public AnimatorChangeHandler() {
@@ -58,7 +62,7 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
}
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, Controller newTop) {
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
canceled = true;
@@ -67,10 +71,21 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
}
}
@Override
public void completeImmediately() {
super.completeImmediately();
needsImmediateCompletion = true;
if (animator != null) {
animator.end();
}
}
public long getAnimationDuration() {
return animationDuration;
}
@Override
public boolean removesFromViewOnPush() {
return removesFromViewOnPush;
}
@@ -84,7 +99,8 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
* @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.
*/
protected abstract Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer);
@NonNull
protected abstract Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer);
/**
* Will be called after the animation is complete to reset the View that was removed to its pre-animation state.
@@ -92,14 +108,14 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
protected abstract void resetFromView(@NonNull View from);
@Override
public final void performChange(@NonNull final ViewGroup container, final View from, final View to, final boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
public final void performChange(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
boolean readyToAnimate = true;
final boolean addingToView = to != null && to.getParent() == null;
if (addingToView) {
if (isPush || from == null) {
container.addView(to);
} else {
} else if (to.getParent() == null) {
container.addView(to, container.indexOfChild(from));
}
@@ -124,9 +140,23 @@ 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) {
if (canceled) {
private void complete(@NonNull ControllerChangeCompletedListener changeListener, @Nullable AnimatorListener animatorListener) {
if (!completed) {
completed = true;
changeListener.onChangeCompleted();
}
if (animator != null) {
if (animatorListener != null) {
animator.removeListener(animatorListener);
}
animator = null;
}
}
private void performAnimation(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, final boolean toAddedToContainer, @NonNull final ControllerChangeCompletedListener changeListener) {
if (canceled) {
complete(changeListener, null);
return;
}
@@ -139,22 +169,29 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(Animator animation) {
changeListener.onChangeCompleted();
if (from != null && (!isPush || removesFromViewOnPush) && needsImmediateCompletion) {
container.removeView(from);
}
complete(changeListener, this);
}
@Override
public void onAnimationEnd(Animator animation) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from);
}
if (!canceled && animator != null) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from);
}
changeListener.onChangeCompleted();
complete(changeListener, this);
if (isPush && from != null) {
resetFromView(from);
if (isPush && from != null) {
resetFromView(from);
}
}
}
});
animator.start();
}
@@ -3,6 +3,7 @@ package com.bluelinelabs.conductor.changehandler;
import android.annotation.TargetApi;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.AutoTransition;
import android.transition.Transition;
import android.view.View;
@@ -14,9 +15,8 @@ import android.view.ViewGroup;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class AutoTransitionChangeHandler extends TransitionChangeHandler {
@Override
@NonNull
protected Transition getTransition(@NonNull ViewGroup container, View from, View to, boolean isPush) {
@Override @NonNull
protected Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
return new AutoTransition();
}
@@ -4,6 +4,7 @@ import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
@@ -26,8 +27,8 @@ public class FadeChangeHandler extends AnimatorChangeHandler {
super(duration, removesFromViewOnPush);
}
@Override
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
@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));
@@ -4,6 +4,7 @@ import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
@@ -26,8 +27,8 @@ public class HorizontalChangeHandler extends AnimatorChangeHandler {
super(duration, removesFromViewOnPush);
}
@Override
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animatorSet = new AnimatorSet();
if (isPush) {
@@ -2,20 +2,28 @@ package com.bluelinelabs.conductor.changehandler;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* A {@link ControllerChangeHandler} that will instantly swap Views with no animations or transitions.
*/
public class SimpleSwapChangeHandler extends ControllerChangeHandler {
public class SimpleSwapChangeHandler extends ControllerChangeHandler implements OnAttachStateChangeListener {
private static final String KEY_REMOVES_FROM_ON_PUSH = "SimpleSwapChangeHandler.removesFromViewOnPush";
private boolean removesFromViewOnPush;
private boolean canceled;
private ViewGroup container;
private ControllerChangeCompletedListener changeListener;
public SimpleSwapChangeHandler() {
this(true);
}
@@ -37,16 +45,61 @@ public class SimpleSwapChangeHandler extends ControllerChangeHandler {
}
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from);
}
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
if (to != null && to.getParent() == null) {
container.addView(to);
}
changeListener.onChangeCompleted();
canceled = true;
}
}
@Override
public void completeImmediately() {
if (changeListener != null) {
changeListener.onChangeCompleted();
changeListener = null;
container.removeOnAttachStateChangeListener(this);
container = null;
}
}
@Override
public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
if (!canceled) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from);
}
if (to != null && to.getParent() == null) {
container.addView(to);
}
}
if (container.getWindowToken() != null) {
changeListener.onChangeCompleted();
} else {
this.changeListener = changeListener;
this.container = container;
container.addOnAttachStateChangeListener(this);
}
}
@Override
public boolean removesFromViewOnPush() {
return removesFromViewOnPush;
}
@Override
public void onViewAttachedToWindow(@NonNull View v) {
v.removeOnAttachStateChangeListener(this);
if (changeListener != null) {
changeListener.onChangeCompleted();
changeListener = null;
container = null;
}
}
@Override
public void onViewDetachedFromWindow(@NonNull View v) { }
}
@@ -3,6 +3,7 @@ package com.bluelinelabs.conductor.changehandler;
import android.annotation.TargetApi;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.Transition;
import android.transition.Transition.TransitionListener;
import android.transition.TransitionManager;
@@ -29,17 +30,17 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
* @param isPush True if this is a push transaction, false if it's a pop.
*/
@NonNull
protected abstract Transition getTransition(@NonNull ViewGroup container, View from, View to, boolean isPush);
protected abstract Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, Controller newTop) {
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
canceled = true;
}
@Override
public void performChange(@NonNull final ViewGroup container, View from, View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
public void performChange(@NonNull final ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
if (canceled) {
changeListener.onChangeCompleted();
return;
@@ -71,9 +72,13 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
if (from != null) {
container.removeView(from);
}
if (to != null) {
if (to != null && to.getParent() == null) {
container.addView(to);
}
}
@Override
public final boolean removesFromViewOnPush() {
return true;
}
}
@@ -3,6 +3,7 @@ package com.bluelinelabs.conductor.changehandler;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
@@ -15,13 +16,10 @@ import com.bluelinelabs.conductor.internal.ClassUtils;
*/
public class TransitionChangeHandlerCompat extends ControllerChangeHandler {
private static final String KEY_TRANSITION_HANDLER_CLASS = "TransitionChangeHandlerCompat.transitionChangeHandler.class";
private static final String KEY_FALLBACK_HANDLER_CLASS = "TransitionChangeHandlerCompat.fallbackChangeHandler.class";
private static final String KEY_TRANSITION_HANDLER_STATE = "TransitionChangeHandlerCompat.transitionChangeHandler.state";
private static final String KEY_FALLBACK_HANDLER_STATE = "TransitionChangeHandlerCompat.fallbackChangeHandler.state";
private static final String KEY_CHANGE_HANDLER_CLASS = "TransitionChangeHandlerCompat.changeHandler.class";
private static final String KEY_HANDLER_STATE = "TransitionChangeHandlerCompat.changeHandler.state";
private TransitionChangeHandler transitionChangeHandler;
private ControllerChangeHandler fallbackChangeHandler;
private ControllerChangeHandler changeHandler;
public TransitionChangeHandlerCompat() { }
@@ -32,49 +30,43 @@ public class TransitionChangeHandlerCompat extends ControllerChangeHandler {
* @param transitionChangeHandler The change handler that will be used on API 21 and above
* @param fallbackChangeHandler The change handler that will be used on APIs below 21
*/
public TransitionChangeHandlerCompat(TransitionChangeHandler transitionChangeHandler, ControllerChangeHandler fallbackChangeHandler) {
this.transitionChangeHandler = transitionChangeHandler;
this.fallbackChangeHandler = fallbackChangeHandler;
public TransitionChangeHandlerCompat(@NonNull TransitionChangeHandler transitionChangeHandler, @NonNull ControllerChangeHandler fallbackChangeHandler) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
changeHandler = transitionChangeHandler;
} else {
changeHandler = fallbackChangeHandler;
}
}
@Override
public void performChange(@NonNull final ViewGroup container, View from, View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
transitionChangeHandler.performChange(container, from, to, isPush, changeListener);
} else {
fallbackChangeHandler.performChange(container, from, to, isPush, changeListener);
}
public void performChange(@NonNull final ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
changeHandler.performChange(container, from, to, isPush, changeListener);
}
@Override
public void saveToBundle(@NonNull Bundle bundle) {
super.saveToBundle(bundle);
bundle.putString(KEY_TRANSITION_HANDLER_CLASS, transitionChangeHandler.getClass().getName());
bundle.putString(KEY_FALLBACK_HANDLER_CLASS, fallbackChangeHandler.getClass().getName());
bundle.putString(KEY_CHANGE_HANDLER_CLASS, changeHandler.getClass().getName());
Bundle transitionBundle = new Bundle();
transitionChangeHandler.saveToBundle(transitionBundle);
bundle.putBundle(KEY_TRANSITION_HANDLER_STATE, transitionBundle);
Bundle fallbackBundle = new Bundle();
fallbackChangeHandler.saveToBundle(fallbackBundle);
bundle.putBundle(KEY_FALLBACK_HANDLER_STATE, fallbackBundle);
Bundle stateBundle = new Bundle();
changeHandler.saveToBundle(stateBundle);
bundle.putBundle(KEY_HANDLER_STATE, stateBundle);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
super.restoreFromBundle(bundle);
String transitionClassName = bundle.getString(KEY_TRANSITION_HANDLER_CLASS);
transitionChangeHandler = ClassUtils.newInstance(transitionClassName);
String className = bundle.getString(KEY_CHANGE_HANDLER_CLASS);
changeHandler = ClassUtils.newInstance(className);
//noinspection ConstantConditions
transitionChangeHandler.restoreFromBundle(bundle.getBundle(KEY_TRANSITION_HANDLER_STATE));
changeHandler.restoreFromBundle(bundle.getBundle(KEY_HANDLER_STATE));
}
String fallbackClassName = bundle.getString(KEY_FALLBACK_HANDLER_CLASS);
fallbackChangeHandler = ClassUtils.newInstance(fallbackClassName);
//noinspection ConstantConditions
fallbackChangeHandler.restoreFromBundle(bundle.getBundle(KEY_FALLBACK_HANDLER_STATE));
@Override
public boolean removesFromViewOnPush() {
return changeHandler.removesFromViewOnPush();
}
}
@@ -4,6 +4,7 @@ import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
@@ -30,8 +31,8 @@ public class VerticalChangeHandler extends AnimatorChangeHandler {
super(duration, removesFromViewOnPush);
}
@Override
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animator = new AnimatorSet();
List<Animator> viewAnimators = new ArrayList<>();
@@ -1,16 +1,13 @@
package com.bluelinelabs.conductor.internal;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
public class ClassUtils {
@SuppressWarnings("unchecked")
public static <T> Class<? extends T> classForName(String className) {
return classForName(className, true);
}
@SuppressWarnings("unchecked")
public static <T> Class<? extends T> classForName(String className, boolean allowEmptyName) {
@Nullable @SuppressWarnings("unchecked")
public static <T> Class<? extends T> classForName(@NonNull String className, boolean allowEmptyName) {
if (allowEmptyName && TextUtils.isEmpty(className)) {
return null;
}
@@ -22,10 +19,10 @@ public class ClassUtils {
}
}
@SuppressWarnings("unchecked")
public static <T> T newInstance(String className) {
@Nullable @SuppressWarnings("unchecked")
public static <T> T newInstance(@NonNull String className) {
try {
Class<? extends T> cls = classForName(className);
Class<? extends T> cls = classForName(className, true);
return cls != null ? cls.newInstance() : null;
} catch (Exception e) {
throw new RuntimeException("An exception occurred while creating a new instance of " + className + ". " + e.getMessage());
@@ -8,7 +8,10 @@ import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuInflater;
@@ -27,15 +30,19 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
private static final String FRAGMENT_TAG = "LifecycleHandler";
private static final String KEY_PENDING_PERMISSION_REQUESTS = "LifecycleHandler.pendingPermissionRequests";
private static final String KEY_PERMISSION_REQUEST_CODES = "LifecycleHandler.permissionRequests";
private static final String KEY_ACTIVITY_REQUEST_CODES = "LifecycleHandler.activityRequests";
private static final String KEY_ROUTER_STATE_PREFIX = "LifecycleHandler.routerState";
private Activity activity;
private boolean hasRegisteredCallbacks;
private boolean destroyed;
private boolean attached;
private SparseArray<String> permissionRequestMap = new SparseArray<>();
private SparseArray<String> activityRequestMap = new SparseArray<>();
private ArrayList<PendingPermissionRequest> pendingPermissionRequests = new ArrayList<>();
private final Map<Integer, ActivityHostedRouter> routerMap = new HashMap<>();
@@ -44,7 +51,8 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
setHasOptionsMenu(true);
}
private static LifecycleHandler findInActivity(Activity activity) {
@Nullable
private static LifecycleHandler findInActivity(@NonNull Activity activity) {
LifecycleHandler lifecycleHandler = (LifecycleHandler)activity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
if (lifecycleHandler != null) {
lifecycleHandler.registerActivityListener(activity);
@@ -52,7 +60,8 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
return lifecycleHandler;
}
public static LifecycleHandler install(Activity activity) {
@NonNull
public static LifecycleHandler install(@NonNull Activity activity) {
LifecycleHandler lifecycleHandler = findInActivity(activity);
if (lifecycleHandler == null) {
lifecycleHandler = new LifecycleHandler();
@@ -62,14 +71,18 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
return lifecycleHandler;
}
public Router getRouter(ViewGroup container, Bundle savedInstanceState) {
@NonNull
public Router getRouter(@NonNull ViewGroup container, @Nullable Bundle savedInstanceState) {
ActivityHostedRouter router = routerMap.get(getRouterHashKey(container));
if (router == null) {
router = new ActivityHostedRouter();
router.setHost(this, container);
if (savedInstanceState != null) {
router.restoreInstanceState(savedInstanceState);
Bundle routerSavedState = savedInstanceState.getBundle(KEY_ROUTER_STATE_PREFIX + router.getContainerId());
if (routerSavedState != null) {
router.restoreInstanceState(routerSavedState);
}
}
routerMap.put(getRouterHashKey(container), router);
} else {
@@ -79,19 +92,21 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
return router;
}
@NonNull
public List<Router> getRouters() {
return new ArrayList<Router>(routerMap.values());
}
@Nullable
public Activity getLifecycleActivity() {
return activity;
}
private static int getRouterHashKey(ViewGroup viewGroup) {
private static int getRouterHashKey(@NonNull ViewGroup viewGroup) {
return viewGroup.getId();
}
private void registerActivityListener(Activity activity) {
private void registerActivityListener(@NonNull Activity activity) {
this.activity = activity;
if (!hasRegisteredCallbacks) {
@@ -106,10 +121,13 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
if (savedInstanceState != null) {
StringSparseArrayParceler permissionParcel = savedInstanceState.getParcelable(KEY_PERMISSION_REQUEST_CODES);
permissionRequestMap = permissionParcel != null ? permissionParcel.getStringSparseArray() : null;
permissionRequestMap = permissionParcel != null ? permissionParcel.getStringSparseArray() : new SparseArray<String>();
StringSparseArrayParceler activityParcel = savedInstanceState.getParcelable(KEY_ACTIVITY_REQUEST_CODES);
activityRequestMap = activityParcel != null ? activityParcel.getStringSparseArray() : null;
activityRequestMap = activityParcel != null ? activityParcel.getStringSparseArray() : new SparseArray<String>();
ArrayList<PendingPermissionRequest> pendingRequests = savedInstanceState.getParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS);
pendingPermissionRequests = pendingRequests != null ? pendingRequests : new ArrayList<PendingPermissionRequest>();
}
}
@@ -119,6 +137,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
outState.putParcelable(KEY_PERMISSION_REQUEST_CODES, new StringSparseArrayParceler(permissionRequestMap));
outState.putParcelable(KEY_ACTIVITY_REQUEST_CODES, new StringSparseArrayParceler(activityRequestMap));
outState.putParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS, pendingPermissionRequests);
}
@Override
@@ -137,21 +156,35 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
public void onAttach(Activity activity) {
super.onAttach(activity);
destroyed = false;
setAttached();
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
destroyed = false;
setAttached();
}
@Override
public void onDetach() {
super.onDetach();
attached = false;
destroyRouters();
}
private void setAttached() {
if (!attached) {
attached = true;
for (int i = pendingPermissionRequests.size() - 1; i >= 0; i--) {
PendingPermissionRequest request = pendingPermissionRequests.remove(i);
requestPermissions(request.instanceId, request.permissions, request.requestCode);
}
}
}
private void destroyRouters() {
if (!destroyed) {
destroyed = true;
@@ -228,11 +261,11 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
return super.onOptionsItemSelected(item);
}
public void registerForActivityResult(String instanceId, int requestCode) {
public void registerForActivityResult(@NonNull String instanceId, int requestCode) {
activityRequestMap.put(requestCode, instanceId);
}
public void unregisterForActivityResults(String instanceId) {
public void unregisterForActivityResults(@NonNull String instanceId) {
for (int i = activityRequestMap.size() - 1; i >= 0; i--) {
if (instanceId.equals(activityRequestMap.get(activityRequestMap.keyAt(i)))) {
activityRequestMap.removeAt(i);
@@ -240,20 +273,24 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
}
}
public void startActivityForResult(String instanceId, Intent intent, int requestCode) {
public void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) {
registerForActivityResult(instanceId, requestCode);
startActivityForResult(intent, requestCode);
}
public void startActivityForResult(String instanceId, Intent intent, int requestCode, Bundle options) {
public void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options) {
registerForActivityResult(instanceId, requestCode);
startActivityForResult(intent, requestCode, options);
}
@TargetApi(Build.VERSION_CODES.M)
public void requestPermissions(String instanceId, String[] permissions, int requestCode) {
permissionRequestMap.put(requestCode, instanceId);
requestPermissions(permissions, requestCode);
public void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
if (attached) {
permissionRequestMap.put(requestCode, instanceId);
requestPermissions(permissions, requestCode);
} else {
pendingPermissionRequests.add(new PendingPermissionRequest(instanceId, permissions, requestCode));
}
}
@Override
@@ -303,11 +340,56 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
if (this.activity == activity) {
for (Router router : routerMap.values()) {
router.saveInstanceState(outState);
Bundle bundle = new Bundle();
router.saveInstanceState(bundle);
outState.putBundle(KEY_ROUTER_STATE_PREFIX + router.getContainerId(), bundle);
}
}
}
@Override
public void onActivityDestroyed(Activity activity) { }
private static class PendingPermissionRequest implements Parcelable {
final String instanceId;
final String[] permissions;
final int requestCode;
PendingPermissionRequest(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
this.instanceId = instanceId;
this.permissions = permissions;
this.requestCode = requestCode;
}
private PendingPermissionRequest(Parcel in) {
instanceId = in.readString();
permissions = in.createStringArray();
requestCode = in.readInt();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeString(instanceId);
out.writeStringArray(permissions);
out.writeInt(requestCode);
}
public static final Parcelable.Creator<PendingPermissionRequest> CREATOR = new Parcelable.Creator<PendingPermissionRequest>() {
@Override
public PendingPermissionRequest createFromParcel(Parcel in) {
return new PendingPermissionRequest(in);
}
@Override
public PendingPermissionRequest[] newArray(int size) {
return new PendingPermissionRequest[size];
}
};
}
}
@@ -1,6 +1,7 @@
package com.bluelinelabs.conductor.internal;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
@@ -9,7 +10,7 @@ import com.bluelinelabs.conductor.ControllerChangeHandler;
public class NoOpControllerChangeHandler extends ControllerChangeHandler {
@Override
public void performChange(@NonNull ViewGroup container, @NonNull View from, @NonNull View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
changeListener.onChangeCompleted();
}
@@ -2,17 +2,18 @@ package com.bluelinelabs.conductor.internal;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.util.SparseArray;
public class StringSparseArrayParceler implements Parcelable {
private final SparseArray<String> stringSparseArray;
public StringSparseArrayParceler(SparseArray<String> stringSparseArray) {
public StringSparseArrayParceler(@NonNull SparseArray<String> stringSparseArray) {
this.stringSparseArray = stringSparseArray;
}
private StringSparseArrayParceler(Parcel in) {
private StringSparseArrayParceler(@NonNull Parcel in) {
stringSparseArray = new SparseArray<>();
final int size = in.readInt();
@@ -22,14 +23,17 @@ public class StringSparseArrayParceler implements Parcelable {
}
}
@NonNull
public SparseArray<String> getStringSparseArray() {
return stringSparseArray;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
final int size = stringSparseArray.size();
@@ -44,10 +48,12 @@ public class StringSparseArrayParceler implements Parcelable {
}
public static final Parcelable.Creator<StringSparseArrayParceler> CREATOR = new Parcelable.Creator<StringSparseArrayParceler>() {
@Override
public StringSparseArrayParceler createFromParcel(Parcel in) {
return new StringSparseArrayParceler(in);
}
@Override
public StringSparseArrayParceler[] newArray(int size) {
return new StringSparseArrayParceler[size];
}
@@ -0,0 +1,72 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.IdRes;
import org.robolectric.Robolectric;
import org.robolectric.util.ActivityController;
public class ActivityProxy {
private ActivityController<TestActivity> activityController;
private AttachFakingFrameLayout view;
public ActivityProxy() {
activityController = Robolectric.buildActivity(TestActivity.class);
@IdRes int containerId = 4;
view = new AttachFakingFrameLayout(activityController.get());
view.setId(containerId);
}
public ActivityProxy create(Bundle savedInstanceState) {
activityController.create(savedInstanceState);
return this;
}
public ActivityProxy start() {
activityController.start();
view.setAttached(true);
return this;
}
public ActivityProxy resume() {
activityController.resume();
return this;
}
public ActivityProxy pause() {
activityController.pause();
return this;
}
public ActivityProxy saveInstanceState(Bundle outState) {
activityController.saveInstanceState(outState);
return this;
}
public ActivityProxy stop() {
activityController.stop();
view.setAttached(false);
return this;
}
public ActivityProxy destroy() {
activityController.destroy();
return this;
}
public ActivityProxy rotate() {
getActivity().isChangingConfigurations = true;
getActivity().recreate();
return this;
}
public TestActivity getActivity() {
return activityController.get();
}
public AttachFakingFrameLayout getView() {
return view;
}
}
@@ -0,0 +1,113 @@
package com.bluelinelabs.conductor;
import android.content.Context;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import java.io.FileDescriptor;
public class AttachFakingFrameLayout extends FrameLayout {
final IBinder fakeWindowToken = new IBinder() {
@Override
public String getInterfaceDescriptor() throws RemoteException {
return null;
}
@Override
public boolean pingBinder() {
return false;
}
@Override
public boolean isBinderAlive() {
return false;
}
@Override
public IInterface queryLocalInterface(String descriptor) {
return null;
}
@Override
public void dump(FileDescriptor fd, String[] args) throws RemoteException {
}
@Override
public void dumpAsync(FileDescriptor fd, String[] args) throws RemoteException {
}
@Override
public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
return false;
}
@Override
public void linkToDeath(DeathRecipient recipient, int flags) throws RemoteException {
}
@Override
public boolean unlinkToDeath(DeathRecipient recipient, int flags) {
return false;
}
};
private boolean reportAttached;
public AttachFakingFrameLayout(Context context) {
super(context);
}
public AttachFakingFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AttachFakingFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public final IBinder getWindowToken() {
return reportAttached ? fakeWindowToken : null;
}
public void setAttached(boolean attached) {
setAttached(attached, true);
}
public void setAttached(boolean attached, boolean reportToViewUtils) {
if (reportAttached != attached) {
reportAttached = attached;
if (reportToViewUtils) {
ViewUtils.reportAttached(this, attached);
}
for (int i = 0; i < getChildCount(); i++) {
ViewUtils.reportAttached(getChildAt(i), attached);
}
}
}
@Override
public void onViewAdded(View child) {
if (reportAttached) {
ViewUtils.reportAttached(child, true);
}
super.onViewAdded(child);
}
@Override
public void onViewRemoved(View child) {
ViewUtils.reportAttached(child, false);
super.onViewRemoved(child);
}
}
@@ -119,9 +119,9 @@ public class CallState implements Parcelable {
"\n restoreInstanceStateCalls=" + restoreInstanceStateCalls +
"\n saveViewStateCalls=" + saveViewStateCalls +
"\n restoreViewStateCalls=" + restoreViewStateCalls +
"\n onActivityResultCalls= " + onActivityResultCalls +
"\n onRequestPermissionsResultCalls= " + onRequestPermissionsResultCalls +
"\n createOptionsMenuCalls= " + createOptionsMenuCalls +
"\n onActivityResultCalls=" + onActivityResultCalls +
"\n onRequestPermissionsResultCalls=" + onRequestPermissionsResultCalls +
"\n createOptionsMenuCalls=" + createOptionsMenuCalls +
"}\n";
}
@@ -1,41 +1,37 @@
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.Controller.LifecycleListener;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeCompletedListener;
import com.bluelinelabs.conductor.MockChangeHandler.ChangeHandlerListener;
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 ControllerLifecycleTests {
private ActivityController<TestActivity> activityController;
private Router router;
private ActivityProxy activityProxy;
private CallState currentCallState;
public void createActivityController(Bundle savedInstanceState) {
activityController = Robolectric.buildActivity(TestActivity.class).create(savedInstanceState).start();
public void createActivityController(Bundle savedInstanceState, boolean includeStartAndResume) {
activityProxy = new ActivityProxy().create(savedInstanceState);
@IdRes int containerId = 4;
FrameLayout routerContainer = new FrameLayout(activityController.get());
routerContainer.setId(containerId);
if (includeStartAndResume) {
activityProxy.start().resume();
}
router = Conductor.attachRouter(activityController.get(), routerContainer, savedInstanceState);
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
@@ -43,7 +39,7 @@ public class ControllerLifecycleTests {
@Before
public void setup() {
createActivityController(null);
createActivityController(null, true);
currentCallState = new CallState();
}
@@ -82,18 +78,20 @@ public class ControllerLifecycleTests {
assertCalls(expectedCallState, controller);
activityController.pause();
activityProxy.getActivity().isDestroying = true;
activityProxy.pause();
assertCalls(expectedCallState, controller);
activityController.stop();
assertCalls(expectedCallState, controller);
activityController.destroy();
activityProxy.stop();
expectedCallState.saveViewStateCalls++;
expectedCallState.detachCalls++;
expectedCallState.destroyViewCalls++;
assertCalls(expectedCallState, controller);
activityProxy.destroy();
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, controller);
}
@@ -112,33 +110,32 @@ public class ControllerLifecycleTests {
assertCalls(expectedCallState, controller);
activityController.get().isChangingConfigurations = true;
activityProxy.getActivity().isChangingConfigurations = true;
Bundle bundle = new Bundle();
activityController.saveInstanceState(bundle);
activityProxy.saveInstanceState(bundle);
expectedCallState.saveViewStateCalls++;
expectedCallState.saveInstanceStateCalls++;
assertCalls(expectedCallState, controller);
activityController.pause();
activityProxy.pause();
assertCalls(expectedCallState, controller);
activityController.stop();
assertCalls(expectedCallState, controller);
activityController.destroy();
activityProxy.stop();
expectedCallState.detachCalls++;
expectedCallState.destroyViewCalls++;
assertCalls(expectedCallState, controller);
createActivityController(bundle);
activityProxy.destroy();
assertCalls(expectedCallState, controller);
createActivityController(bundle, false);
controller = (TestController)router.getControllerWithTag("root");
expectedCallState.restoreInstanceStateCalls++;
expectedCallState.restoreViewStateCalls++;
expectedCallState.changeStartCalls++;
expectedCallState.changeEndCalls++;
expectedCallState.createViewCalls++;
// Lifecycle listener isn't attached during restore, grab the current views from the controller for this stuff...
@@ -147,10 +144,19 @@ public class ControllerLifecycleTests {
currentCallState.changeStartCalls = controller.currentCallState.changeStartCalls;
currentCallState.changeEndCalls = controller.currentCallState.changeEndCalls;
currentCallState.createViewCalls = controller.currentCallState.createViewCalls;
currentCallState.attachCalls = controller.currentCallState.attachCalls;
assertCalls(expectedCallState, controller);
activityController.resume();
activityProxy.start().resume();
currentCallState.changeEndCalls = controller.currentCallState.changeEndCalls;
currentCallState.attachCalls = controller.currentCallState.attachCalls;
expectedCallState.changeEndCalls++;
expectedCallState.attachCalls++;
assertCalls(expectedCallState, controller);
activityProxy.resume();
assertCalls(expectedCallState, controller);
}
@@ -167,16 +173,16 @@ public class ControllerLifecycleTests {
assertCalls(expectedCallState, controller);
activityController.pause();
activityProxy.pause();
Bundle bundle = new Bundle();
activityController.saveInstanceState(bundle);
activityProxy.saveInstanceState(bundle);
expectedCallState.saveInstanceStateCalls++;
expectedCallState.saveViewStateCalls++;
assertCalls(expectedCallState, controller);
activityController.resume();
activityProxy.resume();
}
@Test
@@ -377,22 +383,8 @@ public class ControllerLifecycleTests {
});
router.pushController(RouterTransaction.with(testController)
.pushChangeHandler(new ChangeHandler(new ChangeHandlerListener() {
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
container.addView(to);
ViewUtils.setAttached(to, true);
changeListener.onChangeCompleted();
}
}))
.popChangeHandler(new ChangeHandler(new ChangeHandlerListener() {
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
container.removeView(from);
ViewUtils.setAttached(from, false);
changeListener.onChangeCompleted();
}
})));
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
router.popController(testController);
@@ -407,14 +399,7 @@ public class ControllerLifecycleTests {
public void testChildLifecycle() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(new ChangeHandler(new ChangeHandlerListener() {
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
container.addView(to);
ViewUtils.setAttached(to, true);
changeListener.onChangeCompleted();
}
})));
.pushChangeHandler(new MockChangeHandler()));
TestController child = new TestController();
attachLifecycleListener(child);
@@ -423,7 +408,7 @@ public class ControllerLifecycleTests {
assertCalls(expectedCallState, child);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter
.setRoot(RouterTransaction.with(child)
.pushChangeHandler(getPushHandler(expectedCallState, child))
@@ -440,22 +425,8 @@ public class ControllerLifecycleTests {
public void testChildLifecycle2() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(new ChangeHandler(new ChangeHandlerListener() {
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
container.addView(to);
ViewUtils.setAttached(to, true);
changeListener.onChangeCompleted();
}
}))
.popChangeHandler(new ChangeHandler(new ChangeHandlerListener() {
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
container.removeView(from);
ViewUtils.setAttached(from, false);
changeListener.onChangeCompleted();
}
})));
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
TestController child = new TestController();
attachLifecycleListener(child);
@@ -464,7 +435,7 @@ public class ControllerLifecycleTests {
assertCalls(expectedCallState, child);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter
.setRoot(RouterTransaction.with(child)
.pushChangeHandler(getPushHandler(expectedCallState, child))
@@ -477,44 +448,47 @@ public class ControllerLifecycleTests {
assertCalls(expectedCallState, child);
}
private ChangeHandler getPushHandler(final CallState expectedCallState, final TestController controller) {
return new ChangeHandler(new ChangeHandlerListener() {
private MockChangeHandler getPushHandler(final CallState expectedCallState, final TestController controller) {
return new MockChangeHandler(new ChangeHandlerListener() {
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
void willStartChange() {
expectedCallState.changeStartCalls++;
expectedCallState.createViewCalls++;
assertCalls(expectedCallState, controller);
}
container.addView(to);
ViewUtils.setAttached(to, true);
@Override
void didAttachOrDetach() {
expectedCallState.attachCalls++;
assertCalls(expectedCallState, controller);
}
changeListener.onChangeCompleted();
@Override
void didEndChange() {
expectedCallState.changeEndCalls++;
assertCalls(expectedCallState, controller);
}
});
}
private ChangeHandler getPopHandler(final CallState expectedCallState, final TestController controller) {
return new ChangeHandler(new ChangeHandlerListener() {
private MockChangeHandler getPopHandler(final CallState expectedCallState, final TestController controller) {
return new MockChangeHandler(new ChangeHandlerListener() {
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
void willStartChange() {
expectedCallState.changeStartCalls++;
assertCalls(expectedCallState, controller);
container.removeView(from);
ViewUtils.setAttached(from, false);
}
@Override
void didAttachOrDetach() {
expectedCallState.destroyViewCalls++;
expectedCallState.detachCalls++;
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, controller);
}
changeListener.onChangeCompleted();
@Override
void didEndChange() {
expectedCallState.changeEndCalls++;
assertCalls(expectedCallState, controller);
}
@@ -585,24 +559,4 @@ public class ControllerLifecycleTests {
});
}
interface ChangeHandlerListener {
void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @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, isPush, changeListener);
}
}
}
@@ -3,10 +3,8 @@ package com.bluelinelabs.conductor;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.bluelinelabs.conductor.Controller.RetainViewMode;
@@ -14,26 +12,19 @@ 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 ControllerTests {
private ActivityController<TestActivity> activityController;
private ActivityProxy activityProxy;
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);
activityProxy = new ActivityProxy().create(savedInstanceState).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
@@ -47,24 +38,25 @@ public class ControllerTests {
@Test
public void testViewRetention() {
Controller controller = new TestController();
controller.setRouter(router);
// Test View getting released w/ RELEASE_DETACH
controller.setRetainViewMode(RetainViewMode.RELEASE_DETACH);
Assert.assertNull(controller.getView());
View view = controller.inflate(new FrameLayout(router.getActivity()));
View view = controller.inflate(router.container);
Assert.assertNotNull(controller.getView());
ViewUtils.setAttached(view, true);
ViewUtils.reportAttached(view, true);
Assert.assertNotNull(controller.getView());
ViewUtils.setAttached(view, false);
ViewUtils.reportAttached(view, false);
Assert.assertNull(controller.getView());
// Test View getting retained w/ RETAIN_DETACH
controller.setRetainViewMode(RetainViewMode.RETAIN_DETACH);
view = controller.inflate(new FrameLayout(router.getActivity()));
view = controller.inflate(router.container);
Assert.assertNotNull(controller.getView());
ViewUtils.setAttached(view, true);
ViewUtils.reportAttached(view, true);
Assert.assertNotNull(controller.getView());
ViewUtils.setAttached(view, false);
ViewUtils.reportAttached(view, false);
Assert.assertNotNull(controller.getView());
// Ensure re-setting RELEASE_DETACH releases
@@ -78,7 +70,6 @@ public class ControllerTests {
CallState expectedCallState = new CallState(true);
router.pushController(RouterTransaction.with(controller));
ViewUtils.setAttached(controller.getView(), true);
// Ensure that calling onActivityResult w/o requesting a result doesn't do anything
router.onActivityResult(1, Activity.RESULT_OK, null);
@@ -103,10 +94,8 @@ public class ControllerTests {
TestController child = new TestController();
router.pushController(RouterTransaction.with(parent));
ViewUtils.setAttached(parent.getView(), true);
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null)
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID))
.setRoot(RouterTransaction.with(child));
ViewUtils.setAttached(child.getView(), true);
CallState childExpectedCallState = new CallState(true);
CallState parentExpectedCallState = new CallState(true);
@@ -139,7 +128,6 @@ public class ControllerTests {
CallState expectedCallState = new CallState(true);
router.pushController(RouterTransaction.with(controller));
ViewUtils.setAttached(controller.getView(), true);
// Ensure that calling handleRequestedPermission w/o requesting a result doesn't do anything
router.onRequestPermissionsResult("anotherId", 1, requestedPermissions, new int[] {1});
@@ -163,10 +151,8 @@ public class ControllerTests {
TestController child = new TestController();
router.pushController(RouterTransaction.with(parent));
ViewUtils.setAttached(parent.getView(), true);
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null)
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID))
.setRoot(RouterTransaction.with(child));
ViewUtils.setAttached(child.getView(), true);
CallState childExpectedCallState = new CallState(true);
CallState parentExpectedCallState = new CallState(true);
@@ -193,7 +179,6 @@ public class ControllerTests {
CallState expectedCallState = new CallState(true);
router.pushController(RouterTransaction.with(controller));
ViewUtils.setAttached(controller.getView(), true);
// Ensure that calling onCreateOptionsMenu w/o declaring that we have one doesn't do anything
router.onCreateOptionsMenu(null, null);
@@ -230,10 +215,8 @@ public class ControllerTests {
TestController child = new TestController();
router.pushController(RouterTransaction.with(parent));
ViewUtils.setAttached(parent.getView(), true);
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null)
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID))
.setRoot(RouterTransaction.with(child));
ViewUtils.setAttached(child.getView(), true);
CallState childExpectedCallState = new CallState(true);
CallState parentExpectedCallState = new CallState(true);
@@ -284,7 +267,8 @@ public class ControllerTests {
Assert.assertNull(child1.getParentController());
Assert.assertNull(child2.getParentController());
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.setRoot(RouterTransaction.with(child1));
Assert.assertEquals(1, parent.getChildRouters().size());
@@ -294,7 +278,7 @@ public class ControllerTests {
Assert.assertEquals(parent, child1.getParentController());
Assert.assertNull(child2.getParentController());
childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null);
childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(child2));
Assert.assertEquals(1, parent.getChildRouters().size());
@@ -336,8 +320,8 @@ public class ControllerTests {
Assert.assertNull(child1.getParentController());
Assert.assertNull(child2.getParentController());
Router childRouter1 = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1), null);
Router childRouter2 = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_2), null);
Router childRouter1 = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1));
Router childRouter2 = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_2));
childRouter1.setRoot(RouterTransaction.with(child1));
childRouter2.setRoot(RouterTransaction.with(child2));
@@ -0,0 +1,85 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
public class MockChangeHandler extends ControllerChangeHandler {
private static final String KEY_REMOVES_FROM_VIEW_ON_PUSH = "MockChangeHandler.removesFromViewOnPush";
static class ChangeHandlerListener {
void willStartChange() { }
void didAttachOrDetach() { }
void didEndChange() { }
}
final ChangeHandlerListener listener;
boolean removesFromViewOnPush;
public MockChangeHandler() {
this(true, null);
}
public MockChangeHandler(boolean removesViewOnPush) {
this(removesViewOnPush, null);
}
public MockChangeHandler(@NonNull ChangeHandlerListener listener) {
this(true, listener);
}
public MockChangeHandler(boolean removesFromViewOnPush, ChangeHandlerListener listener) {
this.removesFromViewOnPush = removesFromViewOnPush;
if (listener == null) {
this.listener = new ChangeHandlerListener() { };
} else {
this.listener = listener;
}
}
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
listener.willStartChange();
if (isPush) {
container.addView(to);
listener.didAttachOrDetach();
if (removesFromViewOnPush && from != null) {
container.removeView(from);
}
} else {
container.removeView(from);
listener.didAttachOrDetach();
if (to != null) {
container.addView(to);
}
}
changeListener.onChangeCompleted();
listener.didEndChange();
}
@Override
public boolean removesFromViewOnPush() {
return removesFromViewOnPush;
}
@Override
public void saveToBundle(@NonNull Bundle bundle) {
super.saveToBundle(bundle);
bundle.putBoolean(KEY_REMOVES_FROM_VIEW_ON_PUSH, removesFromViewOnPush);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
super.restoreFromBundle(bundle);
removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_VIEW_ON_PUSH);
}
}
@@ -0,0 +1,234 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.view.ViewGroup;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ReattachCaseTests {
private ActivityProxy activityProxy;
private Router router;
public void createActivityController(Bundle savedInstanceState) {
activityProxy = new ActivityProxy().create(savedInstanceState).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), 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(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
Assert.assertTrue(controllerA.isAttached());
Assert.assertFalse(controllerB.isAttached());
sleepWakeDevice();
Assert.assertTrue(controllerA.isAttached());
Assert.assertFalse(controllerB.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
activityProxy.rotate();
router.rebindIfNeeded();
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(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
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(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
Assert.assertFalse(controllerA.isAttached());
Assert.assertFalse(childController.isAttached());
Assert.assertTrue(controllerB.isAttached());
activityProxy.rotate();
router.rebindIfNeeded();
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(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
Assert.assertTrue(controllerA.isAttached());
Assert.assertFalse(controllerB.isAttached());
Assert.assertFalse(childController.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
Router childRouter = controllerB.getChildRouter((ViewGroup)controllerB.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertTrue(childController.isAttached());
activityProxy.rotate();
router.rebindIfNeeded();
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(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
Assert.assertTrue(controllerA.isAttached());
Assert.assertFalse(controllerB.isAttached());
Assert.assertFalse(childController.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
Router childRouter = controllerB.getChildRouter((ViewGroup)controllerB.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
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(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertTrue(childController.isAttached());
activityProxy.rotate();
router.rebindIfNeeded();
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertTrue(childController.isAttached());
router.handleBack();
childController = new TestController();
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
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() {
activityProxy.saveInstanceState(new Bundle()).pause();
activityProxy.resume();
}
}
@@ -1,16 +1,15 @@
package com.bluelinelabs.conductor;
import android.app.Activity;
import android.widget.FrameLayout;
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 java.util.ArrayList;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class RouterTests {
@@ -19,8 +18,8 @@ public class RouterTests {
@Before
public void setup() {
Activity activity = Robolectric.buildActivity(TestActivity.class).create().get();
router = Conductor.attachRouter(activity, new FrameLayout(activity), null);
ActivityProxy activityProxy = new ActivityProxy().create(null).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), null);
}
@Test
@@ -174,4 +173,149 @@ public class RouterTests {
Assert.assertEquals(controller3, router.getControllerWithTag(controller3Tag));
}
@Test
public void testSetBackstack() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction middleTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = new ArrayList<>();
backstack.add(rootTransaction);
backstack.add(middleTransaction);
backstack.add(topTransaction);
router.setBackstack(backstack, null);
Assert.assertEquals(3, router.getBackstackSize());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
Assert.assertEquals(rootTransaction, fetchedBackstack.get(0));
Assert.assertEquals(middleTransaction, fetchedBackstack.get(1));
Assert.assertEquals(topTransaction, fetchedBackstack.get(2));
}
@Test
public void testNewSetBackstack() {
router.setRoot(RouterTransaction.with(new TestController()));
Assert.assertEquals(1, router.getBackstackSize());
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction middleTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = new ArrayList<>();
backstack.add(rootTransaction);
backstack.add(middleTransaction);
backstack.add(topTransaction);
router.setBackstack(backstack, null);
Assert.assertEquals(3, router.getBackstackSize());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
Assert.assertEquals(rootTransaction, fetchedBackstack.get(0));
Assert.assertEquals(middleTransaction, fetchedBackstack.get(1));
Assert.assertEquals(topTransaction, fetchedBackstack.get(2));
}
@Test
public void testNewSetBackstackWithNoRemoveViewOnPush() {
RouterTransaction oldRootTransaction = RouterTransaction.with(new TestController());
RouterTransaction oldTopTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false));
router.setRoot(oldRootTransaction);
router.pushController(oldTopTransaction);
Assert.assertEquals(2, router.getBackstackSize());
Assert.assertTrue(oldRootTransaction.controller.isAttached());
Assert.assertTrue(oldTopTransaction.controller.isAttached());
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction middleTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false));
RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false));
List<RouterTransaction> backstack = new ArrayList<>();
backstack.add(rootTransaction);
backstack.add(middleTransaction);
backstack.add(topTransaction);
router.setBackstack(backstack, null);
Assert.assertEquals(3, router.getBackstackSize());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
Assert.assertEquals(rootTransaction, fetchedBackstack.get(0));
Assert.assertEquals(middleTransaction, fetchedBackstack.get(1));
Assert.assertEquals(topTransaction, fetchedBackstack.get(2));
Assert.assertFalse(oldRootTransaction.controller.isAttached());
Assert.assertFalse(oldTopTransaction.controller.isAttached());
Assert.assertTrue(rootTransaction.controller.isAttached());
Assert.assertTrue(middleTransaction.controller.isAttached());
Assert.assertTrue(topTransaction.controller.isAttached());
}
@Test
public void testReplaceTopController() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = new ArrayList<>();
backstack.add(rootTransaction);
backstack.add(topTransaction);
router.setBackstack(backstack, null);
Assert.assertEquals(2, router.getBackstackSize());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
Assert.assertEquals(rootTransaction, fetchedBackstack.get(0));
Assert.assertEquals(topTransaction, fetchedBackstack.get(1));
RouterTransaction newTopTransaction = RouterTransaction.with(new TestController());
router.replaceTopController(newTopTransaction);
Assert.assertEquals(2, router.getBackstackSize());
fetchedBackstack = router.getBackstack();
Assert.assertEquals(rootTransaction, fetchedBackstack.get(0));
Assert.assertEquals(newTopTransaction, fetchedBackstack.get(1));
}
@Test
public void testReplaceTopControllerWithNoRemoveViewOnPush() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false));
List<RouterTransaction> backstack = new ArrayList<>();
backstack.add(rootTransaction);
backstack.add(topTransaction);
router.setBackstack(backstack, null);
Assert.assertEquals(2, router.getBackstackSize());
Assert.assertTrue(rootTransaction.controller.isAttached());
Assert.assertTrue(topTransaction.controller.isAttached());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
Assert.assertEquals(rootTransaction, fetchedBackstack.get(0));
Assert.assertEquals(topTransaction, fetchedBackstack.get(1));
RouterTransaction newTopTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false));
router.replaceTopController(newTopTransaction);
newTopTransaction.pushChangeHandler().completeImmediately();
Assert.assertEquals(2, router.getBackstackSize());
fetchedBackstack = router.getBackstack();
Assert.assertEquals(rootTransaction, fetchedBackstack.get(0));
Assert.assertEquals(newTopTransaction, fetchedBackstack.get(1));
Assert.assertTrue(rootTransaction.controller.isAttached());
Assert.assertFalse(topTransaction.controller.isAttached());
Assert.assertTrue(newTopTransaction.controller.isAttached());
}
}
@@ -0,0 +1,100 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.view.ViewGroup;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class TargetControllerTests {
private Router router;
public void createActivityController(Bundle savedInstanceState) {
ActivityProxy activityProxy = new ActivityProxy().create(savedInstanceState).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Before
public void setup() {
createActivityController(null);
}
@Test
public void testSiblingTarget() {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
Assert.assertNull(controllerA.getTargetController());
Assert.assertNull(controllerB.getTargetController());
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
controllerB.setTargetController(controllerA);
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
Assert.assertNull(controllerA.getTargetController());
Assert.assertEquals(controllerA, controllerB.getTargetController());
}
@Test
public void testParentChildTarget() {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
Assert.assertNull(controllerA.getTargetController());
Assert.assertNull(controllerB.getTargetController());
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
controllerB.setTargetController(controllerA);
Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
Assert.assertNull(controllerA.getTargetController());
Assert.assertEquals(controllerA, controllerB.getTargetController());
}
@Test
public void testChildParentTarget() {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
Assert.assertNull(controllerA.getTargetController());
Assert.assertNull(controllerB.getTargetController());
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
controllerA.setTargetController(controllerB);
Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
Assert.assertNull(controllerB.getTargetController());
Assert.assertEquals(controllerB, controllerA.getTargetController());
}
}
@@ -5,10 +5,15 @@ import android.app.Activity;
public class TestActivity extends Activity {
public boolean isChangingConfigurations = false;
public boolean isDestroying = false;
@Override
public boolean isChangingConfigurations() {
return isChangingConfigurations;
}
@Override
public boolean isDestroyed() {
return isDestroying || super.isDestroyed();
}
}
@@ -29,7 +29,7 @@ public class TestController extends Controller {
@Override
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
currentCallState.createViewCalls++;
FrameLayout view = new FrameLayout(inflater.getContext());
FrameLayout view = new AttachFakingFrameLayout(inflater.getContext());
view.setId(VIEW_ID);
FrameLayout childContainer1 = new FrameLayout(inflater.getContext());
@@ -68,7 +68,7 @@ public class TestController extends Controller {
}
@Override
protected void onDestroyView(View view) {
protected void onDestroyView(@NonNull View view) {
super.onDestroyView(view);
currentCallState.destroyViewCalls++;
}
@@ -9,9 +9,26 @@ import java.util.List;
public class ViewUtils {
static void setAttached(View view, boolean attached) {
Object listenerInfo = ReflectionHelpers.callInstanceMethod(view, "getListenerInfo");
List<OnAttachStateChangeListener> listeners = ReflectionHelpers.getField(listenerInfo, "mOnAttachStateChangeListeners");
public static void reportAttached(View view, boolean attached) {
if (view instanceof AttachFakingFrameLayout) {
((AttachFakingFrameLayout)view).setAttached(attached, false);
}
List<OnAttachStateChangeListener> listeners = getAttachStateListeners(view);
// Add, then remove an OnAttachStateChangeListener to initialize the attachStateListeners variable inside a view
if (listeners == null) {
OnAttachStateChangeListener tmpListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) { }
@Override
public void onViewDetachedFromWindow(View v) { }
};
view.addOnAttachStateChangeListener(tmpListener);
view.removeOnAttachStateChangeListener(tmpListener);
listeners = getAttachStateListeners(view);
}
for (OnAttachStateChangeListener listener : listeners) {
if (attached) {
@@ -20,6 +37,12 @@ public class ViewUtils {
listener.onViewDetachedFromWindow(view);
}
}
}
private static List<OnAttachStateChangeListener> getAttachStateListeners(View view) {
Object listenerInfo = ReflectionHelpers.callInstanceMethod(view, "getListenerInfo");
return ReflectionHelpers.getField(listenerInfo, "mOnAttachStateChangeListeners");
}
}
@@ -12,22 +12,20 @@ import com.bluelinelabs.conductor.demo.controllers.HomeController;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
public class MainActivity extends AppCompatActivity implements ActionBarProvider {
public final class MainActivity extends AppCompatActivity implements ActionBarProvider {
@BindView(R.id.toolbar) Toolbar toolbar;
@BindView(R.id.controller_container) ViewGroup container;
private Router router;
private Unbinder unbinder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
unbinder = ButterKnife.bind(this);
ButterKnife.bind(this);
setSupportActionBar(toolbar);
@@ -44,10 +42,4 @@ public class MainActivity extends AppCompatActivity implements ActionBarProvider
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unbinder.unbind();
}
}
@@ -120,7 +120,7 @@ public class CircularRevealChangeHandler extends AnimatorChangeHandler {
this.cy = cy;
}
@Override
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
final float radius = (float) Math.hypot(cx, cy);
Animator animator = null;
@@ -16,7 +16,7 @@ public class CircularRevealChangeHandlerCompat extends CircularRevealChangeHandl
super(fromView, containerView);
}
@Override
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return super.getAnimator(container, from, to, isPush, toAddedToContainer);
@@ -52,7 +52,7 @@ public class FlipChangeHandler extends AnimatorChangeHandler {
this.animationDuration = animationDuration;
}
@Override
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animatorSet = new AnimatorSet();
@@ -15,7 +15,7 @@ public class ScaleFadeChangeHandler extends AnimatorChangeHandler {
super(DEFAULT_ANIMATION_DURATION, true);
}
@Override
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animator = new AnimatorSet();
if (to != null && toAddedToContainer) {
@@ -46,7 +46,7 @@ public class DragDismissController extends BaseController {
}
@Override
protected void onDestroyView(View view) {
protected void onDestroyView(@NonNull View view) {
super.onDestroyView(view);
((ElasticDragDismissFrameLayout)view).removeListener(dragDismissListener);
@@ -27,6 +27,7 @@ import com.bluelinelabs.conductor.ControllerChangeType;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.NavigationDemoController.DisplayUpMode;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import butterknife.BindView;
@@ -79,7 +80,7 @@ public class HomeController extends BaseController {
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.home, menu);
}
@@ -94,7 +95,7 @@ public class HomeController extends BaseController {
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.about) {
SpannableString details = new SpannableString("A small, yet full-featured framework that allows building View-based Android applications");
details.setSpan(new AbsoluteSizeSpan(16, true), 0, details.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
@@ -115,7 +116,7 @@ public class HomeController extends BaseController {
content.append("\n\n");
content.append(link);
getChildRouter(overlayRoot, null)
getChildRouter(overlayRoot)
.setPopsLastView(true)
.setRoot(RouterTransaction.with(new OverlayController(content))
.pushChangeHandler(new FadeChangeHandler())
@@ -133,7 +134,7 @@ public class HomeController extends BaseController {
void onModelRowClick(HomeDemoModel model) {
switch (model) {
case NAVIGATION:
getRouter().pushController(RouterTransaction.with(new NavigationDemoController(0, true))
getRouter().pushController(RouterTransaction.with(new NavigationDemoController(0, DisplayUpMode.SHOW_FOR_CHILDREN_ONLY))
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler())
.tag(NavigationDemoController.TAG_UP_TRANSACTION)
@@ -159,7 +160,7 @@ public class HomeController extends BaseController {
.popChangeHandler(new FadeChangeHandler()));
break;
case OVERLAY:
getChildRouter(overlayRoot, null)
getChildRouter(overlayRoot)
.setPopsLastView(true)
.setRoot(RouterTransaction.with(new OverlayController("I'm an overlay!"))
.pushChangeHandler(new FadeChangeHandler())
@@ -91,7 +91,7 @@ public class MasterDetailListController extends BaseController {
ChildController controller = new ChildController(model.detail, model.backgroundColor, true);
if (twoPaneView) {
getChildRouter(detailContainer, null).setRoot(RouterTransaction.with(controller));
getChildRouter(detailContainer).setRoot(RouterTransaction.with(controller));
} else {
getRouter().pushController(RouterTransaction.with(controller)
.pushChangeHandler(new HorizontalChangeHandler())
@@ -8,6 +8,7 @@ import android.view.ViewGroup;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.NavigationDemoController.DisplayUpMode;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import butterknife.BindViews;
@@ -26,9 +27,9 @@ public class MultipleChildRouterController extends BaseController {
super.onViewBound(view);
for (ViewGroup childContainer : childContainers) {
Router childRouter = getChildRouter(childContainer, null).setPopsLastView(false);
Router childRouter = getChildRouter(childContainer).setPopsLastView(false);
if (!childRouter.hasRootController()) {
childRouter.setRoot(RouterTransaction.with(new NavigationDemoController(0, false)));
childRouter.setRoot(RouterTransaction.with(new NavigationDemoController(0, DisplayUpMode.HIDE)));
}
}
}
@@ -7,6 +7,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.ControllerChangeType;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.demo.R;
@@ -19,27 +21,42 @@ import butterknife.OnClick;
public class NavigationDemoController extends BaseController {
public enum DisplayUpMode {
SHOW,
SHOW_FOR_CHILDREN_ONLY,
HIDE;
private DisplayUpMode getDisplayUpModeForChild() {
switch (this) {
case HIDE:
return HIDE;
default:
return SHOW;
}
}
}
public static final String TAG_UP_TRANSACTION = "NavigationDemoController.up";
private static final String KEY_INDEX = "NavigationDemoController.index";
private static final String KEY_DISPLAY_UP = "NavigationDemoController.displayUp";
private static final String KEY_DISPLAY_UP_MODE = "NavigationDemoController.displayUpMode";
@BindView(R.id.tv_title) TextView tvTitle;
private int index;
private boolean displayUp;
private DisplayUpMode displayUpMode;
public NavigationDemoController(int index, boolean displayUpButton) {
public NavigationDemoController(int index, DisplayUpMode displayUpMode) {
this(new BundleBuilder(new Bundle())
.putInt(KEY_INDEX, index)
.putBoolean(KEY_DISPLAY_UP, displayUpButton)
.putInt(KEY_DISPLAY_UP_MODE, displayUpMode.ordinal())
.build());
}
public NavigationDemoController(Bundle args) {
super(args);
index = args.getInt(KEY_INDEX);
displayUp = args.getBoolean(KEY_DISPLAY_UP);
displayUpMode = DisplayUpMode.values()[args.getInt(KEY_DISPLAY_UP_MODE)];
}
@NonNull
@@ -52,7 +69,7 @@ public class NavigationDemoController extends BaseController {
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
if (!displayUp) {
if (displayUpMode != DisplayUpMode.SHOW) {
view.findViewById(R.id.btn_up).setVisibility(View.GONE);
}
@@ -60,13 +77,36 @@ public class NavigationDemoController extends BaseController {
tvTitle.setText(getResources().getString(R.string.navigation_title, index));
}
@Override
protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
super.onChangeEnded(changeHandler, changeType);
setButtonsEnabled(true);
}
@Override
protected void onChangeStarted(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
super.onChangeStarted(changeHandler, changeType);
setButtonsEnabled(false);
}
@Override
protected String getTitle() {
return "Navigation Demos";
}
private void setButtonsEnabled(boolean enabled) {
final View view = getView();
if (view != null) {
view.findViewById(R.id.btn_next).setEnabled(enabled);
view.findViewById(R.id.btn_up).setEnabled(enabled);
view.findViewById(R.id.btn_pop_to_root).setEnabled(enabled);
}
}
@OnClick(R.id.btn_next) void onNextClicked() {
getRouter().pushController(RouterTransaction.with(new NavigationDemoController(index + 1, displayUp))
getRouter().pushController(RouterTransaction.with(new NavigationDemoController(index + 1, displayUpMode.getDisplayUpModeForChild()))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
}
@@ -12,6 +12,8 @@ import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.support.ControllerPagerAdapter;
import java.util.Locale;
import butterknife.BindView;
public class PagerController extends BaseController {
@@ -27,7 +29,7 @@ public class PagerController extends BaseController {
pagerAdapter = new ControllerPagerAdapter(this, false) {
@Override
public Controller getItem(int position) {
return new ChildController(String.format("Child #%d (Swipe to see more)", position), PAGE_COLORS[position], true);
return new ChildController(String.format(Locale.getDefault(), "Child #%d (Swipe to see more)", position), PAGE_COLORS[position], true);
}
@Override
@@ -50,7 +52,7 @@ public class PagerController extends BaseController {
}
@Override
protected void onDestroyView(View view) {
protected void onDestroyView(@NonNull View view) {
viewPager.setAdapter(null);
super.onDestroyView(view);
}
@@ -16,10 +16,13 @@ import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.ColorUtil;
import java.util.List;
public class ParentController extends BaseController {
private static final int NUMBER_OF_CHILDREN = 5;
private boolean finishing;
private boolean hasShownAll;
@NonNull
@Override
@@ -39,7 +42,7 @@ public class ParentController extends BaseController {
private void addChild(final int index) {
@IdRes final int frameId = getResources().getIdentifier("child_content_" + (index + 1), "id", getActivity().getPackageName());
final ViewGroup container = (ViewGroup)getView().findViewById(frameId);
final Router childRouter = getChildRouter(container, null).setPopsLastView(true);
final Router childRouter = getChildRouter(container).setPopsLastView(true);
if (!childRouter.hasRootController()) {
ChildController childController = new ChildController("Child Controller #" + index, ColorUtil.getMaterialColor(getResources(), index), false);
@@ -47,8 +50,12 @@ public class ParentController extends BaseController {
childController.addLifecycleListener(new LifecycleListener() {
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
if (changeType == ControllerChangeType.PUSH_ENTER && index < NUMBER_OF_CHILDREN - 1) {
addChild(index + 1);
if (changeType == ControllerChangeType.PUSH_ENTER && !hasShownAll) {
if (index < NUMBER_OF_CHILDREN - 1) {
addChild(index + 1);
} else {
hasShownAll = true;
}
} else if (changeType == ControllerChangeType.POP_EXIT) {
if (index > 0) {
removeChild(index - 1);
@@ -66,16 +73,27 @@ public class ParentController extends BaseController {
}
private void removeChild(int index) {
removeChildRouter(getChildRouters().get(index));
List<Router> childRouters = getChildRouters();
if (index < childRouters.size()) {
removeChildRouter(childRouters.get(index));
}
}
@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
@@ -92,7 +92,7 @@ public class RxLifecycleController extends BaseController {
}
@Override
protected void onDestroyView(View view) {
protected void onDestroyView(@NonNull View view) {
super.onDestroyView(view);
Log.i(TAG, "onDestroyView() called");
@@ -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));
}
}
@@ -34,9 +34,10 @@ public abstract class ButterKnifeController extends RxController {
protected void onViewBound(@NonNull View view) { }
@Override
protected void onDestroyView(View view) {
protected void onDestroyView(@NonNull View view) {
super.onDestroyView(view);
unbinder.unbind();
unbinder = null;
}
}
@@ -1,7 +1,10 @@
package com.bluelinelabs.conductor.demo.controllers.base;
import android.os.Bundle;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.ControllerChangeType;
import com.bluelinelabs.conductor.demo.DemoApplication;
public abstract class RefWatchingController extends ButterKnifeController {
@@ -11,10 +14,24 @@ public abstract class RefWatchingController extends ButterKnifeController {
super(args);
}
private boolean hasExited;
@Override
public void onDestroy() {
super.onDestroy();
DemoApplication.refWatcher.watch(this);
if (hasExited) {
DemoApplication.refWatcher.watch(this);
}
}
@Override
protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
super.onChangeEnded(changeHandler, changeType);
hasExited = !changeType.isEnter;
if (isDestroyed()) {
DemoApplication.refWatcher.watch(this);
}
}
}
+18 -18
View File
@@ -1,23 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="mdcolor_300">
<item name="red_300" type="color">@color/red_300</item>
<item name="light_green_300" type="color">@color/light_green_300</item>
<item name="teal_300" type="color">@color/teal_300</item>
<item name="pink_300" type="color">@color/pink_300</item>
<item name="brown_300" type="color">@color/brown_300</item>
<item name="purple_300" type="color">@color/purple_300</item>
<item name="lime_300" type="color">@color/lime_300</item>
<item name="blue_grey_300" type="color">@color/blue_grey_300</item>
<item name="deep_purple_300" type="color">@color/deep_purple_300</item>
<item name="green_300" type="color">@color/green_300</item>
<item name="indigo_300" type="color">@color/indigo_300</item>
<item name="yellow_300" type="color">@color/yellow_300</item>
<item name="blue_300" type="color">@color/blue_300</item>
<item name="orange_300" type="color">@color/orange_300</item>
<item name="light_blue_300" type="color">@color/light_blue_300</item>
<item name="deep_orange_300" type="color">@color/deep_orange_300</item>
<item name="cyan_300" type="color">@color/cyan_300</item>
<item name="grey_300" type="color">@color/grey_300</item>
<item name="md_red_300" type="color">@color/red_300</item>
<item name="md_light_green_300" type="color">@color/light_green_300</item>
<item name="md_teal_300" type="color">@color/teal_300</item>
<item name="md_pink_300" type="color">@color/pink_300</item>
<item name="md_brown_300" type="color">@color/brown_300</item>
<item name="md_purple_300" type="color">@color/purple_300</item>
<item name="md_lime_300" type="color">@color/lime_300</item>
<item name="md_blue_grey_300" type="color">@color/blue_grey_300</item>
<item name="md_deep_purple_300" type="color">@color/deep_purple_300</item>
<item name="md_green_300" type="color">@color/green_300</item>
<item name="md_indigo_300" type="color">@color/indigo_300</item>
<item name="md_yellow_300" type="color">@color/yellow_300</item>
<item name="md_blue_300" type="color">@color/blue_300</item>
<item name="md_orange_300" type="color">@color/orange_300</item>
<item name="md_light_blue_300" type="color">@color/light_blue_300</item>
<item name="md_deep_orange_300" type="color">@color/deep_orange_300</item>
<item name="md_cyan_300" type="color">@color/cyan_300</item>
<item name="md_grey_300" type="color">@color/grey_300</item>
</array>
</resources>
+6 -5
View File
@@ -14,12 +14,13 @@ ext {
picasso = 'com.squareup.picasso:picasso:2.5.2'
leakCanary = 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
leakCanaryNoOp = 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
leakCanary = 'com.squareup.leakcanary:leakcanary-android:1.4'
leakCanaryNoOp = 'com.squareup.leakcanary:leakcanary-android-no-op:1.4'
rxJava = 'io.reactivex:rxjava:1.1.0'
rxAndroid = 'io.reactivex:rxandroid:1.1.0'
rxLifecycle = 'com.trello:rxlifecycle:0.6.0'
rxJava = 'io.reactivex:rxjava:1.2.0'
rxAndroid = 'io.reactivex:rxandroid:1.2.1'
rxLifecycle = 'com.trello:rxlifecycle:0.8.0'
rxLifecycleAndroid = 'com.trello:rxlifecycle-android:0.8.0'
junit = 'junit:junit:4.11'
roboelectric = 'org.robolectric:robolectric:3.0'
+1 -1
View File
@@ -1,4 +1,4 @@
VERSION_NAME=2.0.1-SNAPSHOT
VERSION_NAME=2.0.5-SNAPSHOT
VERSION_CODE=2
GROUP=com.bluelinelabs
+2 -2
View File
@@ -1,6 +1,6 @@
#Tue Jan 19 22:43:12 CST 2016
#Mon Sep 05 10:45:48 CDT 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip