Compare commits

..

110 Commits

Author SHA1 Message Date
Eric Kuck 769d552e88 Fixed typo in lifecycle diagram 2017-02-01 19:13:50 -06:00
Eric Kuck afa4b69d7a Version bump 2017-02-01 19:04:01 -06:00
Eric Kuck a9bdf0dd06 Revamped how child backtacks are handled to be more reliable with unforseen uses of state restoration. Fixes #194 and #217 2017-02-01 18:44:33 -06:00
Eric Kuck 6ffa94ed3a Added documentation for @Nullable’s. Fixes #218 2017-02-01 11:01:29 -06:00
blazeroni 690001ed2a Fixes issue when retrieving an existing controller from a ControllerPagerAdapter (#216) 2017-02-01 10:12:36 -06:00
Eric Kuck 90e015b6b3 Now correctly calls onDetach when the host activity is stopped, even if the view is not detached from the window. Fixes #213 2017-01-31 18:06:23 -06:00
Eric Kuck 44bcd0f977 Fixes #205 2017-01-20 17:35:15 -06:00
Eric Kuck 60d0fabcf4 Fixes #208 2017-01-20 17:15:33 -06:00
Eric Kuck 10a1c8af3e Version bump 2017-01-19 14:21:48 -06:00
Eric Kuck 6834df73e6 Fixes #203 2017-01-19 14:13:26 -06:00
Eric Kuck 04d40a5b90 Minor ViewAttachHandler optimizations 2017-01-19 13:21:02 -06:00
Allan Hasegawa be40900e1e #Fixes 206 (#207) 2017-01-19 13:20:20 -06:00
Eric Kuck f16f7b6d2c Fixes #205 2017-01-19 12:02:31 -06:00
Eric Kuck f74f8391b6 Fixes #204 (also mentioned in #199) 2017-01-19 11:50:07 -06:00
Eric Kuck 2ce8c0a45d Version bump 2017-01-18 13:00:24 -06:00
Eric Kuck 77ad6b4512 Now sets ClassLoader for view state Bundle - potential fix for #198 2017-01-18 12:18:06 -06:00
Eric Kuck efbdf913bd Fixes #201 2017-01-18 11:54:50 -06:00
Eric Kuck dfb01389f7 Fixes #199 2017-01-17 16:45:56 -06:00
Eric Kuck e390261b53 Fixes #197 2017-01-15 17:33:40 -06:00
Eric Kuck 27f5275172 Now throws an exception when a destroyed controller is pushed in order to make it more obvious that controllers can not be reused. 2016-12-26 15:25:16 -06:00
Eric Kuck d15f2b68ab Added missing files from e7c195d910 2016-12-16 11:35:45 -06:00
Eric Kuck 44ed19858b Fixes #185 2016-12-15 15:22:26 -06:00
Eric Kuck e7c195d910 Now internally ensures that ControllerChangeHandlers aren't reused, unless they're specifically designed to be safe for reuse
Made ControllerChangeHandler's copy() method public
Fixes #179
2016-12-15 15:11:25 -06:00
Eric Kuck 23a4dbbb60 Travis yml fix 2016-12-14 20:20:04 -06:00
Eric Kuck 0ac81767dd Travis.yml licenses fix 2016-12-14 20:05:38 -06:00
Eric Kuck 54cdc51557 Added license to travis.yml 2016-12-14 19:48:55 -06:00
Eric Kuck 09ce640d9c Fixes #183 2016-12-14 19:42:10 -06:00
Eric Kuck 01df673a34 Switched lint checks to the new PSI based api (closes #184) 2016-12-14 19:41:41 -06:00
Eric Kuck 553bae0be5 Updated readme to include mention of RxLifecycle2 2016-12-14 17:53:45 -06:00
Eric Kuck 48dc4abcbe Version bump 2016-12-13 17:09:19 -06:00
Eric Kuck 4a814afb5f Fixes #166 2016-12-13 16:56:22 -06:00
Eric Kuck 9cd225e704 Controllers now throw an exception when the user forgets to pass false for LayoutInflater.inflate's attachToRoot parameter. 2016-12-12 14:31:22 -06:00
Eric Kuck c8640af1ac Remove saveState option for RouterPagerAdapters, as users can configure the page before display anyway. 2016-12-12 13:25:57 -06:00
Eric Kuck 43c825f7c2 - Child backstack is now properly restored when Android kills the process
- Added a RouterPagerAdapter, which allows the use of Routers as pages
2016-12-12 13:09:29 -06:00
Eric Kuck 7334ed5300 ControllerPagerAdapter updates to enable using a per-page router if needed. 2016-12-12 12:24:26 -06:00
Eric Kuck 7ea4872ff8 Updated ordering of calls in backstack to be in line with other backstack-affecting calls 2016-12-08 13:25:11 -06:00
Eric Kuck 9655170bd2 Fixes tests 2016-12-07 16:34:38 -06:00
Eric Kuck acce9b1702 Simplified setRoot implementation 2016-12-07 16:17:32 -06:00
Eric Kuck 95baa8baa3 Fixes #172 2016-12-01 18:00:06 -06:00
Eric Kuck 2388fa2d06 Added a demo for the new RxLifecycle2Controller 2016-12-01 17:56:43 -06:00
Vishnu Rajeevan 285eb59da0 add rxlifecycle2 support, fixes #148 (#171) 2016-12-01 17:31:52 -06:00
Eric Kuck ae42ee1674 Attempted fix for #165 2016-12-01 17:28:15 -06:00
Eric Kuck 638b2ad311 Version bump 2016-11-11 11:10:35 -06:00
Eric Kuck 96e068d348 Fixes a controller's internal backstack when setBackstack is used on a child router 2016-11-11 10:44:37 -06:00
Eric Kuck db359d906b Un-deprecated getChildRouter with a tag. Fixes #160. 2016-11-10 13:34:13 -06:00
Eric Kuck 11185458b3 Fixed nullable annotations for menu callbacks 2016-11-09 16:03:20 -06:00
Eric Kuck 977db6b5bf Merge branch 'develop' of github.com:bluelinelabs/Conductor into develop 2016-11-09 15:32:01 -06:00
Eric Kuck 803c20e093 Added UiThread annotations - closes #145 2016-11-09 15:25:28 -06:00
TMTron 9948cb4652 Navigation Demos: "GO UP" is hidden in "Controller #0" - closes #158 (#159)
* Navigation Demos: "GO UP" is hidden in "Controller #0" - this closes #158

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

* Make deploy_snapshot.sh executable
2016-07-29 15:11:04 -05:00
dimsuz 9f640380bf Document a return value of Router.handleBack() (#93) 2016-07-20 17:49:59 -05:00
Eric Kuck a3faaede61 Fixes #97 and cleans up some tests 2016-07-20 15:41:19 -05:00
Eric Kuck 94d8add220 Fixes #98 2016-07-20 14:56:12 -05:00
Eric Kuck 6d4b5a5ef6 Version bump 2016-07-12 13:20:30 -05:00
Eric Kuck ae27c5e453 Another test for #86 2016-07-11 16:30:31 -05:00
Eric Kuck e4d23a7c74 Added some tests around #86 2016-07-11 16:24:07 -05:00
Eric Kuck 778cdcfd58 Controllers now properly persist their retain view modes 2016-07-11 13:37:38 -05:00
Eric Kuck 5e730947aa Corrected logic for first fix on #86 2016-07-11 13:37:22 -05:00
Eric Kuck 71b10c7365 Fixed back handling on child controllers demo 2016-07-08 15:08:05 -05:00
Eric Kuck 8c323b9613 Fixes issue with demo app trying to reuse change handlers 2016-07-08 14:59:19 -05:00
Eric Kuck af45aae110 Fixes 2nd issue associated with #86 2016-07-08 13:33:47 -05:00
Hannes Dorfmann d4c7e5791e Added oss sonatype snapshot url (#90) 2016-07-08 08:23:30 -05:00
Eric Kuck dc4c3a9709 Fixes at least one cause of #86 2016-07-07 20:59:37 -05:00
Eric Kuck ef84fbd547 AnimatorChangeHandlers are now properly cancelable 2016-07-07 18:32:04 -05:00
Eric Kuck 431569763e First pass at a fix for #85 2016-07-07 17:43:26 -05:00
Mykhailo Shchurov 7b5bab3681 Fixed typo in readme (#83) 2016-06-30 07:25:21 -05:00
Eric Kuck 86b32afdcb Fixed the build 2016-06-27 18:06:30 -05:00
Eric Kuck 557e4c2122 V2 (#80)
* Added test for adding and removing children

* [WIP] First pass at nested routers. Still needs a bunch of refactoring, testing, and documentation.

* Pulled save and restore calls out of the lifecycle, as Activity's callbacks make it impossible to fit into a nice little box (#48)

* [WIP] Removed old way of setting root controller in favor of reusing transactions

* [WIP] Added a bottom navigation demo for testing nested routers. Still have a bunch of issues with it.

* [WIP] Child routers now receive events properly (and don't crash most of the time!)

* Fixed a few leaks + handling of back presses in child routers

* Fixed tests

* Simplified making routers not handle back events

* Fixed onAttach callback for Android < 6

* Several child router bug fixes

* [WIP] Backstack getter and setter now exists (#64)

* Fixed some issues with viewpagers

* Added support for saving and restoring controllers in a pager adapter

* Fixed issue with re-attached child controllers prematurely destroying

* Better demo for multiple child routers + upgraded to butterknife 8

* Added basic master/detail flow demo

* Fixed issue where simultaneously pushing 2 controllers with animated transitions would cause the first controller's child routers to never re-load.

* Doc updates

* Removed ControllerTransaction base class, as everything is now added to routers.
Remove RouterTransaction.Builder and replaced it with a more fluent api

* Doc update

* Fixed tesets

* Doc update (had trouble with last version on maven central)

* Just some minor refactoring

* See ya, hungarian notation (#77)

* Should fix #75

* Adds public getters for RouterTransaction fields (#76)

* Doc updates

* Fixed #78 (deeply nested controllers not correctly restored)

* Fixes #79 (child routers no longer monitored after being restored)

* Final doc updates for v2
2016-06-27 17:57:50 -05:00
Matthew Haughton 8b29c1cd56 add link to javadoc.io from README (#72) 2016-06-24 16:30:43 -05:00
Mykhailo Shchurov 30ef5a187b Added cause to the instantiation exception (#62) 2016-06-11 16:01:10 -05:00
Mykhailo Shchurov ebe69bf98b Target Controller large bitmap issue fix 2016-06-06 13:14:56 -05:00
Mateusz Perlak a907263ab8 #57 - hiding keyboard onDetach (#58)
* hiding keyboard onDetach

* removed not needed isActive check
2016-06-06 13:14:33 -05:00
Eric Kuck 7528437f65 HorizontalChangeHandler now correctly resets the X translation. 2016-05-24 14:45:33 -07:00
adi1133 e2e8260876 Add proguard rules (#51)
* Add proguard rules

* Add default constructor to proguard rules
2016-05-16 15:35:16 -05:00
124 changed files with 6825 additions and 2530 deletions
Regular → Executable
View File
+7 -9
View File
@@ -3,9 +3,11 @@ language: android
android:
components:
- tools
- build-tools-23.0.2
- android-23
- build-tools-24.0.3
- android-25
- extra-android-m2repository
licenses:
- '.+'
script:
- ./gradlew test
@@ -14,13 +16,9 @@ jdk:
- oraclejdk8
branches:
except:
- v2
- feature/nested_controllers
- feature/save_restore_lifecycle
before_install:
- chmod +x .buildscript/deploy_snapshot.sh
only:
- develop
- master
after_success:
- .buildscript/deploy_snapshot.sh
+32 -16
View File
@@ -1,4 +1,4 @@
[![Travis Build](https://travis-ci.org/bluelinelabs/Conductor.svg)](https://travis-ci.org/bluelinelabs/Conductor) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Conductor-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/3361)
[![Travis Build](https://travis-ci.org/bluelinelabs/Conductor.svg)](https://travis-ci.org/bluelinelabs/Conductor) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Conductor-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/3361) [![Javadocs](http://javadoc.io/badge/com.bluelinelabs/conductor.svg)](http://javadoc.io/doc/com.bluelinelabs/conductor)
# Conductor
@@ -20,21 +20,37 @@ Conductor is architecture-agnostic and does not try to force any design decision
## Installation
```gradle
compile 'com.bluelinelabs:conductor:1.1.6'
compile 'com.bluelinelabs:conductor:2.1.0'
// If you want the components that go along with
// Android's support libraries (currently just a PagerAdapter):
compile 'com.bluelinelabs:conductor-support:1.1.6'
compile 'com.bluelinelabs:conductor-support:2.1.0'
// If you want RxJava/RxAndroid lifecycle support:
compile 'com.bluelinelabs:conductor-rxlifecycle:1.1.6'
// If you want RxJava lifecycle support:
compile 'com.bluelinelabs:conductor-rxlifecycle:2.1.0'
// If you want RxJava2 lifecycle support:
compile 'com.bluelinelabs:conductor-rxlifecycle2:2.1.0'
```
SNAPSHOT:
```gradle
compile 'com.bluelinelabs:conductor:1.1.7-SNAPSHOT'
compile 'com.bluelinelabs:conductor-support:1.1.7-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle:1.1.7-SNAPSHOT'
compile 'com.bluelinelabs:conductor:2.1.1-SNAPSHOT'
compile 'com.bluelinelabs:conductor-support:2.1.1-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle:2.1.1-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle2:2.1.1-SNAPSHOT'
```
You also have to add the url to the snapshot repository:
```gradle
allprojects {
repositories {
...
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
}
```
## Components to Know
@@ -53,7 +69,7 @@ __ControllerTransaction__ | Transactions are used to define data about adding Co
```java
public class MainActivity extends Activity {
private Router mRouter;
private Router router;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -61,17 +77,17 @@ public class MainActivity extends Activity {
setContentView(R.layout.activity_main);
ViewGroup container = (ViewGroup)findViewById(R.id.controller_container)
ViewGroup container = (ViewGroup)findViewById(R.id.controller_container);
mRouter = Conductor.attachRouter(this, container, savedInstanceState);
if (!mRouter.hasRootController()) {
mRouter.setRoot(new HomeController());
router = Conductor.attachRouter(this, container, savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new HomeController()));
}
}
@Override
public void onBackPressed() {
if (!mRouter.handleBack()) {
if (!router.handleBack()) {
super.onBackPressed();
}
}
@@ -112,8 +128,8 @@ The lifecycle of a Controller is significantly simpler to understand than that o
### Custom Change Handlers
`ControllerChangeHandler` can be subclassed in order to perform different functions when changing between two `Controllers`. Two convenience `ControllerChangeHandler` subclasses are included to cover most basic needs: `AnimatorChangeHandler`, which will use an `Animator` object to transition between two views, and `TransitionChangeHandler`, which will use Lollipop's `Transition` framework for transitioning between views.
### Child Controllers
`addChildController` can be called on a `Controller` in order to add nested `Controller`s. Child `Controller`s will receive all lifecycle callbacks that parents get.
### Child Routers & Controllers
`getChildRouter` can be called on a `Controller` in order to get a nested `Router` into which child `Controller`s can be pushed. This enables creating advanced layouts, such as Master/Detail.
### RxJava Lifecycle
If the RxLifecycle dependency has been added, there is an `RxController` available that can be used along with the standard [RxLifecycle library](https://github.com/trello/RxLifecycle). There is also a `ControllerLifecycleProvider` available if you do not wish to use this subclass.
+2 -6
View File
@@ -3,19 +3,15 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
classpath 'com.android.tools.build:gradle:2.2.3'
}
}
allprojects {
repositories {
jcenter()
mavenLocal()
mavenCentral()
}
}
task wrapper(type: Wrapper) {
gradleVersion = '2.10'
}
apply from: rootProject.file('dependencies.gradle')
+6
View File
@@ -1,5 +1,8 @@
apply plugin: 'java'
targetCompatibility = JavaVersion.VERSION_1_7
sourceCompatibility = JavaVersion.VERSION_1_7
configurations {
lintChecks
}
@@ -8,6 +11,9 @@ dependencies {
compile rootProject.ext.lintapi
compile rootProject.ext.lintchecks
testCompile rootProject.ext.lint
testCompile rootProject.ext.lintTests
lintChecks files(jar)
}
@@ -1,7 +1,6 @@
package com.bluelinelabs.conductor.lint;
import com.android.annotations.NonNull;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.tools.lint.client.api.JavaEvaluator;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
@@ -9,21 +8,13 @@ import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiMethod;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.List;
import lombok.ast.ClassDeclaration;
import lombok.ast.ConstructorDeclaration;
import lombok.ast.Node;
import lombok.ast.NormalTypeBody;
import lombok.ast.StrictListAccessor;
import lombok.ast.TypeMember;
import lombok.ast.VariableDefinition;
public final class ControllerChangeHandlerIssueDetector extends Detector implements Detector.JavaScanner, Detector.ClassScanner {
public final class ControllerChangeHandlerIssueDetector extends Detector implements Detector.JavaPsiScanner {
public static final Issue ISSUE =
Issue.create("ValidControllerChangeHandler", "ControllerChangeHandler not instantiatable",
@@ -34,67 +25,45 @@ public final class ControllerChangeHandlerIssueDetector extends Detector impleme
public ControllerChangeHandlerIssueDetector() { }
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public List<String> applicableSuperClasses() {
return Collections.singletonList("com.bluelinelabs.conductor.ControllerChangeHandler");
}
@Override
public void checkClass(@NonNull JavaContext context, ClassDeclaration node,
@NonNull Node declarationOrAnonymous, @NonNull ResolvedClass cls) {
if (node == null) {
public void checkClass(JavaContext context, PsiClass declaration) {
final JavaEvaluator evaluator = context.getEvaluator();
if (evaluator.isAbstract(declaration)) {
return;
}
final int flags = node.astModifiers().getEffectiveModifierFlags();
if ((flags & Modifier.ABSTRACT) != 0) {
if (!evaluator.isPublic(declaration)) {
String message = String.format("This ControllerChangeHandler class should be public (%1$s)", declaration.getQualifiedName());
context.report(ISSUE, declaration, context.getLocation(declaration), message);
return;
}
if ((flags & Modifier.PUBLIC) == 0) {
String message = String.format("This ControllerChangeHandler class should be public (%1$s)", cls.getName());
context.report(ISSUE, node, context.getLocation(node.astName()), message);
if (declaration.getContainingClass() != null && !evaluator.isStatic(declaration)) {
String message = String.format("This ControllerChangeHandler inner class should be static (%1$s)", declaration.getQualifiedName());
context.report(ISSUE, declaration, context.getLocation(declaration), message);
return;
}
if (cls.getContainingClass() != null && (flags & Modifier.STATIC) == 0) {
String message = String.format("This ControllerChangeHandler inner class should be static (%1$s)", cls.getName());
context.report(ISSUE, node, context.getLocation(node.astName()), message);
return;
}
boolean hasConstructor = false;
boolean hasDefaultConstructor = false;
NormalTypeBody body = node.astBody();
if (body != null) {
for (TypeMember member : body.astMembers()) {
if (member instanceof ConstructorDeclaration) {
hasConstructor = true;
ConstructorDeclaration constructor = (ConstructorDeclaration)member;
if (constructor.astModifiers().isPublic()) {
StrictListAccessor<VariableDefinition, ConstructorDeclaration> params = constructor.astParameters();
if (params.isEmpty()) {
hasDefaultConstructor = true;
break;
}
}
PsiMethod[] constructors = declaration.getConstructors();
for (PsiMethod constructor : constructors) {
if (evaluator.isPublic(constructor)) {
if (constructor.getParameterList().getParametersCount() == 0) {
hasDefaultConstructor = true;
break;
}
}
}
if (hasConstructor && !hasDefaultConstructor) {
if (constructors.length > 0 && !hasDefaultConstructor) {
String message = String.format(
"This ControllerChangeHandler needs to have a public default constructor (`%1$s`)",
cls.getName());
context.report(ISSUE, node, context.getLocation(node.astName()), message);
"This ControllerChangeHandler needs to have a public default constructor (`%1$s`)", declaration.getQualifiedName());
context.report(ISSUE, declaration, context.getLocation(declaration), message);
}
}
}
@@ -1,8 +1,7 @@
package com.bluelinelabs.conductor.lint;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.tools.lint.client.api.JavaEvaluator;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
@@ -10,21 +9,14 @@ import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiParameter;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.List;
import lombok.ast.ClassDeclaration;
import lombok.ast.ConstructorDeclaration;
import lombok.ast.Node;
import lombok.ast.NormalTypeBody;
import lombok.ast.StrictListAccessor;
import lombok.ast.TypeMember;
import lombok.ast.VariableDefinition;
public final class ControllerIssueDetector extends Detector implements Detector.JavaScanner, Detector.ClassScanner {
public final class ControllerIssueDetector extends Detector implements Detector.JavaPsiScanner {
public static final Issue ISSUE =
Issue.create("ValidController", "Controller not instantiatable",
@@ -35,74 +27,56 @@ public final class ControllerIssueDetector extends Detector implements Detector.
public ControllerIssueDetector() { }
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public List<String> applicableSuperClasses() {
return Collections.singletonList("com.bluelinelabs.conductor.Controller");
}
@Override
public void checkClass(@NonNull JavaContext context, ClassDeclaration node,
@NonNull Node declarationOrAnonymous, @NonNull ResolvedClass cls) {
if (node == null) {
public void checkClass(JavaContext context, PsiClass declaration) {
final JavaEvaluator evaluator = context.getEvaluator();
if (evaluator.isAbstract(declaration)) {
return;
}
final int flags = node.astModifiers().getEffectiveModifierFlags();
if ((flags & Modifier.ABSTRACT) != 0) {
if (!evaluator.isPublic(declaration)) {
String message = String.format("This Controller class should be public (%1$s)", declaration.getQualifiedName());
context.report(ISSUE, declaration, context.getLocation(declaration), message);
return;
}
if ((flags & Modifier.PUBLIC) == 0) {
String message = String.format("This Controller class should be public (%1$s)", cls.getName());
context.report(ISSUE, node, context.getLocation(node.astName()), message);
if (declaration.getContainingClass() != null && !evaluator.isStatic(declaration)) {
String message = String.format("This Controller inner class should be static (%1$s)", declaration.getQualifiedName());
context.report(ISSUE, declaration, context.getLocation(declaration), message);
return;
}
if (cls.getContainingClass() != null && (flags & Modifier.STATIC) == 0) {
String message = String.format("This Controller inner class should be static (%1$s)", cls.getName());
context.report(ISSUE, node, context.getLocation(node.astName()), message);
return;
}
boolean hasConstructor = false;
boolean hasDefaultConstructor = false;
boolean hasBundleConstructor = false;
NormalTypeBody body = node.astBody();
if (body != null) {
for (TypeMember member : body.astMembers()) {
if (member instanceof ConstructorDeclaration) {
hasConstructor = true;
ConstructorDeclaration constructor = (ConstructorDeclaration)member;
PsiMethod[] constructors = declaration.getConstructors();
for (PsiMethod constructor : constructors) {
if (evaluator.isPublic(constructor)) {
PsiParameter[] parameters = constructor.getParameterList().getParameters();
if (constructor.astModifiers().isPublic()) {
StrictListAccessor<VariableDefinition, ConstructorDeclaration> params = constructor.astParameters();
if (params.isEmpty()) {
hasDefaultConstructor = true;
break;
} else if (params.size() == 1 &&
(params.first().astTypeReference().getTypeName().equals(SdkConstants.CLASS_BUNDLE)) ||
params.first().astTypeReference().getTypeName().equals("Bundle")) {
hasBundleConstructor = true;
break;
}
}
if (parameters.length == 0) {
hasDefaultConstructor = true;
break;
} else if (parameters.length == 1 &&
parameters[0].getType().equalsToText(SdkConstants.CLASS_BUNDLE) ||
parameters[0].getType().equalsToText("Bundle")) {
hasBundleConstructor = true;
break;
}
}
}
if (hasConstructor && !hasDefaultConstructor && !hasBundleConstructor) {
if (constructors.length > 0 && !hasDefaultConstructor && !hasBundleConstructor) {
String message = String.format(
"This Controller needs to have either a public default constructor or a" +
" public single-argument constructor that takes a Bundle. (`%1$s`)",
cls.getName());
context.report(ISSUE, node, context.getLocation(node.astName()), message);
declaration.getQualifiedName());
context.report(ISSUE, declaration, context.getLocation(declaration), message);
}
}
}
@@ -0,0 +1,96 @@
package com.bluelinelabs.conductor.lint;
import com.android.tools.lint.checks.infrastructure.LintDetectorTest;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Issue;
import org.intellij.lang.annotations.Language;
import java.util.Collections;
import java.util.List;
import static com.google.common.truth.Truth.assertThat;
public class ControllerChangeHandlerDetectorTest extends LintDetectorTest {
private static final String NO_WARNINGS = "No warnings.";
private static final String CONSTRUCTOR =
"src/test/SampleHandler.java:2: Error: This ControllerChangeHandler needs to have a public default constructor (test.SampleHandler) [ValidControllerChangeHandler]\n"
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ "^\n"
+ "1 errors, 0 warnings\n";
private static final String PRIVATE_CLASS_ERROR =
"src/test/SampleHandler.java:2: Error: This ControllerChangeHandler class should be public (test.SampleHandler) [ValidControllerChangeHandler]\n"
+ "private class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ "^\n"
+ "1 errors, 0 warnings\n";
public void testWithNoConstructor() throws Exception {
@Language("JAVA") String source = ""
+ "package test;\n"
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ "}";
assertThat(lintProject(java(source))).isEqualTo(NO_WARNINGS);
}
public void testWithEmptyConstructor() throws Exception {
@Language("JAVA") String source = ""
+ "package test;\n"
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ " public SampleHandler() { }\n"
+ "}";
assertThat(lintProject(java(source))).isEqualTo(NO_WARNINGS);
}
public void testWithInvalidConstructor() throws Exception {
@Language("JAVA") String source = ""
+ "package test;\n"
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ " public SampleHandler(int number) { }\n"
+ "}";
assertThat(lintProject(java(source))).isEqualTo(CONSTRUCTOR);
}
public void testWithEmptyAndInvalidConstructor() throws Exception {
@Language("JAVA") String source = ""
+ "package test;\n"
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ " public SampleHandler() { }\n"
+ " public SampleHandler(int number) { }\n"
+ "}";
assertThat(lintProject(java(source))).isEqualTo(NO_WARNINGS);
}
public void testWithPrivateConstructor() throws Exception {
@Language("JAVA") String source = ""
+ "package test;\n"
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ " private SampleHandler() { }\n"
+ "}";
assertThat(lintProject(java(source))).isEqualTo(CONSTRUCTOR);
}
public void testWithPrivateClass() throws Exception {
@Language("JAVA") String source = ""
+ "package test;\n"
+ "private class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ " public SampleHandler() { }\n"
+ "}";
assertThat(lintProject(java(source))).isEqualTo(PRIVATE_CLASS_ERROR);
}
@Override
protected Detector getDetector() {
return new ControllerChangeHandlerIssueDetector();
}
@Override
protected List<Issue> getIssues() {
return Collections.singletonList(ControllerChangeHandlerIssueDetector.ISSUE);
}
@Override
protected boolean allowCompilationErrors() {
return true;
}
}
@@ -0,0 +1,96 @@
package com.bluelinelabs.conductor.lint;
import com.android.tools.lint.checks.infrastructure.LintDetectorTest;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Issue;
import org.intellij.lang.annotations.Language;
import java.util.Collections;
import java.util.List;
import static com.google.common.truth.Truth.assertThat;
public class ControllerDetectorTest extends LintDetectorTest {
private static final String NO_WARNINGS = "No warnings.";
private static final String CONSTRUCTOR_ERROR =
"src/test/SampleController.java:2: Error: This Controller needs to have either a public default constructor or a public single-argument constructor that takes a Bundle. (test.SampleController) [ValidController]\n"
+ "public class SampleController extends com.bluelinelabs.conductor.Controller {\n"
+ "^\n"
+ "1 errors, 0 warnings\n";
private static final String CLASS_ERROR =
"src/test/SampleController.java:2: Error: This Controller class should be public (test.SampleController) [ValidController]\n"
+ "private class SampleController extends com.bluelinelabs.conductor.Controller {\n"
+ "^\n"
+ "1 errors, 0 warnings\n";
public void testWithNoConstructor() throws Exception {
@Language("JAVA") String source = ""
+ "package test;\n"
+ "public class SampleController extends com.bluelinelabs.conductor.Controller {\n"
+ "}";
assertThat(lintProject(java(source))).isEqualTo(NO_WARNINGS);
}
public void testWithEmptyConstructor() throws Exception {
@Language("JAVA") String source = ""
+ "package test;\n"
+ "public class SampleController extends com.bluelinelabs.conductor.Controller {\n"
+ " public SampleController() { }\n"
+ "}";
assertThat(lintProject(java(source))).isEqualTo(NO_WARNINGS);
}
public void testWithInvalidConstructor() throws Exception {
@Language("JAVA") String source = ""
+ "package test;\n"
+ "public class SampleController extends com.bluelinelabs.conductor.Controller {\n"
+ " public SampleController(int number) { }\n"
+ "}";
assertThat(lintProject(java(source))).isEqualTo(CONSTRUCTOR_ERROR);
}
public void testWithEmptyAndInvalidConstructor() throws Exception {
@Language("JAVA") String source = ""
+ "package test;\n"
+ "public class SampleController extends com.bluelinelabs.conductor.Controller {\n"
+ " public SampleController() { }\n"
+ " public SampleController(int number) { }\n"
+ "}";
assertThat(lintProject(java(source))).isEqualTo(NO_WARNINGS);
}
public void testWithPrivateConstructor() throws Exception {
@Language("JAVA") String source = ""
+ "package test;\n"
+ "public class SampleController extends com.bluelinelabs.conductor.Controller {\n"
+ " private SampleController() { }\n"
+ "}";
assertThat(lintProject(java(source))).isEqualTo(CONSTRUCTOR_ERROR);
}
public void testWithPrivateClass() throws Exception {
@Language("JAVA") String source = ""
+ "package test;\n"
+ "private class SampleController extends com.bluelinelabs.conductor.Controller {\n"
+ " public SampleController() { }\n"
+ "}";
assertThat(lintProject(java(source))).isEqualTo(CLASS_ERROR);
}
@Override
protected Detector getDetector() {
return new ControllerIssueDetector();
}
@Override
protected List<Issue> getIssues() {
return Collections.singletonList(ControllerIssueDetector.ISSUE);
}
@Override
protected boolean allowCompilationErrors() {
return true;
}
}
+1 -5
View File
@@ -7,10 +7,6 @@ android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
@@ -26,8 +22,8 @@ android {
dependencies {
compile rootProject.ext.rxJava
compile rootProject.ext.rxAndroid
compile rootProject.ext.rxLifecycle
compile rootProject.ext.rxLifecycleAndroid
compile project(':conductor')
}
@@ -1,38 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import rx.Observable;
/**
* Interface used for RxController. Can also be used if writing your own Controller component without subclassing RxController.
*/
public interface ControllerLifecycleProvider {
/**
* @return An observable that will have all {@link com.bluelinelabs.conductor.Controller} lifecycle events
*/
@NonNull
@CheckResult
Observable<ControllerEvent> lifecycle();
/**
* Will bind the source until a specific {@link ControllerEvent} occurs.
*
* @param event The {@link ControllerEvent} that should cause onComplete to be called
* @return A {@link rx.Observable.Transformer} that will call onComplete when the event occurs.
*/
@NonNull
@CheckResult
<T> Observable.Transformer<T, T> bindUntilEvent(@NonNull ControllerEvent event);
/**
* Will bind the source until the next reasonable {@link ControllerEvent} occurs.
* @return A {@link rx.Observable.Transformer} that will call onComplete when the event occurs.
*/
@NonNull
@CheckResult
<T> Observable.Transformer<T, T> bindToLifecycle();
}
@@ -5,6 +5,8 @@ import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.Controller;
import com.trello.rxlifecycle.LifecycleProvider;
import com.trello.rxlifecycle.LifecycleTransformer;
import com.trello.rxlifecycle.RxLifecycle;
import rx.Observable;
@@ -13,9 +15,9 @@ import rx.subjects.BehaviorSubject;
/**
* A base {@link Controller} that can be used to expose lifecycle events using RxJava
*/
public abstract class RxController extends Controller implements ControllerLifecycleProvider {
public abstract class RxController extends Controller implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> mLifecycleSubject;
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxController() {
this(null);
@@ -23,28 +25,28 @@ public abstract class RxController extends Controller implements ControllerLifec
public RxController(Bundle args) {
super(args);
mLifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return mLifecycleSubject.asObservable();
return lifecycleSubject.asObservable();
}
@Override
@NonNull
@CheckResult
public final <T> Observable.Transformer<T, T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(mLifecycleSubject, event);
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> Observable.Transformer<T, T> bindToLifecycle() {
return RxControllerLifecycle.bindController(mLifecycleSubject);
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
@@ -14,7 +14,7 @@ public class RxControllerLifecycle {
/**
* Binds the given source to a Controller lifecycle. This is the Controller version of
* {@link com.trello.rxlifecycle.RxLifecycle#bindFragment(Observable)}.
* {@link com.trello.rxlifecycle.android.RxLifecycleAndroid#bindFragment(Observable)}.
*
* @param lifecycle the lifecycle sequence of a Controller
* @return a reusable {@link Observable.Transformer} that unsubscribes the source during the Controller lifecycle
@@ -0,0 +1,52 @@
package com.bluelinelabs.conductor.rxlifecycle;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.RestoreViewOnCreateController;
import com.trello.rxlifecycle.LifecycleProvider;
import com.trello.rxlifecycle.LifecycleTransformer;
import com.trello.rxlifecycle.RxLifecycle;
import rx.Observable;
import rx.subjects.BehaviorSubject;
/**
* A base {@link RestoreViewOnCreateController} that can be used to expose lifecycle events using RxJava
*/
public abstract class RxRestoreViewOnCreateController extends RestoreViewOnCreateController implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxRestoreViewOnCreateController() {
this(null);
}
public RxRestoreViewOnCreateController(Bundle args) {
super(args);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.asObservable();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
+31
View File
@@ -0,0 +1,31 @@
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.rxLifecycle2
compile rootProject.ext.rxLifecycleAndroid2
compile project(':conductor')
}
ext.artifactId = 'conductor-rxlifecycle2'
+3
View File
@@ -0,0 +1,3 @@
POM_NAME=Conductor RxLifecycle2 Extensions
POM_ARTIFACT_ID=conductor-rxlifecycle2
POM_PACKAGING=aar
@@ -0,0 +1,4 @@
<manifest package="com.bluelinelabs.conductor.rxlifecycle2">
<application />
</manifest>
@@ -0,0 +1,12 @@
package com.bluelinelabs.conductor.rxlifecycle2;
public enum ControllerEvent {
CREATE,
CREATE_VIEW,
ATTACH,
DETACH,
DESTROY_VIEW,
DESTROY
}
@@ -0,0 +1,44 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.support.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
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);
controller.addLifecycleListener(new Controller.LifecycleListener() {
@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 preDestroy(@NonNull Controller controller) {
subject.onNext(ControllerEvent.DESTROY);
}
});
return subject;
}
}
@@ -0,0 +1,49 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
import com.trello.rxlifecycle2.LifecycleProvider;
import com.trello.rxlifecycle2.LifecycleTransformer;
import com.trello.rxlifecycle2.RxLifecycle;
import io.reactivex.Observable;
import io.reactivex.subjects.BehaviorSubject;
/**
* A base {@link Controller} that can be used to expose lifecycle events using RxJava
*/
public abstract class RxController extends Controller implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxController(){
this(null);
}
public RxController(@Nullable Bundle args) {
super(args);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
@@ -0,0 +1,41 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.support.annotation.NonNull;
import com.trello.rxlifecycle2.LifecycleTransformer;
import com.trello.rxlifecycle2.OutsideLifecycleException;
import com.trello.rxlifecycle2.RxLifecycle;
import io.reactivex.Observable;
import io.reactivex.functions.Function;
public class RxControllerLifecycle {
/**
* Binds the given source to a Controller lifecycle. This is the Controller version of
* {@link com.trello.rxlifecycle2.android.RxLifecycleAndroid#bindFragment(Observable)}.
*
* @param lifecycle the lifecycle sequence of a Controller
* @return a reusable {@link io.reactivex.ObservableTransformer} that unsubscribes the source during the Controller lifecycle
*/
public static <T> LifecycleTransformer<T> bindController(@NonNull final Observable<ControllerEvent> lifecycle) {
return RxLifecycle.bind(lifecycle, CONTROLLER_LIFECYCLE);
}
private static final Function<ControllerEvent, ControllerEvent> CONTROLLER_LIFECYCLE =
new Function<ControllerEvent, ControllerEvent>() {
@Override
public ControllerEvent apply(ControllerEvent lastEvent) {
switch (lastEvent) {
case CREATE:
return ControllerEvent.DESTROY;
case ATTACH:
return ControllerEvent.DETACH;
case CREATE_VIEW:
return ControllerEvent.DESTROY_VIEW;
case DETACH:
return ControllerEvent.DESTROY;
default:
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
}
}
};
}
@@ -0,0 +1,45 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.RestoreViewOnCreateController;
import com.trello.rxlifecycle2.LifecycleProvider;
import com.trello.rxlifecycle2.LifecycleTransformer;
import com.trello.rxlifecycle2.RxLifecycle;
import io.reactivex.Observable;
import io.reactivex.subjects.BehaviorSubject;
public abstract class RxRestoreViewOnCreateController extends RestoreViewOnCreateController implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxRestoreViewOnCreateController() {
this(null);
}
public RxRestoreViewOnCreateController(Bundle args) {
super(args);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
-4
View File
@@ -7,10 +7,6 @@ android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
@@ -1,24 +1,42 @@
package com.bluelinelabs.conductor.support;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ChildControllerTransaction;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
/**
* @deprecated Use RouterPagerAdapter instead! This implementation was too limited and had too many
* gotchas associated with it.
*
* An adapter for ViewPagers that will handle adding and removing Controllers
*/
@Deprecated
public abstract class ControllerPagerAdapter extends PagerAdapter {
private final Controller mHost;
private static final String KEY_SAVED_PAGES = "ControllerPagerAdapter.savedStates";
private static final String KEY_SAVES_STATE = "ControllerPagerAdapter.savesState";
private static final String KEY_VISIBLE_PAGE_IDS_KEYS = "ControllerPagerAdapter.visiblePageIds.keys";
private static final String KEY_VISIBLE_PAGE_IDS_VALUES = "ControllerPagerAdapter.visiblePageIds.values";
private final Controller host;
private boolean savesState;
private SparseArray<Bundle> savedPages = new SparseArray<>();
private SparseArray<String> visiblePageIds = new SparseArray<>();
/**
* Creates a new ControllerPagerAdapter using the passed host.
*/
public ControllerPagerAdapter(Controller host) {
mHost = host;
public ControllerPagerAdapter(Controller host, boolean saveControllerState) {
this.host = host;
savesState = saveControllerState;
}
/**
@@ -30,21 +48,44 @@ public abstract class ControllerPagerAdapter extends PagerAdapter {
public Object instantiateItem(ViewGroup container, int position) {
final String name = makeControllerName(container.getId(), getItemId(position));
Controller controller = mHost.getChildController(name);
if (controller == null) {
controller = getItem(position);
Router router = host.getChildRouter(container, name);
if (savesState && !router.hasRootController()) {
Bundle routerSavedState = savedPages.get(position);
mHost.addChildController(ChildControllerTransaction.builder(controller, container.getId())
.tag(name)
.build());
if (routerSavedState != null) {
router.restoreInstanceState(routerSavedState);
}
}
return controller;
final Controller controller;
if (!router.hasRootController()) {
controller = getItem(position);
router.setRoot(RouterTransaction.with(controller).tag(name));
} else {
router.rebindIfNeeded();
controller = router.getControllerWithTag(name);
}
if (controller != null) {
visiblePageIds.put(position, controller.getInstanceId());
}
return router.getControllerWithTag(name);
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
mHost.removeChildController((Controller)object);
Router router = ((Controller)object).getRouter();
if (savesState) {
Bundle savedState = new Bundle();
router.saveInstanceState(savedState);
savedPages.put(position, savedState);
}
visiblePageIds.remove(position);
host.removeChildRouter(router);
}
@Override
@@ -52,6 +93,54 @@ public abstract class ControllerPagerAdapter extends PagerAdapter {
return ((Controller)object).getView() == view;
}
@Override
public Parcelable saveState() {
Bundle bundle = new Bundle();
bundle.putBoolean(KEY_SAVES_STATE, savesState);
bundle.putSparseParcelableArray(KEY_SAVED_PAGES, savedPages);
int[] visiblePageIdsKeys = new int[visiblePageIds.size()];
String[] visiblePageIdsValues = new String[visiblePageIds.size()];
for (int i = 0; i < visiblePageIds.size(); i++) {
visiblePageIdsKeys[i] = visiblePageIds.keyAt(i);
visiblePageIdsValues[i] = visiblePageIds.valueAt(i);
}
bundle.putIntArray(KEY_VISIBLE_PAGE_IDS_KEYS, visiblePageIdsKeys);
bundle.putStringArray(KEY_VISIBLE_PAGE_IDS_VALUES, visiblePageIdsValues);
return bundle;
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
Bundle bundle = (Bundle)state;
if (state != null) {
savesState = bundle.getBoolean(KEY_SAVES_STATE, false);
savedPages = bundle.getSparseParcelableArray(KEY_SAVED_PAGES);
int[] visiblePageIdsKeys = bundle.getIntArray(KEY_VISIBLE_PAGE_IDS_KEYS);
String[] visiblePageIdsValues = bundle.getStringArray(KEY_VISIBLE_PAGE_IDS_VALUES);
visiblePageIds = new SparseArray<>(visiblePageIdsKeys.length);
for (int i = 0; i < visiblePageIdsKeys.length; i++) {
visiblePageIds.put(visiblePageIdsKeys[i], visiblePageIdsValues[i]);
}
}
}
/**
* Returns the already instantiated Controller in the specified position or {@code null} if
* this position does not yet have a controller.
*/
@Nullable
public Controller getController(int position) {
String instanceId = visiblePageIds.get(position);
if (instanceId != null) {
return host.getRouter().getControllerWithInstanceId(instanceId);
} else {
return null;
}
}
public long getItemId(int position) {
return position;
}
@@ -60,4 +149,4 @@ public abstract class ControllerPagerAdapter extends PagerAdapter {
return viewId + ":" + id;
}
}
}
@@ -0,0 +1,120 @@
package com.bluelinelabs.conductor.support;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import java.util.List;
/**
* An adapter for ViewPagers that uses Routers as pages
*/
public abstract class RouterPagerAdapter extends PagerAdapter {
private static final String KEY_SAVED_PAGES = "RouterPagerAdapter.savedStates";
private final Controller host;
private SparseArray<Bundle> savedPages = new SparseArray<>();
private SparseArray<Router> visibleRouters = new SparseArray<>();
/**
* Creates a new RouterPagerAdapter using the passed host.
*/
public RouterPagerAdapter(Controller host) {
this.host = host;
}
/**
* Called when a router is instantiated. Here the router's root should be set if needed.
*
* @param router The router used for the page
* @param position The page position to be instantiated.
*/
public abstract void configureRouter(Router router, int position);
@Override
public Object instantiateItem(ViewGroup container, int position) {
final String name = makeRouterName(container.getId(), getItemId(position));
Router router = host.getChildRouter(container, name);
if (!router.hasRootController()) {
Bundle routerSavedState = savedPages.get(position);
if (routerSavedState != null) {
router.restoreInstanceState(routerSavedState);
}
}
router.rebindIfNeeded();
configureRouter(router, position);
visibleRouters.put(position, router);
return router;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Router router = (Router)object;
Bundle savedState = new Bundle();
router.saveInstanceState(savedState);
savedPages.put(position, savedState);
host.removeChildRouter(router);
visibleRouters.remove(position);
}
@Override
public boolean isViewFromObject(View view, Object object) {
Router router = (Router)object;
final List<RouterTransaction> backstack = router.getBackstack();
for (RouterTransaction transaction : backstack) {
if (transaction.controller().getView() == view) {
return true;
}
}
return false;
}
@Override
public Parcelable saveState() {
Bundle bundle = new Bundle();
bundle.putSparseParcelableArray(KEY_SAVED_PAGES, savedPages);
return bundle;
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
Bundle bundle = (Bundle)state;
if (state != null) {
savedPages = bundle.getSparseParcelableArray(KEY_SAVED_PAGES);
}
}
/**
* Returns the already instantiated Router in the specified position or {@code null} if there
* is no router associated with this position.
*/
@Nullable
public Router getRouter(int position) {
return visibleRouters.get(position);
}
public long getItemId(int position) {
return position;
}
private static String makeRouterName(int viewId, long id) {
return viewId + ":" + id;
}
}
+1 -4
View File
@@ -14,10 +14,6 @@ android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
@@ -28,6 +24,7 @@ android {
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
consumerProguardFiles 'proguard-rules.txt'
}
}
+5
View File
@@ -0,0 +1,5 @@
# Retain constructor that is called by using reflection to recreate the Controller
-keepclassmembers public class * extends com.bluelinelabs.conductor.Controller {
public <init>();
public <init>(android.os.Bundle);
}
@@ -0,0 +1,122 @@
package com.bluelinelabs.conductor;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.internal.LifecycleHandler;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
import java.util.List;
public class ActivityHostedRouter extends Router {
private LifecycleHandler lifecycleHandler;
private final TransactionIndexer transactionIndexer = new TransactionIndexer();
public final void setHost(@NonNull LifecycleHandler lifecycleHandler, @NonNull ViewGroup container) {
if (this.lifecycleHandler != lifecycleHandler || this.container != container) {
if (this.container != null && this.container instanceof ControllerChangeListener) {
removeChangeListener((ControllerChangeListener)this.container);
}
if (container instanceof ControllerChangeListener) {
addChangeListener((ControllerChangeListener)container);
}
this.lifecycleHandler = lifecycleHandler;
this.container = container;
}
}
@Override
public void saveInstanceState(@NonNull Bundle outState) {
super.saveInstanceState(outState);
transactionIndexer.saveInstanceState(outState);
}
@Override
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
super.restoreInstanceState(savedInstanceState);
transactionIndexer.restoreInstanceState(savedInstanceState);
}
@Override @Nullable
public Activity getActivity() {
return lifecycleHandler != null ? lifecycleHandler.getLifecycleActivity() : null;
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
super.onActivityDestroyed(activity);
lifecycleHandler = null;
}
@Override
public final void invalidateOptionsMenu() {
if (lifecycleHandler != null && lifecycleHandler.getFragmentManager() != null) {
lifecycleHandler.getFragmentManager().invalidateOptionsMenu();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
lifecycleHandler.onActivityResult(requestCode, resultCode, data);
}
@Override
void startActivity(@NonNull Intent intent) {
lifecycleHandler.startActivity(intent);
}
@Override
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) {
lifecycleHandler.startActivityForResult(instanceId, intent, requestCode);
}
@Override
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options) {
lifecycleHandler.startActivityForResult(instanceId, intent, requestCode, options);
}
@Override
void registerForActivityResult(@NonNull String instanceId, int requestCode) {
lifecycleHandler.registerForActivityResult(instanceId, requestCode);
}
@Override
void unregisterForActivityResults(@NonNull String instanceId) {
lifecycleHandler.unregisterForActivityResults(instanceId);
}
@Override
void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
lifecycleHandler.requestPermissions(instanceId, permissions, requestCode);
}
@Override
boolean hasHost() {
return lifecycleHandler != null;
}
@Override @NonNull
List<Router> getSiblingRouters() {
return lifecycleHandler.getRouters();
}
@Override @NonNull
Router getRootRouter() {
return this;
}
@Override @Nullable
TransactionIndexer getTransactionIndexer() {
return transactionIndexer;
}
}
@@ -1,10 +1,13 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
@@ -12,34 +15,37 @@ class Backstack implements Iterable<RouterTransaction> {
private static final String KEY_ENTRIES = "Backstack.entries";
private final ArrayDeque<RouterTransaction> mBackStack = new ArrayDeque<>();
private final Deque<RouterTransaction> backstack = new ArrayDeque<>();
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isEmpty() {
return mBackStack.isEmpty();
return backstack.isEmpty();
}
public int size() {
return mBackStack.size();
return backstack.size();
}
@Nullable
public RouterTransaction root() {
return mBackStack.size() > 0 ? mBackStack.getLast() : null;
return backstack.size() > 0 ? backstack.getLast() : null;
}
@Override
@Override @NonNull
public Iterator<RouterTransaction> iterator() {
return mBackStack.iterator();
return backstack.iterator();
}
@NonNull
public Iterator<RouterTransaction> reverseIterator() {
return mBackStack.descendingIterator();
return backstack.descendingIterator();
}
public List<RouterTransaction> popTo(RouterTransaction transaction) {
@NonNull
public List<RouterTransaction> popTo(@NonNull RouterTransaction transaction) {
List<RouterTransaction> popped = new ArrayList<>();
if (mBackStack.contains(transaction)) {
while (mBackStack.peek() != transaction) {
if (backstack.contains(transaction)) {
while (backstack.peek() != transaction) {
RouterTransaction poppedTransaction = pop();
popped.add(poppedTransaction);
}
@@ -49,24 +55,27 @@ class Backstack implements Iterable<RouterTransaction> {
return popped;
}
@NonNull
public RouterTransaction pop() {
RouterTransaction popped = mBackStack.pop();
popped.getController().destroy();
RouterTransaction popped = backstack.pop();
popped.controller.destroy();
return popped;
}
@Nullable
public RouterTransaction peek() {
return mBackStack.peek();
return backstack.peek();
}
public void remove(RouterTransaction transaction) {
mBackStack.removeFirstOccurrence(transaction);
public void remove(@NonNull RouterTransaction transaction) {
backstack.removeFirstOccurrence(transaction);
}
public void push(RouterTransaction transaction) {
mBackStack.push(transaction);
public void push(@NonNull RouterTransaction transaction) {
backstack.push(transaction);
}
@NonNull
public List<RouterTransaction> popAll() {
List<RouterTransaction> list = new ArrayList<>();
while (!isEmpty()) {
@@ -75,21 +84,42 @@ class Backstack implements Iterable<RouterTransaction> {
return list;
}
public void detachAndSaveInstanceState(Bundle outState) {
ArrayList<Bundle> entryBundles = new ArrayList<>(mBackStack.size());
for (RouterTransaction entry : mBackStack) {
entryBundles.add(entry.detachAndSaveInstanceState());
public void setBackstack(@NonNull List<RouterTransaction> backstack) {
for (RouterTransaction existingTransaction : this.backstack) {
boolean contains = false;
for (RouterTransaction newTransaction : backstack) {
if (existingTransaction.controller == newTransaction.controller) {
contains = true;
break;
}
}
if (!contains) {
existingTransaction.controller.destroy();
}
}
this.backstack.clear();
for (RouterTransaction transaction : backstack) {
this.backstack.push(transaction);
}
}
public void saveInstanceState(@NonNull Bundle outState) {
ArrayList<Bundle> entryBundles = new ArrayList<>(backstack.size());
for (RouterTransaction entry : backstack) {
entryBundles.add(entry.saveInstanceState());
}
outState.putParcelableArrayList(KEY_ENTRIES, entryBundles);
}
public void restoreInstanceState(Bundle savedInstanceState) {
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
ArrayList<Bundle> entryBundles = savedInstanceState.getParcelableArrayList(KEY_ENTRIES);
if (entryBundles != null) {
Collections.reverse(entryBundles);
for (Bundle transactionBundle : entryBundles) {
mBackStack.push(new RouterTransaction(transactionBundle));
backstack.push(new RouterTransaction(transactionBundle));
}
}
}
@@ -3,6 +3,8 @@ package com.bluelinelabs.conductor;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewGroup;
@@ -17,7 +19,7 @@ import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListen
*/
public class ChangeHandlerFrameLayout extends FrameLayout implements ControllerChangeListener {
private int mInProgressTransactionCount;
private int inProgressTransactionCount;
public ChangeHandlerFrameLayout(Context context) {
super(context);
@@ -38,17 +40,17 @@ public class ChangeHandlerFrameLayout extends FrameLayout implements ControllerC
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return (mInProgressTransactionCount > 0) || super.onInterceptTouchEvent(ev);
return (inProgressTransactionCount > 0) || super.onInterceptTouchEvent(ev);
}
@Override
public void onChangeStarted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler) {
mInProgressTransactionCount++;
public void onChangeStarted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) {
inProgressTransactionCount++;
}
@Override
public void onChangeCompleted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler) {
mInProgressTransactionCount--;
public void onChangeCompleted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) {
inProgressTransactionCount--;
}
}
@@ -1,79 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
/**
* A {@link ControllerTransaction} implementation used for adding child {@link Controller}s.
*/
public class ChildControllerTransaction extends ControllerTransaction {
private static final String KEY_CONTAINER_ID = "ChildControllerTransaction.containerId";
private static final String KEY_ADD_TO_LOCAL_BACKSTACK = "ChildControllerTransaction.addToLocalBackstack";
/** The ID of the ViewGroup that the child {@link Controller} will be added to */
public final int containerId;
/** If true, the hosting {@link Controller} will be responsible for reversing this transaction if the user presses the back button */
public final boolean addToLocalBackstack;
ChildControllerTransaction(Builder builder) {
super(builder);
containerId = builder.containerId;
addToLocalBackstack = builder.addToLocalBackstack;
}
ChildControllerTransaction(@NonNull Bundle bundle) {
super(bundle);
containerId = bundle.getInt(KEY_CONTAINER_ID);
addToLocalBackstack = bundle.getBoolean(KEY_ADD_TO_LOCAL_BACKSTACK);
}
@Override
public Bundle detachAndSaveInstanceState() {
Bundle bundle = super.detachAndSaveInstanceState();
bundle.putInt(KEY_CONTAINER_ID, containerId);
bundle.putBoolean(KEY_ADD_TO_LOCAL_BACKSTACK, addToLocalBackstack);
return bundle;
}
/**
* Creates a new Builder
*
* @param controller The Controller to add as a child
* @param containerId The ID of the ViewGroup to which the controller's view should be added
*/
public static Builder builder(@NonNull Controller controller, @IdRes int containerId) {
return new Builder(controller, containerId);
}
/**
* A {@link ControllerTransaction.Builder} implementation used for adding child {@link Controller}s.
*/
public static class Builder extends ControllerTransaction.Builder<Builder> {
@IdRes final int containerId;
boolean addToLocalBackstack;
Builder(@NonNull Controller controller, @IdRes int containerId) {
super(controller);
this.containerId = containerId;
}
/**
* If true, the hosting {@link Controller} will be responsible for reversing this transaction if the user presses the back button.
*/
public Builder addToLocalBackstack(boolean addToLocalBackstack) {
this.addToLocalBackstack = addToLocalBackstack;
return this;
}
/** Creates the transaction */
public ChildControllerTransaction build() {
return new ChildControllerTransaction(this);
}
}
}
@@ -3,6 +3,8 @@ package com.bluelinelabs.conductor;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.internal.LifecycleHandler;
@@ -26,7 +28,8 @@ public final class Conductor {
* for restoring the Router's state if possible.
* @return A fully configured {@link Router} instance for use with this Activity/ViewGroup pair.
*/
public static Router attachRouter(@NonNull Activity activity, @NonNull ViewGroup container, Bundle savedInstanceState) {
@NonNull @UiThread
public static Router attachRouter(@NonNull Activity activity, @NonNull ViewGroup container, @Nullable Bundle savedInstanceState) {
LifecycleHandler lifecycleHandler = LifecycleHandler.install(activity);
Router router = lifecycleHandler.getRouter(container, savedInstanceState);
File diff suppressed because it is too large Load Diff
+123 -24
View File
@@ -2,15 +2,17 @@ package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import com.bluelinelabs.conductor.ControllerTransaction.ControllerChangeType;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import com.bluelinelabs.conductor.internal.ClassUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* ControllerChangeHandlers are responsible for swapping the View for one Controller to the View
@@ -22,16 +24,21 @@ 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 boolean forceRemoveViewOnPush;
private boolean hasBeenUsed;
/**
* Responsible for swapping Views from one Controller to another.
*
* @param container The container these Views are hosted in.
* @param from The previous View in the container, if any.
* @param to The next View that should be put in the container, if any.
* @param isPush True if this is a push transaction, false if it's a pop.
* @param container The container these Views are hosted in.
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop.
* @param changeListener This listener must be called when any transitions or animations are completed.
*/
public abstract void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener);
public abstract void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener);
public ControllerChangeHandler() {
ensureDefaultConstructor();
@@ -51,6 +58,43 @@ public abstract class ControllerChangeHandler {
*/
public void restoreFromBundle(@NonNull Bundle bundle) { }
/**
* Will be called on change handlers that push a controller if the controller being pushed is
* popped before it has completed.
*
* @param newHandler The change handler that has caused this push to be aborted
* @param newTop The Controller that will now be at the top of the backstack or {@code null}
* if there will be no new Controller at the top
*/
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) { }
/**
* Will be called on change handlers that push a controller if the controller being pushed is
* needs to be attached immediately, without any animations or transitions.
*/
public void completeImmediately() { }
/**
* Returns a copy of this ControllerChangeHandler. This method is internally used by the library, so
* ensure it will return an exact copy of your handler if overriding. If not overriding, the handler
* will be saved and restored from the Bundle format.
*/
@NonNull
public ControllerChangeHandler copy() {
return fromBundle(toBundle());
}
/**
* Returns whether or not this is a reusable ControllerChangeHandler. Defaults to false and should
* ONLY be overridden if there are absolutely no side effects to using this handler more than once.
* In the case that a handler is not reusable, it will be copied using the {@link #copy()} method
* prior to use.
*/
public boolean isReusable() {
return false;
}
@NonNull
final Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putString(KEY_CLASS_NAME, getClass().getName());
@@ -70,7 +114,8 @@ public abstract class ControllerChangeHandler {
}
}
public static ControllerChangeHandler fromBundle(Bundle bundle) {
@Nullable
public static ControllerChangeHandler fromBundle(@Nullable Bundle bundle) {
if (bundle != null) {
String className = bundle.getString(KEY_CLASS_NAME);
ControllerChangeHandler changeHandler = ClassUtils.newInstance(className);
@@ -82,20 +127,57 @@ public abstract class ControllerChangeHandler {
}
}
public static void executeChange(final Controller to, final Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler inHandler) {
executeChange(to, from, isPush, container, inHandler, new ArrayList<ControllerChangeListener>());
static boolean completePushImmediately(@NonNull String controllerInstanceId) {
ControllerChangeHandler changeHandler = inProgressPushHandlers.get(controllerInstanceId);
if (changeHandler != null) {
changeHandler.completeImmediately();
inProgressPushHandlers.remove(controllerInstanceId);
return true;
}
return false;
}
public static void executeChange(final Controller to, final Controller from, final boolean isPush, final ViewGroup container, final ControllerChangeHandler inHandler, @NonNull final List<ControllerChangeListener> listeners) {
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 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() + ")");
}
if (container != null) {
final ControllerChangeHandler handler;
if (inHandler == null) {
handler = new SimpleSwapChangeHandler();
} else if (inHandler.hasBeenUsed && !inHandler.isReusable()) {
handler = inHandler.copy();
} else {
handler = inHandler;
}
handler.hasBeenUsed = true;
if (isPush && to != null) {
inProgressPushHandlers.put(to.getInstanceId(), handler);
if (from != null) {
completePushImmediately(from.getInstanceId());
}
} else if (!isPush && from != null) {
abortPush(from, to, handler);
}
for (ControllerChangeListener listener : listeners) {
listener.onChangeStarted(to, from, isPush, container, inHandler);
listener.onChangeStarted(to, from, isPush, container, handler);
}
final ControllerChangeType toChangeType = isPush ? ControllerChangeType.PUSH_ENTER : ControllerChangeType.POP_ENTER;
final ControllerChangeType fromChangeType = isPush ? ControllerChangeType.PUSH_EXIT : ControllerChangeType.POP_EXIT;
final ControllerChangeHandler handler = inHandler != null ? inHandler : new SimpleSwapChangeHandler();
final View toView;
if (to != null) {
toView = to.inflate(container);
@@ -120,17 +202,33 @@ public abstract class ControllerChangeHandler {
}
if (to != null) {
inProgressPushHandlers.remove(to.getInstanceId());
to.changeEnded(handler, toChangeType);
}
for (ControllerChangeListener listener : listeners) {
listener.onChangeCompleted(to, from, isPush, container, inHandler);
listener.onChangeCompleted(to, from, isPush, container, handler);
}
if (handler.forceRemoveViewOnPush && fromView != null) {
ViewParent fromParent = fromView.getParent();
if (fromParent != null && fromParent instanceof ViewGroup) {
((ViewGroup)fromParent).removeView(fromView);
}
}
}
});
}
}
public boolean removesFromViewOnPush() {
return true;
}
public void setForceRemoveViewOnPush(boolean force) {
forceRemoveViewOnPush = force;
}
/**
* A listener interface useful for allowing external classes to be notified of change events.
*/
@@ -138,23 +236,24 @@ public abstract class ControllerChangeHandler {
/**
* Called when a {@link ControllerChangeHandler} has started changing {@link Controller}s
*
* @param to The new Controller
* @param from The old Controller
* @param isPush True if this is a push operation, or false if it's a pop.
* @param to The new Controller or {@code null} if no Controller is being transitioned to
* @param from The old Controller or {@code null} if there was no Controller before this transition
* @param isPush True if this is a push operation, or false if it's a pop.
* @param container The containing ViewGroup
* @param handler The change handler being used.
* @param handler The change handler being used.
*/
void onChangeStarted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler);
void onChangeStarted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler);
/**
* Called when a {@link ControllerChangeHandler} has completed changing {@link Controller}s
* @param to The new Controller
* @param from The old Controller
* @param isPush True if this was a push operation, or false if it's a pop.
*
* @param to The new Controller or {@code null} if no Controller is being transitioned to
* @param from The old Controller or {@code null} if there was no Controller before this transition
* @param isPush True if this was a push operation, or false if it's a pop
* @param container The containing ViewGroup
* @param handler The change handler that was used.
* @param handler The change handler that was used.
*/
void onChangeCompleted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler);
void onChangeCompleted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler);
}
/**
@@ -0,0 +1,26 @@
package com.bluelinelabs.conductor;
/**
* All possible types of {@link Controller} changes to be used in {@link ControllerChangeHandler}s
*/
public enum ControllerChangeType {
/** The Controller is being pushed to the host container */
PUSH_ENTER(true, true),
/** The Controller is being pushed to the backstack as another Controller is pushed to the host container */
PUSH_EXIT(true, false),
/** The Controller is being popped from the backstack and placed in the host container as another Controller is popped */
POP_ENTER(false, true),
/** The Controller is being popped from the host container */
POP_EXIT(false, false);
public boolean isPush;
public boolean isEnter;
ControllerChangeType(boolean isPush, boolean isEnter) {
this.isPush = isPush;
this.isEnter = isEnter;
}
}
@@ -0,0 +1,206 @@
package com.bluelinelabs.conductor;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
import java.util.ArrayList;
import java.util.List;
class ControllerHostedRouter extends Router {
private final String KEY_HOST_ID = "ControllerHostedRouter.hostId";
private final String KEY_TAG = "ControllerHostedRouter.tag";
private Controller hostController;
@IdRes private int hostId;
private String tag;
ControllerHostedRouter() { }
ControllerHostedRouter(int hostId, @Nullable String tag) {
this.hostId = hostId;
this.tag = tag;
}
final void setHost(@NonNull Controller controller, @NonNull ViewGroup container) {
if (hostController != controller || this.container != container) {
removeHost();
if (container instanceof ControllerChangeListener) {
addChangeListener((ControllerChangeListener)container);
}
hostController = controller;
this.container = container;
}
}
final void removeHost() {
if (container != null && container instanceof ControllerChangeListener) {
removeChangeListener((ControllerChangeListener)container);
}
final List<Controller> controllersToDestroy = new ArrayList<>(destroyingControllers);
for (Controller controller : controllersToDestroy) {
if (controller.getView() != null) {
controller.detach(controller.getView(), true, false);
}
}
for (RouterTransaction transaction : backstack) {
if (transaction.controller.getView() != null) {
transaction.controller.detach(transaction.controller.getView(), true, false);
}
}
prepareForContainerRemoval();
hostController = null;
container = null;
}
final void setDetachFrozen(boolean frozen) {
for (RouterTransaction transaction : backstack) {
transaction.controller.setDetachFrozen(frozen);
}
}
@Override
void destroy(boolean popViews) {
setDetachFrozen(false);
super.destroy(popViews);
}
@Override @Nullable
public Activity getActivity() {
return hostController != null ? hostController.getActivity() : null;
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
super.onActivityDestroyed(activity);
removeHost();
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().onActivityResult(requestCode, resultCode, data);
}
}
@Override
public void invalidateOptionsMenu() {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().invalidateOptionsMenu();
}
}
@Override
void startActivity(@NonNull Intent intent) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().startActivity(intent);
}
}
@Override
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().startActivityForResult(instanceId, intent, requestCode);
}
}
@Override
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().startActivityForResult(instanceId, intent, requestCode, options);
}
}
@Override
void registerForActivityResult(@NonNull String instanceId, int requestCode) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().registerForActivityResult(instanceId, requestCode);
}
}
@Override
void unregisterForActivityResults(@NonNull String instanceId) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().unregisterForActivityResults(instanceId);
}
}
@Override
void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().requestPermissions(instanceId, permissions, requestCode);
}
}
@Override
boolean hasHost() {
return hostController != null;
}
@Override
public void saveInstanceState(@NonNull Bundle outState) {
super.saveInstanceState(outState);
outState.putInt(KEY_HOST_ID, hostId);
outState.putString(KEY_TAG, tag);
}
@Override
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
super.restoreInstanceState(savedInstanceState);
hostId = savedInstanceState.getInt(KEY_HOST_ID);
tag = savedInstanceState.getString(KEY_TAG);
}
@Override
void setControllerRouter(@NonNull Controller controller) {
super.setControllerRouter(controller);
controller.setParentController(hostController);
}
int getHostId() {
return hostId;
}
@Nullable
String getTag() {
return tag;
}
@Override @NonNull
List<Router> getSiblingRouters() {
List<Router> list = new ArrayList<>();
list.addAll(hostController.getChildRouters());
list.addAll(hostController.getRouter().getSiblingRouters());
return list;
}
@Override @NonNull
Router getRootRouter() {
if (hostController != null && hostController.getRouter() != null) {
return hostController.getRouter().getRootRouter();
} else {
return this;
}
}
@Override @Nullable
TransactionIndexer getTransactionIndexer() {
return getRootRouter().getTransactionIndexer();
}
}
@@ -1,156 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
/**
* Metadata used to transition between {@link Controller}s.
*/
public class ControllerTransaction {
/**
* All possible types of {@link Controller} changes to be used in {@link ControllerChangeHandler}s
*/
public enum ControllerChangeType {
/** The Controller is being pushed to the host container */
PUSH_ENTER(true, true),
/** The Controller is being pushed to the backstack as another Controller is pushed to the host container */
PUSH_EXIT(true, false),
/** The Controller is being popped from the backstack and placed in the host container as another Controller is popped */
POP_ENTER(false, true),
/** The Controller is being popped from the host container */
POP_EXIT(false, false);
public boolean isPush;
public boolean isEnter;
ControllerChangeType(boolean isPush, boolean isEnter) {
this.isPush = isPush;
this.isEnter = isEnter;
}
}
private static final String KEY_VIEW_CONTROLLER_BUNDLE = "ControllerTransaction.controller.bundle";
private static final String KEY_PUSH_TRANSITION = "ControllerTransaction.pushControllerChangeHandler";
private static final String KEY_POP_TRANSITION = "ControllerTransaction.popControllerChangeHandler";
private static final String KEY_TAG = "ControllerTransaction.tag";
public final Controller controller;
public final String tag;
private final ControllerChangeHandler mPushControllerChangeHandler;
private final ControllerChangeHandler mPopControllerChangeHandler;
ControllerTransaction(Builder builder) {
controller = builder.controller;
tag = builder.tag;
mPushControllerChangeHandler = builder.pushControllerChangeHandler;
mPopControllerChangeHandler = builder.popControllerChangeHandler;
}
ControllerTransaction(@NonNull Bundle bundle) {
controller = Controller.newInstance(bundle.getBundle(KEY_VIEW_CONTROLLER_BUNDLE));
mPushControllerChangeHandler = ControllerChangeHandler.fromBundle(bundle.getBundle(KEY_PUSH_TRANSITION));
mPopControllerChangeHandler = ControllerChangeHandler.fromBundle(bundle.getBundle(KEY_POP_TRANSITION));
tag = bundle.getString(KEY_TAG);
}
public Controller getController() {
return controller;
}
public String getTag() {
return tag;
}
public ControllerChangeHandler getPushControllerChangeHandler() {
ControllerChangeHandler handler = controller.getOverriddenPushHandler();
if (handler == null) {
handler = mPushControllerChangeHandler;
}
return handler;
}
public ControllerChangeHandler getPopControllerChangeHandler() {
ControllerChangeHandler handler = controller.getOverriddenPopHandler();
if (handler == null) {
handler = mPopControllerChangeHandler;
}
return handler;
}
/**
* Used to serialize this transaction into a Bundle
*/
public Bundle detachAndSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putBundle(KEY_VIEW_CONTROLLER_BUNDLE, controller.detachAndSaveInstanceState());
if (mPushControllerChangeHandler != null) {
bundle.putBundle(KEY_PUSH_TRANSITION, mPushControllerChangeHandler.toBundle());
}
if (mPopControllerChangeHandler != null) {
bundle.putBundle(KEY_POP_TRANSITION, mPopControllerChangeHandler.toBundle());
}
bundle.putString(KEY_TAG, tag);
return bundle;
}
/**
* Builder used to create transactions.
*/
public static class Builder<T extends Builder<T>> {
final Controller controller;
ControllerChangeHandler pushControllerChangeHandler;
ControllerChangeHandler popControllerChangeHandler;
String tag;
public Builder(@NonNull Controller controller) {
this.controller = controller;
}
/**
* The {@link ControllerChangeHandler} that will be used when the {@link Controller} is pushed
* to the screen.
*/
@SuppressWarnings("unchecked")
public T pushChangeHandler(ControllerChangeHandler pushControllerChangeHandler) {
this.pushControllerChangeHandler = pushControllerChangeHandler;
return (T)this;
}
/**
* The {@link ControllerChangeHandler} that will be used when the {@link Controller} is popped
* from the screen.
*/
@SuppressWarnings("unchecked")
public T popChangeHandler(ControllerChangeHandler popControllerChangeHandler) {
this.popControllerChangeHandler = popControllerChangeHandler;
return (T)this;
}
/**
* The tag to use for this transaction. Tags can be used for finding transactions later on.
*/
@SuppressWarnings("unchecked")
public T tag(String tag) {
this.tag = tag;
return (T)this;
}
/**
* Creates the transaction.
*/
public ControllerTransaction build() {
return new ControllerTransaction(this);
}
}
}
@@ -0,0 +1,53 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
/**
* A simple controller subclass that changes the onCreateView signature to include a saved view state parameter.
* This is necessary for some third party libraries like Google Maps, which require passing in a saved state
* bundle at the time of creation.
*/
abstract public class RestoreViewOnCreateController extends Controller {
/**
* Convenience constructor for use when no arguments are needed.
*/
protected RestoreViewOnCreateController() {
super(null);
}
/**
* Constructor that takes arguments that need to be retained across restarts.
*
* @param args Any arguments that need to be retained.
*/
protected RestoreViewOnCreateController(@Nullable Bundle args) {
super(args);
}
@Override @NonNull
protected final View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return onCreateView(inflater, container, viewState);
}
/**
* Called when the controller is ready to display its view. A valid view must be returned. The standard body
* for this method will be {@code return inflater.inflate(R.layout.my_layout, container, false);}, plus
* any binding and state restoration code.
*
* @param inflater The LayoutInflater that should be used to inflate views
* @param container The parent view that this Controller's view will eventually be attached to.
* This Controller's view should NOT be added in this method. It is simply passed in
* so that valid LayoutParams can be used during inflation.
* @param savedViewState A bundle for the view's state, which would have been created in {@link #onSaveViewState(View, Bundle)},
* or {@code null} if no saved state exists.
*/
@NonNull
protected abstract View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState);
}
@@ -4,6 +4,8 @@ import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -13,10 +15,11 @@ import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import com.bluelinelabs.conductor.internal.LifecycleHandler;
import com.bluelinelabs.conductor.internal.NoOpControllerChangeHandler;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@@ -25,43 +28,46 @@ import java.util.List;
* to Activity/containing ViewGroup pairs. Routers do not directly render or push Views to the container ViewGroup,
* but instead defer this responsibility to the {@link ControllerChangeHandler} specified in a given transaction.
*/
public class Router {
public abstract class Router {
private final Backstack mBackStack = new Backstack();
private LifecycleHandler mLifecycleHandler;
private ViewGroup mContainer;
private final List<ControllerChangeListener> mChangeListeners = new ArrayList<>();
private final List<Controller> mDestroyingControllers = new ArrayList<>();
private static final String KEY_BACKSTACK = "Router.backstack";
private static final String KEY_POPS_LAST_VIEW = "Router.popsLastView";
protected final Backstack backstack = new Backstack();
private final List<ControllerChangeListener> changeListeners = new ArrayList<>();
final List<Controller> destroyingControllers = new ArrayList<>();
private boolean popsLastView = false;
ViewGroup container;
/**
* Returns this Router's host Activity
* Returns this Router's host Activity or {@code null} if it has either not yet been attached to
* an Activity or if the Activity has been destroyed.
*/
public Activity getActivity() {
return mLifecycleHandler != null ? mLifecycleHandler.getLifecycleActivity() : null;
}
@Nullable
public abstract Activity getActivity();
/**
* This should be called by the host Activity when its onActivityResult method is called if the instanceId
* of the controller that called startActivityForResult is not known.
*
* @param requestCode The Activity's onActivityResult requestCode
* @param resultCode The Activity's onActivityResult resultCode
* @param data The Activity's onActivityResult data
* @param resultCode The Activity's onActivityResult resultCode
* @param data The Activity's onActivityResult data
*/
public void onActivityResult(int requestCode, int resultCode, Intent data) {
getLifecycleHandler().onActivityResult(requestCode, resultCode, data);
}
public abstract void onActivityResult(int requestCode, int resultCode, @Nullable Intent data);
/**
* This should be called by the host Activity when its onRequestPermissionsResult method is called. The call will be forwarded
* to the {@link Controller} with the instanceId passed in.
*
* @param instanceId The instanceId of the Controller to which this result should be forwarded
* @param requestCode The Activity's onRequestPermissionsResult requestCode
* @param permissions The Activity's onRequestPermissionsResult permissions
* @param instanceId The instanceId of the Controller to which this result should be forwarded
* @param requestCode The Activity's onRequestPermissionsResult requestCode
* @param permissions The Activity's onRequestPermissionsResult permissions
* @param grantResults The Activity's onRequestPermissionsResult grantResults
*/
public void onRequestPermissionsResult(String instanceId, int requestCode, String[] permissions, int[] grantResults) {
public void onRequestPermissionsResult(@NonNull String instanceId, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Controller controller = getControllerWithInstanceId(instanceId);
if (controller != null) {
controller.requestPermissionsResult(requestCode, permissions, grantResults);
@@ -71,10 +77,14 @@ public class Router {
/**
* This should be called by the host Activity when its onBackPressed method is called. The call will be forwarded
* to its top {@link Controller}. If that controller doesn't handle it, then it will be popped.
*
* @return Whether or not a back action was handled by the Router
*/
@UiThread
public boolean handleBack() {
if (!mBackStack.isEmpty()) {
if (mBackStack.peek().controller.handleBack()) {
if (!backstack.isEmpty()) {
//noinspection ConstantConditions
if (backstack.peek().controller.handleBack()) {
return true;
} else if (popCurrentController()) {
return true;
@@ -89,8 +99,13 @@ public class Router {
*
* @return Whether or not this Router still has controllers remaining on it after popping.
*/
@UiThread
public boolean popCurrentController() {
return popController(mBackStack.peek().controller);
RouterTransaction transaction = backstack.peek();
if (transaction == null) {
throw new IllegalStateException("Trying to pop the current controller when there are none on the backstack.");
}
return popController(transaction.controller);
}
/**
@@ -99,26 +114,31 @@ public class Router {
* @param controller The controller that should be popped from this Router
* @return Whether or not this Router still has controllers remaining on it after popping.
*/
public boolean popController(Controller controller) {
RouterTransaction topController = mBackStack.peek();
boolean poppingTopController = topController.controller == controller;
@UiThread
public boolean popController(@NonNull Controller controller) {
RouterTransaction topController = backstack.peek();
boolean poppingTopController = topController != null && topController.controller == controller;
if (poppingTopController) {
trackDestroyingController(mBackStack.pop());
trackDestroyingController(backstack.pop());
} else {
for (RouterTransaction transaction : mBackStack) {
for (RouterTransaction transaction : backstack) {
if (transaction.controller == controller) {
mBackStack.remove(transaction);
backstack.remove(transaction);
break;
}
}
}
if (poppingTopController) {
performControllerChange(mBackStack.peek(), topController, false);
performControllerChange(backstack.peek(), topController, false);
}
return !mBackStack.isEmpty();
if (popsLastView) {
return topController != null;
} else {
return !backstack.isEmpty();
}
}
/**
@@ -127,8 +147,9 @@ public class Router {
* @param transaction The transaction detailing what should be pushed, including the {@link Controller},
* and its push and pop {@link ControllerChangeHandler}, and its tag.
*/
@UiThread
public void pushController(@NonNull RouterTransaction transaction) {
RouterTransaction from = mBackStack.peek();
RouterTransaction from = backstack.peek();
pushToBackstack(transaction);
performControllerChange(transaction, from, true);
}
@@ -139,14 +160,55 @@ public class Router {
* @param transaction The transaction detailing what should be pushed, including the {@link Controller},
* and its push and pop {@link ControllerChangeHandler}, and its tag.
*/
@UiThread
public void replaceTopController(@NonNull RouterTransaction transaction) {
RouterTransaction topTransaction = mBackStack.peek();
if (!mBackStack.isEmpty()) {
trackDestroyingController(mBackStack.pop());
RouterTransaction topTransaction = backstack.peek();
if (!backstack.isEmpty()) {
trackDestroyingController(backstack.pop());
}
final ControllerChangeHandler handler = transaction.pushChangeHandler();
if (topTransaction != null) {
final boolean oldHandlerRemovedViews = topTransaction.pushChangeHandler() == null || topTransaction.pushChangeHandler().removesFromViewOnPush();
final boolean newHandlerRemovesViews = handler == null || handler.removesFromViewOnPush();
if (!oldHandlerRemovedViews && newHandlerRemovesViews) {
for (RouterTransaction visibleTransaction : getVisibleTransactions(backstack.iterator())) {
performControllerChange(null, visibleTransaction, true, handler);
}
}
}
pushToBackstack(transaction);
performControllerChange(transaction, topTransaction, true);
if (handler != null) {
handler.setForceRemoveViewOnPush(true);
}
performControllerChange(transaction.pushChangeHandler(handler), topTransaction, true);
}
void destroy(boolean popViews) {
popsLastView = true;
List<RouterTransaction> poppedControllers = backstack.popAll();
trackDestroyingControllers(poppedControllers);
if (popViews && poppedControllers.size() > 0) {
performControllerChange(null, poppedControllers.get(0), false, poppedControllers.get(0).popChangeHandler());
}
}
public int getContainerId() {
return container != null ? container.getId() : 0;
}
/**
* If set to true, this router will handle back presses by performing a change handler on the last controller and view
* in the stack. This defaults to false so that the developer can either finish its containing Activity or otherwise
* hide its parent view without any strange artifacting.
*/
@NonNull
public Router setPopsLastView(boolean popsLastView) {
this.popsLastView = popsLastView;
return this;
}
/**
@@ -154,6 +216,7 @@ public class Router {
*
* @return Whether or not any {@link Controller}s were popped in order to get to the root transaction
*/
@UiThread
public boolean popToRoot() {
return popToRoot(null);
}
@@ -164,9 +227,11 @@ public class Router {
* @param changeHandler The {@link ControllerChangeHandler} to handle this transaction
* @return Whether or not any {@link Controller}s were popped in order to get to the root transaction
*/
public boolean popToRoot(ControllerChangeHandler changeHandler) {
if (mBackStack.size() > 1) {
popToTransaction(mBackStack.root(), changeHandler);
@UiThread
public boolean popToRoot(@Nullable ControllerChangeHandler changeHandler) {
if (backstack.size() > 1) {
//noinspection ConstantConditions
popToTransaction(backstack.root(), changeHandler);
return true;
} else {
return false;
@@ -179,6 +244,7 @@ public class Router {
* @param tag The tag being popped to
* @return Whether or not any {@link Controller}s were popped in order to get to the transaction with the passed tag
*/
@UiThread
public boolean popToTag(@NonNull String tag) {
return popToTag(tag, null);
}
@@ -186,13 +252,14 @@ public class Router {
/**
* Pops all {@link Controller}s until the {@link Controller} with the passed tag is at the top
*
* @param tag The tag being popped to
* @param tag The tag being popped to
* @param changeHandler The {@link ControllerChangeHandler} to handle this transaction
* @return Whether or not the {@link Controller} with the passed tag is now at the top
*/
public boolean popToTag(@NonNull String tag, ControllerChangeHandler changeHandler) {
for (RouterTransaction transaction : mBackStack) {
if (tag.equals(transaction.tag)) {
@UiThread
public boolean popToTag(@NonNull String tag, @Nullable ControllerChangeHandler changeHandler) {
for (RouterTransaction transaction : backstack) {
if (tag.equals(transaction.tag())) {
popToTransaction(transaction, changeHandler);
return true;
}
@@ -200,94 +267,46 @@ public class Router {
return false;
}
/**
* Sets the root {@link Controller}. If any {@link Controller}s are currently in the backstack, they will be removed.
*
* @param controller The new root {@link Controller}
*/
public void setRoot(@NonNull Controller controller) {
setRoot(controller, null, null);
}
/**
* Sets the root {@link Controller}. If any {@link Controller}s are currently in the backstack, they will be removed.
*
* @param controller The new root {@link Controller}
* @param tag The tag to use for this {@link Controller}
*/
public void setRoot(@NonNull Controller controller, String tag) {
setRoot(controller, tag, null);
}
/**
* Sets the root {@link Controller}. If any {@link Controller}s are currently in the backstack, they will be removed.
*
* @param controller The new root {@link Controller}
* @param changeHandler The {@link ControllerChangeHandler} to use for setting the root
*/
public void setRoot(@NonNull Controller controller, ControllerChangeHandler changeHandler) {
setRoot(controller, null, changeHandler);
}
/**
* Sets the root Controller. If any {@link Controller}s are currently in the backstack, they will be removed.
*
* @param controller The new root {@link Controller}
* @param tag The tag to use for this {@link Controller}
* @param changeHandler The {@link ControllerChangeHandler} to use for setting the root
* @param transaction The transaction detailing what should be pushed, including the {@link Controller},
* and its push and pop {@link ControllerChangeHandler}, and its tag.
*/
public void setRoot(@NonNull Controller controller, String tag, ControllerChangeHandler changeHandler) {
RouterTransaction currentTop = mBackStack.peek();
if (currentTop != null && currentTop.controller.getView() != null) {
final View fromView = currentTop.controller.getView();
final int childCount = mContainer.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = mContainer.getChildAt(i);
if (child != fromView) {
mContainer.removeView(child);
}
}
}
trackDestroyingControllers(mBackStack.popAll());
RouterTransaction transaction = RouterTransaction.builder(controller)
.tag(tag)
.pushChangeHandler(changeHandler != null ? changeHandler : new SimpleSwapChangeHandler())
.popChangeHandler(new SimpleSwapChangeHandler())
.build();
pushToBackstack(transaction);
performControllerChange(transaction, currentTop, true);
@UiThread
public void setRoot(@NonNull RouterTransaction transaction) {
List<RouterTransaction> transactions = new ArrayList<>();
transactions.add(transaction);
setBackstack(transactions, transaction.pushChangeHandler());
}
/**
* Returns the hosted Controller with the given instance id, if available.
* Returns the hosted Controller with the given instance id or {@code null} if no such
* Controller exists in this Router.
*
* @param instanceId The instance ID being searched for
* @return The matching Controller, if one exists
*/
public Controller getControllerWithInstanceId(String instanceId) {
for (ControllerTransaction transaction : mBackStack) {
@Nullable
public Controller getControllerWithInstanceId(@NonNull String instanceId) {
for (RouterTransaction transaction : backstack) {
Controller controllerWithId = transaction.controller.findController(instanceId);
if (controllerWithId != null) {
return controllerWithId;
}
if (controllerWithId != null) {
return controllerWithId;
}
}
return null;
}
/**
* Returns the hosted Controller that was pushed with the given tag, if available.
* Returns the hosted Controller that was pushed with the given tag or {@code null} if no
* such Controller exists in this Router.
*
* @param tag The tag being searched for
* @return The matching Controller, if one exists
*/
public Controller getControllerWithTag(String tag) {
for (ControllerTransaction transaction : mBackStack) {
if (tag.equals(transaction.tag)) {
@Nullable
public Controller getControllerWithTag(@NonNull String tag) {
for (RouterTransaction transaction : backstack) {
if (tag.equals(transaction.tag())) {
return transaction.controller;
}
}
@@ -298,7 +317,69 @@ public class Router {
* Returns the number of {@link Controller}s currently in the backstack
*/
public int getBackstackSize() {
return mBackStack.size();
return backstack.size();
}
/**
* Returns the current backstack, ordered from root to most recently pushed.
*/
@NonNull
public List<RouterTransaction> getBackstack() {
List<RouterTransaction> list = new ArrayList<>();
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
list.add(backstackIterator.next());
}
return list;
}
/**
* Sets the backstack, transitioning from the current top controller to the top of the new stack (if different)
* using the passed {@link ControllerChangeHandler}
*
* @param newBackstack The new backstack
* @param changeHandler An optional change handler to be used to handle the root view of transition
*/
@UiThread
public void setBackstack(@NonNull List<RouterTransaction> newBackstack, @Nullable ControllerChangeHandler changeHandler) {
List<RouterTransaction> oldVisibleTransactions = getVisibleTransactions(backstack.iterator());
removeAllExceptVisibleAndUnowned();
ensureOrderedTransactionIndices(newBackstack);
backstack.setBackstack(newBackstack);
for (RouterTransaction transaction : backstack) {
transaction.onAttachedToRouter();
}
if (newBackstack.size() > 0) {
List<RouterTransaction> reverseNewBackstack = new ArrayList<>(newBackstack);
Collections.reverse(reverseNewBackstack);
List<RouterTransaction> newVisibleTransactions = getVisibleTransactions(reverseNewBackstack.iterator());
boolean visibleTransactionsChanged = !backstacksAreEqual(newVisibleTransactions, oldVisibleTransactions);
if (visibleTransactionsChanged) {
RouterTransaction rootTransaction = oldVisibleTransactions.size() > 0 ? oldVisibleTransactions.get(0) : null;
performControllerChange(newVisibleTransactions.get(0), rootTransaction, true, changeHandler);
for (int i = oldVisibleTransactions.size() - 1; i > 0; i--) {
RouterTransaction transaction = oldVisibleTransactions.get(i);
ControllerChangeHandler localHandler = changeHandler != null ? changeHandler.copy() : new SimpleSwapChangeHandler();
localHandler.setForceRemoveViewOnPush(true);
performControllerChange(null, transaction, true, localHandler);
}
for (int i = 1; i < newVisibleTransactions.size(); i++) {
RouterTransaction transaction = newVisibleTransactions.get(i);
performControllerChange(transaction, newVisibleTransactions.get(i - 1), true, transaction.pushChangeHandler());
}
}
// Ensure all new controllers have a valid router set
for (RouterTransaction transaction : newBackstack) {
transaction.controller.setRouter(this);
}
}
}
/**
@@ -313,9 +394,9 @@ public class Router {
*
* @param changeListener The listener
*/
public void addChangeListener(ControllerChangeListener changeListener) {
if (!mChangeListeners.contains(changeListener)) {
mChangeListeners.add(changeListener);
public void addChangeListener(@NonNull ControllerChangeListener changeListener) {
if (!changeListeners.contains(changeListener)) {
changeListeners.add(changeListener);
}
}
@@ -324,150 +405,196 @@ public class Router {
*
* @param changeListener The listener to be removed
*/
public void removeChangeListener(ControllerChangeListener changeListener) {
mChangeListeners.remove(changeListener);
public void removeChangeListener(@NonNull ControllerChangeListener changeListener) {
changeListeners.remove(changeListener);
}
/**
* Attaches this Router's existing backstack to its container if one exists.
*/
void rebindIfNeeded() {
Iterator<RouterTransaction> backstackIterator = mBackStack.reverseIterator();
@UiThread
public void rebindIfNeeded() {
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
if (transaction.controller.getNeedsAttach()) {
performControllerChange(transaction.controller, null, true, new SimpleSwapChangeHandler(false));
performControllerChange(transaction, null, true, new SimpleSwapChangeHandler(false));
}
}
}
public final void onActivityResult(String instanceId, int requestCode, int resultCode, Intent data) {
public final void onActivityResult(@NonNull String instanceId, int requestCode, int resultCode, @Nullable Intent data) {
Controller controller = getControllerWithInstanceId(instanceId);
if (controller != null) {
controller.onActivityResult(requestCode, resultCode, data);
}
}
public final void onActivityStarted(Activity activity) {
for (RouterTransaction transaction : mBackStack) {
public final void onActivityStarted(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityStarted(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
childRouter.onActivityStarted(activity);
}
}
}
public final void onActivityResumed(Activity activity) {
for (RouterTransaction transaction : mBackStack) {
public final void onActivityResumed(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityResumed(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
childRouter.onActivityResumed(activity);
}
}
}
public final void onActivityPaused(Activity activity) {
for (RouterTransaction transaction : mBackStack) {
public final void onActivityPaused(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityPaused(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
childRouter.onActivityPaused(activity);
}
}
}
public final void onActivityStopped(Activity activity) {
for (RouterTransaction transaction : mBackStack) {
public final void onActivityStopped(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityStopped(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
childRouter.onActivityStopped(activity);
}
}
}
public final void onActivitySaveInstanceState(Activity activity, Bundle outState) {
for (RouterTransaction transaction : mBackStack) {
transaction.controller.prepareForActivityPause();
}
public void onActivityDestroyed(@NonNull Activity activity) {
prepareForContainerRemoval();
changeListeners.clear();
mBackStack.detachAndSaveInstanceState(outState);
}
public final void onActivityDestroyed(Activity activity) {
mContainer.setOnHierarchyChangeListener(null);
mChangeListeners.clear();
for (RouterTransaction transaction : mBackStack) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityDestroyed(activity.isChangingConfigurations());
for (Router childRouter : transaction.controller.getChildRouters()) {
childRouter.onActivityDestroyed(activity);
}
}
for (Controller controller : mDestroyingControllers) {
for (int index = destroyingControllers.size() - 1; index >= 0; index--) {
Controller controller = destroyingControllers.get(index);
controller.activityDestroyed(activity.isChangingConfigurations());
for (Router childRouter : controller.getChildRouters()) {
childRouter.onActivityDestroyed(activity);
}
}
mLifecycleHandler = null;
mContainer = null;
container = null;
}
public final void onRestoreInstanceState(Bundle savedInstanceState) {
mBackStack.restoreInstanceState(savedInstanceState);
public void prepareForHostDetach() {
for (RouterTransaction transaction : backstack) {
if (ControllerChangeHandler.completePushImmediately(transaction.controller.getInstanceId())) {
transaction.controller.setNeedsAttach();
}
transaction.controller.prepareForHostDetach();
}
}
Iterator<RouterTransaction> backstackIterator = mBackStack.reverseIterator();
public void saveInstanceState(@NonNull Bundle outState) {
prepareForHostDetach();
Bundle backstackState = new Bundle();
backstack.saveInstanceState(backstackState);
outState.putParcelable(KEY_BACKSTACK, backstackState);
outState.putBoolean(KEY_POPS_LAST_VIEW, popsLastView);
}
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
Bundle backstackBundle = savedInstanceState.getParcelable(KEY_BACKSTACK);
backstack.restoreInstanceState(backstackBundle);
popsLastView = savedInstanceState.getBoolean(KEY_POPS_LAST_VIEW);
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
backstackIterator.next().controller.setRouter(this);
setControllerRouter(backstackIterator.next().controller);
}
}
public final void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
for (RouterTransaction transaction : mBackStack) {
public final void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
for (RouterTransaction transaction : backstack) {
transaction.controller.createOptionsMenu(menu, inflater);
for (Router childRouter : transaction.controller.getChildRouters()) {
childRouter.onCreateOptionsMenu(menu, inflater);
}
}
}
public final void onPrepareOptionsMenu(Menu menu) {
for (RouterTransaction transaction : mBackStack) {
public final void onPrepareOptionsMenu(@NonNull Menu menu) {
for (RouterTransaction transaction : backstack) {
transaction.controller.prepareOptionsMenu(menu);
for (Router childRouter : transaction.controller.getChildRouters()) {
childRouter.onPrepareOptionsMenu(menu);
}
}
}
public final boolean onOptionsItemSelected(MenuItem item) {
for (RouterTransaction transaction : mBackStack) {
public final boolean onOptionsItemSelected(@NonNull MenuItem item) {
for (RouterTransaction transaction : backstack) {
if (transaction.controller.optionsItemSelected(item)) {
return true;
}
for (Router childRouter : transaction.controller.getChildRouters()) {
if (childRouter.onOptionsItemSelected(item)) {
return true;
}
}
}
return false;
}
final void invalidateOptionsMenu() {
if (mLifecycleHandler != null) {
mLifecycleHandler.getFragmentManager().invalidateOptionsMenu();
}
}
private void popToTransaction(@NonNull RouterTransaction transaction, ControllerChangeHandler changeHandler) {
RouterTransaction topTransaction = mBackStack.peek();
List<RouterTransaction> poppedTransactions = mBackStack.popTo(transaction);
private void popToTransaction(@NonNull RouterTransaction transaction, @Nullable ControllerChangeHandler changeHandler) {
RouterTransaction topTransaction = backstack.peek();
List<RouterTransaction> poppedTransactions = backstack.popTo(transaction);
trackDestroyingControllers(poppedTransactions);
if (poppedTransactions.size() > 0) {
if (changeHandler == null) {
changeHandler = topTransaction.getPopControllerChangeHandler();
changeHandler = topTransaction.popChangeHandler();
}
performControllerChange(mBackStack.peek().controller, topTransaction.controller, false, changeHandler);
performControllerChange(backstack.peek(), topTransaction, false, changeHandler);
}
}
public final void setHost(@NonNull LifecycleHandler lifecycleHandler, @NonNull ViewGroup container) {
if (mLifecycleHandler != lifecycleHandler || mContainer != container) {
if (mContainer != null && mContainer instanceof ControllerChangeListener) {
removeChangeListener((ControllerChangeListener)mContainer);
}
if (container instanceof ControllerChangeListener) {
addChangeListener((ControllerChangeListener)container);
}
mLifecycleHandler = lifecycleHandler;
mContainer = container;
void prepareForContainerRemoval() {
if (container != null) {
container.setOnHierarchyChangeListener(null);
}
}
final LifecycleHandler getLifecycleHandler() {
return mLifecycleHandler;
@NonNull
final List<Controller> getControllers() {
List<Controller> controllers = new ArrayList<>();
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
controllers.add(backstackIterator.next().controller);
}
return controllers;
}
@Nullable
public final Boolean handleRequestedPermission(@NonNull String permission) {
for (ControllerTransaction transaction : mBackStack) {
for (RouterTransaction transaction : backstack) {
if (transaction.controller.didRequestPermission(permission)) {
return transaction.controller.shouldShowRequestPermissionRationale(permission);
}
@@ -475,59 +602,158 @@ public class Router {
return null;
}
private void performControllerChange(RouterTransaction to, RouterTransaction from, boolean isPush) {
private void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush) {
if (isPush && to != null) {
to.onAttachedToRouter();
}
ControllerChangeHandler changeHandler;
if (isPush) {
//noinspection ConstantConditions
changeHandler = to.getPushControllerChangeHandler();
changeHandler = to.pushChangeHandler();
} else if (from != null) {
changeHandler = from.getPopControllerChangeHandler();
changeHandler = from.popChangeHandler();
} else {
changeHandler = new SimpleSwapChangeHandler();
changeHandler = null;
}
performControllerChange(to, from, isPush, changeHandler);
}
private void performControllerChange(@Nullable final RouterTransaction to, @Nullable final RouterTransaction from, boolean isPush, @Nullable ControllerChangeHandler changeHandler) {
Controller toController = to != null ? to.controller : null;
Controller fromController = from != null ? from.controller : null;
performControllerChange(toController, fromController, isPush, changeHandler);
}
private void performControllerChange(final Controller to, final Controller from, boolean isPush, @NonNull ControllerChangeHandler changeHandler) {
if (to != null) {
to.setRouter(this);
} else if (mBackStack.size() == 0) {
to.ensureValidIndex(getTransactionIndexer());
setControllerRouter(toController);
} else if (backstack.size() == 0 && !popsLastView) {
// We're emptying out the backstack. Views get weird if you transition them out, so just no-op it. The hosting
// Activity should be handling this by finishing or at least hiding this view.
changeHandler = new NoOpControllerChangeHandler();
}
if (mContainer != null) {
ControllerChangeHandler.executeChange(to, from, isPush, mContainer, changeHandler, mChangeListeners);
}
ControllerChangeHandler.executeChange(toController, fromController, isPush, container, changeHandler, changeListeners);
}
private void pushToBackstack(@NonNull RouterTransaction entry) {
mBackStack.push(entry);
backstack.push(entry);
}
private void trackDestroyingController(RouterTransaction transaction) {
private void trackDestroyingController(@NonNull RouterTransaction transaction) {
if (!transaction.controller.isDestroyed()) {
mDestroyingControllers.add(transaction.controller);
destroyingControllers.add(transaction.controller);
transaction.controller.addLifecycleListener(new LifecycleListener() {
@Override
public void postDestroy(@NonNull Controller controller) {
mDestroyingControllers.remove(controller);
destroyingControllers.remove(controller);
}
});
}
}
private void trackDestroyingControllers(List<RouterTransaction> transactions) {
private void trackDestroyingControllers(@NonNull List<RouterTransaction> transactions) {
for (RouterTransaction transaction : transactions) {
trackDestroyingController(transaction);
}
}
private void removeAllExceptVisibleAndUnowned() {
List<View> views = new ArrayList<>();
for (RouterTransaction transaction : getVisibleTransactions(backstack.iterator())) {
if (transaction.controller.getView() != null) {
views.add(transaction.controller.getView());
}
}
for (Router router : getSiblingRouters()) {
if (router.container == container) {
addRouterViewsToList(router, views);
}
}
final int childCount = container.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = container.getChildAt(i);
if (!views.contains(child)) {
container.removeView(child);
}
}
}
// Swap around transaction indicies to ensure they don't get thrown out of order by the
// developer rearranging the backstack at runtime.
private void ensureOrderedTransactionIndices(List<RouterTransaction> backstack) {
List<Integer> indices = new ArrayList<>();
for (RouterTransaction transaction : backstack) {
transaction.ensureValidIndex(getTransactionIndexer());
indices.add(transaction.transactionIndex);
}
Collections.sort(indices);
for (int i = 0; i < backstack.size(); i++) {
backstack.get(i).transactionIndex = indices.get(i);
}
}
private void addRouterViewsToList(@NonNull Router router, @NonNull List<View> list) {
for (Controller controller : router.getControllers()) {
if (controller.getView() != null) {
list.add(controller.getView());
}
for (Router child : controller.getChildRouters()) {
addRouterViewsToList(child, list);
}
}
}
private List<RouterTransaction> getVisibleTransactions(@NonNull Iterator<RouterTransaction> backstackIterator) {
List<RouterTransaction> transactions = new ArrayList<>();
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
transactions.add(transaction);
if (transaction.pushChangeHandler() == null || transaction.pushChangeHandler().removesFromViewOnPush()) {
break;
}
}
Collections.reverse(transactions);
return transactions;
}
private boolean backstacksAreEqual(List<RouterTransaction> lhs, List<RouterTransaction> rhs) {
if (lhs.size() != rhs.size()) {
return false;
}
for (int i = 0; i < rhs.size(); i++) {
if (rhs.get(i).controller() != lhs.get(i).controller()) {
return false;
}
}
return true;
}
void setControllerRouter(@NonNull Controller controller) {
controller.setRouter(this);
}
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 registerForActivityResult(@NonNull String instanceId, int requestCode);
abstract void unregisterForActivityResults(@NonNull String instanceId);
abstract void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode);
abstract boolean hasHost();
@NonNull abstract List<Router> getSiblingRouters();
@NonNull abstract Router getRootRouter();
@Nullable abstract TransactionIndexer getTransactionIndexer();
}
@@ -2,43 +2,142 @@ package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
/**
* A {@link ControllerTransaction} implementation used for adding {@link Controller}s to a {@link Router}.
* Metadata used for adding {@link Controller}s to a {@link Router}.
*/
public class RouterTransaction extends ControllerTransaction {
public class RouterTransaction {
private RouterTransaction(Builder builder) {
super(builder);
private static int INVALID_INDEX = -1;
private static final String KEY_VIEW_CONTROLLER_BUNDLE = "RouterTransaction.controller.bundle";
private static final String KEY_PUSH_TRANSITION = "RouterTransaction.pushControllerChangeHandler";
private static final String KEY_POP_TRANSITION = "RouterTransaction.popControllerChangeHandler";
private static final String KEY_TAG = "RouterTransaction.tag";
private static final String KEY_INDEX = "RouterTransaction.transactionIndex";
private static final String KEY_ATTACHED_TO_ROUTER = "RouterTransaction.attachedToRouter";
@NonNull final Controller controller;
private String tag;
private ControllerChangeHandler pushControllerChangeHandler;
private ControllerChangeHandler popControllerChangeHandler;
private boolean attachedToRouter;
int transactionIndex = INVALID_INDEX;
@NonNull
public static RouterTransaction with(@NonNull Controller controller) {
return new RouterTransaction(controller);
}
private RouterTransaction(@NonNull Controller controller) {
this.controller = controller;
}
RouterTransaction(@NonNull Bundle bundle) {
super(bundle);
controller = Controller.newInstance(bundle.getBundle(KEY_VIEW_CONTROLLER_BUNDLE));
pushControllerChangeHandler = ControllerChangeHandler.fromBundle(bundle.getBundle(KEY_PUSH_TRANSITION));
popControllerChangeHandler = ControllerChangeHandler.fromBundle(bundle.getBundle(KEY_POP_TRANSITION));
tag = bundle.getString(KEY_TAG);
transactionIndex = bundle.getInt(KEY_INDEX);
attachedToRouter = bundle.getBoolean(KEY_ATTACHED_TO_ROUTER);
}
void onAttachedToRouter() {
attachedToRouter = true;
}
@NonNull
public Controller controller() {
return controller;
}
@Nullable
public String tag() {
return tag;
}
@NonNull
public RouterTransaction tag(@Nullable String tag) {
if (!attachedToRouter) {
this.tag = tag;
return this;
} else {
throw new RuntimeException(getClass().getSimpleName() + "s can not be modified after being added to a Router.");
}
}
@Nullable
public ControllerChangeHandler pushChangeHandler() {
ControllerChangeHandler handler = controller.getOverriddenPushHandler();
if (handler == null) {
handler = pushControllerChangeHandler;
}
return handler;
}
@NonNull
public RouterTransaction pushChangeHandler(@Nullable ControllerChangeHandler handler) {
if (!attachedToRouter) {
pushControllerChangeHandler = handler;
return this;
} else {
throw new RuntimeException(getClass().getSimpleName() + "s can not be modified after being added to a Router.");
}
}
@Nullable
public ControllerChangeHandler popChangeHandler() {
ControllerChangeHandler handler = controller.getOverriddenPopHandler();
if (handler == null) {
handler = popControllerChangeHandler;
}
return handler;
}
@NonNull
public RouterTransaction popChangeHandler(@Nullable ControllerChangeHandler handler) {
if (!attachedToRouter) {
popControllerChangeHandler = handler;
return this;
} else {
throw new RuntimeException(getClass().getSimpleName() + "s can not be modified after being added to a Router.");
}
}
void ensureValidIndex(@Nullable TransactionIndexer indexer) {
if (indexer == null) {
throw new RuntimeException();
}
if (transactionIndex == INVALID_INDEX && indexer != null) {
transactionIndex = indexer.nextIndex();
}
}
/**
* Creates a new Builder
*
* @param controller The {@link Controller} to add to the {@link Router}
* Used to serialize this transaction into a Bundle
*/
public static Builder builder(@NonNull Controller controller) {
return new Builder(controller);
}
@NonNull
public Bundle saveInstanceState() {
Bundle bundle = new Bundle();
/**
* A {@link ControllerTransaction.Builder} implementation used for adding {@link Controller}s to a {@link Router}.
*/
public static class Builder extends ControllerTransaction.Builder<Builder> {
bundle.putBundle(KEY_VIEW_CONTROLLER_BUNDLE, controller.saveInstanceState());
Builder(@NonNull Controller controller) {
super(controller);
if (pushControllerChangeHandler != null) {
bundle.putBundle(KEY_PUSH_TRANSITION, pushControllerChangeHandler.toBundle());
}
if (popControllerChangeHandler != null) {
bundle.putBundle(KEY_POP_TRANSITION, popControllerChangeHandler.toBundle());
}
/** Creates the transaction */
public RouterTransaction build() {
return new RouterTransaction(this);
}
bundle.putString(KEY_TAG, tag);
bundle.putInt(KEY_INDEX, transactionIndex);
bundle.putBoolean(KEY_ATTACHED_TO_ROUTER, attachedToRouter);
return bundle;
}
}
@@ -1,13 +1,16 @@
package com.bluelinelabs.conductor.changehandler;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
@@ -20,8 +23,12 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
public static final long DEFAULT_ANIMATION_DURATION = -1;
private long mAnimationDuration;
private boolean mRemovesFromViewOnPush;
private long animationDuration;
private boolean removesFromViewOnPush;
private boolean canceled;
private boolean needsImmediateCompletion;
private boolean completed;
private Animator animator;
public AnimatorChangeHandler() {
this(DEFAULT_ANIMATION_DURATION, true);
@@ -36,42 +43,64 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
}
public AnimatorChangeHandler(long duration, boolean removesFromViewOnPush) {
mAnimationDuration = duration;
mRemovesFromViewOnPush = removesFromViewOnPush;
animationDuration = duration;
this.removesFromViewOnPush = removesFromViewOnPush;
}
@Override
public void saveToBundle(@NonNull Bundle bundle) {
super.saveToBundle(bundle);
bundle.putLong(KEY_DURATION, mAnimationDuration);
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, mRemovesFromViewOnPush);
bundle.putLong(KEY_DURATION, animationDuration);
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, removesFromViewOnPush);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
super.restoreFromBundle(bundle);
mAnimationDuration = bundle.getLong(KEY_DURATION);
mRemovesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH);
animationDuration = bundle.getLong(KEY_DURATION);
removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH);
}
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
canceled = true;
if (animator != null) {
animator.cancel();
}
}
@Override
public void completeImmediately() {
super.completeImmediately();
needsImmediateCompletion = true;
if (animator != null) {
animator.end();
}
}
public long getAnimationDuration() {
return mAnimationDuration;
return animationDuration;
}
@Override
public boolean removesFromViewOnPush() {
return mRemovesFromViewOnPush;
return removesFromViewOnPush;
}
/**
* Should be overridden to return the Animator to use while replacing Views.
*
* @param container The container these Views are hosted in.
* @param from The previous View in the container, if any.
* @param to The next View that should be put in the container, if any.
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop.
* @param toAddedToContainer True if the "to" view was added to the container as a part of this ChangeHandler. False if it was already in the hierarchy.
*/
protected abstract Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer);
@NonNull
protected abstract Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer);
/**
* Will be called after the animation is complete to reset the View that was removed to its pre-animation state.
@@ -79,14 +108,14 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
protected abstract void resetFromView(@NonNull View from);
@Override
public final void performChange(@NonNull final ViewGroup container, final View from, final View to, final boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
public final void performChange(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
boolean readyToAnimate = true;
final boolean addingToView = to != null && to.getParent() == null;
if (addingToView) {
if (isPush || from == null) {
container.addView(to);
} else {
} else if (to.getParent() == null) {
container.addView(to, container.indexOfChild(from));
}
@@ -111,32 +140,59 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
}
}
private void performAnimation(@NonNull final ViewGroup container, final View from, View to, final boolean isPush, final boolean toAddedToContainer, @NonNull final ControllerChangeCompletedListener changeListener) {
Animator animator = getAnimator(container, from, to, isPush, toAddedToContainer);
private void complete(@NonNull ControllerChangeCompletedListener changeListener, @Nullable AnimatorListener animatorListener) {
if (!completed) {
completed = true;
changeListener.onChangeCompleted();
}
if (mAnimationDuration > 0) {
animator.setDuration(mAnimationDuration);
if (animator != null) {
if (animatorListener != null) {
animator.removeListener(animatorListener);
}
animator.cancel();
animator = null;
}
}
private void performAnimation(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, final boolean toAddedToContainer, @NonNull final ControllerChangeCompletedListener changeListener) {
if (canceled) {
complete(changeListener, null);
return;
}
animator = getAnimator(container, from, to, isPush, toAddedToContainer);
if (animationDuration > 0) {
animator.setDuration(animationDuration);
}
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(Animator animation) {
changeListener.onChangeCompleted();
if (from != null && (!isPush || removesFromViewOnPush) && needsImmediateCompletion) {
container.removeView(from);
}
complete(changeListener, this);
}
@Override
public void onAnimationEnd(Animator animation) {
if (from != null && (!isPush || mRemovesFromViewOnPush)) {
container.removeView(from);
}
if (!canceled && animator != null) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from);
}
changeListener.onChangeCompleted();
complete(changeListener, this);
if (isPush && from != null) {
resetFromView(from);
if (isPush && from != null) {
resetFromView(from);
}
}
}
});
animator.start();
}
@@ -3,21 +3,28 @@ package com.bluelinelabs.conductor.changehandler;
import android.annotation.TargetApi;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.AutoTransition;
import android.transition.Transition;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* A change handler that will use an AutoTransition.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class AutoTransitionChangeHandler extends TransitionChangeHandler {
@Override
@NonNull
protected Transition getTransition(@NonNull ViewGroup container, View from, View to, boolean isPush) {
@Override @NonNull
protected Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
return new AutoTransition();
}
@Override @NonNull
public ControllerChangeHandler copy() {
return new AutoTransitionChangeHandler();
}
}
@@ -4,9 +4,12 @@ import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* An {@link AnimatorChangeHandler} that will cross fade two views
*/
@@ -26,11 +29,12 @@ public class FadeChangeHandler extends AnimatorChangeHandler {
super(duration, removesFromViewOnPush);
}
@Override
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animator = new AnimatorSet();
if (to != null && toAddedToContainer) {
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, 0, 1));
if (to != null) {
float start = toAddedToContainer ? 0 : to.getAlpha();
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1));
}
if (from != null) {
@@ -44,4 +48,10 @@ public class FadeChangeHandler extends AnimatorChangeHandler {
protected void resetFromView(@NonNull View from) {
from.setAlpha(1);
}
@Override @NonNull
public ControllerChangeHandler copy() {
return new FadeChangeHandler(getAnimationDuration(), removesFromViewOnPush());
}
}
@@ -4,9 +4,12 @@ import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* An {@link AnimatorChangeHandler} that will slide the views left or right, depending on if it's a push or pop.
*/
@@ -26,8 +29,8 @@ public class HorizontalChangeHandler extends AnimatorChangeHandler {
super(duration, removesFromViewOnPush);
}
@Override
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animatorSet = new AnimatorSet();
if (isPush) {
@@ -42,7 +45,9 @@ public class HorizontalChangeHandler extends AnimatorChangeHandler {
animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, from.getWidth()));
}
if (to != null) {
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, -to.getWidth(), 0));
// Allow this to have a nice transition when coming off an aborted push animation
float fromLeft = from != null ? from.getX() : 0;
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, fromLeft - to.getWidth(), 0));
}
}
@@ -51,6 +56,12 @@ public class HorizontalChangeHandler extends AnimatorChangeHandler {
@Override
protected void resetFromView(@NonNull View from) {
from.setTranslationY(0);
from.setTranslationX(0);
}
@Override @NonNull
public ControllerChangeHandler copy() {
return new HorizontalChangeHandler(getAnimationDuration(), removesFromViewOnPush());
}
}
@@ -2,51 +2,114 @@ package com.bluelinelabs.conductor.changehandler;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* A {@link ControllerChangeHandler} that will instantly swap Views with no animations or transitions.
*/
public class SimpleSwapChangeHandler extends ControllerChangeHandler {
public class SimpleSwapChangeHandler extends ControllerChangeHandler implements OnAttachStateChangeListener {
private static final String KEY_REMOVES_FROM_ON_PUSH = "SimpleSwapChangeHandler.removesFromViewOnPush";
private boolean mRemovesFromViewOnPush;
private boolean removesFromViewOnPush;
private boolean canceled;
private ViewGroup container;
private ControllerChangeCompletedListener changeListener;
public SimpleSwapChangeHandler() {
this(true);
}
public SimpleSwapChangeHandler(boolean removesFromViewOnPush) {
mRemovesFromViewOnPush = removesFromViewOnPush;
this.removesFromViewOnPush = removesFromViewOnPush;
}
@Override
public void saveToBundle(@NonNull Bundle bundle) {
super.saveToBundle(bundle);
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, mRemovesFromViewOnPush);
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, removesFromViewOnPush);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
super.restoreFromBundle(bundle);
mRemovesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH);
removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH);
}
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
if (from != null && (!isPush || mRemovesFromViewOnPush)) {
container.removeView(from);
}
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
if (to != null && to.getParent() == null) {
container.addView(to);
}
changeListener.onChangeCompleted();
canceled = true;
}
}
@Override
public void completeImmediately() {
if (changeListener != null) {
changeListener.onChangeCompleted();
changeListener = null;
container.removeOnAttachStateChangeListener(this);
container = null;
}
}
@Override
public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
if (!canceled) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from);
}
if (to != null && to.getParent() == null) {
container.addView(to);
}
}
if (container.getWindowToken() != null) {
changeListener.onChangeCompleted();
} else {
this.changeListener = changeListener;
this.container = container;
container.addOnAttachStateChangeListener(this);
}
}
@Override
public boolean removesFromViewOnPush() {
return removesFromViewOnPush;
}
@Override
public void onViewAttachedToWindow(@NonNull View v) {
v.removeOnAttachStateChangeListener(this);
if (changeListener != null) {
changeListener.onChangeCompleted();
changeListener = null;
container = null;
}
}
@Override
public void onViewDetachedFromWindow(@NonNull View v) { }
@Override @NonNull
public ControllerChangeHandler copy() {
return new SimpleSwapChangeHandler(removesFromViewOnPush());
}
@Override
public boolean isReusable() {
return true;
}
}
@@ -3,12 +3,14 @@ package com.bluelinelabs.conductor.changehandler;
import android.annotation.TargetApi;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.Transition;
import android.transition.Transition.TransitionListener;
import android.transition.TransitionManager;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
@@ -17,19 +19,33 @@ import com.bluelinelabs.conductor.ControllerChangeHandler;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class TransitionChangeHandler extends ControllerChangeHandler {
private boolean canceled;
/**
* Should be overridden to return the Transition to use while replacing Views.
*
* @param container The container these Views are hosted in.
* @param from The previous View in the container, if any.
* @param to The next View that should be put in the container, if any.
* @param isPush True if this is a push transaction, false if it's a pop.
* @param container The container these Views are hosted in
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop
*/
@NonNull
protected abstract Transition getTransition(@NonNull ViewGroup container, View from, View to, boolean isPush);
protected abstract Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
@Override
public void performChange(@NonNull final ViewGroup container, View from, View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
canceled = true;
}
@Override
public void performChange(@NonNull final ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
if (canceled) {
changeListener.onChangeCompleted();
return;
}
Transition transition = getTransition(container, from, to, isPush);
transition.addListener(new TransitionListener() {
@Override
@@ -56,9 +72,14 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
if (from != null) {
container.removeView(from);
}
if (to != null) {
if (to != null && to.getParent() == null) {
container.addView(to);
}
}
@Override
public final boolean removesFromViewOnPush() {
return true;
}
}
@@ -3,6 +3,7 @@ package com.bluelinelabs.conductor.changehandler;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
@@ -15,13 +16,10 @@ import com.bluelinelabs.conductor.internal.ClassUtils;
*/
public class TransitionChangeHandlerCompat extends ControllerChangeHandler {
private static final String KEY_TRANSITION_HANDLER_CLASS = "TransitionChangeHandlerCompat.transitionChangeHandler.class";
private static final String KEY_FALLBACK_HANDLER_CLASS = "TransitionChangeHandlerCompat.fallbackChangeHandler.class";
private static final String KEY_TRANSITION_HANDLER_STATE = "TransitionChangeHandlerCompat.transitionChangeHandler.state";
private static final String KEY_FALLBACK_HANDLER_STATE = "TransitionChangeHandlerCompat.fallbackChangeHandler.state";
private static final String KEY_CHANGE_HANDLER_CLASS = "TransitionChangeHandlerCompat.changeHandler.class";
private static final String KEY_HANDLER_STATE = "TransitionChangeHandlerCompat.changeHandler.state";
private TransitionChangeHandler mTransitionChangeHandler;
private ControllerChangeHandler mFallbackChangeHandler;
private ControllerChangeHandler changeHandler;
public TransitionChangeHandlerCompat() { }
@@ -32,49 +30,52 @@ public class TransitionChangeHandlerCompat extends ControllerChangeHandler {
* @param transitionChangeHandler The change handler that will be used on API 21 and above
* @param fallbackChangeHandler The change handler that will be used on APIs below 21
*/
public TransitionChangeHandlerCompat(TransitionChangeHandler transitionChangeHandler, ControllerChangeHandler fallbackChangeHandler) {
mTransitionChangeHandler = transitionChangeHandler;
mFallbackChangeHandler = fallbackChangeHandler;
public TransitionChangeHandlerCompat(@NonNull TransitionChangeHandler transitionChangeHandler, @NonNull ControllerChangeHandler fallbackChangeHandler) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
changeHandler = transitionChangeHandler;
} else {
changeHandler = fallbackChangeHandler;
}
}
@Override
public void performChange(@NonNull final ViewGroup container, View from, View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mTransitionChangeHandler.performChange(container, from, to, isPush, changeListener);
} else {
mFallbackChangeHandler.performChange(container, from, to, isPush, changeListener);
}
public void performChange(@NonNull final ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
changeHandler.performChange(container, from, to, isPush, changeListener);
}
@Override
public void saveToBundle(@NonNull Bundle bundle) {
super.saveToBundle(bundle);
bundle.putString(KEY_TRANSITION_HANDLER_CLASS, mTransitionChangeHandler.getClass().getName());
bundle.putString(KEY_FALLBACK_HANDLER_CLASS, mFallbackChangeHandler.getClass().getName());
bundle.putString(KEY_CHANGE_HANDLER_CLASS, changeHandler.getClass().getName());
Bundle transitionBundle = new Bundle();
mTransitionChangeHandler.saveToBundle(transitionBundle);
bundle.putBundle(KEY_TRANSITION_HANDLER_STATE, transitionBundle);
Bundle fallbackBundle = new Bundle();
mFallbackChangeHandler.saveToBundle(fallbackBundle);
bundle.putBundle(KEY_FALLBACK_HANDLER_STATE, fallbackBundle);
Bundle stateBundle = new Bundle();
changeHandler.saveToBundle(stateBundle);
bundle.putBundle(KEY_HANDLER_STATE, stateBundle);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
super.restoreFromBundle(bundle);
String transitionClassName = bundle.getString(KEY_TRANSITION_HANDLER_CLASS);
mTransitionChangeHandler = ClassUtils.newInstance(transitionClassName);
String className = bundle.getString(KEY_CHANGE_HANDLER_CLASS);
changeHandler = ClassUtils.newInstance(className);
//noinspection ConstantConditions
mTransitionChangeHandler.restoreFromBundle(bundle.getBundle(KEY_TRANSITION_HANDLER_STATE));
changeHandler.restoreFromBundle(bundle.getBundle(KEY_HANDLER_STATE));
}
String fallbackClassName = bundle.getString(KEY_FALLBACK_HANDLER_CLASS);
mFallbackChangeHandler = ClassUtils.newInstance(fallbackClassName);
//noinspection ConstantConditions
mFallbackChangeHandler.restoreFromBundle(bundle.getBundle(KEY_FALLBACK_HANDLER_STATE));
@Override
public boolean removesFromViewOnPush() {
return changeHandler.removesFromViewOnPush();
}
@Override @NonNull
public ControllerChangeHandler copy() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return new TransitionChangeHandlerCompat((TransitionChangeHandler)changeHandler.copy(), null);
} else {
return new TransitionChangeHandlerCompat(null, changeHandler.copy());
}
}
}
@@ -4,9 +4,12 @@ import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import java.util.ArrayList;
import java.util.List;
@@ -30,8 +33,8 @@ public class VerticalChangeHandler extends AnimatorChangeHandler {
super(duration, removesFromViewOnPush);
}
@Override
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animator = new AnimatorSet();
List<Animator> viewAnimators = new ArrayList<>();
@@ -48,4 +51,9 @@ public class VerticalChangeHandler extends AnimatorChangeHandler {
@Override
protected void resetFromView(@NonNull View from) { }
@Override @NonNull
public ControllerChangeHandler copy() {
return new VerticalChangeHandler(getAnimationDuration(), removesFromViewOnPush());
}
}
@@ -1,16 +1,13 @@
package com.bluelinelabs.conductor.internal;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
public class ClassUtils {
@SuppressWarnings("unchecked")
public static <T> Class<? extends T> classForName(String className) {
return classForName(className, true);
}
@SuppressWarnings("unchecked")
public static <T> Class<? extends T> classForName(String className, boolean allowEmptyName) {
@Nullable @SuppressWarnings("unchecked")
public static <T> Class<? extends T> classForName(@NonNull String className, boolean allowEmptyName) {
if (allowEmptyName && TextUtils.isEmpty(className)) {
return null;
}
@@ -22,10 +19,10 @@ public class ClassUtils {
}
}
@SuppressWarnings("unchecked")
public static <T> T newInstance(String className) {
@Nullable @SuppressWarnings("unchecked")
public static <T> T newInstance(@NonNull String className) {
try {
Class<? extends T> cls = classForName(className);
Class<? extends T> cls = classForName(className, true);
return cls != null ? cls.newInstance() : null;
} catch (Exception e) {
throw new RuntimeException("An exception occurred while creating a new instance of " + className + ". " + e.getMessage());
@@ -4,42 +4,55 @@ import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Application.ActivityLifecycleCallbacks;
import android.app.Fragment;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ActivityHostedRouter;
import com.bluelinelabs.conductor.Router;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class LifecycleHandler extends Fragment implements ActivityLifecycleCallbacks {
private static final String FRAGMENT_TAG = "LifecycleHandler";
private static final String KEY_PENDING_PERMISSION_REQUESTS = "LifecycleHandler.pendingPermissionRequests";
private static final String KEY_PERMISSION_REQUEST_CODES = "LifecycleHandler.permissionRequests";
private static final String KEY_ACTIVITY_REQUEST_CODES = "LifecycleHandler.activityRequests";
private static final String KEY_ROUTER_STATE_PREFIX = "LifecycleHandler.routerState";
private Activity mActivity;
private boolean mHasRegisteredCallbacks;
private Activity activity;
private boolean hasRegisteredCallbacks;
private boolean destroyed;
private boolean attached;
private SparseArray<String> mPermissionRequestMap = new SparseArray<>();
private SparseArray<String> mActivityRequestMap = new SparseArray<>();
private SparseArray<String> permissionRequestMap = new SparseArray<>();
private SparseArray<String> activityRequestMap = new SparseArray<>();
private ArrayList<PendingPermissionRequest> pendingPermissionRequests = new ArrayList<>();
private final Map<Integer, Router> mRouterMap = new HashMap<>();
private final Map<Integer, ActivityHostedRouter> routerMap = new HashMap<>();
public LifecycleHandler() {
setRetainInstance(true);
setHasOptionsMenu(true);
}
private static LifecycleHandler findInActivity(Activity activity) {
@Nullable
private static LifecycleHandler findInActivity(@NonNull Activity activity) {
LifecycleHandler lifecycleHandler = (LifecycleHandler)activity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
if (lifecycleHandler != null) {
lifecycleHandler.registerActivityListener(activity);
@@ -47,7 +60,8 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
return lifecycleHandler;
}
public static LifecycleHandler install(Activity activity) {
@NonNull
public static LifecycleHandler install(@NonNull Activity activity) {
LifecycleHandler lifecycleHandler = findInActivity(activity);
if (lifecycleHandler == null) {
lifecycleHandler = new LifecycleHandler();
@@ -57,16 +71,20 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
return lifecycleHandler;
}
public Router getRouter(ViewGroup container, Bundle savedInstanceState) {
Router router = mRouterMap.get(getRouterHashKey(container));
@NonNull
public Router getRouter(@NonNull ViewGroup container, @Nullable Bundle savedInstanceState) {
ActivityHostedRouter router = routerMap.get(getRouterHashKey(container));
if (router == null) {
router = new Router();
router = new ActivityHostedRouter();
router.setHost(this, container);
if (savedInstanceState != null) {
router.onRestoreInstanceState(savedInstanceState);
Bundle routerSavedState = savedInstanceState.getBundle(KEY_ROUTER_STATE_PREFIX + router.getContainerId());
if (routerSavedState != null) {
router.restoreInstanceState(routerSavedState);
}
}
mRouterMap.put(getRouterHashKey(container), router);
routerMap.put(getRouterHashKey(container), router);
} else {
router.setHost(this, container);
}
@@ -74,19 +92,25 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
return router;
}
public Activity getLifecycleActivity() {
return mActivity;
@NonNull
public List<Router> getRouters() {
return new ArrayList<Router>(routerMap.values());
}
private static int getRouterHashKey(ViewGroup viewGroup) {
@Nullable
public Activity getLifecycleActivity() {
return activity;
}
private static int getRouterHashKey(@NonNull ViewGroup viewGroup) {
return viewGroup.getId();
}
private void registerActivityListener(Activity activity) {
mActivity = activity;
private void registerActivityListener(@NonNull Activity activity) {
this.activity = activity;
if (!mHasRegisteredCallbacks) {
mHasRegisteredCallbacks = true;
if (!hasRegisteredCallbacks) {
hasRegisteredCallbacks = true;
activity.getApplication().registerActivityLifecycleCallbacks(this);
}
}
@@ -97,10 +121,13 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
if (savedInstanceState != null) {
StringSparseArrayParceler permissionParcel = savedInstanceState.getParcelable(KEY_PERMISSION_REQUEST_CODES);
mPermissionRequestMap = permissionParcel != null ? permissionParcel.getStringSparseArray() : null;
permissionRequestMap = permissionParcel != null ? permissionParcel.getStringSparseArray() : new SparseArray<String>();
StringSparseArrayParceler activityParcel = savedInstanceState.getParcelable(KEY_ACTIVITY_REQUEST_CODES);
mActivityRequestMap = activityParcel != null ? activityParcel.getStringSparseArray() : null;
activityRequestMap = activityParcel != null ? activityParcel.getStringSparseArray() : new SparseArray<String>();
ArrayList<PendingPermissionRequest> pendingRequests = savedInstanceState.getParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS);
pendingPermissionRequests = pendingRequests != null ? pendingRequests : new ArrayList<PendingPermissionRequest>();
}
}
@@ -108,22 +135,65 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(KEY_PERMISSION_REQUEST_CODES, new StringSparseArrayParceler(mPermissionRequestMap));
outState.putParcelable(KEY_ACTIVITY_REQUEST_CODES, new StringSparseArrayParceler(mActivityRequestMap));
outState.putParcelable(KEY_PERMISSION_REQUEST_CODES, new StringSparseArrayParceler(permissionRequestMap));
outState.putParcelable(KEY_ACTIVITY_REQUEST_CODES, new StringSparseArrayParceler(activityRequestMap));
outState.putParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS, pendingPermissionRequests);
}
@Override
public void onDestroy() {
super.onDestroy();
if (mActivity != null) {
mActivity.getApplication().unregisterActivityLifecycleCallbacks(this);
if (activity != null) {
activity.getApplication().unregisterActivityLifecycleCallbacks(this);
destroyRouters();
activity = null;
}
}
for (Router router : mRouterMap.values()) {
router.onActivityDestroyed(mActivity);
@SuppressWarnings("deprecation")
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
destroyed = false;
setAttached();
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
destroyed = false;
setAttached();
}
@Override
public void onDetach() {
super.onDetach();
attached = false;
destroyRouters();
}
private void setAttached() {
if (!attached) {
attached = true;
for (int i = pendingPermissionRequests.size() - 1; i >= 0; i--) {
PendingPermissionRequest request = pendingPermissionRequests.remove(i);
requestPermissions(request.instanceId, request.permissions, request.requestCode);
}
}
}
mActivity = null;
private void destroyRouters() {
if (!destroyed) {
destroyed = true;
if (activity != null) {
for (Router router : routerMap.values()) {
router.onActivityDestroyed(activity);
}
}
}
}
@@ -131,9 +201,9 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
String instanceId = mActivityRequestMap.get(requestCode);
String instanceId = activityRequestMap.get(requestCode);
if (instanceId != null) {
for (Router router : mRouterMap.values()) {
for (Router router : routerMap.values()) {
router.onActivityResult(instanceId, requestCode, resultCode, data);
}
}
@@ -143,9 +213,9 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
String instanceId = mPermissionRequestMap.get(requestCode);
String instanceId = permissionRequestMap.get(requestCode);
if (instanceId != null) {
for (Router router : mRouterMap.values()) {
for (Router router : routerMap.values()) {
router.onRequestPermissionsResult(instanceId, requestCode, permissions, grantResults);
}
}
@@ -153,7 +223,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
@Override
public boolean shouldShowRequestPermissionRationale(@NonNull String permission) {
for (Router router : mRouterMap.values()) {
for (Router router : routerMap.values()) {
Boolean handled = router.handleRequestedPermission(permission);
if (handled != null) {
return handled;
@@ -166,7 +236,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
for (Router router : mRouterMap.values()) {
for (Router router : routerMap.values()) {
router.onCreateOptionsMenu(menu, inflater);
}
}
@@ -175,14 +245,14 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
for (Router router : mRouterMap.values()) {
for (Router router : routerMap.values()) {
router.onPrepareOptionsMenu(menu);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
for (Router router : mRouterMap.values()) {
for (Router router : routerMap.values()) {
if (router.onOptionsItemSelected(item)) {
return true;
}
@@ -191,45 +261,49 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
return super.onOptionsItemSelected(item);
}
public void registerForActivityRequest(String instanceId, int requestCode) {
mActivityRequestMap.put(requestCode, instanceId);
public void registerForActivityResult(@NonNull String instanceId, int requestCode) {
activityRequestMap.put(requestCode, instanceId);
}
public void unregisterForActivityRequests(String instanceId) {
for (int i = mActivityRequestMap.size() - 1; i >= 0; i--) {
if (instanceId.equals(mActivityRequestMap.get(mActivityRequestMap.keyAt(i)))) {
mActivityRequestMap.removeAt(i);
public void unregisterForActivityResults(@NonNull String instanceId) {
for (int i = activityRequestMap.size() - 1; i >= 0; i--) {
if (instanceId.equals(activityRequestMap.get(activityRequestMap.keyAt(i)))) {
activityRequestMap.removeAt(i);
}
}
}
public void startActivityForResult(String instanceId, Intent intent, int requestCode) {
registerForActivityRequest(instanceId, requestCode);
public void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) {
registerForActivityResult(instanceId, requestCode);
startActivityForResult(intent, requestCode);
}
public void startActivityForResult(String instanceId, Intent intent, int requestCode, Bundle options) {
registerForActivityRequest(instanceId, requestCode);
public void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options) {
registerForActivityResult(instanceId, requestCode);
startActivityForResult(intent, requestCode, options);
}
@TargetApi(Build.VERSION_CODES.M)
public void requestPermissions(String instanceId, String[] permissions, int requestCode) {
mPermissionRequestMap.put(requestCode, instanceId);
requestPermissions(permissions, requestCode);
public void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
if (attached) {
permissionRequestMap.put(requestCode, instanceId);
requestPermissions(permissions, requestCode);
} else {
pendingPermissionRequests.add(new PendingPermissionRequest(instanceId, permissions, requestCode));
}
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (mActivity == null && findInActivity(activity) == LifecycleHandler.this) {
mActivity = activity;
if (this.activity == null && findInActivity(activity) == LifecycleHandler.this) {
this.activity = activity;
}
}
@Override
public void onActivityStarted(Activity activity) {
if (mActivity == activity) {
for (Router router : mRouterMap.values()) {
if (this.activity == activity) {
for (Router router : routerMap.values()) {
router.onActivityStarted(activity);
}
}
@@ -237,8 +311,8 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
@Override
public void onActivityResumed(Activity activity) {
if (mActivity == activity) {
for (Router router : mRouterMap.values()) {
if (this.activity == activity) {
for (Router router : routerMap.values()) {
router.onActivityResumed(activity);
}
}
@@ -246,8 +320,8 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
@Override
public void onActivityPaused(Activity activity) {
if (mActivity == activity) {
for (Router router : mRouterMap.values()) {
if (this.activity == activity) {
for (Router router : routerMap.values()) {
router.onActivityPaused(activity);
}
}
@@ -255,8 +329,8 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
@Override
public void onActivityStopped(Activity activity) {
if (mActivity == activity) {
for (Router router : mRouterMap.values()) {
if (this.activity == activity) {
for (Router router : routerMap.values()) {
router.onActivityStopped(activity);
}
}
@@ -264,13 +338,58 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
if (mActivity == activity) {
for (Router router : mRouterMap.values()) {
router.onActivitySaveInstanceState(activity, outState);
if (this.activity == activity) {
for (Router router : routerMap.values()) {
Bundle bundle = new Bundle();
router.saveInstanceState(bundle);
outState.putBundle(KEY_ROUTER_STATE_PREFIX + router.getContainerId(), bundle);
}
}
}
@Override
public void onActivityDestroyed(Activity activity) { }
private static class PendingPermissionRequest implements Parcelable {
final String instanceId;
final String[] permissions;
final int requestCode;
PendingPermissionRequest(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
this.instanceId = instanceId;
this.permissions = permissions;
this.requestCode = requestCode;
}
private PendingPermissionRequest(Parcel in) {
instanceId = in.readString();
permissions = in.createStringArray();
requestCode = in.readInt();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeString(instanceId);
out.writeStringArray(permissions);
out.writeInt(requestCode);
}
public static final Parcelable.Creator<PendingPermissionRequest> CREATOR = new Parcelable.Creator<PendingPermissionRequest>() {
@Override
public PendingPermissionRequest createFromParcel(Parcel in) {
return new PendingPermissionRequest(in);
}
@Override
public PendingPermissionRequest[] newArray(int size) {
return new PendingPermissionRequest[size];
}
};
}
}
@@ -1,6 +1,7 @@
package com.bluelinelabs.conductor.internal;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
@@ -9,8 +10,18 @@ import com.bluelinelabs.conductor.ControllerChangeHandler;
public class NoOpControllerChangeHandler extends ControllerChangeHandler {
@Override
public void performChange(@NonNull ViewGroup container, @NonNull View from, @NonNull View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
changeListener.onChangeCompleted();
}
@NonNull
@Override
public ControllerChangeHandler copy() {
return new NoOpControllerChangeHandler();
}
@Override
public boolean isReusable() {
return true;
}
}
@@ -2,52 +2,58 @@ package com.bluelinelabs.conductor.internal;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.util.SparseArray;
public class StringSparseArrayParceler implements Parcelable {
private final SparseArray<String> mStringSparseArray;
private final SparseArray<String> stringSparseArray;
public StringSparseArrayParceler(SparseArray<String> stringSparseArray) {
mStringSparseArray = stringSparseArray;
public StringSparseArrayParceler(@NonNull SparseArray<String> stringSparseArray) {
this.stringSparseArray = stringSparseArray;
}
private StringSparseArrayParceler(Parcel in) {
mStringSparseArray = new SparseArray<>();
private StringSparseArrayParceler(@NonNull Parcel in) {
stringSparseArray = new SparseArray<>();
final int size = in.readInt();
for (int i = 0; i < size; i++) {
mStringSparseArray.put(in.readInt(), in.readString());
stringSparseArray.put(in.readInt(), in.readString());
}
}
@NonNull
public SparseArray<String> getStringSparseArray() {
return mStringSparseArray;
return stringSparseArray;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
final int size = mStringSparseArray.size();
final int size = stringSparseArray.size();
out.writeInt(size);
for (int i = 0; i < size; i++) {
int key = mStringSparseArray.keyAt(i);
int key = stringSparseArray.keyAt(i);
out.writeInt(key);
out.writeString(mStringSparseArray.get(key));
out.writeString(stringSparseArray.get(key));
}
}
public static final Parcelable.Creator<StringSparseArrayParceler> CREATOR = new Parcelable.Creator<StringSparseArrayParceler>() {
@Override
public StringSparseArrayParceler createFromParcel(Parcel in) {
return new StringSparseArrayParceler(in);
}
@Override
public StringSparseArrayParceler[] newArray(int size) {
return new StringSparseArrayParceler[size];
}
@@ -0,0 +1,24 @@
package com.bluelinelabs.conductor.internal;
import android.os.Bundle;
import android.support.annotation.NonNull;
public class TransactionIndexer {
private static final String KEY_INDEX = "TransactionIndexer.currentIndex";
private int currentIndex;
public int nextIndex() {
return ++currentIndex;
}
public void saveInstanceState(@NonNull Bundle outState) {
outState.putInt(KEY_INDEX, currentIndex);
}
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
currentIndex = savedInstanceState.getInt(KEY_INDEX);
}
}
@@ -0,0 +1,152 @@
package com.bluelinelabs.conductor.internal;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
public class ViewAttachHandler implements OnAttachStateChangeListener {
private enum ReportedState {
VIEW_DETACHED,
ACTIVITY_STOPPED,
ATTACHED
}
public interface ViewAttachListener {
void onAttached();
void onDetached(boolean fromActivityStop);
void onViewDetachAfterStop();
}
private interface ChildAttachListener {
void onAttached();
}
private boolean rootAttached = false;
boolean childrenAttached = false;
private boolean activityStopped = false;
private ReportedState reportedState = ReportedState.VIEW_DETACHED;
private ViewAttachListener attachListener;
private OnAttachStateChangeListener childOnAttachStateChangeListener;
public ViewAttachHandler(ViewAttachListener attachListener) {
this.attachListener = attachListener;
}
@Override
public void onViewAttachedToWindow(final View v) {
if (rootAttached) {
return;
}
rootAttached = true;
listenForDeepestChildAttach(v, new ChildAttachListener() {
@Override
public void onAttached() {
childrenAttached = true;
reportAttached();
}
});
}
@Override
public void onViewDetachedFromWindow(View v) {
rootAttached = false;
if (childrenAttached) {
childrenAttached = false;
reportDetached();
}
}
public void listenForAttach(final View view) {
view.addOnAttachStateChangeListener(this);
}
public void unregisterAttachListener(View view) {
view.removeOnAttachStateChangeListener(this);
if (childOnAttachStateChangeListener != null && view instanceof ViewGroup) {
findDeepestChild((ViewGroup)view).removeOnAttachStateChangeListener(childOnAttachStateChangeListener);
}
}
public void onActivityStarted() {
activityStopped = false;
reportAttached();
}
public void onActivityStopped() {
activityStopped = true;
reportDetached();
}
void reportAttached() {
if (rootAttached && childrenAttached && !activityStopped && reportedState != ReportedState.ATTACHED) {
reportedState = ReportedState.ATTACHED;
attachListener.onAttached();
}
}
void reportDetached() {
boolean wasDetachedForActivity = reportedState == ReportedState.ACTIVITY_STOPPED;
boolean isDetachedForActivity = rootAttached;
if (isDetachedForActivity) {
reportedState = ReportedState.ACTIVITY_STOPPED;
} else {
reportedState = ReportedState.VIEW_DETACHED;
}
if (wasDetachedForActivity && !isDetachedForActivity) {
attachListener.onViewDetachAfterStop();
} else {
attachListener.onDetached(isDetachedForActivity);
}
}
void listenForDeepestChildAttach(final View view, final ChildAttachListener attachListener) {
if (!(view instanceof ViewGroup)) {
attachListener.onAttached();
return;
}
ViewGroup viewGroup = (ViewGroup)view;
if (viewGroup.getChildCount() == 0) {
attachListener.onAttached();
return;
}
childOnAttachStateChangeListener = new OnAttachStateChangeListener() {
boolean attached = false;
@Override
public void onViewAttachedToWindow(View v) {
if (!attached) {
attached = true;
attachListener.onAttached();
v.removeOnAttachStateChangeListener(this);
childOnAttachStateChangeListener = null;
}
}
@Override
public void onViewDetachedFromWindow(View v) { }
};
findDeepestChild(viewGroup).addOnAttachStateChangeListener(childOnAttachStateChangeListener);
}
private View findDeepestChild(ViewGroup viewGroup) {
if (viewGroup.getChildCount() == 0) {
return viewGroup;
}
View lastChild = viewGroup.getChildAt(viewGroup.getChildCount() - 1);
if (lastChild instanceof ViewGroup) {
return findDeepestChild((ViewGroup)lastChild);
} else {
return lastChild;
}
}
}
@@ -1,66 +1,69 @@
package com.bluelinelabs.conductor;
import org.junit.Assert;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class BackstackTests {
private Backstack mBackstack;
private Backstack backstack;
@Before
public void setup() {
mBackstack = new Backstack();
backstack = new Backstack();
}
@Test
public void testPush() {
Assert.assertEquals(0, mBackstack.size());
mBackstack.push(RouterTransaction.builder(new TestController()).build());
Assert.assertEquals(1, mBackstack.size());
assertEquals(0, backstack.size());
backstack.push(RouterTransaction.with(new TestController()));
assertEquals(1, backstack.size());
}
@Test
public void testPop() {
mBackstack.push(RouterTransaction.builder(new TestController()).build());
mBackstack.push(RouterTransaction.builder(new TestController()).build());
Assert.assertEquals(2, mBackstack.size());
mBackstack.pop();
Assert.assertEquals(1, mBackstack.size());
mBackstack.pop();
Assert.assertEquals(0, mBackstack.size());
backstack.push(RouterTransaction.with(new TestController()));
backstack.push(RouterTransaction.with(new TestController()));
assertEquals(2, backstack.size());
backstack.pop();
assertEquals(1, backstack.size());
backstack.pop();
assertEquals(0, backstack.size());
}
@Test
public void testPeek() {
RouterTransaction transaction1 = RouterTransaction.builder(new TestController()).build();
RouterTransaction transaction2 = RouterTransaction.builder(new TestController()).build();
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
mBackstack.push(transaction1);
Assert.assertEquals(transaction1, mBackstack.peek());
backstack.push(transaction1);
assertEquals(transaction1, backstack.peek());
mBackstack.push(transaction2);
Assert.assertEquals(transaction2, mBackstack.peek());
backstack.push(transaction2);
assertEquals(transaction2, backstack.peek());
mBackstack.pop();
Assert.assertEquals(transaction1, mBackstack.peek());
backstack.pop();
assertEquals(transaction1, backstack.peek());
}
@Test
public void testPopTo() {
RouterTransaction transaction1 = RouterTransaction.builder(new TestController()).build();
RouterTransaction transaction2 = RouterTransaction.builder(new TestController()).build();
RouterTransaction transaction3 = RouterTransaction.builder(new TestController()).build();
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
RouterTransaction transaction3 = RouterTransaction.with(new TestController());
mBackstack.push(transaction1);
mBackstack.push(transaction2);
mBackstack.push(transaction3);
backstack.push(transaction1);
backstack.push(transaction2);
backstack.push(transaction3);
Assert.assertEquals(3, mBackstack.size());
assertEquals(3, backstack.size());
mBackstack.popTo(transaction1);
backstack.popTo(transaction1);
Assert.assertEquals(1, mBackstack.size());
Assert.assertEquals(transaction1, mBackstack.peek());
assertEquals(1, backstack.size());
assertEquals(transaction1, backstack.peek());
}
}
@@ -2,10 +2,12 @@ package com.bluelinelabs.conductor;
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Assert;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class ControllerChangeHandlerTests {
@Test
@@ -13,26 +15,25 @@ public class ControllerChangeHandlerTests {
HorizontalChangeHandler horizontalChangeHandler = new HorizontalChangeHandler();
FadeChangeHandler fadeChangeHandler = new FadeChangeHandler(120, false);
RouterTransaction transaction = RouterTransaction.builder(new TestController())
RouterTransaction transaction = RouterTransaction.with(new TestController())
.pushChangeHandler(horizontalChangeHandler)
.popChangeHandler(fadeChangeHandler)
.build();
RouterTransaction restoredTransaction = new RouterTransaction(transaction.detachAndSaveInstanceState());
.popChangeHandler(fadeChangeHandler);
RouterTransaction restoredTransaction = new RouterTransaction(transaction.saveInstanceState());
ControllerChangeHandler restoredHorizontal = restoredTransaction.getPushControllerChangeHandler();
ControllerChangeHandler restoredFade = restoredTransaction.getPopControllerChangeHandler();
ControllerChangeHandler restoredHorizontal = restoredTransaction.pushChangeHandler();
ControllerChangeHandler restoredFade = restoredTransaction.popChangeHandler();
Assert.assertEquals(horizontalChangeHandler.getClass(), restoredHorizontal.getClass());
Assert.assertEquals(fadeChangeHandler.getClass(), restoredFade.getClass());
assertEquals(horizontalChangeHandler.getClass(), restoredHorizontal.getClass());
assertEquals(fadeChangeHandler.getClass(), restoredFade.getClass());
HorizontalChangeHandler restoredHorizontalCast = (HorizontalChangeHandler)restoredHorizontal;
FadeChangeHandler restoredFadeCast = (FadeChangeHandler)restoredFade;
Assert.assertEquals(horizontalChangeHandler.getAnimationDuration(), restoredHorizontalCast.getAnimationDuration());
Assert.assertEquals(horizontalChangeHandler.removesFromViewOnPush(), restoredHorizontalCast.removesFromViewOnPush());
assertEquals(horizontalChangeHandler.getAnimationDuration(), restoredHorizontalCast.getAnimationDuration());
assertEquals(horizontalChangeHandler.removesFromViewOnPush(), restoredHorizontalCast.removesFromViewOnPush());
Assert.assertEquals(fadeChangeHandler.getAnimationDuration(), restoredFadeCast.getAnimationDuration());
Assert.assertEquals(fadeChangeHandler.removesFromViewOnPush(), restoredFadeCast.removesFromViewOnPush());
assertEquals(fadeChangeHandler.getAnimationDuration(), restoredFadeCast.getAnimationDuration());
assertEquals(fadeChangeHandler.removesFromViewOnPush(), restoredFadeCast.removesFromViewOnPush());
}
}
@@ -0,0 +1,268 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.ListUtils;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ControllerLifecycleActivityReferenceTests {
private Router router;
private ActivityProxy activityProxy;
public void createActivityController(Bundle savedInstanceState, boolean includeStartAndResume) {
activityProxy = new ActivityProxy().create(savedInstanceState);
if (includeStartAndResume) {
activityProxy.start().resume();
}
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
router.setPopsLastView(true);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Before
public void setup() {
createActivityController(null, true);
}
@Test
public void testSingleControllerActivityOnPush() {
Controller controller = new TestController();
assertNull(controller.getActivity());
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
controller.addLifecycleListener(listener);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertEquals(ListUtils.listOf(true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(), listener.postDetachReferences);
assertEquals(ListUtils.listOf(), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(), listener.postDestroyReferences);
}
@Test
public void testChildControllerActivityOnPush() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
assertNull(child.getActivity());
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertEquals(ListUtils.listOf(true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(), listener.postDetachReferences);
assertEquals(ListUtils.listOf(), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(), listener.postDestroyReferences);
}
@Test
public void testSingleControllerActivityOnPop() {
Controller controller = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
controller.addLifecycleListener(listener);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
router.popCurrentController();
assertEquals(ListUtils.listOf(true, true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(true), listener.postDetachReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyReferences);
}
@Test
public void testChildControllerActivityOnPop() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
childRouter.popCurrentController();
assertEquals(ListUtils.listOf(true, true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(true), listener.postDetachReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyReferences);
}
@Test
public void testChildControllerActivityOnParentPop() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
router.popCurrentController();
assertEquals(ListUtils.listOf(true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(true), listener.postDetachReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyReferences);
}
@Test
public void testSingleControllerActivityOnDestroy() {
Controller controller = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
controller.addLifecycleListener(listener);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
activityProxy.pause().stop(false).destroy();
assertEquals(ListUtils.listOf(true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(true), listener.postDetachReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyReferences);
}
@Test
public void testChildControllerActivityOnDestroy() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
activityProxy.pause().stop(false).destroy();
assertEquals(ListUtils.listOf(true), listener.changeEndReferences);
assertEquals(ListUtils.listOf(true), listener.postCreateViewReferences);
assertEquals(ListUtils.listOf(true), listener.postAttachReferences);
assertEquals(ListUtils.listOf(true), listener.postDetachReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyViewReferences);
assertEquals(ListUtils.listOf(true), listener.postDestroyReferences);
}
static class ActivityReferencingLifecycleListener extends Controller.LifecycleListener {
List<Boolean> changeEndReferences = new ArrayList<>();
List<Boolean> postCreateViewReferences = new ArrayList<>();
List<Boolean> postAttachReferences = new ArrayList<>();
List<Boolean> postDetachReferences = new ArrayList<>();
List<Boolean> postDestroyViewReferences = new ArrayList<>();
List<Boolean> postDestroyReferences = new ArrayList<>();
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
changeEndReferences.add(controller.getActivity() != null);
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
postCreateViewReferences.add(controller.getActivity() != null);
}
@Override
public void postAttach(@NonNull Controller controller, @NonNull View view) {
postAttachReferences.add(controller.getActivity() != null);
}
@Override
public void postDetach(@NonNull Controller controller, @NonNull View view) {
postDetachReferences.add(controller.getActivity() != null);
}
@Override
public void postDestroyView(@NonNull Controller controller) {
postDestroyViewReferences.add(controller.getActivity() != null);
}
@Override
public void postDestroy(@NonNull Controller controller) {
postDestroyReferences.add(controller.getActivity() != null);
}
}
}
@@ -0,0 +1,605 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.CallState;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.MockChangeHandler.ChangeHandlerListener;
import com.bluelinelabs.conductor.util.TestController;
import com.bluelinelabs.conductor.util.ViewUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ControllerLifecycleCallbacksTests {
private Router router;
private ActivityProxy activityProxy;
private CallState currentCallState;
public void createActivityController(Bundle savedInstanceState, boolean includeStartAndResume) {
activityProxy = new ActivityProxy().create(savedInstanceState);
if (includeStartAndResume) {
activityProxy.start().resume();
}
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Before
public void setup() {
createActivityController(null, true);
currentCallState = new CallState();
}
@Test
public void testNormalLifecycle() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller))
.popChangeHandler(getPopHandler(expectedCallState, controller)));
assertCalls(expectedCallState, controller);
router.popCurrentController();
assertNull(controller.getView());
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleWithActivityStop() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller)));
assertCalls(expectedCallState, controller);
activityProxy.getActivity().isDestroying = true;
activityProxy.pause();
assertCalls(expectedCallState, controller);
activityProxy.stop(false);
expectedCallState.detachCalls++;
assertCalls(expectedCallState, controller);
assertNotNull(controller.getView());
ViewUtils.reportAttached(controller.getView(), false);
expectedCallState.saveViewStateCalls++;
expectedCallState.destroyViewCalls++;
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleWithActivityDestroy() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller)));
assertCalls(expectedCallState, controller);
activityProxy.getActivity().isDestroying = true;
activityProxy.pause();
assertCalls(expectedCallState, controller);
activityProxy.stop(true);
expectedCallState.saveViewStateCalls++;
expectedCallState.detachCalls++;
expectedCallState.destroyViewCalls++;
assertCalls(expectedCallState, controller);
activityProxy.destroy();
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleWithActivityConfigurationChange() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller))
.tag("root"));
assertCalls(expectedCallState, controller);
activityProxy.getActivity().isChangingConfigurations = true;
Bundle bundle = new Bundle();
activityProxy.saveInstanceState(bundle);
expectedCallState.saveViewStateCalls++;
expectedCallState.saveInstanceStateCalls++;
assertCalls(expectedCallState, controller);
activityProxy.pause();
assertCalls(expectedCallState, controller);
activityProxy.stop(true);
expectedCallState.detachCalls++;
expectedCallState.destroyViewCalls++;
assertCalls(expectedCallState, controller);
activityProxy.destroy();
assertCalls(expectedCallState, controller);
createActivityController(bundle, false);
controller = (TestController)router.getControllerWithTag("root");
expectedCallState.restoreInstanceStateCalls++;
expectedCallState.restoreViewStateCalls++;
expectedCallState.changeStartCalls++;
expectedCallState.createViewCalls++;
// Lifecycle listener isn't attached during restore, grab the current views from the controller for this stuff...
currentCallState.restoreInstanceStateCalls = controller.currentCallState.restoreInstanceStateCalls;
currentCallState.restoreViewStateCalls = controller.currentCallState.restoreViewStateCalls;
currentCallState.changeStartCalls = controller.currentCallState.changeStartCalls;
currentCallState.changeEndCalls = controller.currentCallState.changeEndCalls;
currentCallState.createViewCalls = controller.currentCallState.createViewCalls;
currentCallState.attachCalls = controller.currentCallState.attachCalls;
assertCalls(expectedCallState, controller);
activityProxy.start().resume();
currentCallState.changeEndCalls = controller.currentCallState.changeEndCalls;
currentCallState.attachCalls = controller.currentCallState.attachCalls;
expectedCallState.changeEndCalls++;
expectedCallState.attachCalls++;
assertCalls(expectedCallState, controller);
activityProxy.resume();
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleWithActivityBackground() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller)));
assertCalls(expectedCallState, controller);
activityProxy.pause();
Bundle bundle = new Bundle();
activityProxy.saveInstanceState(bundle);
expectedCallState.saveInstanceStateCalls++;
expectedCallState.saveViewStateCalls++;
assertCalls(expectedCallState, controller);
activityProxy.resume();
}
@Test
public void testLifecycleCallOrder() {
final TestController testController = new TestController();
final CallState callState = new CallState();
testController.addLifecycleListener(new LifecycleListener() {
@Override
public void preCreateView(@NonNull Controller controller) {
callState.createViewCalls++;
assertEquals(1, callState.createViewCalls);
assertEquals(0, testController.currentCallState.createViewCalls);
assertEquals(0, callState.attachCalls);
assertEquals(0, testController.currentCallState.attachCalls);
assertEquals(0, callState.detachCalls);
assertEquals(0, testController.currentCallState.detachCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
callState.createViewCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(0, callState.attachCalls);
assertEquals(0, testController.currentCallState.attachCalls);
assertEquals(0, callState.detachCalls);
assertEquals(0, testController.currentCallState.detachCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void preAttach(@NonNull Controller controller, @NonNull View view) {
callState.attachCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(1, callState.attachCalls);
assertEquals(0, testController.currentCallState.attachCalls);
assertEquals(0, callState.detachCalls);
assertEquals(0, testController.currentCallState.detachCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postAttach(@NonNull Controller controller, @NonNull View view) {
callState.attachCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(0, callState.detachCalls);
assertEquals(0, testController.currentCallState.detachCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
callState.detachCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(1, callState.detachCalls);
assertEquals(0, testController.currentCallState.detachCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postDetach(@NonNull Controller controller, @NonNull View view) {
callState.detachCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(1, testController.currentCallState.detachCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
callState.destroyViewCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(1, testController.currentCallState.detachCalls);
assertEquals(1, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postDestroyView(@NonNull Controller controller) {
callState.destroyViewCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(1, testController.currentCallState.detachCalls);
assertEquals(2, callState.destroyViewCalls);
assertEquals(1, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void preDestroy(@NonNull Controller controller) {
callState.destroyCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(1, testController.currentCallState.detachCalls);
assertEquals(2, callState.destroyViewCalls);
assertEquals(1, testController.currentCallState.destroyViewCalls);
assertEquals(1, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postDestroy(@NonNull Controller controller) {
callState.destroyCalls++;
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(1, testController.currentCallState.detachCalls);
assertEquals(2, callState.destroyViewCalls);
assertEquals(1, testController.currentCallState.destroyViewCalls);
assertEquals(2, callState.destroyCalls);
assertEquals(1, testController.currentCallState.destroyCalls);
}
});
router.pushController(RouterTransaction.with(testController)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
router.popController(testController);
assertEquals(2, callState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(2, callState.destroyViewCalls);
assertEquals(2, callState.destroyCalls);
}
@Test
public void testChildLifecycle() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
attachLifecycleListener(child);
CallState expectedCallState = new CallState();
assertCalls(expectedCallState, child);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter
.setRoot(RouterTransaction.with(child)
.pushChangeHandler(getPushHandler(expectedCallState, child))
.popChangeHandler(getPopHandler(expectedCallState, child)));
assertCalls(expectedCallState, child);
parent.removeChildRouter(childRouter);
assertCalls(expectedCallState, child);
}
@Test
public void testChildLifecycle2() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
attachLifecycleListener(child);
CallState expectedCallState = new CallState();
assertCalls(expectedCallState, child);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter
.setRoot(RouterTransaction.with(child)
.pushChangeHandler(getPushHandler(expectedCallState, child))
.popChangeHandler(getPopHandler(expectedCallState, child)));
assertCalls(expectedCallState, child);
router.popCurrentController();
expectedCallState.detachCalls++;
expectedCallState.destroyViewCalls++;
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, child);
}
private MockChangeHandler getPushHandler(final CallState expectedCallState, final TestController controller) {
return MockChangeHandler.listeningChangeHandler(new ChangeHandlerListener() {
@Override
public void willStartChange() {
expectedCallState.changeStartCalls++;
expectedCallState.createViewCalls++;
assertCalls(expectedCallState, controller);
}
@Override
public void didAttachOrDetach() {
expectedCallState.attachCalls++;
assertCalls(expectedCallState, controller);
}
@Override
public void didEndChange() {
expectedCallState.changeEndCalls++;
assertCalls(expectedCallState, controller);
}
});
}
private MockChangeHandler getPopHandler(final CallState expectedCallState, final TestController controller) {
return MockChangeHandler.listeningChangeHandler(new ChangeHandlerListener() {
@Override
public void willStartChange() {
expectedCallState.changeStartCalls++;
assertCalls(expectedCallState, controller);
}
@Override
public void didAttachOrDetach() {
expectedCallState.destroyViewCalls++;
expectedCallState.detachCalls++;
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, controller);
}
@Override
public void didEndChange() {
expectedCallState.changeEndCalls++;
assertCalls(expectedCallState, controller);
}
});
}
private void assertCalls(CallState callState, TestController controller) {
assertEquals("Expected call counts and controller call counts do not match.", callState, controller.currentCallState);
assertEquals("Expected call counts and lifecycle call counts do not match.", callState, currentCallState);
}
private void attachLifecycleListener(Controller controller) {
controller.addLifecycleListener(new LifecycleListener() {
@Override
public void onChangeStart(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
currentCallState.changeStartCalls++;
}
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
currentCallState.changeEndCalls++;
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
currentCallState.createViewCalls++;
}
@Override
public void postAttach(@NonNull Controller controller, @NonNull View view) {
currentCallState.attachCalls++;
}
@Override
public void postDestroyView(@NonNull Controller controller) {
currentCallState.destroyViewCalls++;
}
@Override
public void postDetach(@NonNull Controller controller, @NonNull View view) {
currentCallState.detachCalls++;
}
@Override
public void postDestroy(@NonNull Controller controller) {
currentCallState.destroyCalls++;
}
@Override
public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
currentCallState.saveInstanceStateCalls++;
}
@Override
public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
currentCallState.restoreInstanceStateCalls++;
}
@Override
public void onSaveViewState(@NonNull Controller controller, @NonNull Bundle outState) {
currentCallState.saveViewStateCalls++;
}
@Override
public void onRestoreViewState(@NonNull Controller controller, @NonNull Bundle savedViewState) {
currentCallState.restoreViewStateCalls++;
}
});
}
}
@@ -1,627 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeCompletedListener;
import com.bluelinelabs.conductor.ControllerTransaction.ControllerChangeType;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.util.ActivityController;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ControllerLifecycleTests {
private ActivityController<TestActivity> mActivityController;
private Router mRouter;
private CallState mCurrentCallState;
public void createActivityController(Bundle savedInstanceState) {
mActivityController = Robolectric.buildActivity(TestActivity.class).create(savedInstanceState).start();
@IdRes int containerId = 4;
FrameLayout routerContainer = new FrameLayout(mActivityController.get());
routerContainer.setId(containerId);
mRouter = Conductor.attachRouter(mActivityController.get(), routerContainer, savedInstanceState);
if (!mRouter.hasRootController()) {
mRouter.setRoot(new TestController());
}
}
@Before
public void setup() {
createActivityController(null);
mCurrentCallState = new CallState();
}
@Test
public void testNormalLifecycle() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
assertCalls(expectedCallState, controller);
mRouter.pushController(RouterTransaction.builder(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller))
.popChangeHandler(getPopHandler(expectedCallState, controller))
.build()
);
assertCalls(expectedCallState, controller);
mRouter.popCurrentController();
Assert.assertNull(controller.getView());
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleWithActivityDestroy() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
assertCalls(expectedCallState, controller);
mRouter.pushController(RouterTransaction.builder(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller))
.build()
);
assertCalls(expectedCallState, controller);
mActivityController.pause();
assertCalls(expectedCallState, controller);
mActivityController.stop();
assertCalls(expectedCallState, controller);
mActivityController.destroy();
expectedCallState.detachCalls++;
expectedCallState.destroyViewCalls++;
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleWithActivityConfigurationChange() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
assertCalls(expectedCallState, controller);
mRouter.pushController(RouterTransaction.builder(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller))
.tag("root")
.build()
);
assertCalls(expectedCallState, controller);
mActivityController.get().isChangingConfigurations = true;
Bundle bundle = new Bundle();
mActivityController.saveInstanceState(bundle);
expectedCallState.detachCalls++;
expectedCallState.saveViewStateCalls++;
expectedCallState.saveInstanceStateCalls++;
assertCalls(expectedCallState, controller);
mActivityController.pause();
assertCalls(expectedCallState, controller);
mActivityController.stop();
assertCalls(expectedCallState, controller);
mActivityController.destroy();
expectedCallState.destroyViewCalls++;
assertCalls(expectedCallState, controller);
createActivityController(bundle);
controller = (TestController)mRouter.getControllerWithTag("root");
expectedCallState.restoreInstanceStateCalls++;
expectedCallState.restoreViewStateCalls++;
expectedCallState.changeStartCalls++;
expectedCallState.changeEndCalls++;
expectedCallState.createViewCalls++;
// Lifecycle listener isn't attached during restore, grab the current views from the controller for this stuff...
mCurrentCallState.restoreInstanceStateCalls = controller.currentCallState.restoreInstanceStateCalls;
mCurrentCallState.restoreViewStateCalls = controller.currentCallState.restoreViewStateCalls;
mCurrentCallState.changeStartCalls = controller.currentCallState.changeStartCalls;
mCurrentCallState.changeEndCalls = controller.currentCallState.changeEndCalls;
mCurrentCallState.createViewCalls = controller.currentCallState.createViewCalls;
assertCalls(expectedCallState, controller);
mActivityController.resume();
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleWithActivityBackground() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
assertCalls(expectedCallState, controller);
mRouter.pushController(RouterTransaction.builder(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller))
.build()
);
assertCalls(expectedCallState, controller);
mActivityController.pause();
Bundle bundle = new Bundle();
mActivityController.saveInstanceState(bundle);
expectedCallState.detachCalls++;
expectedCallState.saveInstanceStateCalls++;
expectedCallState.saveViewStateCalls++;
assertCalls(expectedCallState, controller);
mActivityController.resume();
expectedCallState.createViewCalls++;
expectedCallState.restoreViewStateCalls++;
}
@Test
public void testLifecycleCallOrder() {
final TestController testController = new TestController();
final CallState callState = new CallState();
testController.addLifecycleListener(new LifecycleListener() {
@Override
public void preCreateView(@NonNull Controller controller) {
callState.createViewCalls++;
Assert.assertEquals(1, callState.createViewCalls);
Assert.assertEquals(0, testController.currentCallState.createViewCalls);
Assert.assertEquals(0, callState.attachCalls);
Assert.assertEquals(0, testController.currentCallState.attachCalls);
Assert.assertEquals(0, callState.detachCalls);
Assert.assertEquals(0, testController.currentCallState.detachCalls);
Assert.assertEquals(0, callState.destroyViewCalls);
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(0, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
callState.createViewCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(0, callState.attachCalls);
Assert.assertEquals(0, testController.currentCallState.attachCalls);
Assert.assertEquals(0, callState.detachCalls);
Assert.assertEquals(0, testController.currentCallState.detachCalls);
Assert.assertEquals(0, callState.destroyViewCalls);
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(0, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void preAttach(@NonNull Controller controller, @NonNull View view) {
callState.attachCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(1, callState.attachCalls);
Assert.assertEquals(0, testController.currentCallState.attachCalls);
Assert.assertEquals(0, callState.detachCalls);
Assert.assertEquals(0, testController.currentCallState.detachCalls);
Assert.assertEquals(0, callState.destroyViewCalls);
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(0, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postAttach(@NonNull Controller controller, @NonNull View view) {
callState.attachCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(2, callState.attachCalls);
Assert.assertEquals(1, testController.currentCallState.attachCalls);
Assert.assertEquals(0, callState.detachCalls);
Assert.assertEquals(0, testController.currentCallState.detachCalls);
Assert.assertEquals(0, callState.destroyViewCalls);
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(0, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
callState.detachCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(2, callState.attachCalls);
Assert.assertEquals(1, testController.currentCallState.attachCalls);
Assert.assertEquals(1, callState.detachCalls);
Assert.assertEquals(0, testController.currentCallState.detachCalls);
Assert.assertEquals(0, callState.destroyViewCalls);
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(0, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postDetach(@NonNull Controller controller, @NonNull View view) {
callState.detachCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(2, callState.attachCalls);
Assert.assertEquals(1, testController.currentCallState.attachCalls);
Assert.assertEquals(2, callState.detachCalls);
Assert.assertEquals(1, testController.currentCallState.detachCalls);
Assert.assertEquals(0, callState.destroyViewCalls);
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(0, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
callState.destroyViewCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(2, callState.attachCalls);
Assert.assertEquals(1, testController.currentCallState.attachCalls);
Assert.assertEquals(2, callState.detachCalls);
Assert.assertEquals(1, testController.currentCallState.detachCalls);
Assert.assertEquals(1, callState.destroyViewCalls);
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(0, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postDestroyView(@NonNull Controller controller) {
callState.destroyViewCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(2, callState.attachCalls);
Assert.assertEquals(1, testController.currentCallState.attachCalls);
Assert.assertEquals(2, callState.detachCalls);
Assert.assertEquals(1, testController.currentCallState.detachCalls);
Assert.assertEquals(2, callState.destroyViewCalls);
Assert.assertEquals(1, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(0, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void preDestroy(@NonNull Controller controller) {
callState.destroyCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(2, callState.attachCalls);
Assert.assertEquals(1, testController.currentCallState.attachCalls);
Assert.assertEquals(2, callState.detachCalls);
Assert.assertEquals(1, testController.currentCallState.detachCalls);
Assert.assertEquals(2, callState.destroyViewCalls);
Assert.assertEquals(1, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(1, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postDestroy(@NonNull Controller controller) {
callState.destroyCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(2, callState.attachCalls);
Assert.assertEquals(1, testController.currentCallState.attachCalls);
Assert.assertEquals(2, callState.detachCalls);
Assert.assertEquals(1, testController.currentCallState.detachCalls);
Assert.assertEquals(2, callState.destroyViewCalls);
Assert.assertEquals(1, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(2, callState.destroyCalls);
Assert.assertEquals(1, testController.currentCallState.destroyCalls);
}
});
mRouter.pushController(RouterTransaction.builder(testController)
.pushChangeHandler(new ChangeHandler(new ChangeHandlerListener() {
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
container.addView(to);
ViewUtils.setAttached(to, true);
changeListener.onChangeCompleted();
}
}))
.popChangeHandler(new ChangeHandler(new ChangeHandlerListener() {
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
container.removeView(from);
ViewUtils.setAttached(from, false);
changeListener.onChangeCompleted();
}
}))
.build());
mRouter.popController(testController);
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(2, callState.attachCalls);
Assert.assertEquals(2, callState.detachCalls);
Assert.assertEquals(2, callState.destroyViewCalls);
Assert.assertEquals(2, callState.destroyCalls);
}
@Test
public void testChildLifecycle() {
Controller parent = new TestController();
mRouter.pushController(RouterTransaction.builder(parent)
.pushChangeHandler(new ChangeHandler(new ChangeHandlerListener() {
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
container.addView(to);
ViewUtils.setAttached(to, true);
changeListener.onChangeCompleted();
}
}))
.build());
TestController child = new TestController();
attachLifecycleListener(child);
CallState expectedCallState = new CallState();
assertCalls(expectedCallState, child);
parent.addChildController(ChildControllerTransaction.builder(child, TestController.VIEW_ID)
.pushChangeHandler(getPushHandler(expectedCallState, child))
.popChangeHandler(getPopHandler(expectedCallState, child))
.build()
);
assertCalls(expectedCallState, child);
parent.removeChildController(child);
assertCalls(expectedCallState, child);
}
@Test
public void testChildLifecycle2() {
Controller parent = new TestController();
mRouter.pushController(RouterTransaction.builder(parent)
.pushChangeHandler(new ChangeHandler(new ChangeHandlerListener() {
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
container.addView(to);
ViewUtils.setAttached(to, true);
changeListener.onChangeCompleted();
}
}))
.popChangeHandler(new ChangeHandler(new ChangeHandlerListener() {
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
container.removeView(from);
ViewUtils.setAttached(from, false);
changeListener.onChangeCompleted();
}
}))
.build());
TestController child = new TestController();
attachLifecycleListener(child);
CallState expectedCallState = new CallState();
assertCalls(expectedCallState, child);
parent.addChildController(ChildControllerTransaction.builder(child, TestController.VIEW_ID)
.pushChangeHandler(getPushHandler(expectedCallState, child))
.popChangeHandler(getPopHandler(expectedCallState, child))
.build()
);
assertCalls(expectedCallState, child);
mRouter.popCurrentController();
ViewUtils.setAttached(child.getView(), false);
expectedCallState.detachCalls++;
expectedCallState.destroyViewCalls++;
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, child);
}
private ChangeHandler getPushHandler(final CallState expectedCallState, final TestController controller) {
return new ChangeHandler(new ChangeHandlerListener() {
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
expectedCallState.changeStartCalls++;
expectedCallState.createViewCalls++;
assertCalls(expectedCallState, controller);
container.addView(to);
ViewUtils.setAttached(to, true);
expectedCallState.attachCalls++;
assertCalls(expectedCallState, controller);
changeListener.onChangeCompleted();
expectedCallState.changeEndCalls++;
assertCalls(expectedCallState, controller);
}
});
}
private ChangeHandler getPopHandler(final CallState expectedCallState, final TestController controller) {
return new ChangeHandler(new ChangeHandlerListener() {
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
expectedCallState.changeStartCalls++;
assertCalls(expectedCallState, controller);
container.removeView(from);
ViewUtils.setAttached(from, false);
expectedCallState.destroyViewCalls++;
expectedCallState.detachCalls++;
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, controller);
changeListener.onChangeCompleted();
expectedCallState.changeEndCalls++;
assertCalls(expectedCallState, controller);
}
});
}
private void assertCalls(CallState callState, TestController controller) {
Assert.assertEquals("Expected call counts and controller call counts do not match.", callState, controller.currentCallState);
Assert.assertEquals("Expected call counts and lifecycle call counts do not match.", callState, mCurrentCallState);
}
private void attachLifecycleListener(Controller controller) {
controller.addLifecycleListener(new LifecycleListener() {
@Override
public void onChangeStart(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
mCurrentCallState.changeStartCalls++;
}
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
mCurrentCallState.changeEndCalls++;
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
mCurrentCallState.createViewCalls++;
}
@Override
public void postAttach(@NonNull Controller controller, @NonNull View view) {
mCurrentCallState.attachCalls++;
}
@Override
public void postDestroyView(@NonNull Controller controller) {
mCurrentCallState.destroyViewCalls++;
}
@Override
public void postDetach(@NonNull Controller controller, @NonNull View view) {
mCurrentCallState.detachCalls++;
}
@Override
public void postDestroy(@NonNull Controller controller) {
mCurrentCallState.destroyCalls++;
}
@Override
public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
mCurrentCallState.saveInstanceStateCalls++;
}
@Override
public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
mCurrentCallState.restoreInstanceStateCalls++;
}
@Override
public void onSaveViewState(@NonNull Controller controller, @NonNull Bundle outState) {
mCurrentCallState.saveViewStateCalls++;
}
@Override
public void onRestoreViewState(@NonNull Controller controller, @NonNull Bundle savedViewState) {
mCurrentCallState.restoreViewStateCalls++;
}
});
}
interface ChangeHandlerListener {
void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener);
}
public static class ChangeHandler extends ControllerChangeHandler {
private ChangeHandlerListener mListener;
public ChangeHandler() { }
public ChangeHandler(ChangeHandlerListener listener) {
mListener = listener;
}
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
mListener.performChange(container, from, to, isPush, changeListener);
}
}
}
@@ -3,38 +3,38 @@ package com.bluelinelabs.conductor;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.view.View;
import android.widget.FrameLayout;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller.RetainViewMode;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.CallState;
import com.bluelinelabs.conductor.util.TestController;
import com.bluelinelabs.conductor.util.ViewUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.util.ActivityController;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ControllerTests {
private ActivityController<TestActivity> mActivityController;
private Router mRouter;
private ActivityProxy activityProxy;
private Router router;
public void createActivityController(Bundle savedInstanceState) {
mActivityController = Robolectric.buildActivity(TestActivity.class).create(savedInstanceState).start();
@IdRes int containerId = 4;
FrameLayout routerContainer = new FrameLayout(mActivityController.get());
routerContainer.setId(containerId);
mRouter = Conductor.attachRouter(mActivityController.get(), routerContainer, savedInstanceState);
if (!mRouter.hasRootController()) {
mRouter.setRoot(new TestController());
activityProxy = new ActivityProxy().create(savedInstanceState).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@@ -46,29 +46,30 @@ public class ControllerTests {
@Test
public void testViewRetention() {
Controller controller = new TestController();
controller.setRouter(router);
// Test View getting released w/ RELEASE_DETACH
controller.setRetainViewMode(RetainViewMode.RELEASE_DETACH);
Assert.assertNull(controller.getView());
View view = controller.inflate(new FrameLayout(mRouter.getActivity()));
Assert.assertNotNull(controller.getView());
ViewUtils.setAttached(view, true);
Assert.assertNotNull(controller.getView());
ViewUtils.setAttached(view, false);
Assert.assertNull(controller.getView());
assertNull(controller.getView());
View view = controller.inflate(router.container);
assertNotNull(controller.getView());
ViewUtils.reportAttached(view, true);
assertNotNull(controller.getView());
ViewUtils.reportAttached(view, false);
assertNull(controller.getView());
// Test View getting retained w/ RETAIN_DETACH
controller.setRetainViewMode(RetainViewMode.RETAIN_DETACH);
view = controller.inflate(new FrameLayout(mRouter.getActivity()));
Assert.assertNotNull(controller.getView());
ViewUtils.setAttached(view, true);
Assert.assertNotNull(controller.getView());
ViewUtils.setAttached(view, false);
Assert.assertNotNull(controller.getView());
view = controller.inflate(router.container);
assertNotNull(controller.getView());
ViewUtils.reportAttached(view, true);
assertNotNull(controller.getView());
ViewUtils.reportAttached(view, false);
assertNotNull(controller.getView());
// Ensure re-setting RELEASE_DETACH releases
controller.setRetainViewMode(RetainViewMode.RELEASE_DETACH);
Assert.assertNull(controller.getView());
assertNull(controller.getView());
}
@Test
@@ -76,22 +77,21 @@ public class ControllerTests {
TestController controller = new TestController();
CallState expectedCallState = new CallState(true);
mRouter.pushController(RouterTransaction.builder(controller).build());
ViewUtils.setAttached(controller.getView(), true);
router.pushController(RouterTransaction.with(controller));
// Ensure that calling onActivityResult w/o requesting a result doesn't do anything
mRouter.onActivityResult(1, Activity.RESULT_OK, null);
router.onActivityResult(1, Activity.RESULT_OK, null);
assertCalls(expectedCallState, controller);
// Ensure starting an activity for result gets us the result back
controller.startActivityForResult(new Intent("action"), 1);
mRouter.onActivityResult(1, Activity.RESULT_OK, null);
router.onActivityResult(1, Activity.RESULT_OK, null);
expectedCallState.onActivityResultCalls++;
assertCalls(expectedCallState, controller);
// Ensure requesting a result w/o calling startActivityForResult works
controller.registerForActivityResult(2);
mRouter.onActivityResult(2, Activity.RESULT_OK, null);
router.onActivityResult(2, Activity.RESULT_OK, null);
expectedCallState.onActivityResultCalls++;
assertCalls(expectedCallState, controller);
}
@@ -101,29 +101,28 @@ public class ControllerTests {
TestController parent = new TestController();
TestController child = new TestController();
mRouter.pushController(RouterTransaction.builder(parent).build());
ViewUtils.setAttached(parent.getView(), true);
parent.addChildController(ChildControllerTransaction.builder(child, TestController.VIEW_ID).build());
ViewUtils.setAttached(child.getView(), true);
router.pushController(RouterTransaction.with(parent));
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID))
.setRoot(RouterTransaction.with(child));
CallState childExpectedCallState = new CallState(true);
CallState parentExpectedCallState = new CallState(true);
// Ensure that calling onActivityResult w/o requesting a result doesn't do anything
mRouter.onActivityResult(1, Activity.RESULT_OK, null);
router.onActivityResult(1, Activity.RESULT_OK, null);
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
// Ensure starting an activity for result gets us the result back
child.startActivityForResult(new Intent("action"), 1);
mRouter.onActivityResult(1, Activity.RESULT_OK, null);
router.onActivityResult(1, Activity.RESULT_OK, null);
childExpectedCallState.onActivityResultCalls++;
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
// Ensure requesting a result w/o calling startActivityForResult works
child.registerForActivityResult(2);
mRouter.onActivityResult(2, Activity.RESULT_OK, null);
router.onActivityResult(2, Activity.RESULT_OK, null);
childExpectedCallState.onActivityResultCalls++;
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
@@ -136,11 +135,10 @@ public class ControllerTests {
TestController controller = new TestController();
CallState expectedCallState = new CallState(true);
mRouter.pushController(RouterTransaction.builder(controller).build());
ViewUtils.setAttached(controller.getView(), true);
router.pushController(RouterTransaction.with(controller));
// Ensure that calling handleRequestedPermission w/o requesting a result doesn't do anything
mRouter.onRequestPermissionsResult("anotherId", 1, requestedPermissions, new int[] {1});
router.onRequestPermissionsResult("anotherId", 1, requestedPermissions, new int[] {1});
assertCalls(expectedCallState, controller);
// Ensure requesting the permission gets us the result back
@@ -148,7 +146,7 @@ public class ControllerTests {
controller.requestPermissions(requestedPermissions, 1);
} catch (NoSuchMethodError ignored) { }
mRouter.onRequestPermissionsResult(controller.getInstanceId(), 1, requestedPermissions, new int[] {1});
router.onRequestPermissionsResult(controller.getInstanceId(), 1, requestedPermissions, new int[] {1});
expectedCallState.onRequestPermissionsResultCalls++;
assertCalls(expectedCallState, controller);
}
@@ -160,16 +158,15 @@ public class ControllerTests {
TestController parent = new TestController();
TestController child = new TestController();
mRouter.pushController(RouterTransaction.builder(parent).build());
ViewUtils.setAttached(parent.getView(), true);
parent.addChildController(ChildControllerTransaction.builder(child, TestController.VIEW_ID).build());
ViewUtils.setAttached(child.getView(), true);
router.pushController(RouterTransaction.with(parent));
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID))
.setRoot(RouterTransaction.with(child));
CallState childExpectedCallState = new CallState(true);
CallState parentExpectedCallState = new CallState(true);
// Ensure that calling handleRequestedPermission w/o requesting a result doesn't do anything
mRouter.onRequestPermissionsResult("anotherId", 1, requestedPermissions, new int[] {1});
router.onRequestPermissionsResult("anotherId", 1, requestedPermissions, new int[] {1});
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
@@ -178,7 +175,7 @@ public class ControllerTests {
child.requestPermissions(requestedPermissions, 1);
} catch (NoSuchMethodError ignored) { }
mRouter.onRequestPermissionsResult(child.getInstanceId(), 1, requestedPermissions, new int[] {1});
router.onRequestPermissionsResult(child.getInstanceId(), 1, requestedPermissions, new int[] {1});
childExpectedCallState.onRequestPermissionsResultCalls++;
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
@@ -189,35 +186,34 @@ public class ControllerTests {
TestController controller = new TestController();
CallState expectedCallState = new CallState(true);
mRouter.pushController(RouterTransaction.builder(controller).build());
ViewUtils.setAttached(controller.getView(), true);
router.pushController(RouterTransaction.with(controller));
// Ensure that calling onCreateOptionsMenu w/o declaring that we have one doesn't do anything
mRouter.onCreateOptionsMenu(null, null);
router.onCreateOptionsMenu(null, null);
assertCalls(expectedCallState, controller);
// Ensure calling onCreateOptionsMenu with a menu works
controller.setHasOptionsMenu(true);
// Ensure it'll still get called back next time onCreateOptionsMenu is called
mRouter.onCreateOptionsMenu(null, null);
router.onCreateOptionsMenu(null, null);
expectedCallState.createOptionsMenuCalls++;
assertCalls(expectedCallState, controller);
// Ensure we stop getting them when we hide it
controller.setOptionsMenuHidden(true);
mRouter.onCreateOptionsMenu(null, null);
router.onCreateOptionsMenu(null, null);
assertCalls(expectedCallState, controller);
// Ensure we get the callback them when we un-hide it
controller.setOptionsMenuHidden(false);
mRouter.onCreateOptionsMenu(null, null);
router.onCreateOptionsMenu(null, null);
expectedCallState.createOptionsMenuCalls++;
assertCalls(expectedCallState, controller);
// Ensure we don't get the callback when we no longer have a menu
controller.setHasOptionsMenu(false);
mRouter.onCreateOptionsMenu(null, null);
router.onCreateOptionsMenu(null, null);
assertCalls(expectedCallState, controller);
}
@@ -226,16 +222,15 @@ public class ControllerTests {
TestController parent = new TestController();
TestController child = new TestController();
mRouter.pushController(RouterTransaction.builder(parent).build());
ViewUtils.setAttached(parent.getView(), true);
parent.addChildController(ChildControllerTransaction.builder(child, TestController.VIEW_ID).build());
ViewUtils.setAttached(child.getView(), true);
router.pushController(RouterTransaction.with(parent));
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID))
.setRoot(RouterTransaction.with(child));
CallState childExpectedCallState = new CallState(true);
CallState parentExpectedCallState = new CallState(true);
// Ensure that calling onCreateOptionsMenu w/o declaring that we have one doesn't do anything
mRouter.onCreateOptionsMenu(null, null);
router.onCreateOptionsMenu(null, null);
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
@@ -243,33 +238,175 @@ public class ControllerTests {
child.setHasOptionsMenu(true);
// Ensure it'll still get called back next time onCreateOptionsMenu is called
mRouter.onCreateOptionsMenu(null, null);
router.onCreateOptionsMenu(null, null);
childExpectedCallState.createOptionsMenuCalls++;
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
// Ensure we stop getting them when we hide it
child.setOptionsMenuHidden(true);
mRouter.onCreateOptionsMenu(null, null);
router.onCreateOptionsMenu(null, null);
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
// Ensure we get the callback them when we un-hide it
child.setOptionsMenuHidden(false);
mRouter.onCreateOptionsMenu(null, null);
router.onCreateOptionsMenu(null, null);
childExpectedCallState.createOptionsMenuCalls++;
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
// Ensure we don't get the callback when we no longer have a menu
child.setHasOptionsMenu(false);
mRouter.onCreateOptionsMenu(null, null);
router.onCreateOptionsMenu(null, null);
assertCalls(childExpectedCallState, child);
assertCalls(parentExpectedCallState, parent);
}
@Test
public void testAddRemoveChildControllers() {
TestController parent = new TestController();
TestController child1 = new TestController();
TestController child2 = new TestController();
router.pushController(RouterTransaction.with(parent));
assertEquals(0, parent.getChildRouters().size());
assertNull(child1.getParentController());
assertNull(child2.getParentController());
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.setRoot(RouterTransaction.with(child1));
assertEquals(1, parent.getChildRouters().size());
assertEquals(childRouter, parent.getChildRouters().get(0));
assertEquals(1, childRouter.getBackstackSize());
assertEquals(child1, childRouter.getControllers().get(0));
assertEquals(parent, child1.getParentController());
assertNull(child2.getParentController());
childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(child2));
assertEquals(1, parent.getChildRouters().size());
assertEquals(childRouter, parent.getChildRouters().get(0));
assertEquals(2, childRouter.getBackstackSize());
assertEquals(child1, childRouter.getControllers().get(0));
assertEquals(child2, childRouter.getControllers().get(1));
assertEquals(parent, child1.getParentController());
assertEquals(parent, child2.getParentController());
childRouter.popController(child2);
assertEquals(1, parent.getChildRouters().size());
assertEquals(childRouter, parent.getChildRouters().get(0));
assertEquals(1, childRouter.getBackstackSize());
assertEquals(child1, childRouter.getControllers().get(0));
assertEquals(parent, child1.getParentController());
assertNull(child2.getParentController());
childRouter.popController(child1);
assertEquals(1, parent.getChildRouters().size());
assertEquals(childRouter, parent.getChildRouters().get(0));
assertEquals(0, childRouter.getBackstackSize());
assertNull(child1.getParentController());
assertNull(child2.getParentController());
}
@Test
public void testAddRemoveChildRouters() {
TestController parent = new TestController();
TestController child1 = new TestController();
TestController child2 = new TestController();
router.pushController(RouterTransaction.with(parent));
assertEquals(0, parent.getChildRouters().size());
assertNull(child1.getParentController());
assertNull(child2.getParentController());
Router childRouter1 = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1));
Router childRouter2 = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_2));
childRouter1.setRoot(RouterTransaction.with(child1));
childRouter2.setRoot(RouterTransaction.with(child2));
assertEquals(2, parent.getChildRouters().size());
assertEquals(childRouter1, parent.getChildRouters().get(0));
assertEquals(childRouter2, parent.getChildRouters().get(1));
assertEquals(1, childRouter1.getBackstackSize());
assertEquals(1, childRouter2.getBackstackSize());
assertEquals(child1, childRouter1.getControllers().get(0));
assertEquals(child2, childRouter2.getControllers().get(0));
assertEquals(parent, child1.getParentController());
assertEquals(parent, child2.getParentController());
parent.removeChildRouter(childRouter2);
assertEquals(1, parent.getChildRouters().size());
assertEquals(childRouter1, parent.getChildRouters().get(0));
assertEquals(1, childRouter1.getBackstackSize());
assertEquals(0, childRouter2.getBackstackSize());
assertEquals(child1, childRouter1.getControllers().get(0));
assertEquals(parent, child1.getParentController());
assertNull(child2.getParentController());
parent.removeChildRouter(childRouter1);
assertEquals(0, parent.getChildRouters().size());
assertEquals(0, childRouter1.getBackstackSize());
assertEquals(0, childRouter2.getBackstackSize());
assertNull(child1.getParentController());
assertNull(child2.getParentController());
}
@Test
public void testRestoredChildRouterBackstack() {
TestController parent = new TestController();
router.pushController(RouterTransaction.with(parent));
ViewUtils.reportAttached(parent.getView(), true);
RouterTransaction childTransaction1 = RouterTransaction.with(new TestController());
RouterTransaction childTransaction2 = RouterTransaction.with(new TestController());
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1));
childRouter.setPopsLastView(true);
childRouter.setRoot(childTransaction1);
childRouter.pushController(childTransaction2);
Bundle savedState = new Bundle();
childRouter.saveInstanceState(savedState);
parent.removeChildRouter(childRouter);
childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1));
assertEquals(0, childRouter.getBackstackSize());
childRouter.restoreInstanceState(savedState);
childRouter.rebindIfNeeded();
assertEquals(2, childRouter.getBackstackSize());
RouterTransaction restoredChildTransaction1 = childRouter.getBackstack().get(0);
RouterTransaction restoredChildTransaction2 = childRouter.getBackstack().get(1);
assertEquals(childTransaction1.transactionIndex, restoredChildTransaction1.transactionIndex);
assertEquals(childTransaction1.controller.getInstanceId(), restoredChildTransaction1.controller.getInstanceId());
assertEquals(childTransaction2.transactionIndex, restoredChildTransaction2.transactionIndex);
assertEquals(childTransaction2.controller.getInstanceId(), restoredChildTransaction2.controller.getInstanceId());
assertTrue(parent.handleBack());
assertEquals(1, childRouter.getBackstackSize());
assertEquals(restoredChildTransaction1, childRouter.getBackstack().get(0));
assertTrue(parent.handleBack());
assertEquals(0, childRouter.getBackstackSize());
}
private void assertCalls(CallState callState, TestController controller) {
Assert.assertEquals("Expected call counts and controller call counts do not match.", callState, controller.currentCallState);
assertEquals("Expected call counts and controller call counts do not match.", callState, controller.currentCallState);
}
}
@@ -1,52 +1,32 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.IdRes;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler;
import junit.framework.Assert;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class ControllerTransactionTests {
@Test
public void testRouterSaveRestore() {
RouterTransaction transaction = RouterTransaction.builder(new TestController())
RouterTransaction transaction = RouterTransaction.with(new TestController())
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new VerticalChangeHandler())
.tag("Test Tag")
.build();
.tag("Test Tag");
Bundle bundle = transaction.detachAndSaveInstanceState();
Bundle bundle = transaction.saveInstanceState();
RouterTransaction restoredTransaction = new RouterTransaction(bundle);
Assert.assertEquals(transaction.getController().getClass(), restoredTransaction.getController().getClass());
Assert.assertEquals(transaction.getPushControllerChangeHandler().getClass(), restoredTransaction.getPushControllerChangeHandler().getClass());
Assert.assertEquals(transaction.getPopControllerChangeHandler().getClass(), restoredTransaction.getPopControllerChangeHandler().getClass());
Assert.assertEquals(transaction.getTag(), restoredTransaction.getTag());
assertEquals(transaction.controller.getClass(), restoredTransaction.controller.getClass());
assertEquals(transaction.pushChangeHandler().getClass(), restoredTransaction.pushChangeHandler().getClass());
assertEquals(transaction.popChangeHandler().getClass(), restoredTransaction.popChangeHandler().getClass());
assertEquals(transaction.tag(), restoredTransaction.tag());
}
@Test
public void testChildSaveRestore() {
@IdRes int layoutId = 234;
ChildControllerTransaction transaction = ChildControllerTransaction.builder(new TestController(), layoutId)
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new VerticalChangeHandler())
.tag("Test Tag")
.build();
Bundle bundle = transaction.detachAndSaveInstanceState();
ChildControllerTransaction restoredTransaction = new ChildControllerTransaction(bundle);
Assert.assertEquals(transaction.containerId, restoredTransaction.containerId);
Assert.assertEquals(transaction.getController().getClass(), restoredTransaction.getController().getClass());
Assert.assertEquals(transaction.getPushControllerChangeHandler().getClass(), restoredTransaction.getPushControllerChangeHandler().getClass());
Assert.assertEquals(transaction.getPopControllerChangeHandler().getClass(), restoredTransaction.getPopControllerChangeHandler().getClass());
Assert.assertEquals(transaction.getTag(), restoredTransaction.getTag());
}
}
@@ -0,0 +1,240 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ReattachCaseTests {
private ActivityProxy activityProxy;
private Router router;
public void createActivityController(Bundle savedInstanceState) {
activityProxy = new ActivityProxy().create(savedInstanceState).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Before
public void setup() {
createActivityController(null);
}
@Test
public void testNeedsAttachingOnPauseAndOrientation() {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
sleepWakeDevice();
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
activityProxy.rotate();
router.rebindIfNeeded();
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
}
@Test
public void testChildNeedsAttachOnPauseAndOrientation() {
final Controller controllerA = new TestController();
final Controller childController = new TestController();
final Controller controllerB = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertTrue(controllerA.isAttached());
assertTrue(childController.isAttached());
assertFalse(controllerB.isAttached());
sleepWakeDevice();
assertTrue(controllerA.isAttached());
assertTrue(childController.isAttached());
assertFalse(controllerB.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertFalse(controllerA.isAttached());
assertFalse(childController.isAttached());
assertTrue(controllerB.isAttached());
activityProxy.rotate();
router.rebindIfNeeded();
assertFalse(controllerA.isAttached());
assertFalse(childController.isAttached());
assertTrue(childController.getNeedsAttach());
assertTrue(controllerB.isAttached());
}
@Test
public void testChildHandleBackOnOrientation() {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
final TestController childController = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
assertFalse(childController.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Router childRouter = controllerB.getChildRouter((ViewGroup)controllerB.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
activityProxy.rotate();
router.rebindIfNeeded();
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
router.handleBack();
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertFalse(childController.isAttached());
router.handleBack();
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
assertFalse(childController.isAttached());
}
// Attempt to test https://github.com/bluelinelabs/Conductor/issues/86#issuecomment-231381271
@Test
public void testReusedChildRouterHandleBackOnOrientation() {
TestController controllerA = new TestController();
TestController controllerB = new TestController();
TestController childController = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
assertFalse(childController.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Router childRouter = controllerB.getChildRouter((ViewGroup)controllerB.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
router.handleBack();
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertFalse(childController.isAttached());
childController = new TestController();
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
activityProxy.rotate();
router.rebindIfNeeded();
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
router.handleBack();
childController = new TestController();
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
router.handleBack();
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertFalse(childController.isAttached());
router.handleBack();
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
assertFalse(childController.isAttached());
}
private void sleepWakeDevice() {
activityProxy.saveInstanceState(new Bundle()).pause();
activityProxy.resume();
}
}
@@ -0,0 +1,239 @@
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;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class RouterChangeHandlerTests {
private Router router;
@Before
public void setup() {
ActivityProxy activityProxy = new ActivityProxy().create(null).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), null);
}
@Test
public void testSetRootHandler() {
MockChangeHandler handler = MockChangeHandler.taggedHandler("root", true);
TestController rootController = new TestController();
router.setRoot(RouterTransaction.with(rootController).pushChangeHandler(handler));
assertTrue(rootController.changeHandlerHistory.isValidHistory);
assertNull(rootController.changeHandlerHistory.latestFromView());
assertNotNull(rootController.changeHandlerHistory.latestToView());
assertEquals(rootController.getView(), rootController.changeHandlerHistory.latestToView());
assertTrue(rootController.changeHandlerHistory.latestIsPush());
assertEquals(handler.tag, rootController.changeHandlerHistory.latestChangeHandler().tag);
}
@Test
public void testPushPopHandlers() {
TestController rootController = new TestController();
router.setRoot(RouterTransaction.with(rootController).pushChangeHandler(MockChangeHandler.defaultHandler()));
View rootView = rootController.getView();
MockChangeHandler pushHandler = MockChangeHandler.taggedHandler("push", true);
MockChangeHandler popHandler = MockChangeHandler.taggedHandler("pop", true);
TestController pushController = new TestController();
router.pushController(RouterTransaction.with(pushController).pushChangeHandler(pushHandler).popChangeHandler(popHandler));
assertTrue(rootController.changeHandlerHistory.isValidHistory);
assertTrue(pushController.changeHandlerHistory.isValidHistory);
assertNotNull(pushController.changeHandlerHistory.latestFromView());
assertNotNull(pushController.changeHandlerHistory.latestToView());
assertEquals(rootView, pushController.changeHandlerHistory.latestFromView());
assertEquals(pushController.getView(), pushController.changeHandlerHistory.latestToView());
assertTrue(pushController.changeHandlerHistory.latestIsPush());
assertEquals(pushHandler.tag, pushController.changeHandlerHistory.latestChangeHandler().tag);
View pushView = pushController.getView();
router.popController(pushController);
assertNotNull(pushController.changeHandlerHistory.latestFromView());
assertNotNull(pushController.changeHandlerHistory.latestToView());
assertEquals(pushView, pushController.changeHandlerHistory.fromViewAt(1));
assertEquals(rootController.getView(), pushController.changeHandlerHistory.latestToView());
assertFalse(pushController.changeHandlerHistory.latestIsPush());
assertEquals(popHandler.tag, pushController.changeHandlerHistory.latestChangeHandler().tag);
}
@Test
public void testResetRootHandlers() {
TestController initialController1 = new TestController();
MockChangeHandler initialPushHandler1 = MockChangeHandler.taggedHandler("initialPush1", true);
MockChangeHandler initialPopHandler1 = MockChangeHandler.taggedHandler("initialPop1", true);
router.setRoot(RouterTransaction.with(initialController1).pushChangeHandler(initialPushHandler1).popChangeHandler(initialPopHandler1));
TestController initialController2 = new TestController();
MockChangeHandler initialPushHandler2 = MockChangeHandler.taggedHandler("initialPush2", false);
MockChangeHandler initialPopHandler2 = MockChangeHandler.taggedHandler("initialPop2", false);
router.pushController(RouterTransaction.with(initialController2).pushChangeHandler(initialPushHandler2).popChangeHandler(initialPopHandler2));
View initialView1 = initialController1.getView();
View initialView2 = initialController2.getView();
TestController newRootController = new TestController();
MockChangeHandler newRootHandler = MockChangeHandler.taggedHandler("newRootHandler", true);
router.setRoot(RouterTransaction.with(newRootController).pushChangeHandler(newRootHandler));
assertTrue(initialController1.changeHandlerHistory.isValidHistory);
assertTrue(initialController2.changeHandlerHistory.isValidHistory);
assertTrue(newRootController.changeHandlerHistory.isValidHistory);
assertEquals(3, initialController1.changeHandlerHistory.size());
assertEquals(2, initialController2.changeHandlerHistory.size());
assertEquals(1, newRootController.changeHandlerHistory.size());
assertNotNull(initialController1.changeHandlerHistory.latestToView());
assertEquals(newRootController.getView(), initialController1.changeHandlerHistory.latestToView());
assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView());
assertEquals(newRootHandler.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController1.changeHandlerHistory.latestIsPush());
assertNull(initialController2.changeHandlerHistory.latestToView());
assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView());
assertEquals(newRootHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController2.changeHandlerHistory.latestIsPush());
assertNotNull(newRootController.changeHandlerHistory.latestToView());
assertEquals(newRootController.getView(), newRootController.changeHandlerHistory.latestToView());
assertEquals(initialView1, newRootController.changeHandlerHistory.latestFromView());
assertEquals(newRootHandler.tag, newRootController.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(newRootController.changeHandlerHistory.latestIsPush());
}
@Test
public void testSetBackstackHandlers() {
TestController initialController1 = new TestController();
MockChangeHandler initialPushHandler1 = MockChangeHandler.taggedHandler("initialPush1", true);
MockChangeHandler initialPopHandler1 = MockChangeHandler.taggedHandler("initialPop1", true);
router.setRoot(RouterTransaction.with(initialController1).pushChangeHandler(initialPushHandler1).popChangeHandler(initialPopHandler1));
TestController initialController2 = new TestController();
MockChangeHandler initialPushHandler2 = MockChangeHandler.taggedHandler("initialPush2", false);
MockChangeHandler initialPopHandler2 = MockChangeHandler.taggedHandler("initialPop2", false);
router.pushController(RouterTransaction.with(initialController2).pushChangeHandler(initialPushHandler2).popChangeHandler(initialPopHandler2));
View initialView1 = initialController1.getView();
View initialView2 = initialController2.getView();
TestController newController1 = new TestController();
TestController newController2 = new TestController();
MockChangeHandler setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true);
List<RouterTransaction> newBackstack = ListUtils.listOf(
RouterTransaction.with(newController1),
RouterTransaction.with(newController2)
);
router.setBackstack(newBackstack, setBackstackHandler);
assertTrue(initialController1.changeHandlerHistory.isValidHistory);
assertTrue(initialController2.changeHandlerHistory.isValidHistory);
assertTrue(newController1.changeHandlerHistory.isValidHistory);
assertEquals(3, initialController1.changeHandlerHistory.size());
assertEquals(2, initialController2.changeHandlerHistory.size());
assertEquals(0, newController1.changeHandlerHistory.size());
assertEquals(1, newController2.changeHandlerHistory.size());
assertNotNull(initialController1.changeHandlerHistory.latestToView());
assertEquals(newController2.getView(), initialController1.changeHandlerHistory.latestToView());
assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController1.changeHandlerHistory.latestIsPush());
assertNull(initialController2.changeHandlerHistory.latestToView());
assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController2.changeHandlerHistory.latestIsPush());
assertNotNull(newController2.changeHandlerHistory.latestToView());
assertEquals(newController2.getView(), newController2.changeHandlerHistory.latestToView());
assertEquals(initialView1, newController2.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, newController2.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(newController2.changeHandlerHistory.latestIsPush());
}
@Test
public void testSetBackstackWithTwoVisibleHandlers() {
TestController initialController1 = new TestController();
MockChangeHandler initialPushHandler1 = MockChangeHandler.taggedHandler("initialPush1", true);
MockChangeHandler initialPopHandler1 = MockChangeHandler.taggedHandler("initialPop1", true);
router.setRoot(RouterTransaction.with(initialController1).pushChangeHandler(initialPushHandler1).popChangeHandler(initialPopHandler1));
TestController initialController2 = new TestController();
MockChangeHandler initialPushHandler2 = MockChangeHandler.taggedHandler("initialPush2", false);
MockChangeHandler initialPopHandler2 = MockChangeHandler.taggedHandler("initialPop2", false);
router.pushController(RouterTransaction.with(initialController2).pushChangeHandler(initialPushHandler2).popChangeHandler(initialPopHandler2));
View initialView1 = initialController1.getView();
View initialView2 = initialController2.getView();
TestController newController1 = new TestController();
TestController newController2 = new TestController();
MockChangeHandler setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true);
List<RouterTransaction> newBackstack = ListUtils.listOf(
RouterTransaction.with(newController1),
RouterTransaction.with(newController2).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler())
);
router.setBackstack(newBackstack, setBackstackHandler);
assertTrue(initialController1.changeHandlerHistory.isValidHistory);
assertTrue(initialController2.changeHandlerHistory.isValidHistory);
assertTrue(newController1.changeHandlerHistory.isValidHistory);
assertEquals(3, initialController1.changeHandlerHistory.size());
assertEquals(2, initialController2.changeHandlerHistory.size());
assertEquals(2, newController1.changeHandlerHistory.size());
assertEquals(1, newController2.changeHandlerHistory.size());
assertNotNull(initialController1.changeHandlerHistory.latestToView());
assertEquals(newController1.getView(), initialController1.changeHandlerHistory.latestToView());
assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController1.changeHandlerHistory.latestIsPush());
assertNull(initialController2.changeHandlerHistory.latestToView());
assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController2.changeHandlerHistory.latestIsPush());
assertNotNull(newController1.changeHandlerHistory.latestToView());
assertEquals(newController1.getView(), newController1.changeHandlerHistory.toViewAt(0));
assertEquals(newController2.getView(), newController1.changeHandlerHistory.latestToView());
assertEquals(initialView1, newController1.changeHandlerHistory.fromViewAt(0));
assertEquals(newController1.getView(), newController1.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, newController1.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(newController1.changeHandlerHistory.latestIsPush());
assertNotNull(newController2.changeHandlerHistory.latestToView());
assertEquals(newController2.getView(), newController2.changeHandlerHistory.latestToView());
assertEquals(newController1.getView(), newController2.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, newController2.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(newController2.changeHandlerHistory.latestIsPush());
}
}
@@ -1,26 +1,35 @@
package com.bluelinelabs.conductor;
import android.app.Activity;
import android.widget.FrameLayout;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.ListUtils;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class RouterTests {
private Router mRouter;
private Router router;
@Before
public void setup() {
Activity activity = Robolectric.buildActivity(TestActivity.class).create().get();
mRouter = Conductor.attachRouter(activity, new FrameLayout(activity), null);
ActivityProxy activityProxy = new ActivityProxy().create(null).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), null);
}
@Test
@@ -29,13 +38,13 @@ public class RouterTests {
Controller rootController = new TestController();
Assert.assertFalse(mRouter.hasRootController());
assertFalse(router.hasRootController());
mRouter.setRoot(rootController, rootTag);
router.setRoot(RouterTransaction.with(rootController).tag(rootTag));
Assert.assertTrue(mRouter.hasRootController());
assertTrue(router.hasRootController());
Assert.assertEquals(rootController, mRouter.getControllerWithTag(rootTag));
assertEquals(rootController, router.getControllerWithTag(rootTag));
}
@Test
@@ -46,21 +55,21 @@ public class RouterTests {
Controller oldRootController = new TestController();
Controller newRootController = new TestController();
mRouter.setRoot(oldRootController, oldRootTag);
mRouter.setRoot(newRootController, newRootTag);
router.setRoot(RouterTransaction.with(oldRootController).tag(oldRootTag));
router.setRoot(RouterTransaction.with(newRootController).tag(newRootTag));
Assert.assertNull(mRouter.getControllerWithTag(oldRootTag));
Assert.assertEquals(newRootController, mRouter.getControllerWithTag(newRootTag));
assertNull(router.getControllerWithTag(oldRootTag));
assertEquals(newRootController, router.getControllerWithTag(newRootTag));
}
@Test
public void testGetByInstanceId() {
Controller controller = new TestController();
mRouter.pushController(RouterTransaction.builder(controller).build());
router.pushController(RouterTransaction.with(controller));
Assert.assertEquals(controller, mRouter.getControllerWithInstanceId(controller.getInstanceId()));
Assert.assertNull(mRouter.getControllerWithInstanceId("fake id"));
assertEquals(controller, router.getControllerWithInstanceId(controller.getInstanceId()));
assertNull(router.getControllerWithInstanceId("fake id"));
}
@Test
@@ -71,16 +80,14 @@ public class RouterTests {
Controller controller1 = new TestController();
Controller controller2 = new TestController();
mRouter.pushController(RouterTransaction.builder(controller1)
.tag(controller1Tag)
.build());
router.pushController(RouterTransaction.with(controller1)
.tag(controller1Tag));
mRouter.pushController(RouterTransaction.builder(controller2)
.tag(controller2Tag)
.build());
router.pushController(RouterTransaction.with(controller2)
.tag(controller2Tag));
Assert.assertEquals(controller1, mRouter.getControllerWithTag(controller1Tag));
Assert.assertEquals(controller2, mRouter.getControllerWithTag(controller2Tag));
assertEquals(controller1, router.getControllerWithTag(controller1Tag));
assertEquals(controller2, router.getControllerWithTag(controller2Tag));
}
@Test
@@ -91,31 +98,29 @@ public class RouterTests {
Controller controller1 = new TestController();
Controller controller2 = new TestController();
mRouter.pushController(RouterTransaction.builder(controller1)
.tag(controller1Tag)
.build());
router.pushController(RouterTransaction.with(controller1)
.tag(controller1Tag));
Assert.assertEquals(1, mRouter.getBackstackSize());
assertEquals(1, router.getBackstackSize());
mRouter.pushController(RouterTransaction.builder(controller2)
.tag(controller2Tag)
.build());
router.pushController(RouterTransaction.with(controller2)
.tag(controller2Tag));
Assert.assertEquals(2, mRouter.getBackstackSize());
assertEquals(2, router.getBackstackSize());
mRouter.popCurrentController();
router.popCurrentController();
Assert.assertEquals(1, mRouter.getBackstackSize());
assertEquals(1, router.getBackstackSize());
Assert.assertEquals(controller1, mRouter.getControllerWithTag(controller1Tag));
Assert.assertNull(mRouter.getControllerWithTag(controller2Tag));
assertEquals(controller1, router.getControllerWithTag(controller1Tag));
assertNull(router.getControllerWithTag(controller2Tag));
mRouter.popCurrentController();
router.popCurrentController();
Assert.assertEquals(0, mRouter.getBackstackSize());
assertEquals(0, router.getBackstackSize());
Assert.assertNull(mRouter.getControllerWithTag(controller1Tag));
Assert.assertNull(mRouter.getControllerWithTag(controller2Tag));
assertNull(router.getControllerWithTag(controller1Tag));
assertNull(router.getControllerWithTag(controller2Tag));
}
@Test
@@ -130,29 +135,25 @@ public class RouterTests {
Controller controller3 = new TestController();
Controller controller4 = new TestController();
mRouter.pushController(RouterTransaction.builder(controller1)
.tag(controller1Tag)
.build());
router.pushController(RouterTransaction.with(controller1)
.tag(controller1Tag));
mRouter.pushController(RouterTransaction.builder(controller2)
.tag(controller2Tag)
.build());
router.pushController(RouterTransaction.with(controller2)
.tag(controller2Tag));
mRouter.pushController(RouterTransaction.builder(controller3)
.tag(controller3Tag)
.build());
router.pushController(RouterTransaction.with(controller3)
.tag(controller3Tag));
mRouter.pushController(RouterTransaction.builder(controller4)
.tag(controller4Tag)
.build());
router.pushController(RouterTransaction.with(controller4)
.tag(controller4Tag));
mRouter.popToTag(controller2Tag);
router.popToTag(controller2Tag);
Assert.assertEquals(2, mRouter.getBackstackSize());
Assert.assertEquals(controller1, mRouter.getControllerWithTag(controller1Tag));
Assert.assertEquals(controller2, mRouter.getControllerWithTag(controller2Tag));
Assert.assertNull(mRouter.getControllerWithTag(controller3Tag));
Assert.assertNull(mRouter.getControllerWithTag(controller4Tag));
assertEquals(2, router.getBackstackSize());
assertEquals(controller1, router.getControllerWithTag(controller1Tag));
assertEquals(controller2, router.getControllerWithTag(controller2Tag));
assertNull(router.getControllerWithTag(controller3Tag));
assertNull(router.getControllerWithTag(controller4Tag));
}
@Test
@@ -165,24 +166,209 @@ public class RouterTests {
Controller controller2 = new TestController();
Controller controller3 = new TestController();
mRouter.pushController(RouterTransaction.builder(controller1)
.tag(controller1Tag)
.build());
router.pushController(RouterTransaction.with(controller1)
.tag(controller1Tag));
mRouter.pushController(RouterTransaction.builder(controller2)
.tag(controller2Tag)
.build());
router.pushController(RouterTransaction.with(controller2)
.tag(controller2Tag));
mRouter.pushController(RouterTransaction.builder(controller3)
.tag(controller3Tag)
.build());
router.pushController(RouterTransaction.with(controller3)
.tag(controller3Tag));
mRouter.popController(controller2);
router.popController(controller2);
Assert.assertEquals(2, mRouter.getBackstackSize());
Assert.assertEquals(controller1, mRouter.getControllerWithTag(controller1Tag));
Assert.assertNull(mRouter.getControllerWithTag(controller2Tag));
Assert.assertEquals(controller3, mRouter.getControllerWithTag(controller3Tag));
assertEquals(2, router.getBackstackSize());
assertEquals(controller1, router.getControllerWithTag(controller1Tag));
assertNull(router.getControllerWithTag(controller2Tag));
assertEquals(controller3, router.getControllerWithTag(controller3Tag));
}
@Test
public void testSetBackstack() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction middleTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = ListUtils.listOf(rootTransaction, middleTransaction, topTransaction);
router.setBackstack(backstack, null);
assertEquals(3, router.getBackstackSize());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(middleTransaction, fetchedBackstack.get(1));
assertEquals(topTransaction, fetchedBackstack.get(2));
}
@Test
public void testNewSetBackstack() {
router.setRoot(RouterTransaction.with(new TestController()));
assertEquals(1, router.getBackstackSize());
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction middleTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = ListUtils.listOf(rootTransaction, middleTransaction, topTransaction);
router.setBackstack(backstack, null);
assertEquals(3, router.getBackstackSize());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(middleTransaction, fetchedBackstack.get(1));
assertEquals(topTransaction, fetchedBackstack.get(2));
assertEquals(router, rootTransaction.controller.getRouter());
assertEquals(router, middleTransaction.controller.getRouter());
assertEquals(router, topTransaction.controller.getRouter());
}
@Test
public void testNewSetBackstackWithNoRemoveViewOnPush() {
RouterTransaction oldRootTransaction = RouterTransaction.with(new TestController());
RouterTransaction oldTopTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
router.setRoot(oldRootTransaction);
router.pushController(oldTopTransaction);
assertEquals(2, router.getBackstackSize());
assertTrue(oldRootTransaction.controller.isAttached());
assertTrue(oldTopTransaction.controller.isAttached());
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
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);
router.setBackstack(backstack, null);
assertEquals(3, router.getBackstackSize());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(middleTransaction, fetchedBackstack.get(1));
assertEquals(topTransaction, fetchedBackstack.get(2));
assertFalse(oldRootTransaction.controller.isAttached());
assertFalse(oldTopTransaction.controller.isAttached());
assertTrue(rootTransaction.controller.isAttached());
assertTrue(middleTransaction.controller.isAttached());
assertTrue(topTransaction.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);
router.setBackstack(backstack, null);
assertEquals(2, router.getBackstackSize());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(topTransaction, fetchedBackstack.get(1));
RouterTransaction newTopTransaction = RouterTransaction.with(new TestController());
router.replaceTopController(newTopTransaction);
assertEquals(2, router.getBackstackSize());
fetchedBackstack = router.getBackstack();
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(newTopTransaction, fetchedBackstack.get(1));
}
@Test
public void testReplaceTopControllerWithNoRemoveViewOnPush() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
List<RouterTransaction> backstack = ListUtils.listOf(rootTransaction, topTransaction);
router.setBackstack(backstack, null);
assertEquals(2, router.getBackstackSize());
assertTrue(rootTransaction.controller.isAttached());
assertTrue(topTransaction.controller.isAttached());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(topTransaction, fetchedBackstack.get(1));
RouterTransaction newTopTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
router.replaceTopController(newTopTransaction);
newTopTransaction.pushChangeHandler().completeImmediately();
assertEquals(2, router.getBackstackSize());
fetchedBackstack = router.getBackstack();
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(newTopTransaction, fetchedBackstack.get(1));
assertTrue(rootTransaction.controller.isAttached());
assertFalse(topTransaction.controller.isAttached());
assertTrue(newTopTransaction.controller.isAttached());
}
@Test
public void testRearrangeTransactionBackstack() {
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = ListUtils.listOf(transaction1, transaction2);
router.setBackstack(backstack, null);
assertEquals(1, transaction1.transactionIndex);
assertEquals(2, transaction2.transactionIndex);
backstack = ListUtils.listOf(transaction2, transaction1);
router.setBackstack(backstack, null);
assertEquals(1, transaction2.transactionIndex);
assertEquals(2, transaction1.transactionIndex);
router.handleBack();
assertEquals(1, router.getBackstackSize());
assertEquals(transaction2, router.getBackstack().get(0));
router.handleBack();
assertEquals(0, router.getBackstackSize());
}
@Test
public void testChildRouterRearrangeTransactionBackstack() {
Controller parent = new TestController();
router.setRoot(RouterTransaction.with(parent));
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1));
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = ListUtils.listOf(transaction1, transaction2);
childRouter.setBackstack(backstack, null);
assertEquals(2, transaction1.transactionIndex);
assertEquals(3, transaction2.transactionIndex);
backstack = ListUtils.listOf(transaction2, transaction1);
childRouter.setBackstack(backstack, null);
assertEquals(2, transaction2.transactionIndex);
assertEquals(3, transaction1.transactionIndex);
childRouter.handleBack();
assertEquals(1, childRouter.getBackstackSize());
assertEquals(transaction2, childRouter.getBackstack().get(0));
childRouter.handleBack();
assertEquals(0, childRouter.getBackstackSize());
}
}
@@ -0,0 +1,106 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class TargetControllerTests {
private Router router;
public void createActivityController(Bundle savedInstanceState) {
ActivityProxy activityProxy = new ActivityProxy().create(savedInstanceState).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Before
public void setup() {
createActivityController(null);
}
@Test
public void testSiblingTarget() {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
assertNull(controllerA.getTargetController());
assertNull(controllerB.getTargetController());
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
controllerB.setTargetController(controllerA);
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertNull(controllerA.getTargetController());
assertEquals(controllerA, controllerB.getTargetController());
}
@Test
public void testParentChildTarget() {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
assertNull(controllerA.getTargetController());
assertNull(controllerB.getTargetController());
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
controllerB.setTargetController(controllerA);
Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertNull(controllerA.getTargetController());
assertEquals(controllerA, controllerB.getTargetController());
}
@Test
public void testChildParentTarget() {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
assertNull(controllerA.getTargetController());
assertNull(controllerB.getTargetController());
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
controllerA.setTargetController(controllerB);
Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertNull(controllerB.getTargetController());
assertEquals(controllerB, controllerA.getTargetController());
}
}
@@ -0,0 +1,236 @@
package com.bluelinelabs.conductor;
import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import com.bluelinelabs.conductor.internal.ViewAttachHandler;
import com.bluelinelabs.conductor.internal.ViewAttachHandler.ViewAttachListener;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.ViewUtils;
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.assertEquals;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ViewAttachHandlerTests {
private Activity activity;
private ViewAttachHandler viewAttachHandler;
private CountingViewAttachListener viewAttachListener;
@Before
public void setup() {
activity = new ActivityProxy().create(null).getActivity();
viewAttachListener = new CountingViewAttachListener();
viewAttachHandler = new ViewAttachHandler(viewAttachListener);
}
@Test
public void testSimpleViewAttachDetach() {
View view = new View(activity);
viewAttachHandler.listenForAttach(view);
assertEquals(0, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(2, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStopped();
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStarted();
assertEquals(3, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
}
@Test
public void testSimpleViewGroupAttachDetach() {
View view = new View(activity);
viewAttachHandler.listenForAttach(view);
assertEquals(0, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(2, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStopped();
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStarted();
assertEquals(3, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
}
@Test
public void testNestedViewGroupAttachDetach() {
ViewGroup view = new LinearLayout(activity);
View child = new LinearLayout(activity);
view.addView(child);
viewAttachHandler.listenForAttach(view);
assertEquals(0, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true, false);
assertEquals(0, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(child, true, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true, false);
ViewUtils.reportAttached(child, true, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(child, true, false);
assertEquals(2, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStopped();
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false, false);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true, false);
ViewUtils.reportAttached(child, true, false);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStarted();
assertEquals(3, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
}
private static class CountingViewAttachListener implements ViewAttachListener {
int attaches;
int detaches;
int detachAfterStops;
@Override
public void onAttached() {
attaches++;
}
@Override
public void onDetached(boolean fromActivityStop) {
detaches++;
}
@Override
public void onViewDetachAfterStop() {
detachAfterStops++;
}
}
}
@@ -1,25 +0,0 @@
package com.bluelinelabs.conductor;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import org.robolectric.util.ReflectionHelpers;
import java.util.List;
public class ViewUtils {
static void setAttached(View view, boolean attached) {
Object listenerInfo = ReflectionHelpers.callInstanceMethod(view, "getListenerInfo");
List<OnAttachStateChangeListener> listeners = ReflectionHelpers.getField(listenerInfo, "mOnAttachStateChangeListeners");
for (OnAttachStateChangeListener listener : listeners) {
if (attached) {
listener.onViewAttachedToWindow(view);
} else {
listener.onViewDetachedFromWindow(view);
}
}
}
}
@@ -0,0 +1,77 @@
package com.bluelinelabs.conductor.util;
import android.os.Bundle;
import android.support.annotation.IdRes;
import org.robolectric.Robolectric;
import org.robolectric.util.ActivityController;
public class ActivityProxy {
private ActivityController<TestActivity> activityController;
private AttachFakingFrameLayout view;
public ActivityProxy() {
activityController = Robolectric.buildActivity(TestActivity.class);
@IdRes int containerId = 4;
view = new AttachFakingFrameLayout(activityController.get());
view.setId(containerId);
}
public ActivityProxy create(Bundle savedInstanceState) {
activityController.create(savedInstanceState);
return this;
}
public ActivityProxy start() {
activityController.start();
view.setAttached(true);
return this;
}
public ActivityProxy resume() {
activityController.resume();
return this;
}
public ActivityProxy pause() {
activityController.pause();
return this;
}
public ActivityProxy saveInstanceState(Bundle outState) {
activityController.saveInstanceState(outState);
return this;
}
public ActivityProxy stop(boolean detachView) {
activityController.stop();
if (detachView) {
view.setAttached(false);
}
return this;
}
public ActivityProxy destroy() {
activityController.destroy();
view.setAttached(false);
return this;
}
public ActivityProxy rotate() {
getActivity().isChangingConfigurations = true;
getActivity().recreate();
return this;
}
public TestActivity getActivity() {
return activityController.get();
}
public AttachFakingFrameLayout getView() {
return view;
}
}
@@ -0,0 +1,113 @@
package com.bluelinelabs.conductor.util;
import android.content.Context;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import java.io.FileDescriptor;
public class AttachFakingFrameLayout extends FrameLayout {
final IBinder fakeWindowToken = new IBinder() {
@Override
public String getInterfaceDescriptor() throws RemoteException {
return null;
}
@Override
public boolean pingBinder() {
return false;
}
@Override
public boolean isBinderAlive() {
return false;
}
@Override
public IInterface queryLocalInterface(String descriptor) {
return null;
}
@Override
public void dump(FileDescriptor fd, String[] args) throws RemoteException {
}
@Override
public void dumpAsync(FileDescriptor fd, String[] args) throws RemoteException {
}
@Override
public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
return false;
}
@Override
public void linkToDeath(DeathRecipient recipient, int flags) throws RemoteException {
}
@Override
public boolean unlinkToDeath(DeathRecipient recipient, int flags) {
return false;
}
};
private boolean reportAttached;
public AttachFakingFrameLayout(Context context) {
super(context);
}
public AttachFakingFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AttachFakingFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public final IBinder getWindowToken() {
return reportAttached ? fakeWindowToken : null;
}
public void setAttached(boolean attached) {
setAttached(attached, true);
}
public void setAttached(boolean attached, boolean reportToViewUtils) {
if (reportAttached != attached) {
reportAttached = attached;
if (reportToViewUtils) {
ViewUtils.reportAttached(this, attached);
}
for (int i = 0; i < getChildCount(); i++) {
ViewUtils.reportAttached(getChildAt(i), attached);
}
}
}
@Override
public void onViewAdded(View child) {
if (reportAttached) {
ViewUtils.reportAttached(child, true);
}
super.onViewAdded(child);
}
@Override
public void onViewRemoved(View child) {
ViewUtils.reportAttached(child, false);
super.onViewRemoved(child);
}
}
@@ -1,4 +1,4 @@
package com.bluelinelabs.conductor;
package com.bluelinelabs.conductor.util;
import android.os.Parcel;
import android.os.Parcelable;
@@ -119,9 +119,9 @@ public class CallState implements Parcelable {
"\n restoreInstanceStateCalls=" + restoreInstanceStateCalls +
"\n saveViewStateCalls=" + saveViewStateCalls +
"\n restoreViewStateCalls=" + restoreViewStateCalls +
"\n onActivityResultCalls= " + onActivityResultCalls +
"\n onRequestPermissionsResultCalls= " + onRequestPermissionsResultCalls +
"\n createOptionsMenuCalls= " + createOptionsMenuCalls +
"\n onActivityResultCalls=" + onActivityResultCalls +
"\n onRequestPermissionsResultCalls=" + onRequestPermissionsResultCalls +
"\n createOptionsMenuCalls=" + createOptionsMenuCalls +
"}\n";
}
@@ -0,0 +1,67 @@
package com.bluelinelabs.conductor.util;
import android.view.View;
import java.util.ArrayList;
import java.util.List;
public class ChangeHandlerHistory {
private List<Entry> entries = new ArrayList<>();
public boolean isValidHistory = true;
public void addEntry(View from, View to, boolean isPush, MockChangeHandler handler) {
entries.add(new Entry(from, to, isPush, handler));
}
public int size() {
return entries.size();
}
public View fromViewAt(int index) {
return entries.get(index).from;
}
public View toViewAt(int index) {
return entries.get(index).to;
}
public boolean isPushAt(int index) {
return entries.get(index).isPush;
}
public MockChangeHandler changeHandlerAt(int index) {
return entries.get(index).changeHandler;
}
public View latestFromView() {
return fromViewAt(size() - 1);
}
public View latestToView() {
return toViewAt(size() - 1);
}
public boolean latestIsPush() {
return isPushAt(size() - 1);
}
public MockChangeHandler latestChangeHandler() {
return changeHandlerAt(size() - 1);
}
private static class Entry {
final View from;
final View to;
final boolean isPush;
final MockChangeHandler changeHandler;
Entry(View from, View to, boolean isPush, MockChangeHandler changeHandler) {
this.from = from;
this.to = to;
this.isPush = isPush;
this.changeHandler = changeHandler;
}
}
}
@@ -0,0 +1,16 @@
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;
}
}
@@ -0,0 +1,118 @@
package com.bluelinelabs.conductor.util;
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.ControllerChangeHandler;
public class MockChangeHandler extends ControllerChangeHandler {
private static final String KEY_REMOVES_FROM_VIEW_ON_PUSH = "MockChangeHandler.removesFromViewOnPush";
private static final String KEY_TAG = "MockChangeHandler.tag";
public static class ChangeHandlerListener {
public void willStartChange() { }
public void didAttachOrDetach() { }
public void didEndChange() { }
}
private final ChangeHandlerListener listener;
private boolean removesFromViewOnPush;
public View from;
public View to;
public String tag;
public static MockChangeHandler defaultHandler() {
return new MockChangeHandler(true, null, null);
}
public static MockChangeHandler noRemoveViewOnPushHandler() {
return new MockChangeHandler(false, null, null);
}
public static MockChangeHandler listeningChangeHandler(@NonNull ChangeHandlerListener listener) {
return new MockChangeHandler(true, null, listener);
}
public static MockChangeHandler taggedHandler(String tag, boolean removeViewOnPush) {
return new MockChangeHandler(removeViewOnPush, tag, null);
}
public MockChangeHandler() {
listener = null;
}
private MockChangeHandler(boolean removesFromViewOnPush, String tag, ChangeHandlerListener listener) {
this.removesFromViewOnPush = removesFromViewOnPush;
if (listener == null) {
this.listener = new ChangeHandlerListener() { };
} else {
this.listener = listener;
}
}
@Override
public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
this.from = from;
this.to = to;
listener.willStartChange();
if (isPush) {
if (to != null) {
container.addView(to);
listener.didAttachOrDetach();
}
if (removesFromViewOnPush && from != null) {
container.removeView(from);
}
} else {
container.removeView(from);
listener.didAttachOrDetach();
if (to != null) {
container.addView(to);
}
}
changeListener.onChangeCompleted();
listener.didEndChange();
}
@Override
public boolean removesFromViewOnPush() {
return removesFromViewOnPush;
}
@Override
public void saveToBundle(@NonNull Bundle bundle) {
super.saveToBundle(bundle);
bundle.putBoolean(KEY_REMOVES_FROM_VIEW_ON_PUSH, removesFromViewOnPush);
bundle.putString(KEY_TAG, tag);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
super.restoreFromBundle(bundle);
removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_VIEW_ON_PUSH);
tag = bundle.getString(KEY_TAG);
}
@NonNull
@Override
public ControllerChangeHandler copy() {
return new MockChangeHandler(removesFromViewOnPush, tag, listener);
}
@Override
public boolean isReusable() {
return true;
}
}
@@ -1,14 +1,19 @@
package com.bluelinelabs.conductor;
package com.bluelinelabs.conductor.util;
import android.app.Activity;
public class TestActivity extends Activity {
public boolean isChangingConfigurations = false;
public boolean isDestroying = false;
@Override
public boolean isChangingConfigurations() {
return isChangingConfigurations;
}
@Override
public boolean isDestroyed() {
return isDestroying || super.isDestroyed();
}
}
@@ -1,4 +1,4 @@
package com.bluelinelabs.conductor;
package com.bluelinelabs.conductor.util;
import android.content.Intent;
import android.os.Bundle;
@@ -11,26 +11,36 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.bluelinelabs.conductor.ControllerTransaction.ControllerChangeType;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.ControllerChangeType;
public class TestController extends Controller {
@IdRes public static final int VIEW_ID = 2342;
@IdRes public static final int CHILD_VIEW_ID_1 = 2343;
@IdRes public static final int CHILD_VIEW_ID_2 = 2344;
private static final String KEY_CALL_STATE = "TestController.currentCallState";
public CallState currentCallState;
public TestController() {
currentCallState = new CallState();
}
public CallState currentCallState = new CallState();
public ChangeHandlerHistory changeHandlerHistory = new ChangeHandlerHistory();
@NonNull
@Override
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
currentCallState.createViewCalls++;
View view = new FrameLayout(inflater.getContext());
FrameLayout view = new AttachFakingFrameLayout(inflater.getContext());
view.setId(VIEW_ID);
FrameLayout childContainer1 = new AttachFakingFrameLayout(inflater.getContext());
childContainer1.setId(CHILD_VIEW_ID_1);
view.addView(childContainer1);
FrameLayout childContainer2 = new AttachFakingFrameLayout(inflater.getContext());
childContainer2.setId(CHILD_VIEW_ID_2);
view.addView(childContainer2);
return view;
}
@@ -44,6 +54,13 @@ public class TestController extends Controller {
protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
super.onChangeEnded(changeHandler, changeType);
currentCallState.changeEndCalls++;
if (changeHandler instanceof MockChangeHandler) {
MockChangeHandler mockHandler = (MockChangeHandler)changeHandler;
changeHandlerHistory.addEntry(mockHandler.from, mockHandler.to, changeType.isPush, mockHandler);
} else {
changeHandlerHistory.isValidHistory = false;
}
}
@Override
@@ -59,7 +76,7 @@ public class TestController extends Controller {
}
@Override
protected void onDestroyView(View view) {
protected void onDestroyView(@NonNull View view) {
super.onDestroyView(view);
currentCallState.destroyViewCalls++;
}
@@ -0,0 +1,61 @@
package com.bluelinelabs.conductor.util;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
import org.robolectric.util.ReflectionHelpers;
import java.util.List;
public class ViewUtils {
public static void reportAttached(View view, boolean attached) {
reportAttached(view, attached, true);
}
public static void reportAttached(View view, boolean attached, boolean propogateToChildren) {
if (view instanceof AttachFakingFrameLayout) {
((AttachFakingFrameLayout)view).setAttached(attached, false);
}
List<OnAttachStateChangeListener> listeners = getAttachStateListeners(view);
// Add, then remove an OnAttachStateChangeListener to initialize the attachStateListeners variable inside a view
if (listeners == null) {
OnAttachStateChangeListener tmpListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) { }
@Override
public void onViewDetachedFromWindow(View v) { }
};
view.addOnAttachStateChangeListener(tmpListener);
view.removeOnAttachStateChangeListener(tmpListener);
listeners = getAttachStateListeners(view);
}
for (OnAttachStateChangeListener listener : listeners) {
if (attached) {
listener.onViewAttachedToWindow(view);
} else {
listener.onViewDetachedFromWindow(view);
}
}
if (propogateToChildren && view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup)view;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
reportAttached(viewGroup.getChildAt(i), attached, true);
}
}
}
private static List<OnAttachStateChangeListener> getAttachStateListeners(View view) {
Object listenerInfo = ReflectionHelpers.callInstanceMethod(view, "getListenerInfo");
return ReflectionHelpers.getField(listenerInfo, "mOnAttachStateChangeListeners");
}
}
+12 -4
View File
@@ -11,11 +11,12 @@ apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
compileSdkVersion 25
buildToolsVersion '24.0.3'
lintOptions {
abortOnError false
abortOnError true
ignore 'UnusedResources'
}
compileOptions {
@@ -26,7 +27,7 @@ android {
defaultConfig {
applicationId "com.bluelinelabs.conductor.demo"
minSdkVersion 16
targetSdkVersion 23
targetSdkVersion 25
versionCode 1
versionName "1.0.0"
}
@@ -37,16 +38,23 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
packagingOptions {
exclude 'META-INF/rxjava.properties'
}
}
dependencies {
compile rootProject.ext.supportV4
compile rootProject.ext.supportDesign
apt rootProject.ext.butterknifeCompiler
compile rootProject.ext.butterknife
compile rootProject.ext.picasso
compile project(':conductor-support')
compile project(':conductor-rxlifecycle')
compile project(':conductor-rxlifecycle2')
debugCompile rootProject.ext.leakCanary
releaseCompile rootProject.ext.leakCanaryNoOp
@@ -7,17 +7,18 @@ import android.view.ViewGroup;
import com.bluelinelabs.conductor.Conductor;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.demo.controllers.HomeController;
import butterknife.Bind;
import butterknife.BindView;
import butterknife.ButterKnife;
public class MainActivity extends AppCompatActivity implements ActionBarProvider {
public final class MainActivity extends AppCompatActivity implements ActionBarProvider {
@Bind(R.id.toolbar) Toolbar mToolbar;
@Bind(R.id.controller_container) ViewGroup mContainer;
@BindView(R.id.toolbar) Toolbar toolbar;
@BindView(R.id.controller_container) ViewGroup container;
private Router mRouter;
private Router router;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -26,25 +27,19 @@ public class MainActivity extends AppCompatActivity implements ActionBarProvider
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
setSupportActionBar(mToolbar);
setSupportActionBar(toolbar);
mRouter = Conductor.attachRouter(this, mContainer, savedInstanceState);
if (!mRouter.hasRootController()) {
mRouter.setRoot(new HomeController());
router = Conductor.attachRouter(this, container, savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new HomeController()));
}
}
@Override
public void onBackPressed() {
if (!mRouter.handleBack()) {
if (!router.handleBack()) {
super.onBackPressed();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
ButterKnife.unbind(this);
}
}
@@ -20,8 +20,8 @@ public class CircularRevealChangeHandler extends AnimatorChangeHandler {
private static final String KEY_CX = "CircularRevealChangeHandler.cx";
private static final String KEY_CY = "CircularRevealChangeHandler.cy";
private int mCx;
private int mCy;
private int cx;
private int cy;
public CircularRevealChangeHandler() { }
@@ -73,8 +73,8 @@ public class CircularRevealChangeHandler extends AnimatorChangeHandler {
int relativeLeft = fromLocation[0] - containerLocation[0];
int relativeTop = fromLocation[1] - containerLocation[1];
mCx = fromView.getWidth() / 2 + relativeLeft;
mCy = fromView.getHeight() / 2 + relativeTop;
cx = fromView.getWidth() / 2 + relativeLeft;
cy = fromView.getHeight() / 2 + relativeTop;
}
/**
@@ -116,18 +116,18 @@ public class CircularRevealChangeHandler extends AnimatorChangeHandler {
*/
public CircularRevealChangeHandler(int cx, int cy, long duration, boolean removesFromViewOnPush) {
super(duration, removesFromViewOnPush);
mCx = cx;
mCy = cy;
this.cx = cx;
this.cy = cy;
}
@Override
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
final float radius = (float) Math.hypot(mCx, mCy);
final float radius = (float) Math.hypot(cx, cy);
Animator animator = null;
if (isPush && to != null) {
animator = ViewAnimationUtils.createCircularReveal(to, mCx, mCy, 0, radius);
animator = ViewAnimationUtils.createCircularReveal(to, cx, cy, 0, radius);
} else if (!isPush && from != null) {
animator = ViewAnimationUtils.createCircularReveal(from, mCx, mCy, radius, 0);
animator = ViewAnimationUtils.createCircularReveal(from, cx, cy, radius, 0);
}
return animator;
}
@@ -138,14 +138,14 @@ public class CircularRevealChangeHandler extends AnimatorChangeHandler {
@Override
public void saveToBundle(@NonNull Bundle bundle) {
super.saveToBundle(bundle);
bundle.putInt(KEY_CX, mCx);
bundle.putInt(KEY_CY, mCy);
bundle.putInt(KEY_CX, cx);
bundle.putInt(KEY_CY, cy);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
super.restoreFromBundle(bundle);
mCx = bundle.getInt(KEY_CX);
mCy = bundle.getInt(KEY_CY);
cx = bundle.getInt(KEY_CX);
cy = bundle.getInt(KEY_CY);
}
}
@@ -16,14 +16,15 @@ public class CircularRevealChangeHandlerCompat extends CircularRevealChangeHandl
super(fromView, containerView);
}
@Override
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return super.getAnimator(container, from, to, isPush, toAddedToContainer);
} else {
AnimatorSet animator = new AnimatorSet();
if (to != null && toAddedToContainer) {
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, 0, 1));
if (to != null) {
float start = toAddedToContainer ? 0 : to.getAlpha();
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1));
}
if (from != null) {
@@ -32,8 +32,8 @@ public class FlipChangeHandler extends AnimatorChangeHandler {
}
}
private final long mAnimationDuration;
private final FlipDirection mFlipDirection;
private final long animationDuration;
private final FlipDirection flipDirection;
public FlipChangeHandler() {
this(FlipDirection.RIGHT);
@@ -48,33 +48,33 @@ public class FlipChangeHandler extends AnimatorChangeHandler {
}
public FlipChangeHandler(FlipDirection flipDirection, long animationDuration) {
mFlipDirection = flipDirection;
mAnimationDuration = animationDuration;
this.flipDirection = flipDirection;
this.animationDuration = animationDuration;
}
@Override
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animatorSet = new AnimatorSet();
if (to != null) {
to.setAlpha(0);
ObjectAnimator rotation = ObjectAnimator.ofFloat(to, mFlipDirection.property, mFlipDirection.inStartRotation, 0).setDuration(mAnimationDuration);
ObjectAnimator rotation = ObjectAnimator.ofFloat(to, flipDirection.property, flipDirection.inStartRotation, 0).setDuration(animationDuration);
rotation.setInterpolator(new AccelerateDecelerateInterpolator());
animatorSet.play(rotation);
Animator alpha = ObjectAnimator.ofFloat(to, View.ALPHA, 1).setDuration(mAnimationDuration / 2);
alpha.setStartDelay(mAnimationDuration / 3);
Animator alpha = ObjectAnimator.ofFloat(to, View.ALPHA, 1).setDuration(animationDuration / 2);
alpha.setStartDelay(animationDuration / 3);
animatorSet.play(alpha);
}
if (from != null) {
ObjectAnimator rotation = ObjectAnimator.ofFloat(from, mFlipDirection.property, 0, mFlipDirection.outEndRotation).setDuration(mAnimationDuration);
ObjectAnimator rotation = ObjectAnimator.ofFloat(from, flipDirection.property, 0, flipDirection.outEndRotation).setDuration(animationDuration);
rotation.setInterpolator(new AccelerateDecelerateInterpolator());
animatorSet.play(rotation);
Animator alpha = ObjectAnimator.ofFloat(from, View.ALPHA, 0).setDuration(mAnimationDuration / 2);
alpha.setStartDelay(mAnimationDuration / 3);
Animator alpha = ObjectAnimator.ofFloat(from, View.ALPHA, 0).setDuration(animationDuration / 2);
alpha.setStartDelay(animationDuration / 3);
animatorSet.play(alpha);
}
@@ -85,7 +85,7 @@ public class FlipChangeHandler extends AnimatorChangeHandler {
protected void resetFromView(@NonNull View from) {
from.setAlpha(1);
switch (mFlipDirection) {
switch (flipDirection) {
case LEFT:
case RIGHT:
from.setRotationY(0);
@@ -15,11 +15,12 @@ public class ScaleFadeChangeHandler extends AnimatorChangeHandler {
super(DEFAULT_ANIMATION_DURATION, true);
}
@Override
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animator = new AnimatorSet();
if (to != null && toAddedToContainer) {
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, 0, 1));
if (to != null) {
float start = toAddedToContainer ? 0 : to.getAlpha();
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1));
}
if (from != null) {
@@ -8,11 +8,11 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
import butterknife.Bind;
import butterknife.BindView;
public class ChildController extends BaseController {
@@ -20,7 +20,7 @@ public class ChildController extends BaseController {
private static final String KEY_BG_COLOR = "ChildController.bgColor";
private static final String KEY_COLOR_IS_RES = "ChildController.colorIsResId";
@Bind(R.id.tv_title) TextView mTvTitle;
@BindView(R.id.tv_title) TextView tvTitle;
public ChildController(String title, int backgroundColor, boolean colorIsResId) {
this(new BundleBuilder(new Bundle())
@@ -44,7 +44,7 @@ public class ChildController extends BaseController {
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
mTvTitle.setText(getArgs().getString(KEY_TITLE));
tvTitle.setText(getArgs().getString(KEY_TITLE));
int bgColor = getArgs().getInt(KEY_BG_COLOR);
if (getArgs().getBoolean(KEY_COLOR_IS_RES)) {
@@ -14,14 +14,14 @@ 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.Bind;
import butterknife.BindView;
@TargetApi(VERSION_CODES.LOLLIPOP)
public class DragDismissController extends BaseController {
@Bind(R.id.tv_lorem_ipsum) TextView mTvLoremIpsum;
@BindView(R.id.tv_lorem_ipsum) TextView tvLoremIpsum;
private final ElasticDragDismissCallback mDragDismissListener = new ElasticDragDismissCallback() {
private final ElasticDragDismissCallback dragDismissListener = new ElasticDragDismissCallback() {
@Override
public void onDragDismissed() {
overridePopHandler(new ScaleFadeChangeHandler());
@@ -37,19 +37,19 @@ public class DragDismissController extends BaseController {
@Override
protected void onViewBound(@NonNull View view) {
((ElasticDragDismissFrameLayout)view).addListener(mDragDismissListener);
((ElasticDragDismissFrameLayout)view).addListener(dragDismissListener);
mTvLoremIpsum.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" +
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
protected void onDestroyView(View view) {
protected void onDestroyView(@NonNull View view) {
super.onDestroyView(view);
((ElasticDragDismissFrameLayout)view).removeListener(mDragDismissListener);
((ElasticDragDismissFrameLayout)view).removeListener(dragDismissListener);
}
@Override
@@ -22,15 +22,15 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.bluelinelabs.conductor.ChildControllerTransaction;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.ControllerTransaction.ControllerChangeType;
import com.bluelinelabs.conductor.ControllerChangeType;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.NavigationDemoController.DisplayUpMode;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import butterknife.Bind;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
@@ -39,12 +39,15 @@ public class HomeController extends BaseController {
public enum HomeDemoModel {
NAVIGATION("Navigation Demos", R.color.red_300),
TRANSITIONS("Transition Demos", R.color.blue_grey_300),
OVERLAY("Overlay Controller", 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("Rx Lifecycle", R.color.teal_300),
RX_LIFECYCLE_2("Rx Lifecycle 2", R.color.brown_300),
OVERLAY("Overlay Controller", R.color.purple_300);
String title;
@ColorRes int color;
@@ -55,7 +58,8 @@ public class HomeController extends BaseController {
}
}
@Bind(R.id.recycler_view) RecyclerView mRecyclerView;
@BindView(R.id.recycler_view) RecyclerView recyclerView;
@BindView(R.id.overlay_root) ViewGroup overlayRoot;
public HomeController() {
setHasOptionsMenu(true);
@@ -71,15 +75,15 @@ public class HomeController extends BaseController {
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
mRecyclerView.setHasFixedSize(true);
mRecyclerView.setLayoutManager(new LinearLayoutManager(view.getContext()));
mRecyclerView.setAdapter(new HomeAdapter(LayoutInflater.from(view.getContext()), HomeDemoModel.values()));
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext()));
recyclerView.setAdapter(new HomeAdapter(LayoutInflater.from(view.getContext()), HomeDemoModel.values()));
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.home, menu);
inflater.inflate(R.menu.home, menu);
}
@Override
@@ -92,7 +96,7 @@ public class HomeController extends BaseController {
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.about) {
SpannableString details = new SpannableString("A small, yet full-featured framework that allows building View-based Android applications");
details.setSpan(new AbsoluteSizeSpan(16, true), 0, details.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
@@ -113,11 +117,11 @@ public class HomeController extends BaseController {
content.append("\n\n");
content.append(link);
addChildController(ChildControllerTransaction.builder(new OverlayController(content), R.id.home_root)
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler())
.addToLocalBackstack(true)
.build());
getChildRouter(overlayRoot)
.setPopsLastView(true)
.setRoot(RouterTransaction.with(new OverlayController(content))
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
return true;
}
return super.onOptionsItemSelected(item);
@@ -131,85 +135,96 @@ public class HomeController extends BaseController {
void onModelRowClick(HomeDemoModel model) {
switch (model) {
case NAVIGATION:
getRouter().pushController(RouterTransaction.builder(new NavigationDemoController(0))
getRouter().pushController(RouterTransaction.with(new NavigationDemoController(0, DisplayUpMode.SHOW_FOR_CHILDREN_ONLY))
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler())
.tag(NavigationDemoController.TAG_UP_TRANSACTION)
.build());
);
break;
case TRANSITIONS:
getRouter().pushController(TransitionDemoController.getRouterTransaction(0, this));
break;
case TARGET_CONTROLLER:
getRouter().pushController(RouterTransaction.builder(new TargetDisplayController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler())
.build());
getRouter().pushController(
RouterTransaction.with(new TargetDisplayController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case VIEW_PAGER:
getRouter().pushController(RouterTransaction.builder(new PagerController())
getRouter().pushController(RouterTransaction.with(new PagerController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler())
.build());
.popChangeHandler(new FadeChangeHandler()));
break;
case CHILD_CONTROLLERS:
getRouter().pushController(RouterTransaction.builder(new ParentController())
getRouter().pushController(RouterTransaction.with(new ParentController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler())
.build());
.popChangeHandler(new FadeChangeHandler()));
break;
case OVERLAY:
addChildController(ChildControllerTransaction.builder(new OverlayController("I'm an Overlay!"), R.id.home_root)
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler())
.addToLocalBackstack(true)
.build());
getChildRouter(overlayRoot)
.setPopsLastView(true)
.setRoot(RouterTransaction.with(new OverlayController("I'm an overlay!"))
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case DRAG_DISMISS:
getRouter().pushController(RouterTransaction.builder(new DragDismissController())
getRouter().pushController(RouterTransaction.with(new DragDismissController())
.pushChangeHandler(new FadeChangeHandler(false))
.popChangeHandler(new FadeChangeHandler())
.build());
.popChangeHandler(new FadeChangeHandler()));
break;
case RX_LIFECYCLE:
getRouter().pushController(RouterTransaction.builder(new RxLifecycleController())
getRouter().pushController(RouterTransaction.with(new RxLifecycleController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler())
.build());
.popChangeHandler(new FadeChangeHandler()));
break;
case RX_LIFECYCLE_2:
getRouter().pushController(RouterTransaction.with(new RxLifecycle2Controller())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case MULTIPLE_CHILD_ROUTERS:
getRouter().pushController(RouterTransaction.with(new MultipleChildRouterController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case MASTER_DETAIL:
getRouter().pushController(RouterTransaction.with(new MasterDetailListController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
}
}
class HomeAdapter extends RecyclerView.Adapter<HomeAdapter.ViewHolder> {
private final LayoutInflater mInflater;
private final HomeDemoModel[] mItems;
private final LayoutInflater inflater;
private final HomeDemoModel[] items;
public HomeAdapter(LayoutInflater inflater, HomeDemoModel[] items) {
mInflater = inflater;
mItems = items;
this.inflater = inflater;
this.items = items;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(mInflater.inflate(R.layout.row_home, parent, false));
return new ViewHolder(inflater.inflate(R.layout.row_home, parent, false));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bind(mItems[position]);
holder.bind(items[position]);
}
@Override
public int getItemCount() {
return mItems.length;
return items.length;
}
class ViewHolder extends RecyclerView.ViewHolder {
@Bind(R.id.tv_title) TextView mTvTitle;
@Bind(R.id.img_dot) ImageView mImgDot;
private HomeDemoModel mModel;
@BindView(R.id.tv_title) TextView tvTitle;
@BindView(R.id.img_dot) ImageView imgDot;
private HomeDemoModel model;
public ViewHolder(View itemView) {
super(itemView);
@@ -217,14 +232,14 @@ public class HomeController extends BaseController {
}
void bind(HomeDemoModel item) {
mModel = item;
mTvTitle.setText(item.title);
mImgDot.getDrawable().setColorFilter(ContextCompat.getColor(getActivity(), item.color), Mode.SRC_ATOP);
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(mModel);
onModelRowClick(model);
}
}
@@ -0,0 +1,158 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
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.TextView;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
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 MasterDetailListController extends BaseController {
private static final String KEY_SELECTED_INDEX = "MasterDetailListController.selectedIndex";
public enum DetailItemModel {
ONE("Item 1", "This is a quick demo of master/detail flow using Conductor. In portrait mode you'll see a standard list. In landscape, you'll see a two-pane layout.", R.color.green_300),
TWO("Item 2", "This is another item.", R.color.cyan_300),
THREE("Item 3", "Wow, a 3rd item!", R.color.deep_purple_300);
String title;
String detail;
int backgroundColor;
DetailItemModel(String title, String detail, int backgroundColor) {
this.title = title;
this.detail = detail;
this.backgroundColor = backgroundColor;
}
}
@BindView(R.id.recycler_view) RecyclerView recyclerView;
@Nullable @BindView(R.id.detail_container) ViewGroup detailContainer;
private int selectedIndex;
private boolean twoPaneView;
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_master_detail_list, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext()));
recyclerView.setAdapter(new DetailItemAdapter(LayoutInflater.from(view.getContext()), DetailItemModel.values()));
twoPaneView = (detailContainer != null);
if (twoPaneView) {
onRowSelected(selectedIndex);
}
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(KEY_SELECTED_INDEX, selectedIndex);
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
selectedIndex = savedInstanceState.getInt(KEY_SELECTED_INDEX);
}
@Override
protected String getTitle() {
return "Master/Detail Flow";
}
void onRowSelected(int index) {
selectedIndex = index;
DetailItemModel model = DetailItemModel.values()[index];
ChildController controller = new ChildController(model.detail, model.backgroundColor, true);
if (twoPaneView) {
getChildRouter(detailContainer).setRoot(RouterTransaction.with(controller));
} else {
getRouter().pushController(RouterTransaction.with(controller)
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
}
}
class DetailItemAdapter extends RecyclerView.Adapter<DetailItemAdapter.ViewHolder> {
private final LayoutInflater inflater;
private final DetailItemModel[] items;
public DetailItemAdapter(LayoutInflater inflater, DetailItemModel[] items) {
this.inflater = inflater;
this.items = items;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(inflater.inflate(R.layout.row_detail_item, parent, false));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bind(items[position], position);
}
@Override
public int getItemCount() {
return items.length;
}
class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.row_root) View root;
@BindView(R.id.tv_title) TextView tvTitle;
private int position;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
void bind(DetailItemModel item, int position) {
tvTitle.setText(item.title);
this.position = position;
if (twoPaneView && position == selectedIndex) {
root.setBackgroundColor(ContextCompat.getColor(root.getContext(), R.color.grey_400));
} else {
root.setBackgroundColor(ContextCompat.getColor(root.getContext(), android.R.color.transparent));
}
}
@OnClick(R.id.row_root)
void onRowClick() {
onRowSelected(position);
notifyDataSetChanged();
}
}
}
}
@@ -0,0 +1,42 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.NavigationDemoController.DisplayUpMode;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import butterknife.BindViews;
public class MultipleChildRouterController extends BaseController {
@BindViews({R.id.container_0, R.id.container_1, R.id.container_2}) ViewGroup[] childContainers;
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_multiple_child_routers, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
for (ViewGroup childContainer : childContainers) {
Router childRouter = getChildRouter(childContainer).setPopsLastView(false);
if (!childRouter.hasRootController()) {
childRouter.setRoot(RouterTransaction.with(new NavigationDemoController(0, DisplayUpMode.HIDE)));
}
}
}
@Override
protected String getTitle() {
return "Child Router Demo";
}
}
@@ -7,6 +7,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.ControllerChangeType;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.demo.R;
@@ -14,28 +16,47 @@ import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
import com.bluelinelabs.conductor.demo.util.ColorUtil;
import butterknife.Bind;
import butterknife.BindView;
import butterknife.OnClick;
public class NavigationDemoController extends BaseController {
public enum DisplayUpMode {
SHOW,
SHOW_FOR_CHILDREN_ONLY,
HIDE;
private DisplayUpMode getDisplayUpModeForChild() {
switch (this) {
case HIDE:
return HIDE;
default:
return SHOW;
}
}
}
public static final String TAG_UP_TRANSACTION = "NavigationDemoController.up";
private static final String KEY_INDEX = "NavigationDemoController.index";
private static final String KEY_DISPLAY_UP_MODE = "NavigationDemoController.displayUpMode";
@Bind(R.id.tv_title) TextView mTvTitle;
@BindView(R.id.tv_title) TextView tvTitle;
private int mIndex;
private int index;
private DisplayUpMode displayUpMode;
public NavigationDemoController(int index) {
public NavigationDemoController(int index, DisplayUpMode displayUpMode) {
this(new BundleBuilder(new Bundle())
.putInt(KEY_INDEX, index)
.putInt(KEY_DISPLAY_UP_MODE, displayUpMode.ordinal())
.build());
}
public NavigationDemoController(Bundle args) {
super(args);
mIndex = args.getInt(KEY_INDEX);
index = args.getInt(KEY_INDEX);
displayUpMode = DisplayUpMode.values()[args.getInt(KEY_DISPLAY_UP_MODE)];
}
@NonNull
@@ -48,8 +69,26 @@ public class NavigationDemoController extends BaseController {
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
view.setBackgroundColor(ColorUtil.getMaterialColor(getResources(), mIndex));
mTvTitle.setText(getResources().getString(R.string.navigation_title, mIndex));
if (displayUpMode != DisplayUpMode.SHOW) {
view.findViewById(R.id.btn_up).setVisibility(View.GONE);
}
view.setBackgroundColor(ColorUtil.getMaterialColor(getResources(), index));
tvTitle.setText(getResources().getString(R.string.navigation_title, index));
}
@Override
protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
super.onChangeEnded(changeHandler, changeType);
setButtonsEnabled(true);
}
@Override
protected void onChangeStarted(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
super.onChangeStarted(changeHandler, changeType);
setButtonsEnabled(false);
}
@Override
@@ -57,12 +96,19 @@ public class NavigationDemoController extends BaseController {
return "Navigation Demos";
}
private void setButtonsEnabled(boolean enabled) {
final View view = getView();
if (view != null) {
view.findViewById(R.id.btn_next).setEnabled(enabled);
view.findViewById(R.id.btn_up).setEnabled(enabled);
view.findViewById(R.id.btn_pop_to_root).setEnabled(enabled);
}
}
@OnClick(R.id.btn_next) void onNextClicked() {
getRouter().pushController(RouterTransaction.builder(new NavigationDemoController(mIndex + 1))
getRouter().pushController(RouterTransaction.with(new NavigationDemoController(index + 1, displayUpMode.getDisplayUpModeForChild()))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler())
.build()
);
.popChangeHandler(new HorizontalChangeHandler()));
}
@OnClick(R.id.btn_up) void onUpClicked() {
@@ -12,13 +12,13 @@ import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
import butterknife.Bind;
import butterknife.BindView;
public class OverlayController extends BaseController {
private static final String KEY_TEXT = "OverlayController.text";
@Bind(R.id.text_view) TextView mTextView;
@BindView(R.id.text_view) TextView textView;
public OverlayController(CharSequence text) {
this(new BundleBuilder(new Bundle())
@@ -39,8 +39,8 @@ public class OverlayController extends BaseController {
@Override
public void onViewBound(@NonNull View view) {
super.onViewBound(view);
mTextView.setText(getArgs().getCharSequence(KEY_TEXT));
mTextView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setText(getArgs().getCharSequence(KEY_TEXT));
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
}
@@ -2,35 +2,39 @@ package com.bluelinelabs.conductor.demo.controllers;
import android.support.annotation.NonNull;
import android.support.design.widget.TabLayout;
import android.support.design.widget.TabLayout.OnTabSelectedListener;
import android.support.design.widget.TabLayout.Tab;
import android.support.design.widget.TabLayout.TabLayoutOnPageChangeListener;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.support.ControllerPagerAdapter;
import com.bluelinelabs.conductor.support.RouterPagerAdapter;
import butterknife.Bind;
import java.util.Locale;
import butterknife.BindView;
public class PagerController extends BaseController {
private int[] PAGE_COLORS = new int[]{R.color.green_300, R.color.cyan_300, R.color.deep_purple_300, R.color.lime_300, R.color.red_300};
@Bind(R.id.tab_layout) TabLayout mTabLayout;
@Bind(R.id.view_pager) ViewPager mViewPager;
@BindView(R.id.tab_layout) TabLayout tabLayout;
@BindView(R.id.view_pager) ViewPager viewPager;
private final ControllerPagerAdapter mPagerAdapter;
private final RouterPagerAdapter pagerAdapter;
public PagerController() {
mPagerAdapter = new ControllerPagerAdapter(this) {
pagerAdapter = new RouterPagerAdapter(this) {
@Override
public Controller getItem(int position) {
return new ChildController(String.format("Child #%d (Swipe to see more)", position), PAGE_COLORS[position], true);
public void configureRouter(Router router, int position) {
if (!router.hasRootController()) {
Controller page = new ChildController(String.format(Locale.getDefault(), "Child #%d (Swipe to see more)", position), PAGE_COLORS[position], true);
router.setRoot(RouterTransaction.with(page));
}
}
@Override
@@ -48,27 +52,13 @@ public class PagerController extends BaseController {
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
mTabLayout.setTabsFromPagerAdapter(mPagerAdapter);
mViewPager.setAdapter(mPagerAdapter);
mViewPager.addOnPageChangeListener(new TabLayoutOnPageChangeListener(mTabLayout));
mTabLayout.setOnTabSelectedListener(new OnTabSelectedListener() {
@Override
public void onTabSelected(Tab tab) {
mViewPager.setCurrentItem(tab.getPosition());
}
@Override
public void onTabUnselected(Tab tab) { }
@Override
public void onTabReselected(Tab tab) { }
});
viewPager.setAdapter(pagerAdapter);
tabLayout.setupWithViewPager(viewPager);
}
@Override
protected void onDestroyView(View view) {
mViewPager.setAdapter(null);
protected void onDestroyView(@NonNull View view) {
viewPager.setAdapter(null);
super.onDestroyView(view);
}
@@ -1,23 +1,28 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ChildControllerTransaction;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.ControllerTransaction.ControllerChangeType;
import com.bluelinelabs.conductor.ControllerChangeType;
import com.bluelinelabs.conductor.Router;
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 com.bluelinelabs.conductor.demo.util.ColorUtil;
import java.util.List;
public class ParentController extends BaseController {
private static final int NUMBER_OF_CHILDREN = 5;
private boolean mFinishing;
private boolean finishing;
private boolean hasShownAll;
@NonNull
@Override
@@ -35,31 +40,62 @@ public class ParentController extends BaseController {
}
private void addChild(final int index) {
String tag = Integer.toString(index);
if (getChildController(tag) == null) {
int frameId = getResources().getIdentifier("child_content_" + (index + 1), "id", getActivity().getPackageName());
@IdRes final int frameId = getResources().getIdentifier("child_content_" + (index + 1), "id", getActivity().getPackageName());
final ViewGroup container = (ViewGroup)getView().findViewById(frameId);
final Router childRouter = getChildRouter(container).setPopsLastView(true);
if (!childRouter.hasRootController()) {
ChildController childController = new ChildController("Child Controller #" + index, ColorUtil.getMaterialColor(getResources(), index), false);
addChildController(ChildControllerTransaction.builder(childController, frameId)
childController.addLifecycleListener(new LifecycleListener() {
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
if (!isBeingDestroyed()) {
if (changeType == ControllerChangeType.PUSH_ENTER && !hasShownAll) {
if (index < NUMBER_OF_CHILDREN - 1) {
addChild(index + 1);
} else {
hasShownAll = true;
}
} else if (changeType == ControllerChangeType.POP_EXIT) {
if (index > 0) {
removeChild(index - 1);
} else {
getRouter().popController(ParentController.this);
}
}
}
}
});
childRouter.setRoot(RouterTransaction.with(childController)
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler())
.tag(tag)
.build());
.popChangeHandler(new FadeChangeHandler()));
}
}
private void removeChild(int index) {
removeChildController(getChildControllers().get(index));
List<Router> childRouters = getChildRouters();
if (index < childRouters.size()) {
removeChildRouter(childRouters.get(index));
}
}
@Override
public boolean handleBack() {
if (getChildControllers().size() == NUMBER_OF_CHILDREN && !mFinishing) {
mFinishing = true;
removeChild(getChildControllers().size() - 1);
int childControllers = 0;
for (Router childRouter : getChildRouters()) {
if (childRouter.hasRootController()) {
childControllers++;
}
}
if (childControllers != NUMBER_OF_CHILDREN || finishing) {
return true;
} else {
finishing = true;
return super.handleBack();
}
return true;
}
@Override
@@ -67,25 +103,4 @@ public class ParentController extends BaseController {
return "Parent/Child Demo";
}
@Override
public void addChildController(ChildControllerTransaction transaction) {
final int index = Integer.parseInt(transaction.tag);
transaction.controller.addLifecycleListener(new LifecycleListener() {
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
if (changeType == ControllerChangeType.PUSH_ENTER && index < NUMBER_OF_CHILDREN - 1) {
addChild(index + 1);
} else if (changeType == ControllerChangeType.POP_EXIT) {
if (index > 0) {
removeChild(index - 1);
} else {
getRouter().popController(ParentController.this);
}
}
}
});
super.addChildController(transaction);
}
}
@@ -0,0 +1,163 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.support.annotation.NonNull;
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.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.demo.ActionBarProvider;
import com.bluelinelabs.conductor.demo.DemoApplication;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.rxlifecycle2.ControllerEvent;
import com.bluelinelabs.conductor.rxlifecycle2.RxController;
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 RxLifecycle2Controller extends RxController {
private static final String TAG = "RxLifecycleController";
@BindView(R.id.tv_title) TextView tvTitle;
private Unbinder unbinder;
private boolean hasExited;
public RxLifecycle2Controller() {
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose(new Action() {
@Override
public void run() {
Log.i(TAG, "Disposing from constructor");
}
})
.compose(this.<Long>bindUntilEvent(ControllerEvent.DESTROY))
.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_rxlifecycle, container, false);
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)");
}
})
.compose(this.<Long>bindUntilEvent(ControllerEvent.DESTROY_VIEW))
.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("RxLifecycle2 Demo");
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose(new Action() {
@Override
public void run() {
Log.i(TAG, "Disposing from onAttach()");
}
})
.compose(this.<Long>bindUntilEvent(ControllerEvent.DETACH))
.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()));
}
}
@@ -7,30 +7,38 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.ControllerChangeType;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.demo.ActionBarProvider;
import com.bluelinelabs.conductor.demo.DemoApplication;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.rxlifecycle.ControllerEvent;
import com.bluelinelabs.conductor.rxlifecycle.RxController;
import java.util.concurrent.TimeUnit;
import butterknife.Bind;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Unbinder;
import rx.Observable;
import rx.functions.Action0;
import rx.functions.Action1;
// Shamelessly borrowed from the official RxLifecycle demo by Trello and adapted for Conductor Controllers
// instead of Activities or Fragments.
public class RxLifecycleController extends BaseController {
public class RxLifecycleController extends RxController {
private static final String TAG = "RxLifecycleController";
@Bind(R.id.tv_title) TextView mTvTitle;
@BindView(R.id.tv_title) TextView tvTitle;
private Unbinder unbinder;
private boolean hasExited;
public RxLifecycleController() {
Observable.interval(1, TimeUnit.SECONDS)
.doOnUnsubscribe(new Action0() {
@Override
@@ -47,11 +55,15 @@ public class RxLifecycleController extends BaseController {
});
}
@NonNull
@Override
public void onViewBound(@NonNull View view) {
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
Log.i(TAG, "onCreateView() called");
mTvTitle.setText(getResources().getString(R.string.rxlifecycle_title, TAG));
View view = inflater.inflate(R.layout.controller_rxlifecycle, container, false);
unbinder = ButterKnife.bind(this, view);
tvTitle.setText(getResources().getString(R.string.rxlifecycle_title, TAG));
Observable.interval(1, TimeUnit.SECONDS)
.doOnUnsubscribe(new Action0() {
@@ -67,6 +79,8 @@ public class RxLifecycleController extends BaseController {
Log.i(TAG, "Started in onCreateView(), running until onDestroyView(): " + num);
}
});
return view;
}
@Override
@@ -75,6 +89,8 @@ public class RxLifecycleController extends BaseController {
Log.i(TAG, "onAttach() called");
(((ActionBarProvider)getActivity()).getSupportActionBar()).setTitle("RxLifecycle Demo");
Observable.interval(1, TimeUnit.SECONDS)
.doOnUnsubscribe(new Action0() {
@Override
@@ -92,10 +108,13 @@ public class RxLifecycleController extends BaseController {
}
@Override
protected void onDestroyView(View view) {
protected void onDestroyView(@NonNull View view) {
super.onDestroyView(view);
Log.i(TAG, "onDestroyView() called");
unbinder.unbind();
unbinder = null;
}
@Override
@@ -110,36 +129,35 @@ public class RxLifecycleController extends BaseController {
super.onDestroy();
Log.i(TAG, "onDestroy() called");
if (hasExited) {
DemoApplication.refWatcher.watch(this);
}
}
@Override
protected String getTitle() {
return "RxLifecycle Demo";
}
protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
super.onChangeEnded(changeHandler, changeType);
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_rxlifecycle, container, false);
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.builder(new TextController("Logcat should now report that the observables from onAttach() and onViewBound() have been unsubscribed from, while the constructor observable is still running."))
getRouter().pushController(RouterTransaction.with(new TextController("Logcat should now report that the observables from onAttach() and onViewBound() have been unsubscribed from, while the constructor observable is still running."))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler())
.build()
);
.popChangeHandler(new HorizontalChangeHandler()));
}
@OnClick(R.id.btn_next_retain_view) void onNextWithRetainClicked() {
setRetainViewMode(RetainViewMode.RETAIN_DETACH);
getRouter().pushController(RouterTransaction.builder(new TextController("Logcat should now report that the observables from onAttach() has been unsubscribed from, while the constructor and onViewBound() observables are still running."))
getRouter().pushController(RouterTransaction.with(new TextController("Logcat should now report that the observables from onAttach() has been unsubscribed from, while the constructor and onViewBound() observables are still running."))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler())
.build()
);
.popChangeHandler(new HorizontalChangeHandler()));
}
}
@@ -18,8 +18,9 @@ import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.TargetTitleEntryController.TargetTitleEntryControllerListener;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.squareup.picasso.Picasso;
import butterknife.Bind;
import butterknife.BindView;
import butterknife.OnClick;
public class TargetDisplayController extends BaseController implements TargetTitleEntryControllerListener {
@@ -29,11 +30,11 @@ public class TargetDisplayController extends BaseController implements TargetTit
private static final String KEY_SELECTED_TEXT = "TargetDisplayController.selectedText";
private static final String KEY_SELECTED_IMAGE = "TargetDisplayController.selectedImage";
@Bind(R.id.tv_selection) TextView mTvSelection;
@Bind(R.id.image_view) ImageView mImageView;
@BindView(R.id.tv_selection) TextView tvSelection;
@BindView(R.id.image_view) ImageView imageView;
private String mSelectedText;
private Uri mImageUri;
private String selectedText;
private Uri imageUri;
@NonNull
@Override
@@ -42,10 +43,9 @@ public class TargetDisplayController extends BaseController implements TargetTit
}
@OnClick(R.id.btn_pick_title) void launchTitlePicker() {
getRouter().pushController(RouterTransaction.builder(new TargetTitleEntryController(this))
getRouter().pushController(RouterTransaction.with(new TargetTitleEntryController(this))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler())
.build());
.popChangeHandler(new HorizontalChangeHandler()));
}
@OnClick(R.id.btn_pick_image) void launchImagePicker() {
@@ -57,14 +57,14 @@ public class TargetDisplayController extends BaseController implements TargetTit
@Override
public void onTitlePicked(String option) {
mSelectedText = option;
selectedText = option;
setTextView();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_SELECT_IMAGE && resultCode == Activity.RESULT_OK) {
mImageUri = data.getData();
imageUri = data.getData();
setImageView();
}
}
@@ -79,18 +79,18 @@ public class TargetDisplayController extends BaseController implements TargetTit
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(KEY_SELECTED_TEXT, mSelectedText);
outState.putString(KEY_SELECTED_IMAGE, mImageUri != null ? mImageUri.toString() : null);
outState.putString(KEY_SELECTED_TEXT, selectedText);
outState.putString(KEY_SELECTED_IMAGE, imageUri != null ? imageUri.toString() : null);
}
@Override
public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mSelectedText = savedInstanceState.getString(KEY_SELECTED_TEXT);
selectedText = savedInstanceState.getString(KEY_SELECTED_TEXT);
String uriString = savedInstanceState.getString(KEY_SELECTED_IMAGE);
if (!TextUtils.isEmpty(uriString)) {
mImageUri = Uri.parse(uriString);
imageUri = Uri.parse(uriString);
}
}
@@ -100,15 +100,19 @@ public class TargetDisplayController extends BaseController implements TargetTit
}
private void setImageView() {
mImageView.setImageURI(mImageUri);
Picasso.with(getActivity())
.load(imageUri)
.fit()
.centerCrop()
.into(imageView);
}
private void setTextView() {
if (mTvSelection != null) {
if (!TextUtils.isEmpty(mSelectedText)) {
mTvSelection.setText(mSelectedText);
if (tvSelection != null) {
if (!TextUtils.isEmpty(selectedText)) {
tvSelection.setText(selectedText);
} else {
mTvSelection.setText("Press pick title to set this title, or pick image to fill in the image view.");
tvSelection.setText("Press pick title to set this title, or pick image to fill in the image view.");
}
}
}
@@ -1,16 +1,18 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import butterknife.Bind;
import butterknife.BindView;
import butterknife.OnClick;
public class TargetTitleEntryController extends BaseController {
@@ -19,7 +21,7 @@ public class TargetTitleEntryController extends BaseController {
void onTitlePicked(String option);
}
@Bind(R.id.edit_text) EditText mEditText;
@BindView(R.id.edit_text) EditText editText;
public <T extends Controller & TargetTitleEntryControllerListener> TargetTitleEntryController(T targetController) {
setTargetController(targetController);
@@ -27,6 +29,12 @@ public class TargetTitleEntryController extends BaseController {
public TargetTitleEntryController() { }
@Override
protected void onDetach(@NonNull View view) {
InputMethodManager imm = (InputMethodManager) editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(editText.getWindowToken(), 0);
}
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
@@ -41,7 +49,7 @@ public class TargetTitleEntryController extends BaseController {
@OnClick(R.id.btn_use_title) void optionPicked() {
Controller targetController = getTargetController();
if (targetController != null) {
((TargetTitleEntryControllerListener)targetController).onTitlePicked(mEditText.getText().toString());
((TargetTitleEntryControllerListener)targetController).onTitlePicked(editText.getText().toString());
getRouter().popController(this);
}
}
@@ -7,17 +7,17 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
import butterknife.Bind;
import butterknife.BindView;
public class TextController extends BaseController {
private static final String KEY_TEXT = "TextController.text";
@Bind(R.id.text_view) TextView mTextView;
@BindView(R.id.text_view) TextView textView;
public TextController(String text) {
this(new BundleBuilder(new Bundle())
@@ -39,7 +39,7 @@ public class TextController extends BaseController {
@Override
public void onViewBound(@NonNull View view) {
super.onViewBound(view);
mTextView.setText(getArgs().getString(KEY_TEXT));
textView.setText(getArgs().getString(KEY_TEXT));
}
}
@@ -25,7 +25,7 @@ import com.bluelinelabs.conductor.demo.changehandler.FlipChangeHandler;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
import butterknife.Bind;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
@@ -56,11 +56,11 @@ public class TransitionDemoController extends BaseController {
}
}
@Bind(R.id.tv_title) TextView mTvTitle;
@Bind(R.id.btn_next) FloatingActionButton mBtnNext;
@Bind(R.id.transition_root) View mContainerView;
@BindView(R.id.tv_title) TextView tvTitle;
@BindView(R.id.btn_next) FloatingActionButton btnNext;
@BindView(R.id.transition_root) View containerView;
private TransitionDemo mTransitionDemo;
private TransitionDemo transitionDemo;
public TransitionDemoController(int index) {
this(new BundleBuilder(new Bundle())
@@ -70,13 +70,13 @@ public class TransitionDemoController extends BaseController {
public TransitionDemoController(Bundle args) {
super(args);
mTransitionDemo = TransitionDemo.fromIndex(args.getInt(KEY_INDEX));
transitionDemo = TransitionDemo.fromIndex(args.getInt(KEY_INDEX));
}
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(mTransitionDemo.layoutId, container, false);
return inflater.inflate(transitionDemo.layoutId, container, false);
}
@Override
@@ -84,11 +84,11 @@ public class TransitionDemoController extends BaseController {
super.onViewBound(view);
View bgView = ButterKnife.findById(view, R.id.bg_view);
if (mTransitionDemo.colorId != 0 && bgView != null) {
bgView.setBackgroundColor(ContextCompat.getColor(getActivity(), mTransitionDemo.colorId));
if (transitionDemo.colorId != 0 && bgView != null) {
bgView.setBackgroundColor(ContextCompat.getColor(getActivity(), transitionDemo.colorId));
}
final int nextIndex = mTransitionDemo.ordinal() + 1;
final int nextIndex = transitionDemo.ordinal() + 1;
int buttonColor = 0;
if (nextIndex < TransitionDemo.values().length) {
buttonColor = TransitionDemo.fromIndex(nextIndex).colorId;
@@ -97,8 +97,8 @@ public class TransitionDemoController extends BaseController {
buttonColor = TransitionDemo.fromIndex(0).colorId;
}
mBtnNext.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(getActivity(), buttonColor)));
mTvTitle.setText(mTransitionDemo.title);
btnNext.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(getActivity(), buttonColor)));
tvTitle.setText(transitionDemo.title);
}
@Override
@@ -107,7 +107,7 @@ public class TransitionDemoController extends BaseController {
}
@OnClick(R.id.btn_next) void onNextClicked() {
final int nextIndex = mTransitionDemo.ordinal() + 1;
final int nextIndex = transitionDemo.ordinal() + 1;
if (nextIndex < TransitionDemo.values().length) {
getRouter().pushController(getRouterTransaction(nextIndex, this));
@@ -117,12 +117,12 @@ public class TransitionDemoController extends BaseController {
}
public ControllerChangeHandler getChangeHandler(Controller from) {
switch (mTransitionDemo) {
switch (transitionDemo) {
case VERTICAL:
return new VerticalChangeHandler();
case CIRCULAR:
TransitionDemoController demoController = (TransitionDemoController)from;
return new CircularRevealChangeHandlerCompat(demoController.mBtnNext, demoController.mContainerView);
return new CircularRevealChangeHandlerCompat(demoController.btnNext, demoController.containerView);
case FADE:
return new FadeChangeHandler();
case FLIP:
@@ -140,12 +140,10 @@ public class TransitionDemoController extends BaseController {
public static RouterTransaction getRouterTransaction(int index, Controller fromController) {
TransitionDemoController toController = new TransitionDemoController(index);
ControllerChangeHandler changeHandler = toController.getChangeHandler(fromController);
return RouterTransaction.builder(toController)
.pushChangeHandler(changeHandler)
.popChangeHandler(changeHandler)
.build();
return RouterTransaction.with(toController)
.pushChangeHandler(toController.getChangeHandler(fromController))
.popChangeHandler(toController.getChangeHandler(fromController));
}
}
@@ -5,6 +5,7 @@ import android.support.annotation.NonNull;
import android.support.v7.app.ActionBar;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.demo.ActionBarProvider;
public abstract class BaseController extends RefWatchingController {
@@ -29,6 +30,14 @@ public abstract class BaseController extends RefWatchingController {
}
protected void setTitle() {
Controller parentController = getParentController();
while (parentController != null) {
if (parentController instanceof BaseController && ((BaseController)parentController).getTitle() != null) {
return;
}
parentController = parentController.getParentController();
}
String title = getTitle();
ActionBar actionBar = getActionBar();
if (title != null && actionBar != null) {
@@ -6,11 +6,14 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.rxlifecycle.RxController;
import com.bluelinelabs.conductor.Controller;
import butterknife.ButterKnife;
import butterknife.Unbinder;
public abstract class ButterKnifeController extends RxController {
public abstract class ButterKnifeController extends Controller {
private Unbinder unbinder;
protected ButterKnifeController() { }
protected ButterKnifeController(Bundle args) {
@@ -23,7 +26,7 @@ public abstract class ButterKnifeController extends RxController {
@Override
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
View view = inflateView(inflater, container);
ButterKnife.bind(this, view);
unbinder = ButterKnife.bind(this, view);
onViewBound(view);
return view;
}
@@ -31,9 +34,10 @@ public abstract class ButterKnifeController extends RxController {
protected void onViewBound(@NonNull View view) { }
@Override
protected void onDestroyView(View view) {
protected void onDestroyView(@NonNull View view) {
super.onDestroyView(view);
ButterKnife.unbind(this);
unbinder.unbind();
unbinder = null;
}
}
@@ -1,7 +1,10 @@
package com.bluelinelabs.conductor.demo.controllers.base;
import android.os.Bundle;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.ControllerChangeType;
import com.bluelinelabs.conductor.demo.DemoApplication;
public abstract class RefWatchingController extends ButterKnifeController {
@@ -11,10 +14,24 @@ public abstract class RefWatchingController extends ButterKnifeController {
super(args);
}
private boolean hasExited;
@Override
public void onDestroy() {
super.onDestroy();
DemoApplication.refWatcher.watch(this);
if (hasExited) {
DemoApplication.refWatcher.watch(this);
}
}
@Override
protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
super.onChangeEnded(changeHandler, changeType);
hasExited = !changeType.isEnter;
if (isDestroyed()) {
DemoApplication.refWatcher.watch(this);
}
}
}

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