Compare commits

..

59 Commits

Author SHA1 Message Date
Eric Kuck 55e8aebb87 Version bump 2017-07-05 13:02:07 -05:00
Eric Kuck 272c507f91 Args are now restored to a controller after process death even if there is only a no-arg constructor. Fixes #313 2017-07-05 10:26:56 -05:00
Hannes Struß 8e945cde52 Fixes ControllerScopeProvider.from return type (#319) 2017-06-25 14:23:28 -05:00
Eric Kuck c17287db67 shouldShowRequestPermissionRationale now returns the correct result. Fixes #317 2017-06-24 12:29:35 -05:00
Eric Kuck 625a3fcfd6 Fixes fade change handler animation when popping and removesFromViewOnPush is false 2017-06-21 17:11:37 -05:00
Eric Kuck dd2ecf2f83 onContextUnavailable now correctly called before onDestroy even if the host Activity is still around 2017-06-16 08:37:21 -05:00
Eric Kuck 0efc2b3daf Fixed rx lifecycle handling for subscriptions in the onContextAvailable callback 2017-06-15 10:27:05 -05:00
Eric Kuck 4b4c3a82b8 Added context available/unavailable to lifecycle listeners. Now supported by all rx lifecycle modules as well. 2017-06-12 12:14:18 -05:00
Eric Kuck 7d0599fc5d Fixed tests 2017-06-02 16:15:37 -05:00
Eric Kuck 0adea89d34 Arch components lifecycle module now labeled as an alpha 2017-06-02 15:55:22 -05:00
Eric Kuck 044363517c Added modules for Autodispose and architecture components's Lifecycle
Cleaned up the demo and directory structure a bit now that there are so many external modules
2017-06-02 15:47:56 -05:00
Eric Kuck a2f11d5d51 The final controller popped in a router now has onDestroy properly called in it, even if the pop change handler is a no-op. 2017-05-26 16:09:56 -05:00
Eric Kuck bcd8ddbfb5 - Added ability to query current controller lifecycle state (needed for things like Google's Lifecycle and Uber's AutoDispose)
- Added onContextAvailable and onContextUnavailable callback to controllers (helpful for dependency injection)
2017-05-26 13:42:29 -05:00
Eric Kuck 479e3f0474 Pop changes that happen rapidly are now properly immediately completed instead of aborted. 2017-05-26 07:01:24 -05:00
Eric Kuck c298ca905c Fixes leaked activity introduced in e64fe1c610 2017-05-26 06:14:05 -05:00
Eric Kuck aebb19effa Fixes attach state management when a controller is pushed while the Activity is paused. (Fixes #303) 2017-05-24 03:05:42 -05:00
Eric Kuck e64fe1c610 Fixes issue when immediately creating two routers in the same Activity caused by the async nature of fragment transactions (fixes #299) 2017-05-24 03:01:36 -05:00
Frieder Bluemle 2357297531 Project updates (#264)
* Update Gradle wrapper to 3.5

* Fix whitespace errors

* Remove executable bit from regular files

* Fix DrawableRes annotation

* Update Android Gradle plugin to 2.3.2

* Update build tools to 25.0.3

* Do not ignore Lint errors

* Replace android-apt with annotationProcessor

* Update unmock to 0.6.0

* Update support libs to 25.3.1
2017-05-18 10:35:30 -05:00
Eric Kuck 926f7da6ce Now ensures that parent controller is restored after process death (fixes #300) 2017-05-18 10:29:12 -05:00
Eric Kuck d260dfcf12 Change handlers that occur after a deferred change handler are now deferred until the preceding handler completes to ensure correct ordering. Handlers will only be deferred when the containing view isn’t yet fully attached and they could cause a NPE in ViewGroup.java. 2017-05-18 07:36:02 -05:00
Eric Kuck a84b2fb4c4 Reverts 81a499d and attempts to fix #274, #285, and #287 in a more reliable way. 2017-05-10 12:19:00 +03:00
Eric Kuck 6aca3f578a Fixes a case that caused the incorrect change handler to execute when setting the backstack (fixes #286) 2017-05-10 11:37:10 +03:00
Eric Kuck 45ab54b5d7 Fixes issue where calling setBackstack repeatedly could result in a situation where multiple ControllerChangeHandlers are executing at the same time and interfering with each others states (#263) 2017-05-04 11:26:18 -05:00
Eric Kuck ca84419e0c Version bump 2017-05-03 16:28:23 -05:00
Eric Kuck a04dec7ec1 Fixes views never attaching if the host activity is stopped before inflation. Fixes #273 2017-05-03 16:03:58 -05:00
Eric Kuck 81a499d121 Now handles sequences of immediate pushing and popping of controllers much better. Also guards against NPEs due to popping a controller during onAttach. Fixes #274 2017-05-03 14:42:17 -05:00
Eric Kuck ff8ab621bc From controller’s initial position now correctly based on translationX in HorizontalChangeHandler. Fixes #279 2017-05-02 12:31:26 -05:00
Eric Kuck cdb5e5a978 Ensures AnimatorChangeHandlers can’t animate more than once per change 2017-05-02 11:27:01 -05:00
Eric Kuck effa410eae Fixes #269 in the demo app 2017-04-24 07:43:35 -05:00
Paul Woitaschek df27bfaa3d Made removeLifecycleListener final too (#268) 2017-04-13 13:45:33 -05:00
Eric Kuck a865c210b6 addLifecycleListener method now final. Fixes #267 2017-04-13 13:39:03 -05:00
Leonardo Ferrari 7820748cce Update README.md (#261)
fix latest version value
2017-04-06 09:30:02 -05:00
Eric Kuck 2147b2aa5e Version bump 2017-04-05 17:09:26 -05:00
fergusonm 19418617dd Pass along the correct view state (#258) 2017-04-04 07:59:12 -05:00
Eric Kuck e1924bf8a7 Args and savedInstanceState bundles now have classLoaders properly set. Fixes #246. 2017-03-31 09:38:20 -05:00
Eric Kuck 6d3faaebe3 Now ensures view hierarchy-affecting calls are made on the main thread. Fixes #255 2017-03-31 09:24:18 -05:00
Eric Kuck 17639129b9 Fixed issue where child controllers added while the parent is in the process of transitioning off the screen would not be properly restored - Fixes #256. 2017-03-31 09:01:13 -05:00
Valery 1c809095ec Fix image display (#251)
Github doesn't understand spaces in file name
2017-03-21 10:16:00 -05:00
Valery 3bc563de38 Fixed readme table (#250) 2017-03-21 09:58:21 -05:00
Eric Kuck 7beb94f8cc Fixed readme table 2017-03-20 16:56:19 -05:00
Eric Kuck 2b32a30c1a Version bump 2017-03-03 11:53:29 -06:00
Eric Kuck 893ffc0461 Version bump 2017-03-03 11:50:59 -06:00
Eric Kuck 0df11e3224 Build tools updates 2017-03-03 09:22:37 -06:00
Eric Kuck 75ad389424 Fixed a few warnings 2017-03-01 16:51:45 -06:00
Eric Kuck 00577823dc Moved popToTransaction calls over to use setBackstack internally. Fixes #239 2017-03-01 16:33:19 -06:00
Eric Kuck 671a117b96 Added ability to specify the maximum number of pages for which states should be saved. Fixes #236 2017-02-23 16:52:50 -06:00
Eric Kuck 3d1c2d392c Added startIntentSenderForResult method to controller. Fixes #235 2017-02-23 12:01:47 -06:00
Eric Kuck b7611e1a1b Added a RecyclerView -> RecyclerView shared element transition demo 2017-02-20 16:57:56 -06:00
Eric Kuck d13af316d3 Dialog background no longer fades out over the top of the outgoing dialog 2017-02-17 12:52:51 -06:00
Eric Kuck 7d5cc26ea4 Replaced png icon with vector drawable 2017-02-17 10:47:58 -06:00
Eric Kuck c4d881ac47 Added some better shared element demos 2017-02-17 10:40:38 -06:00
Eduardo Alejandro Pool Aké 0a53b9f07a Dependency updates 2017-02-16 11:20:25 -06:00
Eric Kuck c2ad655af2 Added fade to dialog’s immersive background
Moved FabToDialogTransitionChangeHandler over to the library’s TransitionChangeHandler
2017-02-16 11:13:04 -06:00
Simon Vergauwen a888073e1b DialogToFabTransition (#229) 2017-02-16 10:24:49 -06:00
Eric Kuck df68655b13 Added missing comments to new methods 2017-02-14 13:52:20 -06:00
Eric Kuck 86227ae3b3 TransitionChangeHandler is now much more flexible (doesn’t force you to add/remove views) 2017-02-14 13:50:13 -06:00
Paul Woitaschek 97878b1ad6 List simplification (#225)
* Use a singleton list for transactions
* Use built-in java list functions
2017-02-08 10:37:07 -06:00
Eric Kuck 314ee2b456 Routers now properly remove views of all owned controllers on destroy (fixes #221) 2017-02-07 10:22:33 -06:00
Paul Woitaschek f4ef47c2d2 Added missing nullity annotations for the pager adapters (#219) 2017-02-02 08:21:38 -06:00
158 changed files with 4203 additions and 520 deletions
Executable → Regular
View File
+1 -1
View File
@@ -3,7 +3,7 @@ language: android
android:
components:
- tools
- build-tools-24.0.3
- build-tools-25.0.3
- android-25
- extra-android-m2repository
licenses:
+15 -16
View File
@@ -4,8 +4,8 @@
A small, yet full-featured framework that allows building View-based Android applications. Conductor provides a light-weight wrapper around standard Android Views that does just about everything you'd want:
| Conductor
------|------------------------------
| | Conductor |
|-----------|-------------|
:tada: | Easy integration
:point_up: | Single Activity apps without using Fragments
:recycle: | Simple but powerful lifecycle management
@@ -20,29 +20,28 @@ Conductor is architecture-agnostic and does not try to force any design decision
## Installation
```gradle
compile 'com.bluelinelabs:conductor:2.1.0'
compile 'com.bluelinelabs:conductor:2.1.4'
// If you want the components that go along with
// Android's support libraries (currently just a PagerAdapter):
compile 'com.bluelinelabs:conductor-support:2.1.0'
compile 'com.bluelinelabs:conductor-support:2.1.4'
// If you want RxJava lifecycle support:
compile 'com.bluelinelabs:conductor-rxlifecycle:2.1.0'
compile 'com.bluelinelabs:conductor-rxlifecycle:2.1.4'
// If you want RxJava2 lifecycle support:
compile 'com.bluelinelabs:conductor-rxlifecycle2:2.1.0'
compile 'com.bluelinelabs:conductor-rxlifecycle2:2.1.4'
// If you want RxJava2 Autodispose support:
compile 'com.bluelinelabs:conductor-autodispose:2.1.4'
// If you want Controllers that are Lifecycle-aware (architecture components):
compile 'com.bluelinelabs:conductor-arch-components-lifecycle:0.1.1'
```
SNAPSHOT:
```gradle
compile 'com.bluelinelabs:conductor:2.1.1-SNAPSHOT'
compile 'com.bluelinelabs:conductor-support:2.1.1-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle:2.1.1-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle2:2.1.1-SNAPSHOT'
```
You also have to add the url to the snapshot repository:
Just use `2.1.5-SNAPSHOT` as your version number in any of the above dependencies and add the url to the snapshot repository:
```gradle
allprojects {
@@ -55,7 +54,7 @@ allprojects {
## Components to Know
| Conductor Components
| | Conductor Components |
------|------------------------------
__Controller__ | The Controller is the View wrapper that will give you all of your lifecycle management features. Think of it as a lighter-weight and more predictable Fragment alternative with an easier to manage lifecycle.
__Router__ | A Router implements navigation and backstack handling for Controllers. Router objects are attached to Activity/containing ViewGroup pairs. Routers do not directly render or push Views to the container ViewGroup, but instead defer this responsibility to the ControllerChangeHandler specified in a given transaction.
@@ -118,7 +117,7 @@ public class HomeController extends Controller {
The lifecycle of a Controller is significantly simpler to understand than that of a Fragment. A lifecycle diagram is shown below:
![Controller Lifecycle](docs/Controller Lifecycle.jpg)
![Controller Lifecycle](docs/Controller%20Lifecycle.jpg)
## Advanced Topics
Executable → Regular
+2 -1
View File
@@ -3,7 +3,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
classpath 'com.android.tools.build:gradle:2.3.2'
}
}
@@ -11,6 +11,7 @@ allprojects {
repositories {
jcenter()
mavenCentral()
maven { url 'https://maven.google.com' }
}
}
@@ -66,4 +66,4 @@ public final class ControllerChangeHandlerIssueDetector extends Detector impleme
context.report(ISSUE, declaration, context.getLocation(declaration), message);
}
}
}
}
@@ -79,4 +79,4 @@ public final class ControllerIssueDetector extends Detector implements Detector.
context.report(ISSUE, declaration, context.getLocation(declaration), message);
}
}
}
}
@@ -11,4 +11,4 @@ public final class IssueRegistry extends com.android.tools.lint.client.api.Issue
ControllerIssueDetector.ISSUE,
ControllerChangeHandlerIssueDetector.ISSUE);
}
}
}
@@ -21,9 +21,9 @@ android {
}
dependencies {
compile rootProject.ext.supportAppCompat
compile rootProject.ext.archComopnentsLifecycle
compile project(':conductor')
}
ext.artifactId = 'conductor-support'
ext.artifactId = 'conductor-arch-components-lifecycle'
@@ -0,0 +1,4 @@
POM_NAME=Conductor Architecture Components Lifecycle Extensions
POM_ARTIFACT_ID=conductor-archlifecycle
POM_PACKAGING=aar
VERSION_NAME=0.1.2-SNAPSHOT
@@ -0,0 +1,3 @@
<manifest package="com.bluelinelabs.conductor.archlifecycle">
<application />
</manifest>
@@ -0,0 +1,79 @@
package com.bluelinelabs.conductor.archlifecycle;
import android.arch.lifecycle.Lifecycle.Event;
import android.arch.lifecycle.Lifecycle.State;
import android.arch.lifecycle.LifecycleRegistry;
import android.arch.lifecycle.LifecycleRegistryOwner;
import android.support.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
public class ControllerLifecycleRegistryOwner extends LifecycleListener implements LifecycleRegistryOwner {
private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);
public ControllerLifecycleRegistryOwner(Controller controller) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_CREATE);
lifecycleRegistry.markState(State.CREATED);
controller.addLifecycleListener(new LifecycleListener() {
@Override
public void preCreateView(@NonNull Controller controller) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_START);
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.markState(State.STARTED);
}
@Override
public void preAttach(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_RESUME);
}
@Override
public void postAttach(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.markState(State.RESUMED);
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_PAUSE);
}
@Override
public void postDetach(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.markState(State.STARTED);
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_STOP);
}
@Override
public void postDestroyView(@NonNull Controller controller) {
lifecycleRegistry.markState(State.CREATED);
}
@Override
public void preDestroy(@NonNull Controller controller) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_DESTROY);
}
@Override
public void postDestroy(@NonNull Controller controller) {
lifecycleRegistry.markState(State.DESTROYED);
}
});
}
@Override
public LifecycleRegistry getLifecycle() {
return lifecycleRegistry;
}
}
@@ -0,0 +1,17 @@
package com.bluelinelabs.conductor.archlifecycle;
import android.arch.lifecycle.LifecycleRegistry;
import android.arch.lifecycle.LifecycleRegistryOwner;
import com.bluelinelabs.conductor.Controller;
public abstract class LifecycleController extends Controller implements LifecycleRegistryOwner {
private final ControllerLifecycleRegistryOwner lifecycleRegistryOwner = new ControllerLifecycleRegistryOwner(this);
@Override
public LifecycleRegistry getLifecycle() {
return lifecycleRegistryOwner.getLifecycle();
}
}
@@ -0,0 +1,17 @@
package com.bluelinelabs.conductor.archlifecycle;
import android.arch.lifecycle.LifecycleRegistry;
import android.arch.lifecycle.LifecycleRegistryOwner;
import com.bluelinelabs.conductor.RestoreViewOnCreateController;
public abstract class LifecycleRestoreViewOnCreateController extends RestoreViewOnCreateController implements LifecycleRegistryOwner {
private final ControllerLifecycleRegistryOwner lifecycleRegistryOwner = new ControllerLifecycleRegistryOwner(this);
@Override
public LifecycleRegistry getLifecycle() {
return lifecycleRegistryOwner.getLifecycle();
}
}
@@ -0,0 +1,30 @@
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
compile rootProject.ext.rxJava2
compile rootProject.ext.autodispose
compile project(':conductor')
}
ext.artifactId = 'conductor-autodispose'
@@ -0,0 +1,3 @@
POM_NAME=Conductor AutoDispose Extensions
POM_ARTIFACT_ID=conductor-autodispose
POM_PACKAGING=aar
@@ -0,0 +1,3 @@
<manifest package="com.bluelinelabs.conductor.autodispose">
<application />
</manifest>
@@ -0,0 +1,14 @@
package com.bluelinelabs.conductor.autodispose;
public enum ControllerEvent {
CREATE,
CONTEXT_AVAILABLE,
CREATE_VIEW,
ATTACH,
DETACH,
DESTROY_VIEW,
CONTEXT_UNAVAILABLE,
DESTROY
}
@@ -0,0 +1,71 @@
package com.bluelinelabs.conductor.autodispose;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.uber.autodispose.OutsideLifecycleException;
import io.reactivex.subjects.BehaviorSubject;
public class ControllerLifecycleSubjectHelper {
private ControllerLifecycleSubjectHelper() { }
@NonNull
public static BehaviorSubject<ControllerEvent> create(Controller controller) {
ControllerEvent initialState;
if (controller.isBeingDestroyed() || controller.isDestroyed()) {
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
} else if (controller.isAttached()) {
initialState = ControllerEvent.ATTACH;
} else if (controller.getView() != null) {
initialState = ControllerEvent.CREATE_VIEW;
} else if (controller.getActivity() != null) {
initialState = ControllerEvent.CONTEXT_AVAILABLE;
} else {
initialState = ControllerEvent.CREATE;
}
final BehaviorSubject<ControllerEvent> subject = BehaviorSubject.createDefault(initialState);
controller.addLifecycleListener(new Controller.LifecycleListener() {
@Override
public void preContextAvailable(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CONTEXT_AVAILABLE);
}
@Override
public void preCreateView(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CREATE_VIEW);
}
@Override
public void preAttach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.ATTACH);
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DETACH);
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DESTROY_VIEW);
}
@Override
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
subject.onNext(ControllerEvent.CONTEXT_UNAVAILABLE);
}
@Override
public void preDestroy(@NonNull Controller controller) {
subject.onNext(ControllerEvent.DESTROY);
}
});
return subject;
}
}
@@ -0,0 +1,59 @@
package com.bluelinelabs.conductor.autodispose;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.Controller;
import com.uber.autodispose.LifecycleScopeProvider;
import com.uber.autodispose.OutsideLifecycleException;
import io.reactivex.Observable;
import io.reactivex.functions.Function;
import io.reactivex.subjects.BehaviorSubject;
public class ControllerScopeProvider implements LifecycleScopeProvider<ControllerEvent> {
private static final Function<ControllerEvent, ControllerEvent> CORRESPONDING_EVENTS =
new Function<ControllerEvent, ControllerEvent>() {
@Override
public ControllerEvent apply(ControllerEvent lastEvent) throws Exception {
switch (lastEvent) {
case CREATE:
return ControllerEvent.DESTROY;
case CONTEXT_AVAILABLE:
return ControllerEvent.CONTEXT_UNAVAILABLE;
case CREATE_VIEW:
return ControllerEvent.DESTROY_VIEW;
case ATTACH:
return ControllerEvent.DETACH;
case DETACH:
return ControllerEvent.DESTROY;
default:
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
}
}
};
@NonNull private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public static ControllerScopeProvider from(@NonNull Controller controller) {
return new ControllerScopeProvider(controller);
}
private ControllerScopeProvider(@NonNull Controller controller) {
lifecycleSubject = ControllerLifecycleSubjectHelper.create(controller);
}
@Override
public Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
public Function<ControllerEvent, ControllerEvent> correspondingEvents() {
return CORRESPONDING_EVENTS;
}
@Override
public ControllerEvent peekLifecycle() {
return lifecycleSubject.getValue();
}
}
@@ -29,4 +29,3 @@ dependencies {
}
ext.artifactId = 'conductor-rxlifecycle'
@@ -1,3 +1,3 @@
<manifest package="com.bluelinelabs.conductor.rxlifecycle">
<application />
</manifest>
</manifest>
@@ -3,10 +3,12 @@ package com.bluelinelabs.conductor.rxlifecycle;
public enum ControllerEvent {
CREATE,
CONTEXT_AVAILABLE,
CREATE_VIEW,
ATTACH,
DETACH,
DESTROY_VIEW,
CONTEXT_UNAVAILABLE,
DESTROY
}
@@ -1,10 +1,12 @@
package com.bluelinelabs.conductor.rxlifecycle;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.trello.rxlifecycle.OutsideLifecycleException;
import rx.subjects.BehaviorSubject;
@@ -17,9 +19,27 @@ public class ControllerLifecycleSubjectHelper {
private ControllerLifecycleSubjectHelper() { }
public static BehaviorSubject<ControllerEvent> create(Controller controller) {
final BehaviorSubject<ControllerEvent> subject = BehaviorSubject.create(ControllerEvent.CREATE);
ControllerEvent initialState;
if (controller.isBeingDestroyed() || controller.isDestroyed()) {
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
} else if (controller.isAttached()) {
initialState = ControllerEvent.ATTACH;
} else if (controller.getView() != null) {
initialState = ControllerEvent.CREATE_VIEW;
} else if (controller.getActivity() != null) {
initialState = ControllerEvent.CONTEXT_AVAILABLE;
} else {
initialState = ControllerEvent.CREATE;
}
final BehaviorSubject<ControllerEvent> subject = BehaviorSubject.create(initialState);
controller.addLifecycleListener(new LifecycleListener() {
@Override
public void preContextAvailable(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CONTEXT_AVAILABLE);
}
@Override
public void preCreateView(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CREATE_VIEW);
@@ -40,6 +60,11 @@ public class ControllerLifecycleSubjectHelper {
subject.onNext(ControllerEvent.DESTROY_VIEW);
}
@Override
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
subject.onNext(ControllerEvent.CONTEXT_UNAVAILABLE);
}
@Override
public void preDestroy(@NonNull Controller controller) {
subject.onNext(ControllerEvent.DESTROY);
@@ -49,4 +49,4 @@ public abstract class RxController extends Controller implements LifecycleProvid
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
}
@@ -17,7 +17,7 @@ public class RxControllerLifecycle {
* {@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
* @return a reusable {@link rx.Observable.Transformer} that unsubscribes the source during the Controller lifecycle
*/
@NonNull
@CheckResult
@@ -32,6 +32,8 @@ public class RxControllerLifecycle {
switch (lastEvent) {
case CREATE:
return ControllerEvent.DESTROY;
case CONTEXT_AVAILABLE:
return ControllerEvent.CONTEXT_UNAVAILABLE;
case ATTACH:
return ControllerEvent.DETACH;
case CREATE_VIEW:
@@ -49,4 +49,4 @@ public abstract class RxRestoreViewOnCreateController extends RestoreViewOnCreat
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
}
@@ -1,4 +1,3 @@
<manifest package="com.bluelinelabs.conductor.rxlifecycle2">
<application />
</manifest>
@@ -3,10 +3,12 @@ package com.bluelinelabs.conductor.rxlifecycle2;
public enum ControllerEvent {
CREATE,
CONTEXT_AVAILABLE,
CREATE_VIEW,
ATTACH,
DETACH,
DESTROY_VIEW,
CONTEXT_UNAVAILABLE,
DESTROY
}
@@ -1,18 +1,40 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.trello.rxlifecycle2.OutsideLifecycleException;
import io.reactivex.subjects.BehaviorSubject;
public class ControllerLifecycleSubjectHelper {
private ControllerLifecycleSubjectHelper() {
}
public static BehaviorSubject<ControllerEvent> create(Controller controller){
final BehaviorSubject<ControllerEvent> subject = BehaviorSubject.createDefault(ControllerEvent.CREATE);
public static BehaviorSubject<ControllerEvent> create(Controller controller) {
ControllerEvent initialState;
if (controller.isBeingDestroyed() || controller.isDestroyed()) {
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
} else if (controller.isAttached()) {
initialState = ControllerEvent.ATTACH;
} else if (controller.getView() != null) {
initialState = ControllerEvent.CREATE_VIEW;
} else if (controller.getActivity() != null) {
initialState = ControllerEvent.CONTEXT_AVAILABLE;
} else {
initialState = ControllerEvent.CREATE;
}
final BehaviorSubject<ControllerEvent> subject = BehaviorSubject.createDefault(initialState);
controller.addLifecycleListener(new Controller.LifecycleListener() {
@Override
public void preContextAvailable(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CONTEXT_AVAILABLE);
}
@Override
public void preCreateView(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CREATE_VIEW);
@@ -33,6 +55,11 @@ public class ControllerLifecycleSubjectHelper {
subject.onNext(ControllerEvent.DESTROY_VIEW);
}
@Override
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
subject.onNext(ControllerEvent.CONTEXT_UNAVAILABLE);
}
@Override
public void preDestroy(@NonNull Controller controller) {
subject.onNext(ControllerEvent.DESTROY);
@@ -27,6 +27,8 @@ public class RxControllerLifecycle {
switch (lastEvent) {
case CREATE:
return ControllerEvent.DESTROY;
case CONTEXT_AVAILABLE:
return ControllerEvent.CONTEXT_UNAVAILABLE;
case ATTACH:
return ControllerEvent.DETACH;
case CREATE_VIEW:
+48
View File
@@ -0,0 +1,48 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'de.mobilej.unmock:UnMockPlugin:0.6.0'
}
}
apply plugin: 'com.android.library'
apply plugin: 'de.mobilej.unmock'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
testCompile rootProject.ext.junit
testCompile rootProject.ext.roboelectric
compile rootProject.ext.supportAppCompat
compile project(':conductor')
unmock 'org.robolectric:android-all:4.3_r2-robolectric-0'
}
unMock {
keep "android.os.Bundle"
keep "android.os.BaseBundle"
}
ext.artifactId = 'conductor-support'
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
@@ -1,3 +1,3 @@
<manifest package="com.bluelinelabs.conductor.support">
<application />
</manifest>
</manifest>
@@ -2,6 +2,7 @@ package com.bluelinelabs.conductor.support;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.util.SparseArray;
@@ -34,7 +35,7 @@ public abstract class ControllerPagerAdapter extends PagerAdapter {
/**
* Creates a new ControllerPagerAdapter using the passed host.
*/
public ControllerPagerAdapter(Controller host, boolean saveControllerState) {
public ControllerPagerAdapter(@NonNull Controller host, boolean saveControllerState) {
this.host = host;
savesState = saveControllerState;
}
@@ -42,6 +43,7 @@ public abstract class ControllerPagerAdapter extends PagerAdapter {
/**
* Return the Controller associated with a specified position.
*/
@NonNull
public abstract Controller getItem(int position);
@Override
@@ -2,6 +2,7 @@ package com.bluelinelabs.conductor.support;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.util.SparseArray;
@@ -12,6 +13,7 @@ import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import java.util.ArrayList;
import java.util.List;
/**
@@ -20,15 +22,19 @@ import java.util.List;
public abstract class RouterPagerAdapter extends PagerAdapter {
private static final String KEY_SAVED_PAGES = "RouterPagerAdapter.savedStates";
private static final String KEY_MAX_PAGES_TO_STATE_SAVE = "RouterPagerAdapter.maxPagesToStateSave";
private static final String KEY_SAVE_PAGE_HISTORY = "RouterPagerAdapter.savedPageHistory";
private final Controller host;
private int maxPagesToStateSave = Integer.MAX_VALUE;
private SparseArray<Bundle> savedPages = new SparseArray<>();
private SparseArray<Router> visibleRouters = new SparseArray<>();
private ArrayList<Integer> savedPageHistory = new ArrayList<>();
/**
* Creates a new RouterPagerAdapter using the passed host.
*/
public RouterPagerAdapter(Controller host) {
public RouterPagerAdapter(@NonNull Controller host) {
this.host = host;
}
@@ -38,7 +44,21 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
* @param router The router used for the page
* @param position The page position to be instantiated.
*/
public abstract void configureRouter(Router router, int position);
public abstract void configureRouter(@NonNull Router router, int position);
/**
* Sets the maximum number of pages that will have their states saved. When this number is exceeded,
* the page that was state saved least recently will have its state removed from the save data.
*/
public void setMaxPagesToStateSave(int maxPagesToStateSave) {
if (maxPagesToStateSave < 0) {
throw new IllegalArgumentException("Only positive integers may be passed for maxPagesToStateSave.");
}
this.maxPagesToStateSave = maxPagesToStateSave;
ensurePagesSaved();
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
@@ -50,6 +70,7 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
if (routerSavedState != null) {
router.restoreInstanceState(routerSavedState);
savedPages.remove(position);
}
}
@@ -68,6 +89,11 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
router.saveInstanceState(savedState);
savedPages.put(position, savedState);
savedPageHistory.remove((Integer)position);
savedPageHistory.add(position);
ensurePagesSaved();
host.removeChildRouter(router);
visibleRouters.remove(position);
@@ -89,6 +115,8 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
public Parcelable saveState() {
Bundle bundle = new Bundle();
bundle.putSparseParcelableArray(KEY_SAVED_PAGES, savedPages);
bundle.putInt(KEY_MAX_PAGES_TO_STATE_SAVE, maxPagesToStateSave);
bundle.putIntegerArrayList(KEY_SAVE_PAGE_HISTORY, savedPageHistory);
return bundle;
}
@@ -97,6 +125,8 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
Bundle bundle = (Bundle)state;
if (state != null) {
savedPages = bundle.getSparseParcelableArray(KEY_SAVED_PAGES);
maxPagesToStateSave = bundle.getInt(KEY_MAX_PAGES_TO_STATE_SAVE);
savedPageHistory = bundle.getIntegerArrayList(KEY_SAVE_PAGE_HISTORY);
}
}
@@ -113,8 +143,19 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
return position;
}
SparseArray<Bundle> getSavedPages() {
return savedPages;
}
private void ensurePagesSaved() {
while (savedPages.size() > maxPagesToStateSave) {
int positionToRemove = savedPageHistory.remove(0);
savedPages.remove(positionToRemove);
}
}
private static String makeRouterName(int viewId, long id) {
return viewId + ":" + id;
}
}
}
@@ -0,0 +1,113 @@
package com.bluelinelabs.conductor.support;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.util.SparseArray;
import android.widget.FrameLayout;
import com.bluelinelabs.conductor.Conductor;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.support.util.FakePager;
import com.bluelinelabs.conductor.support.util.TestController;
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;
import static org.junit.Assert.assertEquals;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class StateSaveTests {
private FakePager pager;
private RouterPagerAdapter pagerAdapter;
public void createActivityController(Bundle savedInstanceState) {
ActivityController<Activity> activityController = Robolectric.buildActivity(Activity.class).create().start().resume();
Router router = Conductor.attachRouter(activityController.get(), new FrameLayout(activityController.get()), savedInstanceState);
TestController controller = new TestController();
router.setRoot(RouterTransaction.with(controller));
pager = new FakePager(new FrameLayout(activityController.get()));
pager.setOffscreenPageLimit(1);
pagerAdapter = new RouterPagerAdapter(controller) {
@Override
public void configureRouter(@NonNull Router router, int position) {
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Override
public int getCount() {
return 20;
}
};
pager.setAdapter(pagerAdapter);
}
@Before
public void setup() {
createActivityController(null);
}
@Test
public void testNoMaxSaves() {
// Load all pages
for (int i = 0; i < pagerAdapter.getCount(); i++) {
pager.pageTo(i);
}
pager.pageTo(pagerAdapter.getCount() / 2);
// Ensure all non-visible pages are saved
assertEquals(pagerAdapter.getCount() - 1 - pager.getOffscreenPageLimit() * 2, pagerAdapter.getSavedPages().size());
}
@Test
public void testMaxSavedSet() {
final int maxPages = 3;
pagerAdapter.setMaxPagesToStateSave(maxPages);
// Load all pages
for (int i = 0; i < pagerAdapter.getCount(); i++) {
pager.pageTo(i);
}
final int firstSelectedItem = pagerAdapter.getCount() / 2;
pager.pageTo(firstSelectedItem);
SparseArray<Bundle> savedPages = pagerAdapter.getSavedPages();
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size());
// Ensure correct pages are saved
assertEquals(pagerAdapter.getCount() - 3, savedPages.keyAt(0));
assertEquals(pagerAdapter.getCount() - 2, savedPages.keyAt(1));
assertEquals(pagerAdapter.getCount() - 1, savedPages.keyAt(2));
final int secondSelectedItem = 1;
pager.pageTo(secondSelectedItem);
savedPages = pagerAdapter.getSavedPages();
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size());
// Ensure correct pages are saved
assertEquals(firstSelectedItem - 1, savedPages.keyAt(0));
assertEquals(firstSelectedItem, savedPages.keyAt(1));
assertEquals(firstSelectedItem + 1, savedPages.keyAt(2));
}
}
@@ -0,0 +1,61 @@
package com.bluelinelabs.conductor.support.util;
import android.util.SparseArray;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.support.RouterPagerAdapter;
import java.util.ArrayList;
import java.util.List;
public class FakePager {
private ViewGroup container;
private int offscreenPageLimit;
private final SparseArray<Object> pages = new SparseArray<>();
private RouterPagerAdapter adapter;
public FakePager(ViewGroup container) {
this.container = container;
}
public void setAdapter(RouterPagerAdapter adapter) {
this.adapter = adapter;
}
public void pageTo(int page) {
int firstPage = Math.max(0, page - offscreenPageLimit);
int lastPage = Math.min(adapter.getCount() - 1, page + offscreenPageLimit);
List<Integer> pagesI = new ArrayList<>();
for (int i = 0; i < pages.size(); i++) {
pagesI.add(pages.keyAt(i));
}
for (int i = pages.size() - 1; i >= 0; i--) {
int key = pages.keyAt(i);
if (key < firstPage || key > lastPage) {
adapter.destroyItem(container, key, pages.get(key));
pages.remove(key);
}
}
for (int key = firstPage; key <= lastPage; key++) {
if (pages.get(key) == null) {
pages.put(key, adapter.instantiateItem(container, key));
}
}
adapter.setPrimaryItem(container, page, pages.get(page));
}
public int getOffscreenPageLimit() {
return offscreenPageLimit;
}
public void setOffscreenPageLimit(int offscreenPageLimit) {
this.offscreenPageLimit = offscreenPageLimit;
}
}
@@ -0,0 +1,18 @@
package com.bluelinelabs.conductor.support.util;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.bluelinelabs.conductor.Controller;
public class TestController extends Controller {
@NonNull @Override
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return new FrameLayout(inflater.getContext());
}
}
Executable → Regular
+3 -3
View File
@@ -3,7 +3,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'de.mobilej.unmock:UnMockPlugin:0.3.6'
classpath 'de.mobilej.unmock:UnMockPlugin:0.6.0'
}
}
@@ -39,11 +39,11 @@ dependencies {
compile rootProject.ext.supportAnnotations
lintChecks project(path: ':conductor-lint', configuration: 'lintChecks')
unmock 'org.robolectric:android-all:4.3_r2-robolectric-0'
}
unMock {
downloadFrom 'https://oss.sonatype.org/content/groups/public/org/robolectric/android-all/4.3_r2-robolectric-0/android-all-4.3_r2-robolectric-0.jar'
keep "android.os.Bundle"
keep "android.os.BaseBundle"
keep "android.text.TextUtils"
+1 -1
View File
@@ -2,4 +2,4 @@
-keepclassmembers public class * extends com.bluelinelabs.conductor.Controller {
public <init>();
public <init>(android.os.Bundle);
}
}
+1 -1
View File
@@ -1,3 +1,3 @@
<manifest package="com.bluelinelabs.conductor">
<application />
</manifest>
</manifest>
@@ -2,6 +2,8 @@ package com.bluelinelabs.conductor;
import android.app.Activity;
import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -30,6 +32,8 @@ public class ActivityHostedRouter extends Router {
this.lifecycleHandler = lifecycleHandler;
this.container = container;
watchContainerAttach();
}
}
@@ -85,6 +89,12 @@ public class ActivityHostedRouter extends Router {
lifecycleHandler.startActivityForResult(instanceId, intent, requestCode, options);
}
@Override
void startIntentSenderForResult(@NonNull String instanceId, @NonNull IntentSender intent, int requestCode, @Nullable Intent fillInIntent,
int flagsMask, int flagsValues, int extraFlags, @Nullable Bundle options) throws SendIntentException {
lifecycleHandler.startIntentSenderForResult(instanceId, intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options);
}
@Override
void registerForActivityResult(@NonNull String instanceId, int requestCode) {
lifecycleHandler.registerForActivityResult(instanceId, requestCode);
@@ -119,4 +129,10 @@ public class ActivityHostedRouter extends Router {
TransactionIndexer getTransactionIndexer() {
return transactionIndexer;
}
@Override
public void onContextAvailable() {
super.onContextAvailable();
}
}
+17 -13
View File
@@ -18,16 +18,16 @@ class Backstack implements Iterable<RouterTransaction> {
private final Deque<RouterTransaction> backstack = new ArrayDeque<>();
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isEmpty() {
boolean isEmpty() {
return backstack.isEmpty();
}
public int size() {
int size() {
return backstack.size();
}
@Nullable
public RouterTransaction root() {
RouterTransaction root() {
return backstack.size() > 0 ? backstack.getLast() : null;
}
@@ -37,12 +37,12 @@ class Backstack implements Iterable<RouterTransaction> {
}
@NonNull
public Iterator<RouterTransaction> reverseIterator() {
Iterator<RouterTransaction> reverseIterator() {
return backstack.descendingIterator();
}
@NonNull
public List<RouterTransaction> popTo(@NonNull RouterTransaction transaction) {
List<RouterTransaction> popTo(@NonNull RouterTransaction transaction) {
List<RouterTransaction> popped = new ArrayList<>();
if (backstack.contains(transaction)) {
while (backstack.peek() != transaction) {
@@ -56,27 +56,27 @@ class Backstack implements Iterable<RouterTransaction> {
}
@NonNull
public RouterTransaction pop() {
RouterTransaction pop() {
RouterTransaction popped = backstack.pop();
popped.controller.destroy();
return popped;
}
@Nullable
public RouterTransaction peek() {
RouterTransaction peek() {
return backstack.peek();
}
public void remove(@NonNull RouterTransaction transaction) {
void remove(@NonNull RouterTransaction transaction) {
backstack.removeFirstOccurrence(transaction);
}
public void push(@NonNull RouterTransaction transaction) {
void push(@NonNull RouterTransaction transaction) {
backstack.push(transaction);
}
@NonNull
public List<RouterTransaction> popAll() {
List<RouterTransaction> popAll() {
List<RouterTransaction> list = new ArrayList<>();
while (!isEmpty()) {
list.add(pop());
@@ -84,7 +84,7 @@ class Backstack implements Iterable<RouterTransaction> {
return list;
}
public void setBackstack(@NonNull List<RouterTransaction> backstack) {
void setBackstack(@NonNull List<RouterTransaction> backstack) {
for (RouterTransaction existingTransaction : this.backstack) {
boolean contains = false;
for (RouterTransaction newTransaction : backstack) {
@@ -105,7 +105,11 @@ class Backstack implements Iterable<RouterTransaction> {
}
}
public void saveInstanceState(@NonNull Bundle outState) {
boolean contains(@NonNull RouterTransaction transaction) {
return backstack.contains(transaction);
}
void saveInstanceState(@NonNull Bundle outState) {
ArrayList<Bundle> entryBundles = new ArrayList<>(backstack.size());
for (RouterTransaction entry : backstack) {
entryBundles.add(entry.saveInstanceState());
@@ -114,7 +118,7 @@ class Backstack implements Iterable<RouterTransaction> {
outState.putParcelableArrayList(KEY_ENTRIES, entryBundles);
}
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
void restoreInstanceState(@NonNull Bundle savedInstanceState) {
ArrayList<Bundle> entryBundles = savedInstanceState.getParcelableArrayList(KEY_ENTRIES);
if (entryBundles != null) {
Collections.reverse(entryBundles);
@@ -8,14 +8,15 @@ import android.support.annotation.UiThread;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.internal.LifecycleHandler;
import com.bluelinelabs.conductor.internal.ThreadUtils;
/**
* Point of initial interaction with Conductor. Used to attach a {@link Router} to your Activity.
*/
public final class Conductor {
private Conductor() {}
/**
* Conductor will create a {@link Router} that has been initialized for your Activity and containing ViewGroup.
* If an existing {@link Router} is already associated with this Activity/ViewGroup pair, either in memory
@@ -30,6 +31,8 @@ public final class Conductor {
*/
@NonNull @UiThread
public static Router attachRouter(@NonNull Activity activity, @NonNull ViewGroup container, @Nullable Bundle savedInstanceState) {
ThreadUtils.ensureMainThread();
LifecycleHandler lifecycleHandler = LifecycleHandler.install(activity);
Router router = lifecycleHandler.getRouter(container, savedInstanceState);
+120 -15
View File
@@ -4,6 +4,7 @@ import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
@@ -54,7 +55,7 @@ public abstract class Controller {
private static final String KEY_OVERRIDDEN_PUSH_HANDLER = "Controller.overriddenPushHandler";
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";
static final String KEY_VIEW_STATE_BUNDLE = "Controller.viewState.bundle";
private static final String KEY_RETAIN_VIEW_MODE = "Controller.retainViewMode";
private final Bundle args;
@@ -86,21 +87,34 @@ public abstract class Controller {
private final ArrayList<String> requestedPermissions = new ArrayList<>();
private final ArrayList<RouterRequiringFunc> onRouterSetListeners = new ArrayList<>();
private WeakReference<View> destroyedView;
private boolean isPerformingExitTransition;
private boolean isContextAvailable;
@NonNull
static Controller newInstance(@NonNull Bundle bundle) {
final String className = bundle.getString(KEY_CLASS_NAME);
//noinspection ConstantConditions
Constructor[] constructors = ClassUtils.classForName(className, false).getConstructors();
Class cls = ClassUtils.classForName(className, false);
Constructor[] constructors = cls.getConstructors();
Constructor bundleConstructor = getBundleConstructor(constructors);
Bundle args = bundle.getBundle(KEY_ARGS);
if (args != null) {
args.setClassLoader(cls.getClassLoader());
}
Controller controller;
try {
if (bundleConstructor != null) {
controller = (Controller)bundleConstructor.newInstance(bundle.getBundle(KEY_ARGS));
controller = (Controller)bundleConstructor.newInstance(args);
} else {
//noinspection ConstantConditions
controller = (Controller)getDefaultConstructor(constructors).newInstance();
// Restore the args that existed before the last process death
if (args != null) {
controller.args.putAll(args);
}
}
} catch (Exception e) {
throw new RuntimeException("An exception occurred while creating a new instance of " + className + ". " + e.getMessage(), e);
@@ -123,7 +137,7 @@ public abstract class Controller {
* @param args Any arguments that need to be retained.
*/
protected Controller(@Nullable Bundle args) {
this.args = args != null ? args : new Bundle();
this.args = args != null ? args : new Bundle(getClass().getClassLoader());
instanceId = UUID.randomUUID().toString();
ensureRequiredConstructor();
}
@@ -210,6 +224,10 @@ public abstract class Controller {
childRouter = new ControllerHostedRouter(container.getId(), tag);
childRouter.setHost(this, container);
childRouters.add(childRouter);
if (isPerformingExitTransition) {
childRouter.setDetachFrozen(true);
}
}
} else if (!childRouter.hasHost()) {
childRouter.setHost(this, container);
@@ -394,6 +412,19 @@ public abstract class Controller {
*/
protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { }
/**
* Called when this Controller has a Context available to it. This will happen very early on in the lifecycle
* (before a view is created). If the host activity is re-created (ex: for orientation change), this will be
* called again when the new context is available.
*/
protected void onContextAvailable(@NonNull Context context) { }
/**
* Called when this Controller's Context is no longer available. This can happen when the Controller is
* destroyed or when the host Activity is destroyed.
*/
protected void onContextUnavailable() { }
/**
* Called when this Controller is attached to its host ViewGroup
*
@@ -494,6 +525,14 @@ public abstract class Controller {
});
}
/**
* Calls startIntentSenderForResult(IntentSender, int, Intent, int, int, int, Bundle) from this Controller's host Activity.
*/
public final void startIntentSenderForResult(@NonNull final IntentSender intent, final int requestCode, @Nullable final Intent fillInIntent, final int flagsMask,
final int flagsValues, final int extraFlags, @Nullable final Bundle options) throws IntentSender.SendIntentException {
router.startIntentSenderForResult(instanceId, intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options);
}
/**
* Registers this Controller to handle onActivityResult responses. Calling this method is NOT
* necessary when calling {@link #startActivityForResult(Intent, int)}
@@ -537,7 +576,7 @@ public abstract class Controller {
* @param permission A permission this Controller has requested
*/
public boolean shouldShowRequestPermissionRationale(@NonNull String permission) {
return false;
return Build.VERSION.SDK_INT >= 23 && getActivity().shouldShowRequestPermissionRationale(permission);
}
/**
@@ -584,7 +623,7 @@ public abstract class Controller {
*
* @param lifecycleListener The listener
*/
public void addLifecycleListener(@NonNull LifecycleListener lifecycleListener) {
public final void addLifecycleListener(@NonNull LifecycleListener lifecycleListener) {
if (!lifecycleListeners.contains(lifecycleListener)) {
lifecycleListeners.add(lifecycleListener);
}
@@ -595,7 +634,7 @@ public abstract class Controller {
*
* @param lifecycleListener The listener to be removed
*/
public void removeLifecycleListener(@NonNull LifecycleListener lifecycleListener) {
public final void removeLifecycleListener(@NonNull LifecycleListener lifecycleListener) {
lifecycleListeners.remove(lifecycleListener);
}
@@ -712,8 +751,8 @@ public abstract class Controller {
return false;
}
final void setNeedsAttach() {
needsAttach = true;
final void setNeedsAttach(boolean needsAttach) {
this.needsAttach = needsAttach;
}
final void prepareForHostDetach() {
@@ -752,6 +791,29 @@ public abstract class Controller {
}
}
final void onContextAvailable() {
final Context context = router.getActivity();
if (context != null && !isContextAvailable) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preContextAvailable(this);
}
isContextAvailable = true;
onContextAvailable(context);
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postContextAvailable(this, context);
}
}
for (Router childRouter : childRouters) {
childRouter.onContextAvailable();
}
}
final void executeWithRouter(@NonNull RouterRequiringFunc listener) {
if (router != null) {
listener.execute();
@@ -790,12 +852,27 @@ public abstract class Controller {
onActivityStopped(activity);
}
final void activityDestroyed(boolean isChangingConfigurations) {
if (isChangingConfigurations) {
final void activityDestroyed(@NonNull Activity activity) {
if (activity.isChangingConfigurations()) {
detach(view, true, false);
} else {
destroy(true);
}
if (isContextAvailable) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preContextUnavailable(this, activity);
}
isContextAvailable = false;
onContextUnavailable();
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postContextUnavailable(this);
}
}
}
private void attach(@NonNull View view) {
@@ -822,7 +899,7 @@ public abstract class Controller {
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postAttach(this, view);
lifecycleListener.postAttach(Controller.this, view);
}
}
@@ -967,6 +1044,21 @@ public abstract class Controller {
}
private void performDestroy() {
if (isContextAvailable) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preContextUnavailable(this, getActivity());
}
isContextAvailable = false;
onContextUnavailable();
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postContextUnavailable(this);
}
}
if (!destroyed) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
@@ -1017,7 +1109,7 @@ public abstract class Controller {
view.saveHierarchyState(hierarchyState);
viewState.putSparseParcelableArray(KEY_VIEW_STATE_HIERARCHY, hierarchyState);
Bundle stateBundle = new Bundle();
Bundle stateBundle = new Bundle(getClass().getClassLoader());
onSaveViewState(view, stateBundle);
viewState.putBundle(KEY_VIEW_STATE_BUNDLE, stateBundle);
@@ -1030,7 +1122,9 @@ public abstract class Controller {
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));
Bundle savedViewState = viewState.getBundle(KEY_VIEW_STATE_BUNDLE);
savedViewState.setClassLoader(getClass().getClassLoader());
onRestoreViewState(view, savedViewState);
restoreChildControllerHosts();
@@ -1071,7 +1165,7 @@ public abstract class Controller {
}
outState.putParcelableArrayList(KEY_CHILD_ROUTERS, childBundles);
Bundle savedState = new Bundle();
Bundle savedState = new Bundle(getClass().getClassLoader());
onSaveInstanceState(savedState);
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
@@ -1106,6 +1200,9 @@ public abstract class Controller {
}
this.savedInstanceState = savedInstanceState.getBundle(KEY_SAVED_STATE);
if (this.savedInstanceState != null) {
this.savedInstanceState.setClassLoader(getClass().getClassLoader());
}
performOnRestoreInstanceState();
}
@@ -1124,6 +1221,7 @@ public abstract class Controller {
final void changeStarted(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
if (!changeType.isEnter) {
isPerformingExitTransition = true;
for (ControllerHostedRouter router : childRouters) {
router.setDetachFrozen(true);
}
@@ -1139,6 +1237,7 @@ public abstract class Controller {
final void changeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
if (!changeType.isEnter) {
isPerformingExitTransition = false;
for (ControllerHostedRouter router : childRouters) {
router.setDetachFrozen(false);
}
@@ -1250,6 +1349,12 @@ public abstract class Controller {
public void preDestroy(@NonNull Controller controller) { }
public void postDestroy(@NonNull Controller controller) { }
public void preContextAvailable(@NonNull Controller controller) { }
public void postContextAvailable(@NonNull Controller controller, @NonNull Context context) { }
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) { }
public void postContextUnavailable(@NonNull Controller controller) { }
public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) { }
public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) { }
@@ -24,7 +24,7 @@ public abstract class ControllerChangeHandler {
private static final String KEY_CLASS_NAME = "ControllerChangeHandler.className";
private static final String KEY_SAVED_STATE = "ControllerChangeHandler.savedState";
private static final Map<String, ControllerChangeHandler> inProgressPushHandlers = new HashMap<>();
private static final Map<String, ChangeHandlerData> inProgressChangeHandlers = new HashMap<>();
private boolean forceRemoveViewOnPush;
private boolean hasBeenUsed;
@@ -127,29 +127,34 @@ public abstract class ControllerChangeHandler {
}
}
static boolean completePushImmediately(@NonNull String controllerInstanceId) {
ControllerChangeHandler changeHandler = inProgressPushHandlers.get(controllerInstanceId);
if (changeHandler != null) {
changeHandler.completeImmediately();
inProgressPushHandlers.remove(controllerInstanceId);
static boolean completeHandlerImmediately(@NonNull String controllerInstanceId) {
ChangeHandlerData changeHandlerData = inProgressChangeHandlers.get(controllerInstanceId);
if (changeHandlerData != null) {
changeHandlerData.changeHandler.completeImmediately();
inProgressChangeHandlers.remove(controllerInstanceId);
return true;
}
return false;
}
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());
static void abortOrComplete(@NonNull Controller toAbort, @Nullable Controller newController, @NonNull ControllerChangeHandler newChangeHandler) {
ChangeHandlerData changeHandlerData = inProgressChangeHandlers.get(toAbort.getInstanceId());
if (changeHandlerData != null) {
if (changeHandlerData.isPush) {
changeHandlerData.changeHandler.onAbortPush(newChangeHandler, newController);
} else {
changeHandlerData.changeHandler.completeImmediately();
}
inProgressChangeHandlers.remove(toAbort.getInstanceId());
}
}
static void executeChange(@Nullable final Controller to, @Nullable final Controller from, final boolean isPush, @Nullable final ViewGroup container, @Nullable final ControllerChangeHandler inHandler, @NonNull final List<ControllerChangeListener> listeners) {
if (isPush && to != null && to.isDestroyed()) {
throw new IllegalStateException("Trying to push a controller that has already been destroyed. (" + to.getClass().getSimpleName() + ")");
}
static void executeChange(@NonNull final ChangeTransaction transaction) {
executeChange(transaction.to, transaction.from, transaction.isPush, transaction.container, transaction.changeHandler, transaction.listeners);
}
private 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;
if (inHandler == null) {
@@ -161,14 +166,16 @@ public abstract class ControllerChangeHandler {
}
handler.hasBeenUsed = true;
if (isPush && to != null) {
inProgressPushHandlers.put(to.getInstanceId(), handler);
if (from != null) {
completePushImmediately(from.getInstanceId());
if (from != null) {
if (isPush) {
completeHandlerImmediately(from.getInstanceId());
} else {
abortOrComplete(from, to, handler);
}
} else if (!isPush && from != null) {
abortPush(from, to, handler);
}
if (to != null) {
inProgressChangeHandlers.put(to.getInstanceId(), new ChangeHandlerData(handler, isPush));
}
for (ControllerChangeListener listener : listeners) {
@@ -202,7 +209,7 @@ public abstract class ControllerChangeHandler {
}
if (to != null) {
inProgressPushHandlers.remove(to.getInstanceId());
inProgressChangeHandlers.remove(to.getInstanceId());
to.changeEnded(handler, toChangeType);
}
@@ -216,6 +223,10 @@ public abstract class ControllerChangeHandler {
((ViewGroup)fromParent).removeView(fromView);
}
}
if (handler.removesFromViewOnPush() && from != null) {
from.setNeedsAttach(false);
}
}
});
}
@@ -229,6 +240,24 @@ public abstract class ControllerChangeHandler {
forceRemoveViewOnPush = force;
}
static class ChangeTransaction {
@Nullable final Controller to;
@Nullable final Controller from;
final boolean isPush;
@Nullable final ViewGroup container;
@Nullable final ControllerChangeHandler changeHandler;
@NonNull final List<ControllerChangeListener> listeners;
public ChangeTransaction(@Nullable Controller to, @Nullable Controller from, boolean isPush, @Nullable ViewGroup container, @Nullable ControllerChangeHandler changeHandler, @NonNull List<ControllerChangeListener> listeners) {
this.to = to;
this.from = from;
this.isPush = isPush;
this.container = container;
this.changeHandler = changeHandler;
this.listeners = listeners;
}
}
/**
* A listener interface useful for allowing external classes to be notified of change events.
*/
@@ -267,4 +296,14 @@ public abstract class ControllerChangeHandler {
void onChangeCompleted();
}
private static class ChangeHandlerData {
public final ControllerChangeHandler changeHandler;
public final boolean isPush;
public ChangeHandlerData(ControllerChangeHandler changeHandler, boolean isPush) {
this.changeHandler = changeHandler;
this.isPush = isPush;
}
}
}
@@ -2,6 +2,8 @@ package com.bluelinelabs.conductor;
import android.app.Activity;
import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
@@ -23,6 +25,7 @@ class ControllerHostedRouter extends Router {
@IdRes private int hostId;
private String tag;
private boolean isDetachFrozen;
ControllerHostedRouter() { }
@@ -41,6 +44,12 @@ class ControllerHostedRouter extends Router {
hostController = controller;
this.container = container;
for (RouterTransaction transaction : backstack) {
transaction.controller.setParentController(controller);
}
watchContainerAttach();
}
}
@@ -67,6 +76,7 @@ class ControllerHostedRouter extends Router {
}
final void setDetachFrozen(boolean frozen) {
isDetachFrozen = frozen;
for (RouterTransaction transaction : backstack) {
transaction.controller.setDetachFrozen(frozen);
}
@@ -78,6 +88,24 @@ class ControllerHostedRouter extends Router {
super.destroy(popViews);
}
@Override
protected void pushToBackstack(@NonNull RouterTransaction entry) {
if (isDetachFrozen) {
entry.controller.setDetachFrozen(true);
}
super.pushToBackstack(entry);
}
@Override
public void setBackstack(@NonNull List<RouterTransaction> newBackstack, @Nullable ControllerChangeHandler changeHandler) {
if (isDetachFrozen) {
for (RouterTransaction transaction : newBackstack) {
transaction.controller.setDetachFrozen(true);
}
}
super.setBackstack(newBackstack, changeHandler);
}
@Override @Nullable
public Activity getActivity() {
return hostController != null ? hostController.getActivity() : null;
@@ -125,6 +153,13 @@ class ControllerHostedRouter extends Router {
}
}
@Override
void startIntentSenderForResult(@NonNull String instanceId, @NonNull IntentSender intent, int requestCode, @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, @Nullable Bundle options) throws SendIntentException {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().startIntentSenderForResult(instanceId, intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options);
}
}
@Override
void registerForActivityResult(@NonNull String instanceId, int requestCode) {
if (hostController != null && hostController.getRouter() != null) {
@@ -32,7 +32,7 @@ abstract public class RestoreViewOnCreateController extends Controller {
@Override @NonNull
protected final View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return onCreateView(inflater, container, viewState);
return onCreateView(inflater, container, viewState == null ? null : viewState.getBundle(KEY_VIEW_STATE_BUNDLE));
}
/**
+186 -34
View File
@@ -2,6 +2,7 @@ package com.bluelinelabs.conductor;
import android.app.Activity;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -13,9 +14,11 @@ import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.bluelinelabs.conductor.ControllerChangeHandler.ChangeTransaction;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import com.bluelinelabs.conductor.internal.NoOpControllerChangeHandler;
import com.bluelinelabs.conductor.internal.ThreadUtils;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
import java.util.ArrayList;
@@ -33,11 +36,13 @@ 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();
final Backstack backstack = new Backstack();
private final List<ControllerChangeListener> changeListeners = new ArrayList<>();
private final List<ChangeTransaction> pendingControllerChanges = new ArrayList<>();
final List<Controller> destroyingControllers = new ArrayList<>();
private boolean popsLastView = false;
boolean containerFullyAttached = false;
ViewGroup container;
@@ -82,6 +87,8 @@ public abstract class Router {
*/
@UiThread
public boolean handleBack() {
ThreadUtils.ensureMainThread();
if (!backstack.isEmpty()) {
//noinspection ConstantConditions
if (backstack.peek().controller.handleBack()) {
@@ -99,8 +106,11 @@ public abstract class Router {
*
* @return Whether or not this Router still has controllers remaining on it after popping.
*/
@SuppressWarnings("WeakerAccess")
@UiThread
public boolean popCurrentController() {
ThreadUtils.ensureMainThread();
RouterTransaction transaction = backstack.peek();
if (transaction == null) {
throw new IllegalStateException("Trying to pop the current controller when there are none on the backstack.");
@@ -116,26 +126,39 @@ public abstract class Router {
*/
@UiThread
public boolean popController(@NonNull Controller controller) {
RouterTransaction topController = backstack.peek();
boolean poppingTopController = topController != null && topController.controller == controller;
ThreadUtils.ensureMainThread();
RouterTransaction topTransaction = backstack.peek();
boolean poppingTopController = topTransaction != null && topTransaction.controller == controller;
if (poppingTopController) {
trackDestroyingController(backstack.pop());
performControllerChange(backstack.peek(), topTransaction, false);
} else {
RouterTransaction removedTransaction = null;
RouterTransaction nextTransaction = null;
for (RouterTransaction transaction : backstack) {
if (transaction.controller == controller) {
if (controller.isAttached()) {
trackDestroyingController(transaction);
}
backstack.remove(transaction);
removedTransaction = transaction;
} else if (removedTransaction != null) {
if (!transaction.controller.isAttached()) {
nextTransaction = transaction;
}
break;
}
}
}
if (poppingTopController) {
performControllerChange(backstack.peek(), topController, false);
if (removedTransaction != null) {
performControllerChange(nextTransaction, removedTransaction, false);
}
}
if (popsLastView) {
return topController != null;
return topTransaction != null;
} else {
return !backstack.isEmpty();
}
@@ -149,6 +172,8 @@ public abstract class Router {
*/
@UiThread
public void pushController(@NonNull RouterTransaction transaction) {
ThreadUtils.ensureMainThread();
RouterTransaction from = backstack.peek();
pushToBackstack(transaction);
performControllerChange(transaction, from, true);
@@ -160,8 +185,11 @@ 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.
*/
@SuppressWarnings("WeakerAccess")
@UiThread
public void replaceTopController(@NonNull RouterTransaction transaction) {
ThreadUtils.ensureMainThread();
RouterTransaction topTransaction = backstack.peek();
if (!backstack.isEmpty()) {
trackDestroyingController(backstack.pop());
@@ -169,6 +197,7 @@ public abstract class Router {
final ControllerChangeHandler handler = transaction.pushChangeHandler();
if (topTransaction != null) {
//noinspection ConstantConditions
final boolean oldHandlerRemovedViews = topTransaction.pushChangeHandler() == null || topTransaction.pushChangeHandler().removesFromViewOnPush();
final boolean newHandlerRemovesViews = handler == null || handler.removesFromViewOnPush();
if (!oldHandlerRemovedViews && newHandlerRemovesViews) {
@@ -188,11 +217,24 @@ public abstract class Router {
void destroy(boolean popViews) {
popsLastView = true;
List<RouterTransaction> poppedControllers = backstack.popAll();
final List<RouterTransaction> poppedControllers = backstack.popAll();
trackDestroyingControllers(poppedControllers);
if (popViews && poppedControllers.size() > 0) {
performControllerChange(null, poppedControllers.get(0), false, poppedControllers.get(0).popChangeHandler());
RouterTransaction topTransaction = poppedControllers.get(0);
topTransaction.controller().addLifecycleListener(new LifecycleListener() {
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
if (changeType == ControllerChangeType.POP_EXIT) {
for (int i = poppedControllers.size() - 1; i > 0; i--) {
RouterTransaction transaction = poppedControllers.get(i);
performControllerChange(null, transaction, true, new SimpleSwapChangeHandler());
}
}
}
});
performControllerChange(null, topTransaction, false, topTransaction.popChangeHandler());
}
}
@@ -218,6 +260,8 @@ public abstract class Router {
*/
@UiThread
public boolean popToRoot() {
ThreadUtils.ensureMainThread();
return popToRoot(null);
}
@@ -227,8 +271,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
*/
@SuppressWarnings("WeakerAccess")
@UiThread
public boolean popToRoot(@Nullable ControllerChangeHandler changeHandler) {
ThreadUtils.ensureMainThread();
if (backstack.size() > 1) {
//noinspection ConstantConditions
popToTransaction(backstack.root(), changeHandler);
@@ -246,6 +293,8 @@ public abstract class Router {
*/
@UiThread
public boolean popToTag(@NonNull String tag) {
ThreadUtils.ensureMainThread();
return popToTag(tag, null);
}
@@ -256,8 +305,11 @@ 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
*/
@SuppressWarnings("WeakerAccess")
@UiThread
public boolean popToTag(@NonNull String tag, @Nullable ControllerChangeHandler changeHandler) {
ThreadUtils.ensureMainThread();
for (RouterTransaction transaction : backstack) {
if (tag.equals(transaction.tag())) {
popToTransaction(transaction, changeHandler);
@@ -275,8 +327,9 @@ public abstract class Router {
*/
@UiThread
public void setRoot(@NonNull RouterTransaction transaction) {
List<RouterTransaction> transactions = new ArrayList<>();
transactions.add(transaction);
ThreadUtils.ensureMainThread();
List<RouterTransaction> transactions = Collections.singletonList(transaction);
setBackstack(transactions, transaction.pushChangeHandler());
}
@@ -316,6 +369,7 @@ public abstract class Router {
/**
* Returns the number of {@link Controller}s currently in the backstack
*/
@SuppressWarnings("WeakerAccess")
public int getBackstackSize() {
return backstack.size();
}
@@ -340,10 +394,15 @@ public abstract class Router {
* @param newBackstack The new backstack
* @param changeHandler An optional change handler to be used to handle the root view of transition
*/
@SuppressWarnings("WeakerAccess")
@UiThread
public void setBackstack(@NonNull List<RouterTransaction> newBackstack, @Nullable ControllerChangeHandler changeHandler) {
ThreadUtils.ensureMainThread();
List<RouterTransaction> oldVisibleTransactions = getVisibleTransactions(backstack.iterator());
boolean newRootRequiresPush = !(newBackstack.size() > 0 && backstack.contains(newBackstack.get(0)));
removeAllExceptVisibleAndUnowned();
ensureOrderedTransactionIndices(newBackstack);
@@ -359,19 +418,35 @@ public abstract class Router {
boolean visibleTransactionsChanged = !backstacksAreEqual(newVisibleTransactions, oldVisibleTransactions);
if (visibleTransactionsChanged) {
RouterTransaction rootTransaction = oldVisibleTransactions.size() > 0 ? oldVisibleTransactions.get(0) : null;
performControllerChange(newVisibleTransactions.get(0), rootTransaction, true, changeHandler);
RouterTransaction oldRootTransaction = oldVisibleTransactions.size() > 0 ? oldVisibleTransactions.get(0) : null;
RouterTransaction newRootTransaction = newVisibleTransactions.get(0);
for (int i = oldVisibleTransactions.size() - 1; i > 0; i--) {
RouterTransaction transaction = oldVisibleTransactions.get(i);
ControllerChangeHandler localHandler = changeHandler != null ? changeHandler.copy() : new SimpleSwapChangeHandler();
localHandler.setForceRemoveViewOnPush(true);
performControllerChange(null, transaction, true, localHandler);
// Replace the old root with the new one
if (oldRootTransaction == null || oldRootTransaction.controller != newRootTransaction.controller) {
// Ensure the existing root controller is fully pushed to the view hierarchy
if (oldRootTransaction != null) {
ControllerChangeHandler.completeHandlerImmediately(oldRootTransaction.controller.getInstanceId());
}
performControllerChange(newRootTransaction, oldRootTransaction, newRootRequiresPush, changeHandler);
}
// Remove all visible controllers that were previously on the backstack
for (int i = oldVisibleTransactions.size() - 1; i > 0; i--) {
RouterTransaction transaction = oldVisibleTransactions.get(i);
if (!newVisibleTransactions.contains(transaction)) {
ControllerChangeHandler localHandler = changeHandler != null ? changeHandler.copy() : new SimpleSwapChangeHandler();
localHandler.setForceRemoveViewOnPush(true);
ControllerChangeHandler.completeHandlerImmediately(transaction.controller.getInstanceId());
performControllerChange(null, transaction, newRootRequiresPush, localHandler);
}
}
// Add any new controllers to the backstack
for (int i = 1; i < newVisibleTransactions.size(); i++) {
RouterTransaction transaction = newVisibleTransactions.get(i);
performControllerChange(transaction, newVisibleTransactions.get(i - 1), true, transaction.pushChangeHandler());
if (!oldVisibleTransactions.contains(transaction)) {
performControllerChange(transaction, newVisibleTransactions.get(i - 1), true, transaction.pushChangeHandler());
}
}
}
@@ -394,6 +469,7 @@ public abstract class Router {
*
* @param changeListener The listener
*/
@SuppressWarnings("WeakerAccess")
public void addChangeListener(@NonNull ControllerChangeListener changeListener) {
if (!changeListeners.contains(changeListener)) {
changeListeners.add(changeListener);
@@ -405,6 +481,7 @@ public abstract class Router {
*
* @param changeListener The listener to be removed
*/
@SuppressWarnings("WeakerAccess")
public void removeChangeListener(@NonNull ControllerChangeListener changeListener) {
changeListeners.remove(changeListener);
}
@@ -414,6 +491,8 @@ public abstract class Router {
*/
@UiThread
public void rebindIfNeeded() {
ThreadUtils.ensureMainThread();
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
@@ -476,7 +555,7 @@ public abstract class Router {
changeListeners.clear();
for (RouterTransaction transaction : backstack) {
transaction.controller.activityDestroyed(activity.isChangingConfigurations());
transaction.controller.activityDestroyed(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
childRouter.onActivityDestroyed(activity);
@@ -485,7 +564,7 @@ public abstract class Router {
for (int index = destroyingControllers.size() - 1; index >= 0; index--) {
Controller controller = destroyingControllers.get(index);
controller.activityDestroyed(activity.isChangingConfigurations());
controller.activityDestroyed(activity);
for (Router childRouter : controller.getChildRouters()) {
childRouter.onActivityDestroyed(activity);
@@ -495,10 +574,10 @@ public abstract class Router {
container = null;
}
public void prepareForHostDetach() {
void prepareForHostDetach() {
for (RouterTransaction transaction : backstack) {
if (ControllerChangeHandler.completePushImmediately(transaction.controller.getInstanceId())) {
transaction.controller.setNeedsAttach();
if (ControllerChangeHandler.completeHandlerImmediately(transaction.controller.getInstanceId())) {
transaction.controller.setNeedsAttach(true);
}
transaction.controller.prepareForHostDetach();
}
@@ -516,6 +595,7 @@ public abstract class Router {
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
Bundle backstackBundle = savedInstanceState.getParcelable(KEY_BACKSTACK);
//noinspection ConstantConditions
backstack.restoreInstanceState(backstackBundle);
popsLastView = savedInstanceState.getBoolean(KEY_POPS_LAST_VIEW);
@@ -561,25 +641,51 @@ public abstract class Router {
}
private void popToTransaction(@NonNull RouterTransaction transaction, @Nullable ControllerChangeHandler changeHandler) {
RouterTransaction topTransaction = backstack.peek();
List<RouterTransaction> poppedTransactions = backstack.popTo(transaction);
trackDestroyingControllers(poppedTransactions);
if (backstack.size() > 0) {
RouterTransaction topTransaction = backstack.peek();
List<RouterTransaction> updatedBackstack = new ArrayList<>();
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
RouterTransaction existingTransaction = backstackIterator.next();
updatedBackstack.add(existingTransaction);
if (existingTransaction == transaction) {
break;
}
}
if (poppedTransactions.size() > 0) {
if (changeHandler == null) {
//noinspection ConstantConditions
changeHandler = topTransaction.popChangeHandler();
}
performControllerChange(backstack.peek(), topTransaction, false, changeHandler);
setBackstack(updatedBackstack, changeHandler);
}
}
void watchContainerAttach() {
container.post(new Runnable() {
@Override
public void run() {
containerFullyAttached = true;
}
});
}
void prepareForContainerRemoval() {
containerFullyAttached = false;
if (container != null) {
container.setOnHierarchyChangeListener(null);
}
}
void onContextAvailable() {
for (RouterTransaction transaction : backstack) {
transaction.controller.onContextAvailable();
}
}
@NonNull
final List<Controller> getControllers() {
List<Controller> controllers = new ArrayList<>();
@@ -620,23 +726,65 @@ public abstract class Router {
performControllerChange(to, from, isPush, changeHandler);
}
private void performControllerChange(@Nullable final RouterTransaction to, @Nullable final RouterTransaction from, boolean isPush, @Nullable ControllerChangeHandler changeHandler) {
private void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush, @Nullable ControllerChangeHandler changeHandler) {
Controller toController = to != null ? to.controller : null;
Controller fromController = from != null ? from.controller : null;
boolean forceDetachDestroy = false;
if (to != null) {
to.ensureValidIndex(getTransactionIndexer());
setControllerRouter(toController);
} else if (backstack.size() == 0 && !popsLastView) {
// We're emptying out the backstack. Views get weird if you transition them out, so just no-op it. The hosting
// Activity should be handling this by finishing or at least hiding this view.
// We're emptying out the backstack. Views get weird if you transition them out, so just no-op it. The host
// Activity or controller should be handling this by finishing or at least hiding this view.
changeHandler = new NoOpControllerChangeHandler();
forceDetachDestroy = true;
}
ControllerChangeHandler.executeChange(toController, fromController, isPush, container, changeHandler, changeListeners);
performControllerChange(toController, fromController, isPush, changeHandler);
if (forceDetachDestroy && fromController != null && fromController.getView() != null) {
fromController.detach(fromController.getView(), true, false);
}
}
private void pushToBackstack(@NonNull RouterTransaction entry) {
private void performControllerChange(@Nullable final Controller to, @Nullable final Controller from, final boolean isPush, @Nullable final ControllerChangeHandler changeHandler) {
if (isPush && to != null && to.isDestroyed()) {
throw new IllegalStateException("Trying to push a controller that has already been destroyed. (" + to.getClass().getSimpleName() + ")");
}
final ChangeTransaction transaction = new ChangeTransaction(to, from, isPush, container, changeHandler, changeListeners);
if (pendingControllerChanges.size() > 0) {
// If we already have changes queued up (awaiting full container attach), queue this one up as well so they don't happen
// out of order.
pendingControllerChanges.add(transaction);
} else if (from != null && (changeHandler == null || changeHandler.removesFromViewOnPush()) && !containerFullyAttached) {
// If the change handler will remove the from view, we have to make sure the container is fully attached first so we avoid NPEs
// within ViewGroup (details on issue #287). Post this to the container to ensure the attach is complete before we try to remove
// anything.
pendingControllerChanges.add(transaction);
container.post(new Runnable() {
@Override
public void run() {
performPendingControllerChanges();
}
});
} else {
ControllerChangeHandler.executeChange(transaction);
}
}
void performPendingControllerChanges() {
// We're intentionally using dynamic size checking (list.size()) here so we can account for changes
// that occur during this loop (ex: if a controller is popped from within onAttach)
for (int i = 0; i < pendingControllerChanges.size(); i++) {
ControllerChangeHandler.executeChange(pendingControllerChanges.get(i));
}
pendingControllerChanges.clear();
}
protected void pushToBackstack(@NonNull RouterTransaction entry) {
backstack.push(entry);
}
@@ -717,6 +865,7 @@ public abstract class Router {
RouterTransaction transaction = backstackIterator.next();
transactions.add(transaction);
//noinspection ConstantConditions
if (transaction.pushChangeHandler() == null || transaction.pushChangeHandler().removesFromViewOnPush()) {
break;
}
@@ -742,12 +891,15 @@ public abstract class Router {
void setControllerRouter(@NonNull Controller controller) {
controller.setRouter(this);
controller.onContextAvailable();
}
abstract void invalidateOptionsMenu();
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 startIntentSenderForResult(@NonNull String instanceId, @NonNull IntentSender intent, int requestCode, @Nullable Intent fillInIntent, int flagsMask,
int flagsValues, int extraFlags, @Nullable Bundle options) throws IntentSender.SendIntentException;
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);
@@ -140,4 +140,4 @@ public class RouterTransaction {
return bundle;
}
}
}
@@ -21,6 +21,7 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
private static final String KEY_DURATION = "AnimatorChangeHandler.duration";
private static final String KEY_REMOVES_FROM_ON_PUSH = "AnimatorChangeHandler.removesFromViewOnPush";
@SuppressWarnings("WeakerAccess")
public static final long DEFAULT_ANIMATION_DURATION = -1;
private long animationDuration;
@@ -29,15 +30,19 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
private boolean needsImmediateCompletion;
private boolean completed;
private Animator animator;
private OnAnimationReadyOrAbortedListener onAnimationReadyOrAbortedListener;
@SuppressWarnings("WeakerAccess")
public AnimatorChangeHandler() {
this(DEFAULT_ANIMATION_DURATION, true);
}
@SuppressWarnings("WeakerAccess")
public AnimatorChangeHandler(boolean removesFromViewOnPush) {
this(DEFAULT_ANIMATION_DURATION, removesFromViewOnPush);
}
@SuppressWarnings("WeakerAccess")
public AnimatorChangeHandler(long duration) {
this(duration, true);
}
@@ -68,6 +73,8 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
canceled = true;
if (animator != null) {
animator.cancel();
} else if (onAnimationReadyOrAbortedListener != null) {
onAnimationReadyOrAbortedListener.onReadyOrAborted();
}
}
@@ -78,6 +85,8 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
needsImmediateCompletion = true;
if (animator != null) {
animator.end();
} else if (onAnimationReadyOrAbortedListener != null) {
onAnimationReadyOrAbortedListener.onReadyOrAborted();
}
}
@@ -121,17 +130,8 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
if (to.getWidth() <= 0 && to.getHeight() <= 0) {
readyToAnimate = false;
to.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
final ViewTreeObserver observer = to.getViewTreeObserver();
if (observer.isAlive()) {
observer.removeOnPreDrawListener(this);
}
performAnimation(container, from, to, isPush, addingToView, changeListener);
return true;
}
});
onAnimationReadyOrAbortedListener = new OnAnimationReadyOrAbortedListener(container, from, to, isPush, true, changeListener);
to.getViewTreeObserver().addOnPreDrawListener(onAnimationReadyOrAbortedListener);
}
}
@@ -153,6 +153,8 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
animator.cancel();
animator = null;
}
onAnimationReadyOrAbortedListener = 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) {
@@ -160,6 +162,16 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
complete(changeListener, null);
return;
}
if (needsImmediateCompletion) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from);
}
complete(changeListener, null);
if (isPush && from != null) {
resetFromView(from);
}
return;
}
animator = getAnimator(container, from, to, isPush, toAddedToContainer);
@@ -196,4 +208,46 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
animator.start();
}
private class OnAnimationReadyOrAbortedListener implements ViewTreeObserver.OnPreDrawListener {
@NonNull final ViewGroup container;
@Nullable final View from;
@Nullable final View to;
final boolean isPush;
final boolean addingToView;
@NonNull final ControllerChangeCompletedListener changeListener;
private boolean hasRun;
OnAnimationReadyOrAbortedListener(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean addingToView, @NonNull ControllerChangeCompletedListener changeListener) {
this.container = container;
this.from = from;
this.to = to;
this.isPush = isPush;
this.addingToView = addingToView;
this.changeListener = changeListener;
}
@Override
public boolean onPreDraw() {
onReadyOrAborted();
return true;
}
void onReadyOrAborted() {
if (!hasRun) {
hasRun = true;
if (to != null) {
final ViewTreeObserver observer = to.getViewTreeObserver();
if (observer.isAlive()) {
observer.removeOnPreDrawListener(this);
}
}
performAnimation(container, from, to, isPush, addingToView, changeListener);
}
}
}
}
@@ -37,7 +37,7 @@ public class FadeChangeHandler extends AnimatorChangeHandler {
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1));
}
if (from != null) {
if (from != null && (!isPush || removesFromViewOnPush())) {
animator.play(ObjectAnimator.ofFloat(from, View.ALPHA, 0));
}
@@ -46,7 +46,7 @@ public class HorizontalChangeHandler extends AnimatorChangeHandler {
}
if (to != null) {
// Allow this to have a nice transition when coming off an aborted push animation
float fromLeft = from != null ? from.getX() : 0;
float fromLeft = from != null ? from.getTranslationX() : 0;
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, fromLeft - to.getWidth(), 0));
}
}
@@ -19,15 +19,20 @@ import com.bluelinelabs.conductor.ControllerChangeHandler;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class TransitionChangeHandler extends ControllerChangeHandler {
public interface OnTransitionPreparedListener {
void onPrepared();
}
private boolean canceled;
private boolean needsImmediateCompletion;
/**
* Should be overridden to return the Transition to use while replacing Views.
*
* @param container The container these Views are hosted in
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop
*/
@NonNull
protected abstract Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
@@ -40,13 +45,25 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
}
@Override
public void performChange(@NonNull final ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
public void completeImmediately() {
super.completeImmediately();
needsImmediateCompletion = true;
}
@Override
public void performChange(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
if (canceled) {
changeListener.onChangeCompleted();
return;
}
if (needsImmediateCompletion) {
executePropertyChanges(container, from, to, null, isPush);
changeListener.onChangeCompleted();
return;
}
Transition transition = getTransition(container, from, to, isPush);
final Transition transition = getTransition(container, from, to, isPush);
transition.addListener(new TransitionListener() {
@Override
public void onTransitionStart(Transition transition) { }
@@ -68,8 +85,48 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
public void onTransitionResume(Transition transition) { }
});
TransitionManager.beginDelayedTransition(container, transition);
if (from != null) {
prepareForTransition(container, from, to, transition, isPush, new OnTransitionPreparedListener() {
@Override
public void onPrepared() {
if (!canceled) {
TransitionManager.beginDelayedTransition(container, transition);
executePropertyChanges(container, from, to, transition, isPush);
}
}
});
}
@Override
public boolean removesFromViewOnPush() {
return true;
}
/**
* Called before a transition occurs. This can be used to reorder views, set their transition names, etc. The transition will begin
* when {@code onTransitionPreparedListener} is called.
*
* @param container The container these Views are hosted in
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param transition The transition that is being prepared for
* @param isPush True if this is a push transaction, false if it's a pop
*/
public void prepareForTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @NonNull Transition transition, boolean isPush, @NonNull OnTransitionPreparedListener onTransitionPreparedListener) {
onTransitionPreparedListener.onPrepared();
}
/**
* This should set all view properties needed for the transition to work properly. By default it removes the "from" view
* and adds the "to" view.
*
* @param container The container these Views are hosted in
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param transition The transition with which {@code TransitionManager.beginDelayedTransition} has been called. This will be null only if another ControllerChangeHandler immediately overrides this one.
* @param isPush True if this is a push transaction, false if it's a pop
*/
public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @Nullable Transition transition, boolean isPush) {
if (from != null && (removesFromViewOnPush() || !isPush) && from.getParent() == container) {
container.removeView(from);
}
if (to != null && to.getParent() == null) {
@@ -77,9 +134,4 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
}
}
@Override
public final boolean removesFromViewOnPush() {
return true;
}
}
@@ -7,6 +7,7 @@ import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.internal.ClassUtils;
@@ -78,4 +79,19 @@ public class TransitionChangeHandlerCompat extends ControllerChangeHandler {
}
}
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
changeHandler.onAbortPush(newHandler, newTop);
}
@Override
public void completeImmediately() {
changeHandler.completeImmediately();
}
@Override
public void setForceRemoveViewOnPush(boolean force) {
changeHandler.setForceRemoveViewOnPush(force);
}
}
@@ -6,6 +6,7 @@ import android.app.Application.ActivityLifecycleCallbacks;
import android.app.Fragment;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
@@ -40,6 +41,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
private boolean destroyed;
private boolean attached;
private static final Map<Activity, LifecycleHandler> activeLifecycleHandlers = new HashMap<>();
private SparseArray<String> permissionRequestMap = new SparseArray<>();
private SparseArray<String> activityRequestMap = new SparseArray<>();
private ArrayList<PendingPermissionRequest> pendingPermissionRequests = new ArrayList<>();
@@ -53,7 +55,10 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
@Nullable
private static LifecycleHandler findInActivity(@NonNull Activity activity) {
LifecycleHandler lifecycleHandler = (LifecycleHandler)activity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
LifecycleHandler lifecycleHandler = activeLifecycleHandlers.get(activity);
if (lifecycleHandler == null) {
lifecycleHandler = (LifecycleHandler)activity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
}
if (lifecycleHandler != null) {
lifecycleHandler.registerActivityListener(activity);
}
@@ -112,6 +117,11 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
if (!hasRegisteredCallbacks) {
hasRegisteredCallbacks = true;
activity.getApplication().registerActivityLifecycleCallbacks(this);
// Since Fragment transactions are async, we have to keep an <Activity, LifecycleHandler> map in addition
// to trying to find the LifecycleHandler fragment in the Activity to handle the case of the developer
// trying to immediately get > 1 router in the same Activity. See issue #299.
activeLifecycleHandlers.put(activity, this);
}
}
@@ -146,6 +156,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
if (activity != null) {
activity.getApplication().unregisterActivityLifecycleCallbacks(this);
activeLifecycleHandlers.remove(activity);
destroyRouters();
activity = null;
}
@@ -183,6 +194,10 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
requestPermissions(request.instanceId, request.permissions, request.requestCode);
}
}
for (ActivityHostedRouter router : routerMap.values()) {
router.onContextAvailable();
}
}
private void destroyRouters() {
@@ -283,6 +298,14 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
startActivityForResult(intent, requestCode, options);
}
@TargetApi(Build.VERSION_CODES.N)
public void startIntentSenderForResult(@NonNull String instanceId, @NonNull IntentSender intent, int requestCode,
@Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,
@Nullable Bundle options) throws IntentSender.SendIntentException {
registerForActivityResult(instanceId, requestCode);
startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options);
}
@TargetApi(Build.VERSION_CODES.M)
public void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
if (attached) {
@@ -297,6 +320,10 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (this.activity == null && findInActivity(activity) == LifecycleHandler.this) {
this.activity = activity;
for (ActivityHostedRouter router : routerMap.values()) {
router.onContextAvailable();
}
}
}
@@ -348,7 +375,9 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
}
@Override
public void onActivityDestroyed(Activity activity) { }
public void onActivityDestroyed(Activity activity) {
activeLifecycleHandlers.remove(activity);
}
private static class PendingPermissionRequest implements Parcelable {
final String instanceId;
@@ -24,4 +24,4 @@ public class NoOpControllerChangeHandler extends ControllerChangeHandler {
public boolean isReusable() {
return true;
}
}
}
@@ -0,0 +1,20 @@
package com.bluelinelabs.conductor.internal;
import android.os.Looper;
import android.util.AndroidRuntimeException;
public class ThreadUtils {
public static void ensureMainThread() {
if (Looper.getMainLooper().getThread() != Thread.currentThread()) {
throw new CalledFromWrongThreadException("Methods that affect the view hierarchy can can only be called from the main thread.");
}
}
private static final class CalledFromWrongThreadException extends AndroidRuntimeException {
CalledFromWrongThreadException(String msg) {
super(msg);
}
}
}
@@ -23,7 +23,7 @@ public class ViewAttachHandler implements OnAttachStateChangeListener {
}
private boolean rootAttached = false;
boolean childrenAttached = false;
private boolean childrenAttached = false;
private boolean activityStopped = false;
private ReportedState reportedState = ReportedState.VIEW_DETACHED;
private ViewAttachListener attachListener;
@@ -55,7 +55,7 @@ public class ViewAttachHandler implements OnAttachStateChangeListener {
rootAttached = false;
if (childrenAttached) {
childrenAttached = false;
reportDetached();
reportDetached(false);
}
}
@@ -78,34 +78,33 @@ public class ViewAttachHandler implements OnAttachStateChangeListener {
public void onActivityStopped() {
activityStopped = true;
reportDetached();
reportDetached(true);
}
void reportAttached() {
private void reportAttached() {
if (rootAttached && childrenAttached && !activityStopped && reportedState != ReportedState.ATTACHED) {
reportedState = ReportedState.ATTACHED;
attachListener.onAttached();
}
}
void reportDetached() {
private void reportDetached(boolean detachedForActivity) {
boolean wasDetachedForActivity = reportedState == ReportedState.ACTIVITY_STOPPED;
boolean isDetachedForActivity = rootAttached;
if (isDetachedForActivity) {
if (detachedForActivity) {
reportedState = ReportedState.ACTIVITY_STOPPED;
} else {
reportedState = ReportedState.VIEW_DETACHED;
}
if (wasDetachedForActivity && !isDetachedForActivity) {
if (wasDetachedForActivity && !detachedForActivity) {
attachListener.onViewDetachAfterStop();
} else {
attachListener.onDetached(isDetachedForActivity);
attachListener.onDetached(detachedForActivity);
}
}
void listenForDeepestChildAttach(final View view, final ChildAttachListener attachListener) {
private void listenForDeepestChildAttach(final View view, final ChildAttachListener attachListener) {
if (!(view instanceof ViewGroup)) {
attachListener.onAttached();
return;
@@ -6,7 +6,6 @@ import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.ListUtils;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
@@ -17,6 +16,8 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertEquals;
@@ -62,12 +63,12 @@ public class ControllerLifecycleActivityReferenceTests {
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertEquals(ListUtils.listOf(true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(), listener.postDetachReferences);
assertEquals(ListUtils.listOf(), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(), listener.postDestroyReferences);
assertEquals(Collections.singletonList(true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.emptyList(), listener.postDetachReferences);
assertEquals(Collections.emptyList(), listener.postDestroyViewReferences);
assertEquals(Collections.emptyList(), listener.postDestroyReferences);
}
@Test
@@ -89,12 +90,12 @@ public class ControllerLifecycleActivityReferenceTests {
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertEquals(ListUtils.listOf(true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(), listener.postDetachReferences);
assertEquals(ListUtils.listOf(), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(), listener.postDestroyReferences);
assertEquals(Collections.singletonList(true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.emptyList(), listener.postDetachReferences);
assertEquals(Collections.emptyList(), listener.postDestroyViewReferences);
assertEquals(Collections.emptyList(), listener.postDestroyReferences);
}
@Test
@@ -110,12 +111,12 @@ public class ControllerLifecycleActivityReferenceTests {
router.popCurrentController();
assertEquals(ListUtils.listOf(true, true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(true), listener.postDetachReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyReferences);
assertEquals(Arrays.asList(true, true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.singletonList(true), listener.postDetachReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
@Test
@@ -139,12 +140,12 @@ public class ControllerLifecycleActivityReferenceTests {
childRouter.popCurrentController();
assertEquals(ListUtils.listOf(true, true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(true), listener.postDetachReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyReferences);
assertEquals(Arrays.asList(true, true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.singletonList(true), listener.postDetachReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
@Test
@@ -168,12 +169,12 @@ public class ControllerLifecycleActivityReferenceTests {
router.popCurrentController();
assertEquals(ListUtils.listOf(true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(true), listener.postDetachReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyReferences);
assertEquals(Collections.singletonList(true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.singletonList(true), listener.postDetachReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
@Test
@@ -189,12 +190,12 @@ public class ControllerLifecycleActivityReferenceTests {
activityProxy.pause().stop(false).destroy();
assertEquals(ListUtils.listOf(true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(true), listener.postDetachReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyReferences);
assertEquals(Collections.singletonList(true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.singletonList(true), listener.postDetachReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
@Test
@@ -218,21 +219,21 @@ public class ControllerLifecycleActivityReferenceTests {
activityProxy.pause().stop(false).destroy();
assertEquals(ListUtils.listOf(true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(true), listener.postDetachReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyReferences);
assertEquals(Collections.singletonList(true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.singletonList(true), listener.postDetachReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
static class ActivityReferencingLifecycleListener extends Controller.LifecycleListener {
List<Boolean> changeEndReferences = new ArrayList<>();
List<Boolean> postCreateViewReferences = new ArrayList<>();
List<Boolean> postAttachReferences = new ArrayList<>();
List<Boolean> postDetachReferences = new ArrayList<>();
List<Boolean> postDestroyViewReferences = new ArrayList<>();
List<Boolean> postDestroyReferences = new ArrayList<>();
final List<Boolean> changeEndReferences = new ArrayList<>();
final List<Boolean> postCreateViewReferences = new ArrayList<>();
final List<Boolean> postAttachReferences = new ArrayList<>();
final List<Boolean> postDetachReferences = new ArrayList<>();
final List<Boolean> postDestroyViewReferences = new ArrayList<>();
final List<Boolean> postDestroyReferences = new ArrayList<>();
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
@@ -1,5 +1,6 @@
package com.bluelinelabs.conductor;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.view.View;
@@ -49,7 +50,7 @@ public class ControllerLifecycleCallbacksTests {
public void setup() {
createActivityController(null, true);
currentCallState = new CallState();
currentCallState = new CallState(false);
}
@Test
@@ -57,7 +58,7 @@ public class ControllerLifecycleCallbacksTests {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
@@ -78,7 +79,7 @@ public class ControllerLifecycleCallbacksTests {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
@@ -109,7 +110,7 @@ public class ControllerLifecycleCallbacksTests {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
@@ -131,6 +132,7 @@ public class ControllerLifecycleCallbacksTests {
activityProxy.destroy();
expectedCallState.contextUnavailableCalls++;
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, controller);
}
@@ -140,7 +142,7 @@ public class ControllerLifecycleCallbacksTests {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
@@ -167,11 +169,13 @@ public class ControllerLifecycleCallbacksTests {
assertCalls(expectedCallState, controller);
activityProxy.destroy();
expectedCallState.contextUnavailableCalls++;
assertCalls(expectedCallState, controller);
createActivityController(bundle, false);
controller = (TestController)router.getControllerWithTag("root");
expectedCallState.contextAvailableCalls++;
expectedCallState.restoreInstanceStateCalls++;
expectedCallState.restoreViewStateCalls++;
expectedCallState.changeStartCalls++;
@@ -184,6 +188,7 @@ public class ControllerLifecycleCallbacksTests {
currentCallState.changeEndCalls = controller.currentCallState.changeEndCalls;
currentCallState.createViewCalls = controller.currentCallState.createViewCalls;
currentCallState.attachCalls = controller.currentCallState.attachCalls;
currentCallState.contextAvailableCalls = controller.currentCallState.contextAvailableCalls;
assertCalls(expectedCallState, controller);
@@ -204,7 +209,7 @@ public class ControllerLifecycleCallbacksTests {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
@@ -222,12 +227,14 @@ public class ControllerLifecycleCallbacksTests {
assertCalls(expectedCallState, controller);
activityProxy.resume();
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleCallOrder() {
final TestController testController = new TestController();
final CallState callState = new CallState();
final CallState callState = new CallState(false);
testController.addLifecycleListener(new LifecycleListener() {
@Override
@@ -443,7 +450,7 @@ public class ControllerLifecycleCallbacksTests {
TestController child = new TestController();
attachLifecycleListener(child);
CallState expectedCallState = new CallState();
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, child);
@@ -470,7 +477,7 @@ public class ControllerLifecycleCallbacksTests {
TestController child = new TestController();
attachLifecycleListener(child);
CallState expectedCallState = new CallState();
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, child);
@@ -486,6 +493,7 @@ public class ControllerLifecycleCallbacksTests {
expectedCallState.detachCalls++;
expectedCallState.destroyViewCalls++;
expectedCallState.contextUnavailableCalls++;
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, child);
@@ -495,6 +503,7 @@ public class ControllerLifecycleCallbacksTests {
return MockChangeHandler.listeningChangeHandler(new ChangeHandlerListener() {
@Override
public void willStartChange() {
expectedCallState.contextAvailableCalls++;
expectedCallState.changeStartCalls++;
expectedCallState.createViewCalls++;
assertCalls(expectedCallState, controller);
@@ -526,6 +535,7 @@ public class ControllerLifecycleCallbacksTests {
public void didAttachOrDetach() {
expectedCallState.destroyViewCalls++;
expectedCallState.detachCalls++;
expectedCallState.contextUnavailableCalls++;
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, controller);
}
@@ -555,6 +565,16 @@ public class ControllerLifecycleCallbacksTests {
currentCallState.changeEndCalls++;
}
@Override
public void postContextAvailable(@NonNull Controller controller, @NonNull Context context) {
currentCallState.contextAvailableCalls++;
}
@Override
public void postContextUnavailable(@NonNull Controller controller) {
currentCallState.contextUnavailableCalls++;
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
currentCallState.createViewCalls++;
@@ -3,7 +3,6 @@ package com.bluelinelabs.conductor;
import android.view.View;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.ListUtils;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
@@ -13,6 +12,7 @@ import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
@@ -142,7 +142,7 @@ public class RouterChangeHandlerTests {
TestController newController2 = new TestController();
MockChangeHandler setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true);
List<RouterTransaction> newBackstack = ListUtils.listOf(
List<RouterTransaction> newBackstack = Arrays.asList(
RouterTransaction.with(newController1),
RouterTransaction.with(newController2)
);
@@ -194,7 +194,7 @@ public class RouterChangeHandlerTests {
TestController newController2 = new TestController();
MockChangeHandler setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true);
List<RouterTransaction> newBackstack = ListUtils.listOf(
List<RouterTransaction> newBackstack = Arrays.asList(
RouterTransaction.with(newController1),
RouterTransaction.with(newController2).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler())
);
@@ -2,8 +2,9 @@ package com.bluelinelabs.conductor;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.ListUtils;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
@@ -13,6 +14,7 @@ import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
@@ -189,7 +191,7 @@ public class RouterTests {
RouterTransaction middleTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = ListUtils.listOf(rootTransaction, middleTransaction, topTransaction);
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, middleTransaction, topTransaction);
router.setBackstack(backstack, null);
assertEquals(3, router.getBackstackSize());
@@ -210,7 +212,7 @@ public class RouterTests {
RouterTransaction middleTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = ListUtils.listOf(rootTransaction, middleTransaction, topTransaction);
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, middleTransaction, topTransaction);
router.setBackstack(backstack, null);
assertEquals(3, router.getBackstackSize());
@@ -241,7 +243,7 @@ public class RouterTests {
RouterTransaction middleTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
List<RouterTransaction> backstack = ListUtils.listOf(rootTransaction, middleTransaction, topTransaction);
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, middleTransaction, topTransaction);
router.setBackstack(backstack, null);
assertEquals(3, router.getBackstackSize());
@@ -258,12 +260,54 @@ public class RouterTests {
assertTrue(topTransaction.controller.isAttached());
}
@Test
public void testPopToRoot() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, transaction1, transaction2);
router.setBackstack(backstack, null);
assertEquals(3, router.getBackstackSize());
router.popToRoot();
assertEquals(1, router.getBackstackSize());
assertEquals(rootTransaction, router.getBackstack().get(0));
assertTrue(rootTransaction.controller.isAttached());
assertFalse(transaction1.controller.isAttached());
assertFalse(transaction2.controller.isAttached());
}
@Test
public void testPopToRootWithNoRemoveViewOnPush() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new HorizontalChangeHandler(false));
RouterTransaction transaction1 = RouterTransaction.with(new TestController()).pushChangeHandler(new HorizontalChangeHandler(false));
RouterTransaction transaction2 = RouterTransaction.with(new TestController()).pushChangeHandler(new HorizontalChangeHandler(false));
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, transaction1, transaction2);
router.setBackstack(backstack, null);
assertEquals(3, router.getBackstackSize());
router.popToRoot();
assertEquals(1, router.getBackstackSize());
assertEquals(rootTransaction, router.getBackstack().get(0));
assertTrue(rootTransaction.controller.isAttached());
assertFalse(transaction1.controller.isAttached());
assertFalse(transaction2.controller.isAttached());
}
@Test
public void testReplaceTopController() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = ListUtils.listOf(rootTransaction, topTransaction);
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, topTransaction);
router.setBackstack(backstack, null);
assertEquals(2, router.getBackstackSize());
@@ -287,7 +331,7 @@ public class RouterTests {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
List<RouterTransaction> backstack = ListUtils.listOf(rootTransaction, topTransaction);
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, topTransaction);
router.setBackstack(backstack, null);
assertEquals(2, router.getBackstackSize());
@@ -319,13 +363,13 @@ public class RouterTests {
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = ListUtils.listOf(transaction1, transaction2);
List<RouterTransaction> backstack = Arrays.asList(transaction1, transaction2);
router.setBackstack(backstack, null);
assertEquals(1, transaction1.transactionIndex);
assertEquals(2, transaction2.transactionIndex);
backstack = ListUtils.listOf(transaction2, transaction1);
backstack = Arrays.asList(transaction2, transaction1);
router.setBackstack(backstack, null);
assertEquals(1, transaction2.transactionIndex);
@@ -350,13 +394,13 @@ public class RouterTests {
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = ListUtils.listOf(transaction1, transaction2);
List<RouterTransaction> backstack = Arrays.asList(transaction1, transaction2);
childRouter.setBackstack(backstack, null);
assertEquals(2, transaction1.transactionIndex);
assertEquals(3, transaction2.transactionIndex);
backstack = ListUtils.listOf(transaction2, transaction1);
backstack = Arrays.asList(transaction2, transaction1);
childRouter.setBackstack(backstack, null);
assertEquals(2, transaction2.transactionIndex);
@@ -371,4 +415,20 @@ public class RouterTests {
assertEquals(0, childRouter.getBackstackSize());
}
@Test
public void testRemovesAllViewsOnDestroy() {
Controller controller1 = new TestController();
Controller controller2 = new TestController();
router.setRoot(RouterTransaction.with(controller1));
router.pushController(RouterTransaction.with(controller2)
.pushChangeHandler(new FadeChangeHandler(false)));
assertEquals(2, router.container.getChildCount());
router.destroy(true);
assertEquals(0, router.container.getChildCount());
}
}
@@ -0,0 +1,132 @@
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 com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ViewLeakTests {
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 testPop() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller));
assertNotNull(controller.getView());
router.popCurrentController();
assertNull(controller.getView());
}
@Test
public void testPopWhenPushNeverAdded() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverAddChangeHandler()));
assertNotNull(controller.getView());
router.popCurrentController();
assertNull(controller.getView());
}
@Test
public void testPopWhenPushNeverCompleted() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverCompleteChangeHandler()));
assertNotNull(controller.getView());
router.popCurrentController();
assertNull(controller.getView());
}
@Test
public void testActivityStop() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller));
assertNotNull(controller.getView());
activityProxy.stop(true);
assertNull(controller.getView());
}
@Test
public void testActivityStopWhenPushNeverCompleted() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverCompleteChangeHandler()));
assertNotNull(controller.getView());
activityProxy.stop(true);
assertNull(controller.getView());
}
@Test
public void testActivityDestroyWhenPushNeverAdded() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverAddChangeHandler()));
assertNotNull(controller.getView());
activityProxy.stop(true).destroy();
assertNull(controller.getView());
}
public static class NeverAddChangeHandler extends ControllerChangeHandler {
@Override
public void performChange(@NonNull final ViewGroup container, @Nullable View from, @Nullable final View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
if (from != null) {
container.removeView(from);
}
}
}
public static class NeverCompleteChangeHandler extends ControllerChangeHandler {
@Override
public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
if (from != null) {
container.removeView(from);
}
container.addView(to);
}
}
}
@@ -19,10 +19,8 @@ public class CallState implements Parcelable {
public int onActivityResultCalls;
public int onRequestPermissionsResultCalls;
public int createOptionsMenuCalls;
public CallState() {
this(false);
}
public int contextAvailableCalls;
public int contextUnavailableCalls;
public CallState(boolean setupForAddedController) {
if (setupForAddedController) {
@@ -30,6 +28,7 @@ public class CallState implements Parcelable {
changeEndCalls++;
createViewCalls++;
attachCalls++;
contextAvailableCalls++;
}
}
@@ -42,8 +41,14 @@ public class CallState implements Parcelable {
return false;
}
CallState callState = (CallState)o;
CallState callState = (CallState) o;
if (contextAvailableCalls != callState.contextAvailableCalls) {
return false;
}
if (contextUnavailableCalls != callState.contextUnavailableCalls) {
return false;
}
if (changeStartCalls != callState.changeStartCalls) {
return false;
}
@@ -102,6 +107,8 @@ public class CallState implements Parcelable {
result = 31 * result + onActivityResultCalls;
result = 31 * result + onRequestPermissionsResultCalls;
result = 31 * result + createOptionsMenuCalls;
result = 31 * result + contextAvailableCalls;
result = 31 * result + contextUnavailableCalls;
return result;
}
@@ -122,6 +129,8 @@ public class CallState implements Parcelable {
"\n onActivityResultCalls=" + onActivityResultCalls +
"\n onRequestPermissionsResultCalls=" + onRequestPermissionsResultCalls +
"\n createOptionsMenuCalls=" + createOptionsMenuCalls +
"\n contextAvailableCalls=" + contextAvailableCalls +
"\n contextUnavailableCalls=" + contextUnavailableCalls +
"}\n";
}
@@ -144,11 +153,13 @@ public class CallState implements Parcelable {
out.writeInt(onActivityResultCalls);
out.writeInt(onRequestPermissionsResultCalls);
out.writeInt(createOptionsMenuCalls);
out.writeInt(contextAvailableCalls);
out.writeInt(contextUnavailableCalls);
}
public static final Parcelable.Creator<CallState> CREATOR = new Parcelable.Creator<CallState>() {
public static final Parcelable.Creator<CallState> CREATOR = new Parcelable.Creator<CallState>() {
public CallState createFromParcel(Parcel in) {
CallState state = new CallState();
CallState state = new CallState(false);
state.changeStartCalls = in.readInt();
state.changeEndCalls = in.readInt();
@@ -164,6 +175,8 @@ public class CallState implements Parcelable {
state.onActivityResultCalls = in.readInt();
state.onRequestPermissionsResultCalls = in.readInt();
state.createOptionsMenuCalls = in.readInt();
state.contextAvailableCalls = in.readInt();
state.contextUnavailableCalls = in.readInt();
return state;
}
@@ -172,4 +185,4 @@ public class CallState implements Parcelable {
return new CallState[size];
}
};
}
}
@@ -1,16 +0,0 @@
package com.bluelinelabs.conductor.util;
import java.util.ArrayList;
import java.util.List;
public class ListUtils {
public static <T> List<T> listOf(T... elements) {
List<T> list = new ArrayList<>();
for (T element : elements) {
list.add(element);
}
return list;
}
}
@@ -1,5 +1,6 @@
package com.bluelinelabs.conductor.util;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.IdRes;
@@ -23,7 +24,7 @@ public class TestController extends Controller {
private static final String KEY_CALL_STATE = "TestController.currentCallState";
public CallState currentCallState = new CallState();
public CallState currentCallState = new CallState(false);
public ChangeHandlerHistory changeHandlerHistory = new ChangeHandlerHistory();
@NonNull
@@ -63,6 +64,18 @@ public class TestController extends Controller {
}
}
@Override
protected void onContextAvailable(@NonNull Context context) {
super.onContextAvailable(context);
currentCallState.contextAvailableCalls++;
}
@Override
protected void onContextUnavailable() {
super.onContextUnavailable();
currentCallState.contextUnavailableCalls++;
}
@Override
protected void onAttach(@NonNull View view) {
super.onAttach(view);
Executable → Regular
+11 -23
View File
@@ -1,23 +1,8 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
android {
compileSdkVersion 25
buildToolsVersion '24.0.3'
lintOptions {
abortOnError true
ignore 'UnusedResources'
}
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
@@ -26,10 +11,11 @@ android {
defaultConfig {
applicationId "com.bluelinelabs.conductor.demo"
minSdkVersion 16
targetSdkVersion 25
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.0"
vectorDrawables.useSupportLibrary true
}
buildTypes {
@@ -48,13 +34,15 @@ dependencies {
compile rootProject.ext.supportV4
compile rootProject.ext.supportDesign
apt rootProject.ext.butterknifeCompiler
annotationProcessor rootProject.ext.butterknifeCompiler
compile rootProject.ext.butterknife
compile rootProject.ext.picasso
compile project(':conductor-support')
compile project(':conductor-rxlifecycle')
compile project(':conductor-rxlifecycle2')
compile project(':conductor-modules:support')
compile project(':conductor-modules:rxlifecycle')
compile project(':conductor-modules:rxlifecycle2')
compile project(':conductor-modules:autodispose')
compile project(':conductor-modules:arch-components-lifecycle')
debugCompile rootProject.ext.leakCanary
releaseCompile rootProject.ext.leakCanaryNoOp
Vendored Executable → Regular
View File
View File
View File
@@ -0,0 +1,110 @@
package com.bluelinelabs.conductor.demo.changehandler;
import android.annotation.TargetApi;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.transition.Fade;
import android.transition.Transition;
import android.transition.TransitionSet;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.changehandler.TransitionChangeHandler;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.changehandler.transitions.FabTransform;
import com.bluelinelabs.conductor.demo.util.AnimUtils;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class FabToDialogTransitionChangeHandler extends TransitionChangeHandler {
private View fab;
private View dialogBackground;
private ViewGroup fabParent;
@NonNull @Override
protected Transition getTransition(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, boolean isPush) {
Transition backgroundFade = new Fade();
backgroundFade.addTarget(R.id.dialog_background);
Transition fabTransform = new FabTransform(ContextCompat.getColor(container.getContext(), R.color.colorAccent), R.drawable.ic_github_face);
TransitionSet set = new TransitionSet();
set.addTransition(backgroundFade);
set.addTransition(fabTransform);
return set;
}
@Override
public void prepareForTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @NonNull Transition transition, boolean isPush, @NonNull OnTransitionPreparedListener onTransitionPreparedListener) {
fab = isPush ? from.findViewById(R.id.fab) : to.findViewById(R.id.fab);
fabParent = (ViewGroup)fab.getParent();
if (!isPush) {
/*
* Before we transition back we want to remove the fab
* in order to add it again for the TransitionManager to be able to detect the change
*/
fabParent.removeView(fab);
fab.setVisibility(View.VISIBLE);
/*
* Before we transition back we need to move the dialog's background to the new view
* so its fade won't take place over the fab transition
*/
dialogBackground = from.findViewById(R.id.dialog_background);
((ViewGroup)dialogBackground.getParent()).removeView(dialogBackground);
fabParent.addView(dialogBackground);
}
onTransitionPreparedListener.onPrepared();
}
@Override
public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @Nullable Transition transition, boolean isPush) {
if (isPush) {
fabParent.removeView(fab);
container.addView(to);
/*
* After the transition is finished we have to add the fab back to the original container.
* Because otherwise we will be lost when trying to transition back.
* Set it to invisible because we don't want it to jump back after the transition
*/
AnimUtils.TransitionEndListener endListener = new AnimUtils.TransitionEndListener() {
@Override
public void onTransitionCompleted(Transition transition) {
fab.setVisibility(View.GONE);
fabParent.addView(fab);
fab = null;
fabParent = null;
}
};
if (transition != null) {
transition.addListener(endListener);
} else {
endListener.onTransitionCompleted(null);
}
} else {
dialogBackground.setVisibility(View.INVISIBLE);
fabParent.addView(fab);
container.removeView(from);
AnimUtils.TransitionEndListener endListener = new AnimUtils.TransitionEndListener() {
@Override
public void onTransitionCompleted(Transition transition) {
fabParent.removeView(dialogBackground);
dialogBackground = null;
}
};
if (transition != null) {
transition.addListener(endListener);
} else {
endListener.onTransitionCompleted(null);
}
}
}
}
@@ -0,0 +1,164 @@
package com.bluelinelabs.conductor.demo.changehandler;
import android.annotation.TargetApi;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.Transition;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnPreDrawListener;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import java.util.ArrayList;
import java.util.List;
/**
* A TransitionChangeHandler that will wait for views with the passed transition names to be fully laid out
* before executing. An OnPreDrawListener will be added to the "to" view, then to all of its subviews that
* match the transaction names we're interested in. Once all of the views are fully ready, the "to" view
* is set to invisible so that it'll fade in nicely, and the views that we want to use as shared elements
* are removed from their containers, then immediately re-added within the beginDelayedTransition call so
* the system picks them up as shared elements.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class SharedElementDelayingChangeHandler extends ArcFadeMoveChangeHandler {
private static final String KEY_WAIT_FOR_TRANSITION_NAMES = "SharedElementDelayingChangeHandler.waitForTransitionNames";
private final ArrayList<String> waitForTransitionNames;
private final ArrayList<ViewParentPair> removedViews = new ArrayList<>();
private OnPreDrawListener onPreDrawListener;
public SharedElementDelayingChangeHandler() {
waitForTransitionNames = new ArrayList<>();
}
public SharedElementDelayingChangeHandler(@NonNull List<String> waitForTransitionNames) {
this.waitForTransitionNames = new ArrayList<>(waitForTransitionNames);
}
@Override
public void prepareForTransition(@NonNull final ViewGroup container, @Nullable View from, @Nullable final View to, @NonNull Transition transition, boolean isPush, @NonNull final OnTransitionPreparedListener onTransitionPreparedListener) {
if (to != null && to.getParent() == null && waitForTransitionNames.size() > 0) {
onPreDrawListener = new OnPreDrawListener() {
boolean addedSubviewListeners;
@Override
public boolean onPreDraw() {
List<View> foundViews = new ArrayList<>();
for (String transitionName : waitForTransitionNames) {
foundViews.add(getViewWithTransitionName(to, transitionName));
}
if (!foundViews.contains(null) && !addedSubviewListeners) {
addedSubviewListeners = true;
for (final View view : foundViews) {
view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
view.getViewTreeObserver().removeOnPreDrawListener(this);
waitForTransitionNames.remove(view.getTransitionName());
ViewGroup parent = (ViewGroup)view.getParent();
removedViews.add(new ViewParentPair(view, parent));
parent.removeView(view);
if (waitForTransitionNames.size() == 0) {
to.getViewTreeObserver().removeOnPreDrawListener(onPreDrawListener);
onPreDrawListener = null;
to.setVisibility(View.INVISIBLE);
onTransitionPreparedListener.onPrepared();
}
return true;
}
});
}
}
return false;
}
};
to.getViewTreeObserver().addOnPreDrawListener(onPreDrawListener);
container.addView(to);
} else {
onTransitionPreparedListener.onPrepared();
}
}
@Override
public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @Nullable Transition transition, boolean isPush) {
if (to != null) {
to.setVisibility(View.VISIBLE);
for (ViewParentPair removedView : removedViews) {
removedView.parent.addView(removedView.view);
}
removedViews.clear();
}
super.executePropertyChanges(container, from, to, transition, isPush);
}
@Override
public void saveToBundle(@NonNull Bundle bundle) {
bundle.putStringArrayList(KEY_WAIT_FOR_TRANSITION_NAMES, waitForTransitionNames);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
List<String> savedNames = bundle.getStringArrayList(KEY_WAIT_FOR_TRANSITION_NAMES);
if (savedNames != null) {
waitForTransitionNames.addAll(savedNames);
}
}
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
removedViews.clear();
}
@Nullable
View getViewWithTransitionName(@NonNull View view, @NonNull String transitionName) {
if (transitionName.equals(view.getTransitionName())) {
return view;
}
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup)view;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View viewWithTransitionName = getViewWithTransitionName(viewGroup.getChildAt(i), transitionName);
if (viewWithTransitionName != null) {
return viewWithTransitionName;
}
}
}
return null;
}
private static class ViewParentPair {
View view;
ViewGroup parent;
public ViewParentPair(View view, ViewGroup parent) {
this.view = view;
this.parent = parent;
}
}
}
@@ -0,0 +1,295 @@
/*
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Example from https://github.com/nickbutcher/plaid
*/
package com.bluelinelabs.conductor.demo.changehandler.transitions;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.ColorInt;
import android.support.annotation.DrawableRes;
import android.support.v4.content.ContextCompat;
import android.transition.Transition;
import android.transition.TransitionValues;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver.OnPreDrawListener;
import android.view.animation.Interpolator;
import com.bluelinelabs.conductor.demo.util.AnimUtils;
import java.util.ArrayList;
import java.util.List;
import static android.view.View.MeasureSpec.makeMeasureSpec;
/**
* A transition between a FAB & another surface using a circular reveal moving along an arc.
* <p>
* See: https://www.google.com/design/spec/motion/transforming-material.html#transforming-material-radial-transformation
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class FabTransform extends Transition {
private static final long DEFAULT_DURATION = 240L;
private static final String PROP_BOUNDS = "plaid:fabTransform:bounds";
private static final String[] TRANSITION_PROPERTIES = {
PROP_BOUNDS
};
private final int color;
private final int icon;
public FabTransform(@ColorInt int fabColor, @DrawableRes int fabIconResId) {
color = fabColor;
icon = fabIconResId;
setPathMotion(new GravityArcMotion());
setDuration(DEFAULT_DURATION);
}
@Override
public String[] getTransitionProperties() {
return TRANSITION_PROPERTIES;
}
@Override
public void captureStartValues(TransitionValues transitionValues) {
captureValues(transitionValues);
}
@Override
public void captureEndValues(TransitionValues transitionValues) {
captureValues(transitionValues);
}
@Override
public Animator createAnimator(final ViewGroup sceneRoot,
final TransitionValues startValues,
final TransitionValues endValues) {
if (startValues == null || endValues == null) return null;
final Rect startBounds = (Rect) startValues.values.get(PROP_BOUNDS);
final Rect endBounds = (Rect) endValues.values.get(PROP_BOUNDS);
final boolean fromFab = endBounds.width() > startBounds.width();
final View view = endValues.view;
final Rect dialogBounds = fromFab ? endBounds : startBounds;
final Interpolator fastOutSlowInInterpolator =
AnimUtils.getFastOutSlowInInterpolator();
final long duration = getDuration();
final long halfDuration = duration / 2;
final long twoThirdsDuration = duration * 2 / 3;
if (!fromFab) {
// Force measure / layout the dialog back to it's original bounds
view.measure(
makeMeasureSpec(startBounds.width(), View.MeasureSpec.EXACTLY),
makeMeasureSpec(startBounds.height(), View.MeasureSpec.EXACTLY));
view.layout(startBounds.left, startBounds.top, startBounds.right, startBounds.bottom);
}
final int translationX = startBounds.centerX() - endBounds.centerX();
final int translationY = startBounds.centerY() - endBounds.centerY();
if (fromFab) {
view.setTranslationX(translationX);
view.setTranslationY(translationY);
}
// Add a color overlay to fake appearance of the FAB
final ColorDrawable fabColor = new ColorDrawable(color);
fabColor.setBounds(0, 0, dialogBounds.width(), dialogBounds.height());
if (!fromFab) fabColor.setAlpha(0);
view.getOverlay().add(fabColor);
// Add an icon overlay again to fake the appearance of the FAB
final Drawable fabIcon =
ContextCompat.getDrawable(sceneRoot.getContext(), icon).mutate();
final int iconLeft = (dialogBounds.width() - fabIcon.getIntrinsicWidth()) / 2;
final int iconTop = (dialogBounds.height() - fabIcon.getIntrinsicHeight()) / 2;
fabIcon.setBounds(iconLeft, iconTop,
iconLeft + fabIcon.getIntrinsicWidth(),
iconTop + fabIcon.getIntrinsicHeight());
if (!fromFab) fabIcon.setAlpha(0);
view.getOverlay().add(fabIcon);
// Since the view that's being transition to always seems to be on the top (z-order), we have
// to make a copy of the "from" view and put it in the "to" view's overlay, then fade it out.
// There has to be another way to do this, right?
Drawable dialogView = null;
if (!fromFab) {
startValues.view.setDrawingCacheEnabled(true);
startValues.view.buildDrawingCache();
Bitmap viewBitmap = startValues.view.getDrawingCache();
dialogView = new BitmapDrawable(view.getResources(), viewBitmap);
dialogView.setBounds(0, 0, dialogBounds.width(), dialogBounds.height());
view.getOverlay().add(dialogView);
}
// Circular clip from/to the FAB size
final Animator circularReveal;
if (fromFab) {
circularReveal = ViewAnimationUtils.createCircularReveal(view,
view.getWidth() / 2,
view.getHeight() / 2,
startBounds.width() / 2,
(float) Math.hypot(endBounds.width() / 2, endBounds.height() / 2));
circularReveal.setInterpolator(
AnimUtils.getFastOutLinearInInterpolator());
} else {
circularReveal = ViewAnimationUtils.createCircularReveal(view,
view.getWidth() / 2,
view.getHeight() / 2,
(float) Math.hypot(startBounds.width() / 2, startBounds.height() / 2),
endBounds.width() / 2);
circularReveal.setInterpolator(
AnimUtils.getLinearOutSlowInInterpolator());
// Persist the end clip i.e. stay at FAB size after the reveal has run
circularReveal.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
final ViewOutlineProvider fabOutlineProvider = view.getOutlineProvider();
view.setOutlineProvider(new ViewOutlineProvider() {
boolean hasRun = false;
@Override
public void getOutline(final View view, Outline outline) {
final int left = (view.getWidth() - endBounds.width()) / 2;
final int top = (view.getHeight() - endBounds.height()) / 2;
outline.setOval(
left, top, left + endBounds.width(), top + endBounds.height());
if (!hasRun) {
hasRun = true;
view.setClipToOutline(true);
// We have to remove this as soon as it's laid out so we can get the shadow back
view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (view.getWidth() == endBounds.width() && view.getHeight() == endBounds.height()) {
view.setOutlineProvider(fabOutlineProvider);
view.setClipToOutline(false);
view.getViewTreeObserver().removeOnPreDrawListener(this);
return true;
}
return true;
}
});
}
}
});
}
});
}
circularReveal.setDuration(duration);
// Translate to end position along an arc
final Animator translate = ObjectAnimator.ofFloat(
view,
View.TRANSLATION_X,
View.TRANSLATION_Y,
fromFab ? getPathMotion().getPath(translationX, translationY, 0, 0)
: getPathMotion().getPath(0, 0, -translationX, -translationY));
translate.setDuration(duration);
translate.setInterpolator(fastOutSlowInInterpolator);
// Fade contents of non-FAB view in/out
List<Animator> fadeContents = null;
if (view instanceof ViewGroup) {
final ViewGroup vg = ((ViewGroup) view);
fadeContents = new ArrayList<>(vg.getChildCount());
for (int i = vg.getChildCount() - 1; i >= 0; i--) {
final View child = vg.getChildAt(i);
final Animator fade =
ObjectAnimator.ofFloat(child, View.ALPHA, fromFab ? 1f : 0f);
if (fromFab) {
child.setAlpha(0f);
}
fade.setDuration(twoThirdsDuration);
fade.setInterpolator(fastOutSlowInInterpolator);
fadeContents.add(fade);
}
}
// Fade in/out the fab color & icon overlays
final Animator colorFade = ObjectAnimator.ofInt(fabColor, "alpha", fromFab ? 0 : 255);
final Animator iconFade = ObjectAnimator.ofInt(fabIcon, "alpha", fromFab ? 0 : 255);
if (!fromFab) {
colorFade.setStartDelay(halfDuration);
iconFade.setStartDelay(halfDuration);
}
colorFade.setDuration(halfDuration);
iconFade.setDuration(halfDuration);
colorFade.setInterpolator(fastOutSlowInInterpolator);
iconFade.setInterpolator(fastOutSlowInInterpolator);
// Run all animations together
final AnimatorSet transition = new AnimatorSet();
transition.playTogether(circularReveal, translate, colorFade, iconFade);
transition.playTogether(fadeContents);
if (dialogView != null) {
final Animator dialogViewFade = ObjectAnimator.ofInt(dialogView, "alpha", 0).setDuration(twoThirdsDuration);
dialogViewFade.setInterpolator(fastOutSlowInInterpolator);
transition.playTogether(dialogViewFade);
}
transition.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// Clean up
view.getOverlay().clear();
if (!fromFab) {
view.setTranslationX(0);
view.setTranslationY(0);
view.setTranslationZ(0);
view.measure(
makeMeasureSpec(endBounds.width(), View.MeasureSpec.EXACTLY),
makeMeasureSpec(endBounds.height(), View.MeasureSpec.EXACTLY));
view.layout(endBounds.left, endBounds.top, endBounds.right, endBounds.bottom);
}
}
});
return new AnimUtils.NoPauseAnimator(transition);
}
private void captureValues(TransitionValues transitionValues) {
final View view = transitionValues.view;
if (view == null || view.getWidth() <= 0 || view.getHeight() <= 0) return;
transitionValues.values.put(PROP_BOUNDS, new Rect(view.getLeft(), view.getTop(),
view.getRight(), view.getBottom()));
}
}
@@ -0,0 +1,217 @@
/*
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bluelinelabs.conductor.demo.changehandler.transitions;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Path;
import android.os.Build;
import android.transition.ArcMotion;
import android.util.AttributeSet;
/**
* A tweak to {@link ArcMotion} which slightly alters the path calculation. In the real world
* gravity slows upward motion and accelerates downward motion. This class emulates this behavior
* to make motion paths appear more natural.
* <p>
* See https://www.google.com/design/spec/motion/movement.html#movement-movement-within-screen-bounds
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class GravityArcMotion extends ArcMotion {
private static final float DEFAULT_MIN_ANGLE_DEGREES = 0;
private static final float DEFAULT_MAX_ANGLE_DEGREES = 70;
private static final float DEFAULT_MAX_TANGENT = (float)
Math.tan(Math.toRadians(DEFAULT_MAX_ANGLE_DEGREES/2));
private float mMinimumHorizontalAngle = 0;
private float mMinimumVerticalAngle = 0;
private float mMaximumAngle = DEFAULT_MAX_ANGLE_DEGREES;
private float mMinimumHorizontalTangent = 0;
private float mMinimumVerticalTangent = 0;
private float mMaximumTangent = DEFAULT_MAX_TANGENT;
public GravityArcMotion() {}
public GravityArcMotion(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* @inheritDoc
*/
@Override
public void setMinimumHorizontalAngle(float angleInDegrees) {
mMinimumHorizontalAngle = angleInDegrees;
mMinimumHorizontalTangent = toTangent(angleInDegrees);
}
/**
* @inheritDoc
*/
@Override
public float getMinimumHorizontalAngle() {
return mMinimumHorizontalAngle;
}
/**
* @inheritDoc
*/
@Override
public void setMinimumVerticalAngle(float angleInDegrees) {
mMinimumVerticalAngle = angleInDegrees;
mMinimumVerticalTangent = toTangent(angleInDegrees);
}
/**
* @inheritDoc
*/
@Override
public float getMinimumVerticalAngle() {
return mMinimumVerticalAngle;
}
/**
* @inheritDoc
*/
@Override
public void setMaximumAngle(float angleInDegrees) {
mMaximumAngle = angleInDegrees;
mMaximumTangent = toTangent(angleInDegrees);
}
/**
* @inheritDoc
*/
@Override
public float getMaximumAngle() {
return mMaximumAngle;
}
private static float toTangent(float arcInDegrees) {
if (arcInDegrees < 0 || arcInDegrees > 90) {
throw new IllegalArgumentException("Arc must be between 0 and 90 degrees");
}
return (float) Math.tan(Math.toRadians(arcInDegrees / 2));
}
@Override
public Path getPath(float startX, float startY, float endX, float endY) {
// Here's a little ascii art to show how this is calculated:
// c---------- b
// \ / |
// \ d |
// \ / e
// a----f
// This diagram assumes that the horizontal distance is less than the vertical
// distance between The start point (a) and end point (b).
// d is the midpoint between a and b. c is the center point of the circle with
// This path is formed by assuming that start and end points are in
// an arc on a circle. The end point is centered in the circle vertically
// and start is a point on the circle.
// Triangles bfa and bde form similar right triangles. The control points
// for the cubic Bezier arc path are the midpoints between a and e and e and b.
Path path = new Path();
path.moveTo(startX, startY);
float ex;
float ey;
if (startY == endY) {
ex = (startX + endX) / 2;
ey = startY + mMinimumHorizontalTangent * Math.abs(endX - startX) / 2;
} else if (startX == endX) {
ex = startX + mMinimumVerticalTangent * Math.abs(endY - startY) / 2;
ey = (startY + endY) / 2;
} else {
float deltaX = endX - startX;
/**
* This is the only change to ArcMotion
*/
float deltaY;
if (endY < startY) {
deltaY = startY - endY; // Y is inverted compared to diagram above.
} else {
deltaY = endY - startY;
}
/**
* End changes
*/
// hypotenuse squared.
float h2 = deltaX * deltaX + deltaY * deltaY;
// Midpoint between start and end
float dx = (startX + endX) / 2;
float dy = (startY + endY) / 2;
// Distance squared between end point and mid point is (1/2 hypotenuse)^2
float midDist2 = h2 * 0.25f;
float minimumArcDist2 = 0;
if (Math.abs(deltaX) < Math.abs(deltaY)) {
// Similar triangles bfa and bde mean that (ab/fb = eb/bd)
// Therefore, eb = ab * bd / fb
// ab = hypotenuse
// bd = hypotenuse/2
// fb = deltaY
float eDistY = h2 / (2 * deltaY);
ey = endY + eDistY;
ex = endX;
minimumArcDist2 = midDist2 * mMinimumVerticalTangent
* mMinimumVerticalTangent;
} else {
// Same as above, but flip X & Y
float eDistX = h2 / (2 * deltaX);
ex = endX + eDistX;
ey = endY;
minimumArcDist2 = midDist2 * mMinimumHorizontalTangent
* mMinimumHorizontalTangent;
}
float arcDistX = dx - ex;
float arcDistY = dy - ey;
float arcDist2 = arcDistX * arcDistX + arcDistY * arcDistY;
float maximumArcDist2 = midDist2 * mMaximumTangent * mMaximumTangent;
float newArcDistance2 = 0;
if (arcDist2 < minimumArcDist2) {
newArcDistance2 = minimumArcDist2;
} else if (arcDist2 > maximumArcDist2) {
newArcDistance2 = maximumArcDist2;
}
if (newArcDistance2 != 0) {
float ratio2 = newArcDistance2 / arcDist2;
float ratio = (float) Math.sqrt(ratio2);
ex = dx + (ratio * (ex - dx));
ey = dy + (ratio * (ey - dy));
}
}
float controlX1 = (startX + ex) / 2;
float controlY1 = (startY + ey) / 2;
float controlX2 = (ex + endX) / 2;
float controlY2 = (ey + endY) / 2;
path.cubicTo(controlX1, controlY1, controlX2, controlY2, endX, endY);
return path;
}
}
@@ -0,0 +1,152 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.arch.lifecycle.Lifecycle.Event;
import android.arch.lifecycle.LifecycleObserver;
import android.arch.lifecycle.OnLifecycleEvent;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.LayoutInflater;
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.archlifecycle.LifecycleController;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.demo.ActionBarProvider;
import com.bluelinelabs.conductor.demo.DemoApplication;
import com.bluelinelabs.conductor.demo.R;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Unbinder;
public class ArchLifecycleController extends LifecycleController {
private static final String TAG = "ArchLifecycleController";
@BindView(R.id.tv_title) TextView tvTitle;
private Unbinder unbinder;
private boolean hasExited;
public ArchLifecycleController() {
LifecycleObserver lifecycleObserver = new LifecycleObserver() {
@OnLifecycleEvent(Event.ON_CREATE)
void onCreate() {
Log.d(TAG, "LifecycleObserver onCreate() called");
}
@OnLifecycleEvent(Event.ON_START)
void onStart() {
Log.d(TAG, "LifecycleObserver onStart() called");
}
@OnLifecycleEvent(Event.ON_RESUME)
void onResume() {
Log.d(TAG, "LifecycleObserver onResume() called");
}
@OnLifecycleEvent(Event.ON_PAUSE)
void onPause() {
Log.d(TAG, "LifecycleObserver onPause() called");
}
@OnLifecycleEvent(Event.ON_STOP)
void onStop() {
Log.d(TAG, "LifecycleObserver onStop() called");
}
@OnLifecycleEvent(Event.ON_DESTROY)
void onDestroy() {
Log.d(TAG, "LifecycleObserver onDestroy() called");
}
};
Log.i(TAG, "constructor called");
getLifecycle().addObserver(lifecycleObserver);
}
@NonNull
@Override
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
Log.i(TAG, "onCreateView() called");
View view = inflater.inflate(R.layout.controller_lifecycle, container, false);
view.setBackgroundColor(ContextCompat.getColor(container.getContext(), R.color.orange_300));
unbinder = ButterKnife.bind(this, view);
tvTitle.setText(getResources().getString(R.string.rxlifecycle_title, TAG));
return view;
}
@Override
protected void onAttach(@NonNull View view) {
super.onAttach(view);
Log.i(TAG, "onAttach() called");
(((ActionBarProvider) getActivity()).getSupportActionBar()).setTitle("Arch Components Lifecycle Demo");
}
@Override
protected void onDestroyView(@NonNull View view) {
super.onDestroyView(view);
Log.i(TAG, "onDestroyView() called");
unbinder.unbind();
unbinder = null;
}
@Override
protected void onDetach(@NonNull View view) {
super.onDetach(view);
Log.i(TAG, "onDetach() called");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i(TAG, "onDestroy() called");
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);
}
}
@OnClick(R.id.btn_next_release_view) void onNextWithReleaseClicked() {
setRetainViewMode(RetainViewMode.RELEASE_DETACH);
getRouter().pushController(RouterTransaction.with(new TextController("Logcat should now report that the Controller's onDetach() and LifecycleObserver's onPause() methods were called, followed by the Controller's onDestroyView() and LifecycleObserver's onStop()."))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
}
@OnClick(R.id.btn_next_retain_view) void onNextWithRetainClicked() {
setRetainViewMode(RetainViewMode.RETAIN_DETACH);
getRouter().pushController(RouterTransaction.with(new TextController("Logcat should now report that the Controller's onDetach() and LifecycleObserver's onPause() methods were called."))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
}
}
@@ -0,0 +1,170 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.ControllerChangeType;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.demo.ActionBarProvider;
import com.bluelinelabs.conductor.demo.DemoApplication;
import com.bluelinelabs.conductor.demo.R;
import com.uber.autodispose.LifecycleScopeProvider;
import com.uber.autodispose.ObservableScoper;
import java.util.concurrent.TimeUnit;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Unbinder;
import io.reactivex.Observable;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;
// Shamelessly borrowed from the official RxLifecycle demo by Trello and adapted for Conductor Controllers
// instead of Activities or Fragments.
public class AutodisposeController extends Controller {
private static final String TAG = "AutodisposeController";
@BindView(R.id.tv_title) TextView tvTitle;
private Unbinder unbinder;
private boolean hasExited;
private final LifecycleScopeProvider scopeProvider = ControllerScopeProvider.from(this);
public AutodisposeController() {
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose(new Action() {
@Override
public void run() {
Log.i(TAG, "Disposing from constructor");
}
})
.to(new ObservableScoper<Long>(scopeProvider))
.subscribe(new Consumer<Long>() {
@Override
public void accept(Long num) {
Log.i(TAG, "Started in constructor, running until onDestroy(): " + num);
}
});
}
@NonNull
@Override
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
Log.i(TAG, "onCreateView() called");
View view = inflater.inflate(R.layout.controller_lifecycle, container, false);
view.setBackgroundColor(ContextCompat.getColor(container.getContext(), R.color.purple_300));
unbinder = ButterKnife.bind(this, view);
tvTitle.setText(getResources().getString(R.string.rxlifecycle_title, TAG));
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose(new Action() {
@Override
public void run() {
Log.i(TAG, "Disposing from onCreateView()");
}
})
.to(new ObservableScoper<Long>(scopeProvider))
.subscribe(new Consumer<Long>() {
@Override
public void accept(Long num) {
Log.i(TAG, "Started in onCreateView(), running until onDestroyView(): " + num);
}
});
return view;
}
@Override
protected void onAttach(@NonNull View view) {
super.onAttach(view);
Log.i(TAG, "onAttach() called");
(((ActionBarProvider) getActivity()).getSupportActionBar()).setTitle("Autodispose Demo");
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose(new Action() {
@Override
public void run() {
Log.i(TAG, "Disposing from onAttach()");
}
})
.to(new ObservableScoper<Long>(scopeProvider))
.subscribe(new Consumer<Long>() {
@Override
public void accept(Long num) {
Log.i(TAG, "Started in onAttach(), running until onDetach(): " + num);
}
});
}
@Override
protected void onDestroyView(@NonNull View view) {
super.onDestroyView(view);
Log.i(TAG, "onDestroyView() called");
unbinder.unbind();
unbinder = null;
}
@Override
protected void onDetach(@NonNull View view) {
super.onDetach(view);
Log.i(TAG, "onDetach() called");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i(TAG, "onDestroy() called");
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);
}
}
@OnClick(R.id.btn_next_release_view)
void onNextWithReleaseClicked() {
setRetainViewMode(RetainViewMode.RELEASE_DETACH);
getRouter().pushController(RouterTransaction.with(new TextController("Logcat should now report that the observables from onAttach() and onViewBound() have been disposed of, while the constructor observable is still running."))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
}
@OnClick(R.id.btn_next_retain_view)
void onNextWithRetainClicked() {
setRetainViewMode(RetainViewMode.RETAIN_DETACH);
getRouter().pushController(RouterTransaction.with(new TextController("Logcat should now report that the observables from onAttach() has been disposed of, while the constructor and onViewBound() observables are still running."))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
}
}
@@ -0,0 +1,169 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
import butterknife.BindView;
import butterknife.ButterKnife;
public class CityDetailController extends BaseController {
private static final String KEY_TITLE = "CityDetailController.title";
private static final String KEY_IMAGE = "CityDetailController.image";
private static final String[] LIST_ROWS = new String[] {
"• This is a city.",
"• There's some cool stuff about it.",
"• But really this is just a demo, not a city guide app.",
"• This demo is meant to show some nice transitions, as long as you're on Lollipop or later.",
"• You should have seen some sweet shared element transitions using the ImageView and the TextView in the \"header\" above.",
"• This transition utilized some callbacks to ensure all the necessary rows in the RecyclerView were laid about before the transition occurred.",
"• Just adding some more lines so it scrolls now...\n\n\n\n\n\n\nThe end."
};
@BindView(R.id.recycler_view) RecyclerView recyclerView;
@DrawableRes private int imageDrawableRes;
private String title;
public CityDetailController(@DrawableRes int imageDrawableRes, String title) {
this(new BundleBuilder(new Bundle())
.putInt(KEY_IMAGE, imageDrawableRes)
.putString(KEY_TITLE, title)
.build());
}
public CityDetailController(Bundle args) {
super(args);
imageDrawableRes = getArgs().getInt(KEY_IMAGE);
title = getArgs().getString(KEY_TITLE);
}
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_city_detail, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext()));
recyclerView.setAdapter(new CityDetailAdapter(LayoutInflater.from(view.getContext()), title, imageDrawableRes, LIST_ROWS, title));
}
@Override
protected String getTitle() {
return title;
}
static class CityDetailAdapter extends RecyclerView.Adapter<CityDetailAdapter.ViewHolder> {
private static final int VIEW_TYPE_HEADER = 0;
private static final int VIEW_TYPE_DETAIL = 1;
private final LayoutInflater inflater;
private final String title;
@DrawableRes private final int imageDrawableRes;
private final String imageViewTransitionName;
private final String textViewTransitionName;
private final String[] details;
public CityDetailAdapter(LayoutInflater inflater, String title, @DrawableRes int imageDrawableRes, String[] details, String transitionNameBase) {
this.inflater = inflater;
this.title = title;
this.imageDrawableRes = imageDrawableRes;
this.details = details;
imageViewTransitionName = inflater.getContext().getResources().getString(R.string.transition_tag_image_named, transitionNameBase);
textViewTransitionName = inflater.getContext().getResources().getString(R.string.transition_tag_title_named, transitionNameBase);
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return VIEW_TYPE_HEADER;
} else {
return VIEW_TYPE_DETAIL;
}
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == VIEW_TYPE_HEADER) {
return new HeaderViewHolder(inflater.inflate(R.layout.row_city_header, parent, false));
} else {
return new DetailViewHolder(inflater.inflate(R.layout.row_city_detail, parent, false));
}
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
if (getItemViewType(position) == VIEW_TYPE_HEADER) {
((HeaderViewHolder)holder).bind(imageDrawableRes, title, imageViewTransitionName, textViewTransitionName);
} else {
((DetailViewHolder)holder).bind(details[position - 1]);
}
}
@Override
public int getItemCount() {
return 1 + details.length;
}
static class ViewHolder extends RecyclerView.ViewHolder {
ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
static class HeaderViewHolder extends ViewHolder {
@BindView(R.id.image_view) ImageView imageView;
@BindView(R.id.text_view) TextView textView;
public HeaderViewHolder(View itemView) {
super(itemView);
}
void bind(@DrawableRes int imageDrawableRes, String title, String imageTransitionName, String textViewTransitionName) {
imageView.setImageResource(imageDrawableRes);
textView.setText(title);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
imageView.setTransitionName(imageTransitionName);
textView.setTransitionName(textViewTransitionName);
}
}
}
static class DetailViewHolder extends ViewHolder {
@BindView(R.id.text_view) TextView textView;
public DetailViewHolder(View itemView) {
super(itemView);
}
void bind(String detail) {
textView.setText(detail);
}
}
}
}
@@ -0,0 +1,174 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.graphics.PorterDuff.Mode;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
import com.bluelinelabs.conductor.changehandler.TransitionChangeHandlerCompat;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.changehandler.SharedElementDelayingChangeHandler;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
public class CityGridController extends BaseController {
private static final String KEY_TITLE = "CityGridController.title";
private static final String KEY_DOT_COLOR = "CityGridController.dotColor";
private static final String KEY_FROM_POSITION = "CityGridController.position";
private static final CityModel[] CITY_MODELS = new CityModel[] {
new CityModel(R.drawable.chicago, "Chicago"),
new CityModel(R.drawable.jakarta, "Jakarta"),
new CityModel(R.drawable.london, "London"),
new CityModel(R.drawable.sao_paulo, "Sao Paulo"),
new CityModel(R.drawable.tokyo, "Tokyo")
};
@BindView(R.id.tv_title) TextView tvTitle;
@BindView(R.id.img_dot) ImageView imgDot;
@BindView(R.id.recycler_view) RecyclerView recyclerView;
private String title;
private int dotColor;
private int fromPosition;
public CityGridController(String title, int dotColor, int fromPosition) {
this(new BundleBuilder(new Bundle())
.putString(KEY_TITLE, title)
.putInt(KEY_DOT_COLOR, dotColor)
.putInt(KEY_FROM_POSITION, fromPosition)
.build());
}
public CityGridController(Bundle args) {
super(args);
title = getArgs().getString(KEY_TITLE);
dotColor = getArgs().getInt(KEY_DOT_COLOR);
fromPosition = getArgs().getInt(KEY_FROM_POSITION);
}
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_city_grid, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
tvTitle.setText(title);
imgDot.getDrawable().setColorFilter(ContextCompat.getColor(getActivity(), dotColor), Mode.SRC_ATOP);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
tvTitle.setTransitionName(getResources().getString(R.string.transition_tag_title_indexed, fromPosition));
imgDot.setTransitionName(getResources().getString(R.string.transition_tag_dot_indexed, fromPosition));
}
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new GridLayoutManager(view.getContext(), 2));
recyclerView.setAdapter(new CityGridAdapter(LayoutInflater.from(view.getContext()), CITY_MODELS));
}
@Override
protected String getTitle() {
return "Shared Element Demos";
}
void onModelRowClick(CityModel model) {
String imageTransitionName = getResources().getString(R.string.transition_tag_image_named, model.title);
String titleTransitionName = getResources().getString(R.string.transition_tag_title_named, model.title);
List<String> names = new ArrayList<>();
names.add(imageTransitionName);
names.add(titleTransitionName);
getRouter().pushController(RouterTransaction.with(new CityDetailController(model.drawableRes, model.title))
.pushChangeHandler(new TransitionChangeHandlerCompat(new SharedElementDelayingChangeHandler(names), new FadeChangeHandler()))
.popChangeHandler(new TransitionChangeHandlerCompat(new SharedElementDelayingChangeHandler(names), new FadeChangeHandler())));
}
class CityGridAdapter extends RecyclerView.Adapter<CityGridAdapter.ViewHolder> {
private final LayoutInflater inflater;
private final CityModel[] items;
public CityGridAdapter(LayoutInflater inflater, CityModel[] items) {
this.inflater = inflater;
this.items = items;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(inflater.inflate(R.layout.row_city_grid, parent, false));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bind(items[position]);
}
@Override
public int getItemCount() {
return items.length;
}
class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.tv_title) TextView textView;
@BindView(R.id.img_city) ImageView imageView;
private CityModel model;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
void bind(CityModel item) {
model = item;
imageView.setImageResource(item.drawableRes);
textView.setText(item.title);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
textView.setTransitionName(getResources().getString(R.string.transition_tag_title_named, model.title));
imageView.setTransitionName(getResources().getString(R.string.transition_tag_image_named, model.title));
}
}
@OnClick(R.id.row_root)
void onRowClick() {
onModelRowClick(model);
}
}
}
private static class CityModel {
@DrawableRes int drawableRes;
String title;
public CityModel(@DrawableRes int drawableRes, String title) {
this.drawableRes = drawableRes;
this.title = title;
}
}
}
@@ -0,0 +1,60 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
import butterknife.BindView;
import butterknife.OnClick;
public class DialogController extends BaseController {
private static final String KEY_TITLE = "DialogController.title";
private static final String KEY_DESCRIPTION = "DialogController.description";
@BindView(R.id.tv_title) TextView tvTitle;
@BindView(R.id.tv_description) TextView tvDescription;
public DialogController(CharSequence title, CharSequence description) {
this(new BundleBuilder(new Bundle())
.putCharSequence(KEY_TITLE, title)
.putCharSequence(KEY_DESCRIPTION, description)
.build());
}
public DialogController(Bundle args) {
super(args);
}
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_dialog, container, false);
}
@Override
public void onViewBound(@NonNull View view) {
super.onViewBound(view);
tvTitle.setText(getArgs().getCharSequence(KEY_TITLE));
tvDescription.setText(getArgs().getCharSequence(KEY_DESCRIPTION));
tvDescription.setMovementMethod(LinkMovementMethod.getInstance());
}
@OnClick({R.id.dismiss, R.id.dialog_window})
public void dismissDialog() {
getRouter().popController(this);
}
@Override
public boolean handleBack() {
return super.handleBack();
}
}
@@ -6,7 +6,6 @@ import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.changehandler.ScaleFadeChangeHandler;
@@ -14,13 +13,9 @@ import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.widget.ElasticDragDismissFrameLayout;
import com.bluelinelabs.conductor.demo.widget.ElasticDragDismissFrameLayout.ElasticDragDismissCallback;
import butterknife.BindView;
@TargetApi(VERSION_CODES.LOLLIPOP)
public class DragDismissController extends BaseController {
@BindView(R.id.tv_lorem_ipsum) TextView tvLoremIpsum;
private final ElasticDragDismissCallback dragDismissListener = new ElasticDragDismissCallback() {
@Override
public void onDragDismissed() {
@@ -38,11 +33,6 @@ public class DragDismissController extends BaseController {
@Override
protected void onViewBound(@NonNull View view) {
((ElasticDragDismissFrameLayout)view).addListener(dragDismissListener);
tvLoremIpsum.setText("Lorem ipsum dolor sit amet, volutpat lacus egestas integer vitae, tempus potenti posuere dolore, elit cras ut vulputate pede eros. Pharetra curabitur, cum ultrices nisi nulla, non a est diamlorem in pede. Feugiat vivamus id, leo massa, pede ligula libero wisi, posuere nec interdum risus. Mauris eros. Scelerisque etiam dignissim, sem odio magna posuere libero in. Eget non posuere, rutrum nunc ut, ipsum ornare, vestibulum nisl turpis, urna interdum. Arcu mi velit. Sem dolor amet sed hymenaeos tempor. Cras felis.\n" +
"Tempus risus vehicula, mauris nulla interdum purus sodales suspendisse, morbi ultrices tempus vitae vestibulum porttitor, vel mus enim tellus non massa in, quis nec fermentum scelerisque sem congue dolores. Accumsan lacinia urna eu feugiat, habitasse wisi cras id nonummy sapien, a pede. Turpis ac donec, adipiscing ut faucibus, odio ut morbi, habitant volutpat lacinia. At vitae ipsum, porttitor wisi hendrerit pellentesque sapien, suspendisse pellentesque praesent sit tellus varius. At in ligula neque imperdiet eget, viverra lectus risus est sem feugiat. Diam amet non phasellus, sed enim ante etiam lorem id at, feugiat eu urna, urna posuere. Amet vel, vehicula ac in fermentum id, elit mauris dolor, eget orci in nec non in vestibulum. Tortor risus mattis, massa vel condimentum non ornare, pede nunc curabitur dui, eu dolorum luctus duis pellentesque. Nec in imperdiet ac tortor pulvinar bibendum. Parturient lacus luctus posuere, quis luctus sociis tellus iaculis.\n" +
"Eu nec, nulla ut neque ac suspendisse, ac purus mattis pretium orci orci, penatibus sollicitudin pellentesque massa, felis et molestie natoque justo vitae. Laoreet nunc nulla augue semper, nulla ridiculus elit in quis bibendum ultrices, integer duis. Dolor ut donec ligula vehicula odio, in lacinia mattis ut quos, semper libero nulla, et euismod ut, nec curabitur turpis aliquet mauris sit a. Tellus interdum dignissim felis. Ultrices dignissim ut, enim urna adipiscing, nunc accumsan justo odio fringilla magna penatibus. Amet integer metus sollicitudin tristique libero dolor, augue nulla pellentesque, massa in suspendisse, adipiscing donec neque nunc. Malesuada non, luctus vestibulum, et at taciti orci felis. Aliquam dolor nibh erat in tincidunt in, risus suscipit integer, quis dolor quis quam. Ultrices dictum, dignissim interdum aenean pede ornare pretium, vehicula adipiscing enim nec magna mollis duis. Imperdiet dolor velit phasellus, laoreet sed ridiculus sollicitudin sit viverra metus, integer sit nunc fusce nonummy augue. Maecenas adipiscing porttitor eu risus nunc malesuada, quo vitae blandit amet feugiat nunc. Pede hac duis in.\n" +
"Lorem posuere ridiculus donec, volutpat vehicula erat nunc ut, justo occaecati vivamus aliquam massa, felis etiam, sed feugiat molestie eu. Et leo non dignissim nam. Fringilla pretium suspendisse vitae nascetur massa hymenaeos, lectus ut senectus amet, a enim. Pellentesque integer erat, mauris morbi pellentesque, sodales phasellus turpis purus nulla porta, massa vulputate consectetuer habitasse malesuada pulvinar, vehicula in elit eros interdum ut. Et enim vulputate, aliquam donec ullamcorper et, vel consequat tincidunt. Ipsum quam sed ante quis at, ultrices eget. Rutrum qui et velit possimus in, odio amet ut adipiscing sed nec, a tellus ut, quam molestie. Nisl adipiscing euismod nec, eget facilisis ac sit. Suspendisse elit amet consequat dolor senectus vivamus, at scelerisque erat, odio doloribus velit et felis neque, turpis adipiscing, arcu varius placerat leo.");
}
@Override
@@ -0,0 +1,147 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.graphics.PorterDuff.Mode;
import android.support.annotation.ColorRes;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.bluelinelabs.conductor.ControllerChangeHandler;
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.base.BaseController;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
public class ExternalModulesController extends BaseController {
private enum DemoModel {
RX_LIFECYCLE("Rx Lifecycle", R.color.red_300),
RX_LIFECYCLE_2("Rx Lifecycle 2", R.color.blue_grey_300),
AUTODISPOSE("Autodispose", R.color.purple_300),
ARCH_LIFECYCLE("Arch Components Lifecycle", R.color.orange_300);
String title;
@ColorRes int color;
DemoModel(String title, @ColorRes int color) {
this.title = title;
this.color = color;
}
}
@BindView(R.id.recycler_view) RecyclerView recyclerView;
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_additional_modules, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext()));
recyclerView.setAdapter(new AdditionalModulesAdapter(LayoutInflater.from(view.getContext()), DemoModel.values()));
}
@Override
protected void onChangeStarted(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
if (changeType.isEnter) {
setTitle();
}
}
@Override
protected String getTitle() {
return "External Module Demos";
}
void onModelRowClick(DemoModel model) {
switch (model) {
case RX_LIFECYCLE:
getRouter().pushController(RouterTransaction.with(new RxLifecycleController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case RX_LIFECYCLE_2:
getRouter().pushController(RouterTransaction.with(new RxLifecycle2Controller())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case AUTODISPOSE:
getRouter().pushController(RouterTransaction.with(new AutodisposeController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case ARCH_LIFECYCLE:
getRouter().pushController(RouterTransaction.with(new ArchLifecycleController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
}
}
class AdditionalModulesAdapter extends RecyclerView.Adapter<AdditionalModulesAdapter.ViewHolder> {
private final LayoutInflater inflater;
private final DemoModel[] items;
public AdditionalModulesAdapter(LayoutInflater inflater, DemoModel[] items) {
this.inflater = inflater;
this.items = items;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(inflater.inflate(R.layout.row_home, parent, false));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bind(items[position]);
}
@Override
public int getItemCount() {
return items.length;
}
class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.tv_title) TextView tvTitle;
@BindView(R.id.img_dot) ImageView imgDot;
private DemoModel model;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
void bind(DemoModel item) {
model = item;
tvTitle.setText(item.title);
imgDot.getDrawable().setColorFilter(ContextCompat.getColor(getActivity(), item.color), Mode.SRC_ATOP);
}
@OnClick(R.id.row_root)
void onRowClick() {
onModelRowClick(model);
}
}
}
}
@@ -3,6 +3,8 @@ package com.bluelinelabs.conductor.demo.controllers;
import android.content.Intent;
import android.graphics.PorterDuff.Mode;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.ColorRes;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
@@ -26,7 +28,11 @@ import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.ControllerChangeType;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.changehandler.TransitionChangeHandlerCompat;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.changehandler.ArcFadeMoveChangeHandler;
import com.bluelinelabs.conductor.demo.changehandler.FabToDialogTransitionChangeHandler;
import com.bluelinelabs.conductor.demo.controllers.NavigationDemoController.DisplayUpMode;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
@@ -36,30 +42,31 @@ import butterknife.OnClick;
public class HomeController extends BaseController {
public enum HomeDemoModel {
private enum DemoModel {
NAVIGATION("Navigation Demos", R.color.red_300),
TRANSITIONS("Transition Demos", R.color.blue_grey_300),
SHARED_ELEMENT_TRANSITIONS("Shared Element Demos", R.color.purple_300),
CHILD_CONTROLLERS("Child Controllers", R.color.orange_300),
VIEW_PAGER("ViewPager", R.color.green_300),
TARGET_CONTROLLER("Target Controller", R.color.pink_300),
MULTIPLE_CHILD_ROUTERS("Multiple Child Routers", R.color.deep_orange_300),
MASTER_DETAIL("Master Detail", R.color.grey_300),
DRAG_DISMISS("Drag Dismiss", R.color.lime_300),
RX_LIFECYCLE("Rx Lifecycle", R.color.teal_300),
RX_LIFECYCLE_2("Rx Lifecycle 2", R.color.brown_300),
OVERLAY("Overlay Controller", R.color.purple_300);
EXTERNAL_MODULES("Bonus Modules", R.color.teal_300);
String title;
@ColorRes int color;
HomeDemoModel(String title, @ColorRes int color) {
DemoModel(String title, @ColorRes int color) {
this.title = title;
this.color = color;
}
}
private static final String KEY_FAB_VISIBILITY = "HomeController.fabVisibility";
@BindView(R.id.recycler_view) RecyclerView recyclerView;
@BindView(R.id.overlay_root) ViewGroup overlayRoot;
@BindView(R.id.fab) View fab;
public HomeController() {
setHasOptionsMenu(true);
@@ -77,7 +84,21 @@ public class HomeController extends BaseController {
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext()));
recyclerView.setAdapter(new HomeAdapter(LayoutInflater.from(view.getContext()), HomeDemoModel.values()));
recyclerView.setAdapter(new HomeAdapter(LayoutInflater.from(view.getContext()), DemoModel.values()));
}
@Override
protected void onSaveViewState(@NonNull View view, @NonNull Bundle outState) {
super.onSaveViewState(view, outState);
outState.putInt(KEY_FAB_VISIBILITY, fab.getVisibility());
}
@Override
protected void onRestoreViewState(@NonNull View view, @NonNull Bundle savedViewState) {
super.onRestoreViewState(view, savedViewState);
//noinspection WrongConstant
fab.setVisibility(savedViewState.getInt(KEY_FAB_VISIBILITY));
}
@Override
@@ -98,30 +119,7 @@ public class HomeController extends BaseController {
@Override
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);
final String url = "https://github.com/bluelinelabs/Conductor";
SpannableString link = new SpannableString(url);
link.setSpan(new URLSpan(url) {
@Override
public void onClick(View widget) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
}, 0, link.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
SpannableStringBuilder content = new SpannableStringBuilder();
content.append("Conductor");
content.append("\n\n");
content.append(details);
content.append("\n\n");
content.append(link);
getChildRouter(overlayRoot)
.setPopsLastView(true)
.setRoot(RouterTransaction.with(new OverlayController(content))
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
onFabClicked(false);
return true;
}
return super.onOptionsItemSelected(item);
@@ -132,7 +130,40 @@ public class HomeController extends BaseController {
return "Conductor Demos";
}
void onModelRowClick(HomeDemoModel model) {
@OnClick(R.id.fab)
public void onFabClicked() {
onFabClicked(true);
}
private void onFabClicked(boolean fromFab) {
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);
final String url = "https://github.com/bluelinelabs/Conductor";
SpannableString link = new SpannableString(url);
link.setSpan(new URLSpan(url) {
@Override
public void onClick(View widget) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
}, 0, link.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
SpannableStringBuilder description = new SpannableStringBuilder();
description.append(details);
description.append("\n\n");
description.append(link);
ControllerChangeHandler pushHandler = fromFab ? new TransitionChangeHandlerCompat(new FabToDialogTransitionChangeHandler(), new FadeChangeHandler(false)) : new FadeChangeHandler(false);
ControllerChangeHandler popHandler = fromFab ? new TransitionChangeHandlerCompat(new FabToDialogTransitionChangeHandler(), new FadeChangeHandler()) : new FadeChangeHandler();
getRouter()
.pushController(RouterTransaction.with(new DialogController("Conductor", description))
.pushChangeHandler(pushHandler)
.popChangeHandler(popHandler));
}
void onModelRowClick(DemoModel model, int position) {
switch (model) {
case NAVIGATION:
getRouter().pushController(RouterTransaction.with(new NavigationDemoController(0, DisplayUpMode.SHOW_FOR_CHILDREN_ONLY))
@@ -160,27 +191,20 @@ public class HomeController extends BaseController {
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case OVERLAY:
getChildRouter(overlayRoot)
.setPopsLastView(true)
.setRoot(RouterTransaction.with(new OverlayController("I'm an overlay!"))
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
case SHARED_ELEMENT_TRANSITIONS:
getRouter().pushController(RouterTransaction.with(new CityGridController(model.title, model.color, position))
.pushChangeHandler(new TransitionChangeHandlerCompat(new ArcFadeMoveChangeHandler(), new FadeChangeHandler()))
.popChangeHandler(new TransitionChangeHandlerCompat(new ArcFadeMoveChangeHandler(), new FadeChangeHandler())));
break;
case DRAG_DISMISS:
getRouter().pushController(RouterTransaction.with(new DragDismissController())
.pushChangeHandler(new FadeChangeHandler(false))
.popChangeHandler(new FadeChangeHandler()));
break;
case RX_LIFECYCLE:
getRouter().pushController(RouterTransaction.with(new RxLifecycleController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case RX_LIFECYCLE_2:
getRouter().pushController(RouterTransaction.with(new RxLifecycle2Controller())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
case EXTERNAL_MODULES:
getRouter().pushController(RouterTransaction.with(new ExternalModulesController())
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
break;
case MULTIPLE_CHILD_ROUTERS:
getRouter().pushController(RouterTransaction.with(new MultipleChildRouterController())
@@ -198,9 +222,9 @@ public class HomeController extends BaseController {
class HomeAdapter extends RecyclerView.Adapter<HomeAdapter.ViewHolder> {
private final LayoutInflater inflater;
private final HomeDemoModel[] items;
private final DemoModel[] items;
public HomeAdapter(LayoutInflater inflater, HomeDemoModel[] items) {
public HomeAdapter(LayoutInflater inflater, DemoModel[] items) {
this.inflater = inflater;
this.items = items;
}
@@ -212,7 +236,7 @@ public class HomeController extends BaseController {
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bind(items[position]);
holder.bind(position, items[position]);
}
@Override
@@ -224,22 +248,29 @@ public class HomeController extends BaseController {
@BindView(R.id.tv_title) TextView tvTitle;
@BindView(R.id.img_dot) ImageView imgDot;
private HomeDemoModel model;
private DemoModel model;
private int position;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
void bind(HomeDemoModel item) {
void bind(int position, DemoModel item) {
model = item;
tvTitle.setText(item.title);
imgDot.getDrawable().setColorFilter(ContextCompat.getColor(getActivity(), item.color), Mode.SRC_ATOP);
this.position = position;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
tvTitle.setTransitionName(getResources().getString(R.string.transition_tag_title_indexed, position));
imgDot.setTransitionName(getResources().getString(R.string.transition_tag_dot_indexed, position));
}
}
@OnClick(R.id.row_root)
void onRowClick() {
onModelRowClick(model);
onModelRowClick(model, position);
}
}
@@ -1,46 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
import butterknife.BindView;
public class OverlayController extends BaseController {
private static final String KEY_TEXT = "OverlayController.text";
@BindView(R.id.text_view) TextView textView;
public OverlayController(CharSequence text) {
this(new BundleBuilder(new Bundle())
.putCharSequence(KEY_TEXT, text)
.build());
}
public OverlayController(Bundle args) {
super(args);
}
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_overlay, container, false);
}
@Override
public void onViewBound(@NonNull View view) {
super.onViewBound(view);
textView.setText(getArgs().getCharSequence(KEY_TEXT));
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
}
@@ -30,7 +30,7 @@ public class PagerController extends BaseController {
public PagerController() {
pagerAdapter = new RouterPagerAdapter(this) {
@Override
public void configureRouter(Router router, int position) {
public void configureRouter(@NonNull Router router, int position) {
if (!router.hasRootController()) {
Controller page = new ChildController(String.format(Locale.getDefault(), "Child #%d (Swipe to see more)", position), PAGE_COLORS[position], true);
router.setRoot(RouterTransaction.with(page));
@@ -49,6 +49,12 @@ public class PagerController extends BaseController {
};
}
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_pager, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
@@ -58,16 +64,13 @@ public class PagerController extends BaseController {
@Override
protected void onDestroyView(@NonNull View view) {
viewPager.setAdapter(null);
if (!getActivity().isChangingConfigurations()) {
viewPager.setAdapter(null);
}
tabLayout.setupWithViewPager(null);
super.onDestroyView(view);
}
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_pager, container, false);
}
@Override
protected String getTitle() {
return "ViewPager Demo";
@@ -1,6 +1,7 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -60,7 +61,8 @@ public class RxLifecycle2Controller extends RxController {
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
Log.i(TAG, "onCreateView() called");
View view = inflater.inflate(R.layout.controller_rxlifecycle, container, false);
View view = inflater.inflate(R.layout.controller_lifecycle, container, false);
view.setBackgroundColor(ContextCompat.getColor(container.getContext(), R.color.blue_grey_300));
unbinder = ButterKnife.bind(this, view);
tvTitle.setText(getResources().getString(R.string.rxlifecycle_title, TAG));
@@ -69,7 +71,7 @@ public class RxLifecycle2Controller extends RxController {
.doOnDispose(new Action() {
@Override
public void run() {
Log.i(TAG, "Disposing from onCreateView)");
Log.i(TAG, "Disposing from onCreateView()");
}
})
.compose(this.<Long>bindUntilEvent(ControllerEvent.DESTROY_VIEW))
@@ -1,6 +1,7 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -60,7 +61,8 @@ public class RxLifecycleController extends RxController {
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
Log.i(TAG, "onCreateView() called");
View view = inflater.inflate(R.layout.controller_rxlifecycle, container, false);
View view = inflater.inflate(R.layout.controller_lifecycle, container, false);
view.setBackgroundColor(ContextCompat.getColor(container.getContext(), R.color.red_300));
unbinder = ButterKnife.bind(this, view);
tvTitle.setText(getResources().getString(R.string.rxlifecycle_title, TAG));

Some files were not shown because too many files have changed in this diff Show More