diff --git a/.travis.yml b/.travis.yml index 036aca54..2cdcbcd1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,11 @@ language: android android: components: - tools - - build-tools-23.0.2 - - android-23 + - build-tools-25.0.0 + - android-25 - extra-android-m2repository + licenses: + - '.+' script: - ./gradlew test diff --git a/README.md b/README.md index 7bf1d020..b8de1e25 100644 --- a/README.md +++ b/README.md @@ -20,22 +20,26 @@ Conductor is architecture-agnostic and does not try to force any design decision ## Installation ```gradle -compile 'com.bluelinelabs:conductor:2.0.3' +compile 'com.bluelinelabs:conductor:2.1.1' // If you want the components that go along with // Android's support libraries (currently just a PagerAdapter): -compile 'com.bluelinelabs:conductor-support:2.0.3' +compile 'com.bluelinelabs:conductor-support:2.1.1' -// If you want RxJava/RxAndroid lifecycle support: -compile 'com.bluelinelabs:conductor-rxlifecycle:2.0.3' +// If you want RxJava lifecycle support: +compile 'com.bluelinelabs:conductor-rxlifecycle:2.1.1' + +// If you want RxJava2 lifecycle support: +compile 'com.bluelinelabs:conductor-rxlifecycle2:2.1.1' ``` SNAPSHOT: ```gradle -compile 'com.bluelinelabs:conductor:2.0.4-SNAPSHOT' -compile 'com.bluelinelabs:conductor-support:2.0.4-SNAPSHOT' -compile 'com.bluelinelabs:conductor-rxlifecycle:2.0.4-SNAPSHOT' +compile 'com.bluelinelabs:conductor:2.1.2-SNAPSHOT' +compile 'com.bluelinelabs:conductor-support:2.1.2-SNAPSHOT' +compile 'com.bluelinelabs:conductor-rxlifecycle:2.1.2-SNAPSHOT' +compile 'com.bluelinelabs:conductor-rxlifecycle2:2.1.2-SNAPSHOT' ``` You also have to add the url to the snapshot repository: diff --git a/build.gradle b/build.gradle index 87b40877..9b76e6fd 100755 --- a/build.gradle +++ b/build.gradle @@ -3,19 +3,15 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.0' + classpath 'com.android.tools.build:gradle:2.3.0' } } allprojects { repositories { jcenter() - mavenLocal() + mavenCentral() } } -task wrapper(type: Wrapper) { - gradleVersion = '2.10' -} - apply from: rootProject.file('dependencies.gradle') diff --git a/conductor-lint/build.gradle b/conductor-lint/build.gradle index 06ca3154..f333be25 100644 --- a/conductor-lint/build.gradle +++ b/conductor-lint/build.gradle @@ -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) } diff --git a/conductor-lint/src/main/java/com/bluelinelabs/conductor/lint/ControllerChangeHandlerIssueDetector.java b/conductor-lint/src/main/java/com/bluelinelabs/conductor/lint/ControllerChangeHandlerIssueDetector.java index 8fb48df3..e40e08cb 100644 --- a/conductor-lint/src/main/java/com/bluelinelabs/conductor/lint/ControllerChangeHandlerIssueDetector.java +++ b/conductor-lint/src/main/java/com/bluelinelabs/conductor/lint/ControllerChangeHandlerIssueDetector.java @@ -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.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 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 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); } } } \ No newline at end of file diff --git a/conductor-lint/src/main/java/com/bluelinelabs/conductor/lint/ControllerIssueDetector.java b/conductor-lint/src/main/java/com/bluelinelabs/conductor/lint/ControllerIssueDetector.java index 3aba3bbb..afababb6 100644 --- a/conductor-lint/src/main/java/com/bluelinelabs/conductor/lint/ControllerIssueDetector.java +++ b/conductor-lint/src/main/java/com/bluelinelabs/conductor/lint/ControllerIssueDetector.java @@ -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.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 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; - - if (constructor.astModifiers().isPublic()) { - StrictListAccessor 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; - } - } + PsiMethod[] constructors = declaration.getConstructors(); + for (PsiMethod constructor : constructors) { + if (evaluator.isPublic(constructor)) { + PsiParameter[] parameters = constructor.getParameterList().getParameters(); + + 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); } } } \ No newline at end of file diff --git a/conductor-lint/src/test/java/com/bluelinelabs/conductor/lint/ControllerChangeHandlerDetectorTest.java b/conductor-lint/src/test/java/com/bluelinelabs/conductor/lint/ControllerChangeHandlerDetectorTest.java new file mode 100644 index 00000000..24969c00 --- /dev/null +++ b/conductor-lint/src/test/java/com/bluelinelabs/conductor/lint/ControllerChangeHandlerDetectorTest.java @@ -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 getIssues() { + return Collections.singletonList(ControllerChangeHandlerIssueDetector.ISSUE); + } + + @Override + protected boolean allowCompilationErrors() { + return true; + } +} diff --git a/conductor-lint/src/test/java/com/bluelinelabs/conductor/lint/ControllerDetectorTest.java b/conductor-lint/src/test/java/com/bluelinelabs/conductor/lint/ControllerDetectorTest.java new file mode 100644 index 00000000..b63df209 --- /dev/null +++ b/conductor-lint/src/test/java/com/bluelinelabs/conductor/lint/ControllerDetectorTest.java @@ -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 getIssues() { + return Collections.singletonList(ControllerIssueDetector.ISSUE); + } + + @Override + protected boolean allowCompilationErrors() { + return true; + } +} diff --git a/conductor-rxlifecycle/build.gradle b/conductor-rxlifecycle/build.gradle index a7f75249..a374d0d5 100755 --- a/conductor-rxlifecycle/build.gradle +++ b/conductor-rxlifecycle/build.gradle @@ -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,7 +22,6 @@ android { dependencies { compile rootProject.ext.rxJava - compile rootProject.ext.rxAndroid compile rootProject.ext.rxLifecycle compile rootProject.ext.rxLifecycleAndroid diff --git a/conductor-rxlifecycle2/build.gradle b/conductor-rxlifecycle2/build.gradle new file mode 100644 index 00000000..8b8582ea --- /dev/null +++ b/conductor-rxlifecycle2/build.gradle @@ -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' diff --git a/conductor-rxlifecycle2/gradle.properties b/conductor-rxlifecycle2/gradle.properties new file mode 100644 index 00000000..217f8814 --- /dev/null +++ b/conductor-rxlifecycle2/gradle.properties @@ -0,0 +1,3 @@ +POM_NAME=Conductor RxLifecycle2 Extensions +POM_ARTIFACT_ID=conductor-rxlifecycle2 +POM_PACKAGING=aar diff --git a/conductor-rxlifecycle2/src/main/AndroidManifest.xml b/conductor-rxlifecycle2/src/main/AndroidManifest.xml new file mode 100644 index 00000000..ab33109b --- /dev/null +++ b/conductor-rxlifecycle2/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/conductor-rxlifecycle2/src/main/java/com/bluelinelabs/conductor/rxlifecycle2/ControllerEvent.java b/conductor-rxlifecycle2/src/main/java/com/bluelinelabs/conductor/rxlifecycle2/ControllerEvent.java new file mode 100644 index 00000000..940d5309 --- /dev/null +++ b/conductor-rxlifecycle2/src/main/java/com/bluelinelabs/conductor/rxlifecycle2/ControllerEvent.java @@ -0,0 +1,12 @@ +package com.bluelinelabs.conductor.rxlifecycle2; + +public enum ControllerEvent { + + CREATE, + CREATE_VIEW, + ATTACH, + DETACH, + DESTROY_VIEW, + DESTROY + +} diff --git a/conductor-rxlifecycle2/src/main/java/com/bluelinelabs/conductor/rxlifecycle2/ControllerLifecycleSubjectHelper.java b/conductor-rxlifecycle2/src/main/java/com/bluelinelabs/conductor/rxlifecycle2/ControllerLifecycleSubjectHelper.java new file mode 100644 index 00000000..f028b9f5 --- /dev/null +++ b/conductor-rxlifecycle2/src/main/java/com/bluelinelabs/conductor/rxlifecycle2/ControllerLifecycleSubjectHelper.java @@ -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 create(Controller controller){ + final BehaviorSubject 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; + } +} diff --git a/conductor-rxlifecycle2/src/main/java/com/bluelinelabs/conductor/rxlifecycle2/RxController.java b/conductor-rxlifecycle2/src/main/java/com/bluelinelabs/conductor/rxlifecycle2/RxController.java new file mode 100644 index 00000000..899c0d1c --- /dev/null +++ b/conductor-rxlifecycle2/src/main/java/com/bluelinelabs/conductor/rxlifecycle2/RxController.java @@ -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 { + private final BehaviorSubject lifecycleSubject; + + public RxController(){ + this(null); + } + + public RxController(@Nullable Bundle args) { + super(args); + lifecycleSubject = ControllerLifecycleSubjectHelper.create(this); + } + + @Override + @NonNull + @CheckResult + public final Observable lifecycle() { + return lifecycleSubject.hide(); + } + + @Override + @NonNull + @CheckResult + public final LifecycleTransformer bindUntilEvent(@NonNull ControllerEvent event) { + return RxLifecycle.bindUntilEvent(lifecycleSubject, event); + } + + @Override + @NonNull + @CheckResult + public final LifecycleTransformer bindToLifecycle() { + return RxControllerLifecycle.bindController(lifecycleSubject); + } +} diff --git a/conductor-rxlifecycle2/src/main/java/com/bluelinelabs/conductor/rxlifecycle2/RxControllerLifecycle.java b/conductor-rxlifecycle2/src/main/java/com/bluelinelabs/conductor/rxlifecycle2/RxControllerLifecycle.java new file mode 100644 index 00000000..adfe6a51 --- /dev/null +++ b/conductor-rxlifecycle2/src/main/java/com/bluelinelabs/conductor/rxlifecycle2/RxControllerLifecycle.java @@ -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 LifecycleTransformer bindController(@NonNull final Observable lifecycle) { + return RxLifecycle.bind(lifecycle, CONTROLLER_LIFECYCLE); + } + + private static final Function CONTROLLER_LIFECYCLE = + new Function() { + @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."); + } + } + }; +} diff --git a/conductor-rxlifecycle2/src/main/java/com/bluelinelabs/conductor/rxlifecycle2/RxRestoreViewOnCreateController.java b/conductor-rxlifecycle2/src/main/java/com/bluelinelabs/conductor/rxlifecycle2/RxRestoreViewOnCreateController.java new file mode 100644 index 00000000..c95ddc9a --- /dev/null +++ b/conductor-rxlifecycle2/src/main/java/com/bluelinelabs/conductor/rxlifecycle2/RxRestoreViewOnCreateController.java @@ -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 { + private final BehaviorSubject lifecycleSubject; + + public RxRestoreViewOnCreateController() { + this(null); + } + + public RxRestoreViewOnCreateController(Bundle args) { + super(args); + lifecycleSubject = ControllerLifecycleSubjectHelper.create(this); + } + + @Override + @NonNull + @CheckResult + public final Observable lifecycle() { + return lifecycleSubject.hide(); + } + + @Override + @NonNull + @CheckResult + public final LifecycleTransformer bindUntilEvent(@NonNull ControllerEvent event) { + return RxLifecycle.bindUntilEvent(lifecycleSubject, event); + } + + @Override + @NonNull + @CheckResult + public final LifecycleTransformer bindToLifecycle() { + return RxControllerLifecycle.bindController(lifecycleSubject); + } +} diff --git a/conductor-support/build.gradle b/conductor-support/build.gradle index 41e7d752..26b2d80a 100755 --- a/conductor-support/build.gradle +++ b/conductor-support/build.gradle @@ -1,16 +1,19 @@ -apply from: rootProject.file('dependencies.gradle') -apply from: rootProject.file('gradle-mvn-push.gradle') +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'de.mobilej.unmock:UnMockPlugin:0.3.6' + } +} apply plugin: 'com.android.library' +apply plugin: 'de.mobilej.unmock' android { compileSdkVersion rootProject.ext.compileSdkVersion buildToolsVersion rootProject.ext.buildToolsVersion - lintOptions { - abortOnError false - } - compileOptions { sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 @@ -25,9 +28,22 @@ android { } dependencies { + testCompile rootProject.ext.junit + testCompile rootProject.ext.roboelectric + compile rootProject.ext.supportAppCompat compile project(':conductor') } +unMock { + downloadFrom 'https://oss.sonatype.org/content/groups/public/org/robolectric/android-all/4.3_r2-robolectric-0/android-all-4.3_r2-robolectric-0.jar' + + keep "android.os.Bundle" + keep "android.os.BaseBundle" +} + ext.artifactId = 'conductor-support' +apply from: rootProject.file('dependencies.gradle') +apply from: rootProject.file('gradle-mvn-push.gradle') + diff --git a/conductor-support/src/main/java/com/bluelinelabs/conductor/support/ControllerPagerAdapter.java b/conductor-support/src/main/java/com/bluelinelabs/conductor/support/ControllerPagerAdapter.java index 3af9a3c6..98f35f3f 100644 --- a/conductor-support/src/main/java/com/bluelinelabs/conductor/support/ControllerPagerAdapter.java +++ b/conductor-support/src/main/java/com/bluelinelabs/conductor/support/ControllerPagerAdapter.java @@ -2,6 +2,8 @@ import android.os.Bundle; import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.view.PagerAdapter; import android.util.SparseArray; import android.view.View; @@ -12,21 +14,28 @@ import com.bluelinelabs.conductor.RouterTransaction; /** + * @deprecated Use RouterPagerAdapter instead! This implementation was too limited and had too many + * gotchas associated with it. + * * An adapter for ViewPagers that will handle adding and removing Controllers */ +@Deprecated public abstract class ControllerPagerAdapter extends PagerAdapter { private static final String KEY_SAVED_PAGES = "ControllerPagerAdapter.savedStates"; 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 savedPages = new SparseArray<>(); + private SparseArray visiblePageIds = new SparseArray<>(); /** * Creates a new ControllerPagerAdapter using the passed host. */ - public ControllerPagerAdapter(Controller host, boolean saveControllerState) { + public ControllerPagerAdapter(@NonNull Controller host, boolean saveControllerState) { this.host = host; savesState = saveControllerState; } @@ -34,6 +43,7 @@ public ControllerPagerAdapter(Controller host, boolean saveControllerState) { /** * Return the Controller associated with a specified position. */ + @NonNull public abstract Controller getItem(int position); @Override @@ -49,11 +59,17 @@ public Object instantiateItem(ViewGroup container, int position) { } } + final Controller controller; if (!router.hasRootController()) { - router.setRoot(RouterTransaction.with(getItem(position)) - .tag(name)); + 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); @@ -69,6 +85,8 @@ public void destroyItem(ViewGroup container, int position, Object object) { savedPages.put(position, savedState); } + visiblePageIds.remove(position); + host.removeChildRouter(router); } @@ -82,6 +100,16 @@ 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; } @@ -91,6 +119,27 @@ public void restoreState(Parcelable state, ClassLoader loader) { 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; } } @@ -102,4 +151,4 @@ private static String makeControllerName(int viewId, long id) { return viewId + ":" + id; } -} \ No newline at end of file +} diff --git a/conductor-support/src/main/java/com/bluelinelabs/conductor/support/RouterPagerAdapter.java b/conductor-support/src/main/java/com/bluelinelabs/conductor/support/RouterPagerAdapter.java new file mode 100644 index 00000000..637297e7 --- /dev/null +++ b/conductor-support/src/main/java/com/bluelinelabs/conductor/support/RouterPagerAdapter.java @@ -0,0 +1,161 @@ +package com.bluelinelabs.conductor.support; + +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.view.PagerAdapter; +import android.util.SparseArray; +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.ArrayList; +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 static final String KEY_MAX_PAGES_TO_STATE_SAVE = "RouterPagerAdapter.maxPagesToStateSave"; + private static final String KEY_SAVE_PAGE_HISTORY = "RouterPagerAdapter.savedPageHistory"; + + private final Controller host; + private int maxPagesToStateSave = Integer.MAX_VALUE; + private SparseArray savedPages = new SparseArray<>(); + private SparseArray visibleRouters = new SparseArray<>(); + private ArrayList savedPageHistory = new ArrayList<>(); + + /** + * Creates a new RouterPagerAdapter using the passed host. + */ + public RouterPagerAdapter(@NonNull 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(@NonNull Router router, int position); + + /** + * Sets the maximum number of pages that will have their states saved. When this number is exceeded, + * the page that was state saved least recently will have its state removed from the save data. + */ + public void setMaxPagesToStateSave(int maxPagesToStateSave) { + if (maxPagesToStateSave < 0) { + throw new IllegalArgumentException("Only positive integers may be passed for maxPagesToStateSave."); + } + + this.maxPagesToStateSave = maxPagesToStateSave; + + ensurePagesSaved(); + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + 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); + savedPages.remove(position); + } + } + + 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); + + savedPageHistory.remove((Integer)position); + savedPageHistory.add(position); + + ensurePagesSaved(); + + host.removeChildRouter(router); + + visibleRouters.remove(position); + } + + @Override + public boolean isViewFromObject(View view, Object object) { + Router router = (Router)object; + final List 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); + bundle.putInt(KEY_MAX_PAGES_TO_STATE_SAVE, maxPagesToStateSave); + bundle.putIntegerArrayList(KEY_SAVE_PAGE_HISTORY, savedPageHistory); + return bundle; + } + + @Override + public void restoreState(Parcelable state, ClassLoader loader) { + Bundle bundle = (Bundle)state; + if (state != null) { + savedPages = bundle.getSparseParcelableArray(KEY_SAVED_PAGES); + maxPagesToStateSave = bundle.getInt(KEY_MAX_PAGES_TO_STATE_SAVE); + savedPageHistory = bundle.getIntegerArrayList(KEY_SAVE_PAGE_HISTORY); + } + } + + /** + * 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; + } + + SparseArray getSavedPages() { + return savedPages; + } + + private void ensurePagesSaved() { + while (savedPages.size() > maxPagesToStateSave) { + int positionToRemove = savedPageHistory.remove(0); + savedPages.remove(positionToRemove); + } + } + + private static String makeRouterName(int viewId, long id) { + return viewId + ":" + id; + } + +} \ No newline at end of file diff --git a/conductor-support/src/test/java/com/bluelinelabs/conductor/support/StateSaveTests.java b/conductor-support/src/test/java/com/bluelinelabs/conductor/support/StateSaveTests.java new file mode 100644 index 00000000..17656d88 --- /dev/null +++ b/conductor-support/src/test/java/com/bluelinelabs/conductor/support/StateSaveTests.java @@ -0,0 +1,113 @@ +package com.bluelinelabs.conductor.support; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.util.SparseArray; +import android.widget.FrameLayout; + +import com.bluelinelabs.conductor.Conductor; +import com.bluelinelabs.conductor.Router; +import com.bluelinelabs.conductor.RouterTransaction; +import com.bluelinelabs.conductor.support.util.FakePager; +import com.bluelinelabs.conductor.support.util.TestController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.util.ActivityController; + +import static org.junit.Assert.assertEquals; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class StateSaveTests { + + private FakePager pager; + private RouterPagerAdapter pagerAdapter; + + public void createActivityController(Bundle savedInstanceState) { + ActivityController activityController = Robolectric.buildActivity(Activity.class).create().start().resume(); + Router router = Conductor.attachRouter(activityController.get(), new FrameLayout(activityController.get()), savedInstanceState); + TestController controller = new TestController(); + router.setRoot(RouterTransaction.with(controller)); + + pager = new FakePager(new FrameLayout(activityController.get())); + pager.setOffscreenPageLimit(1); + + pagerAdapter = new RouterPagerAdapter(controller) { + @Override + public void configureRouter(@NonNull Router router, int position) { + if (!router.hasRootController()) { + router.setRoot(RouterTransaction.with(new TestController())); + } + } + + @Override + public int getCount() { + return 20; + } + }; + + pager.setAdapter(pagerAdapter); + } + + @Before + public void setup() { + createActivityController(null); + } + + @Test + public void testNoMaxSaves() { + // Load all pages + for (int i = 0; i < pagerAdapter.getCount(); i++) { + pager.pageTo(i); + } + + pager.pageTo(pagerAdapter.getCount() / 2); + + // Ensure all non-visible pages are saved + assertEquals(pagerAdapter.getCount() - 1 - pager.getOffscreenPageLimit() * 2, pagerAdapter.getSavedPages().size()); + } + + @Test + public void testMaxSavedSet() { + final int maxPages = 3; + pagerAdapter.setMaxPagesToStateSave(maxPages); + + // Load all pages + for (int i = 0; i < pagerAdapter.getCount(); i++) { + pager.pageTo(i); + } + + final int firstSelectedItem = pagerAdapter.getCount() / 2; + pager.pageTo(firstSelectedItem); + + SparseArray savedPages = pagerAdapter.getSavedPages(); + + // Ensure correct number of pages are saved + assertEquals(maxPages, savedPages.size()); + + // Ensure correct pages are saved + assertEquals(pagerAdapter.getCount() - 3, savedPages.keyAt(0)); + assertEquals(pagerAdapter.getCount() - 2, savedPages.keyAt(1)); + assertEquals(pagerAdapter.getCount() - 1, savedPages.keyAt(2)); + + final int secondSelectedItem = 1; + pager.pageTo(secondSelectedItem); + + savedPages = pagerAdapter.getSavedPages(); + + // Ensure correct number of pages are saved + assertEquals(maxPages, savedPages.size()); + + // Ensure correct pages are saved + assertEquals(firstSelectedItem - 1, savedPages.keyAt(0)); + assertEquals(firstSelectedItem, savedPages.keyAt(1)); + assertEquals(firstSelectedItem + 1, savedPages.keyAt(2)); + } + +} diff --git a/conductor-support/src/test/java/com/bluelinelabs/conductor/support/util/FakePager.java b/conductor-support/src/test/java/com/bluelinelabs/conductor/support/util/FakePager.java new file mode 100644 index 00000000..d70ffe45 --- /dev/null +++ b/conductor-support/src/test/java/com/bluelinelabs/conductor/support/util/FakePager.java @@ -0,0 +1,61 @@ +package com.bluelinelabs.conductor.support.util; + +import android.util.SparseArray; +import android.view.ViewGroup; + +import com.bluelinelabs.conductor.support.RouterPagerAdapter; + +import java.util.ArrayList; +import java.util.List; + +public class FakePager { + + private ViewGroup container; + private int offscreenPageLimit; + private final SparseArray pages = new SparseArray<>(); + + private RouterPagerAdapter adapter; + + public FakePager(ViewGroup container) { + this.container = container; + } + + public void setAdapter(RouterPagerAdapter adapter) { + this.adapter = adapter; + } + + public void pageTo(int page) { + int firstPage = Math.max(0, page - offscreenPageLimit); + int lastPage = Math.min(adapter.getCount() - 1, page + offscreenPageLimit); + + List pagesI = new ArrayList<>(); + for (int i = 0; i < pages.size(); i++) { + pagesI.add(pages.keyAt(i)); + } + + for (int i = pages.size() - 1; i >= 0; i--) { + int key = pages.keyAt(i); + + if (key < firstPage || key > lastPage) { + adapter.destroyItem(container, key, pages.get(key)); + pages.remove(key); + } + } + + for (int key = firstPage; key <= lastPage; key++) { + if (pages.get(key) == null) { + pages.put(key, adapter.instantiateItem(container, key)); + } + } + + adapter.setPrimaryItem(container, page, pages.get(page)); + } + + public int getOffscreenPageLimit() { + return offscreenPageLimit; + } + + public void setOffscreenPageLimit(int offscreenPageLimit) { + this.offscreenPageLimit = offscreenPageLimit; + } +} diff --git a/conductor-support/src/test/java/com/bluelinelabs/conductor/support/util/TestController.java b/conductor-support/src/test/java/com/bluelinelabs/conductor/support/util/TestController.java new file mode 100644 index 00000000..1b799f68 --- /dev/null +++ b/conductor-support/src/test/java/com/bluelinelabs/conductor/support/util/TestController.java @@ -0,0 +1,18 @@ +package com.bluelinelabs.conductor.support.util; + +import android.support.annotation.NonNull; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.bluelinelabs.conductor.Controller; + +public class TestController extends Controller { + + @NonNull @Override + protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { + return new FrameLayout(inflater.getContext()); + } + +} diff --git a/conductor/build.gradle b/conductor/build.gradle index f67acee5..a3ce9c01 100755 --- a/conductor/build.gradle +++ b/conductor/build.gradle @@ -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 diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/ActivityHostedRouter.java b/conductor/src/main/java/com/bluelinelabs/conductor/ActivityHostedRouter.java index 4f9f3cc8..5b9b0f90 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/ActivityHostedRouter.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/ActivityHostedRouter.java @@ -2,18 +2,23 @@ import android.app.Activity; import android.content.Intent; +import android.content.IntentSender; +import android.content.IntentSender.SendIntentException; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.view.ViewGroup; import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener; import com.bluelinelabs.conductor.internal.LifecycleHandler; +import com.bluelinelabs.conductor.internal.TransactionIndexer; import java.util.List; public class ActivityHostedRouter extends Router { private LifecycleHandler lifecycleHandler; + private final TransactionIndexer transactionIndexer = new TransactionIndexer(); public final void setHost(@NonNull LifecycleHandler lifecycleHandler, @NonNull ViewGroup container) { if (this.lifecycleHandler != lifecycleHandler || this.container != container) { @@ -31,12 +36,26 @@ public final void setHost(@NonNull LifecycleHandler lifecycleHandler, @NonNull V } @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(Activity activity) { + public void onActivityDestroyed(@NonNull Activity activity) { super.onActivityDestroyed(activity); lifecycleHandler = null; } @@ -49,37 +68,43 @@ public final void invalidateOptionsMenu() { } @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { lifecycleHandler.onActivityResult(requestCode, resultCode, data); } @Override - void startActivity(Intent intent) { + void startActivity(@NonNull Intent intent) { lifecycleHandler.startActivity(intent); } @Override - void startActivityForResult(String instanceId, Intent intent, int requestCode) { + void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) { lifecycleHandler.startActivityForResult(instanceId, intent, requestCode); } @Override - void startActivityForResult(String instanceId, Intent intent, int requestCode, Bundle options) { + void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options) { lifecycleHandler.startActivityForResult(instanceId, intent, requestCode, options); } @Override - void registerForActivityResult(String instanceId, int requestCode) { + void startIntentSenderForResult(@NonNull String instanceId, @NonNull IntentSender intent, int requestCode, @Nullable Intent fillInIntent, + int flagsMask, int flagsValues, int extraFlags, @Nullable Bundle options) throws SendIntentException { + lifecycleHandler.startIntentSenderForResult(instanceId, intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options); + } + + @Override + void registerForActivityResult(@NonNull String instanceId, int requestCode) { lifecycleHandler.registerForActivityResult(instanceId, requestCode); } @Override - void unregisterForActivityResults(String instanceId) { + void unregisterForActivityResults(@NonNull String instanceId) { lifecycleHandler.unregisterForActivityResults(instanceId); } @Override - void requestPermissions(String instanceId, @NonNull String[] permissions, int requestCode) { + void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) { lifecycleHandler.requestPermissions(instanceId, permissions, requestCode); } @@ -88,13 +113,18 @@ boolean hasHost() { return lifecycleHandler != null; } - @Override + @Override @NonNull List getSiblingRouters() { return lifecycleHandler.getRouters(); } - @Override + @Override @NonNull Router getRootRouter() { return this; } + + @Override @Nullable + TransactionIndexer getTransactionIndexer() { + return transactionIndexer; + } } diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/Backstack.java b/conductor/src/main/java/com/bluelinelabs/conductor/Backstack.java index 3ac02106..75266feb 100755 --- a/conductor/src/main/java/com/bluelinelabs/conductor/Backstack.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/Backstack.java @@ -1,6 +1,8 @@ package com.bluelinelabs.conductor; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import java.util.ArrayDeque; import java.util.ArrayList; @@ -13,34 +15,37 @@ class Backstack implements Iterable { private static final String KEY_ENTRIES = "Backstack.entries"; - private final Deque backStack = new ArrayDeque<>(); + private final Deque backstack = new ArrayDeque<>(); @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean isEmpty() { - return backStack.isEmpty(); + boolean isEmpty() { + return backstack.isEmpty(); } - public int size() { - return backStack.size(); + int size() { + return backstack.size(); } - public RouterTransaction root() { - return backStack.size() > 0 ? backStack.getLast() : null; + @Nullable + RouterTransaction root() { + return backstack.size() > 0 ? backstack.getLast() : null; } - @Override + @Override @NonNull public Iterator iterator() { - return backStack.iterator(); + return backstack.iterator(); } - public Iterator reverseIterator() { - return backStack.descendingIterator(); + @NonNull + Iterator reverseIterator() { + return backstack.descendingIterator(); } - public List popTo(RouterTransaction transaction) { + @NonNull + List popTo(@NonNull RouterTransaction transaction) { List popped = new ArrayList<>(); - if (backStack.contains(transaction)) { - while (backStack.peek() != transaction) { + if (backstack.contains(transaction)) { + while (backstack.peek() != transaction) { RouterTransaction poppedTransaction = pop(); popped.add(poppedTransaction); } @@ -50,25 +55,28 @@ public List popTo(RouterTransaction transaction) { return popped; } - public RouterTransaction pop() { - RouterTransaction popped = backStack.pop(); + @NonNull + RouterTransaction pop() { + RouterTransaction popped = backstack.pop(); popped.controller.destroy(); return popped; } - public RouterTransaction peek() { - return backStack.peek(); + @Nullable + RouterTransaction peek() { + return backstack.peek(); } - public void remove(RouterTransaction transaction) { - backStack.removeFirstOccurrence(transaction); + void remove(@NonNull RouterTransaction transaction) { + backstack.removeFirstOccurrence(transaction); } - public void push(RouterTransaction transaction) { - backStack.push(transaction); + void push(@NonNull RouterTransaction transaction) { + backstack.push(transaction); } - public List popAll() { + @NonNull + List popAll() { List list = new ArrayList<>(); while (!isEmpty()) { list.add(pop()); @@ -76,8 +84,8 @@ public List popAll() { return list; } - public void setBackstack(List backstack) { - for (RouterTransaction existingTransaction : backStack) { + void setBackstack(@NonNull List backstack) { + for (RouterTransaction existingTransaction : this.backstack) { boolean contains = false; for (RouterTransaction newTransaction : backstack) { if (existingTransaction.controller == newTransaction.controller) { @@ -91,27 +99,31 @@ public void setBackstack(List backstack) { } } - backStack.clear(); + this.backstack.clear(); for (RouterTransaction transaction : backstack) { - backStack.push(transaction); + this.backstack.push(transaction); } } - public void saveInstanceState(Bundle outState) { - ArrayList entryBundles = new ArrayList<>(backStack.size()); - for (RouterTransaction entry : backStack) { + boolean contains(@NonNull RouterTransaction transaction) { + return backstack.contains(transaction); + } + + void saveInstanceState(@NonNull Bundle outState) { + ArrayList entryBundles = new ArrayList<>(backstack.size()); + for (RouterTransaction entry : backstack) { entryBundles.add(entry.saveInstanceState()); } outState.putParcelableArrayList(KEY_ENTRIES, entryBundles); } - public void restoreInstanceState(Bundle savedInstanceState) { + void restoreInstanceState(@NonNull Bundle savedInstanceState) { ArrayList entryBundles = savedInstanceState.getParcelableArrayList(KEY_ENTRIES); if (entryBundles != null) { Collections.reverse(entryBundles); for (Bundle transactionBundle : entryBundles) { - backStack.push(new RouterTransaction(transactionBundle)); + backstack.push(new RouterTransaction(transactionBundle)); } } } diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/ChangeHandlerFrameLayout.java b/conductor/src/main/java/com/bluelinelabs/conductor/ChangeHandlerFrameLayout.java index 89eb4be6..50eefd0c 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/ChangeHandlerFrameLayout.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/ChangeHandlerFrameLayout.java @@ -3,6 +3,8 @@ import android.annotation.TargetApi; import android.content.Context; import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.ViewGroup; @@ -42,12 +44,12 @@ public boolean onInterceptTouchEvent(MotionEvent ev) { } @Override - public void onChangeStarted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler) { + public void onChangeStarted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) { inProgressTransactionCount++; } @Override - public void onChangeCompleted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler) { + public void onChangeCompleted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) { inProgressTransactionCount--; } diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/Conductor.java b/conductor/src/main/java/com/bluelinelabs/conductor/Conductor.java index 3bc84704..d9226b1e 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/Conductor.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/Conductor.java @@ -3,6 +3,8 @@ 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 @@ private 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); diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java b/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java index 08d6c95f..fc47f94b 100755 --- a/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java @@ -4,12 +4,14 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.IntentSender; import android.content.res.Resources; import android.os.Build; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.IdRes; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.SparseArray; import android.view.LayoutInflater; @@ -17,20 +19,19 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup; -import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener; import com.bluelinelabs.conductor.internal.ClassUtils; import com.bluelinelabs.conductor.internal.RouterRequiringFunc; +import com.bluelinelabs.conductor.internal.ViewAttachHandler; +import com.bluelinelabs.conductor.internal.ViewAttachHandler.ViewAttachListener; import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; -import java.util.Deque; -import java.util.Iterator; +import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.UUID; @@ -80,27 +81,15 @@ public abstract class Controller { private ControllerChangeHandler overriddenPushHandler; private ControllerChangeHandler overriddenPopHandler; private RetainViewMode retainViewMode = RetainViewMode.RELEASE_DETACH; - private OnAttachStateChangeListener onAttachStateChangeListener; + private ViewAttachHandler viewAttachHandler; private final List childRouters = new ArrayList<>(); private final List lifecycleListeners = new ArrayList<>(); private final ArrayList requestedPermissions = new ArrayList<>(); private final ArrayList onRouterSetListeners = new ArrayList<>(); - private final Deque childBackstack = new ArrayDeque<>(); private WeakReference destroyedView; - private final ControllerChangeListener childRouterChangeListener = new ControllerChangeListener() { - @Override - public void onChangeStarted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler) { - if (isPush) { - onChildControllerPushed(to); - } - } - - @Override - public void onChangeCompleted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler) { } - }; - - static Controller newInstance(Bundle bundle) { + @NonNull + static Controller newInstance(@NonNull Bundle bundle) { final String className = bundle.getString(KEY_CLASS_NAME); //noinspection ConstantConditions Constructor[] constructors = ClassUtils.classForName(className, false).getConstructors(); @@ -134,8 +123,8 @@ protected Controller() { * * @param args Any arguments that need to be retained. */ - protected Controller(Bundle args) { - this.args = args; + protected Controller(@Nullable Bundle args) { + this.args = args != null ? args : new Bundle(); instanceId = UUID.randomUUID().toString(); ensureRequiredConstructor(); } @@ -163,11 +152,50 @@ public final Router getRouter() { /** * Returns any arguments that were set in this Controller's constructor */ + @NonNull public Bundle getArgs() { return args; } - public final Router getChildRouter(@NonNull ViewGroup container, String tag) { + /** + * Retrieves the child {@link Router} for the given container. If no child router for this container + * exists yet, it will be created. + * + * @param container The ViewGroup that hosts the child Router + */ + @NonNull + public final Router getChildRouter(@NonNull ViewGroup container) { + return getChildRouter(container, null); + } + + /** + * Retrieves the child {@link Router} for the given container/tag combination. If no child router for + * this container exists yet, it will be created. Note that multiple routers should not exist + * in the same container unless a lot of care is taken to maintain order between them. Avoid using + * the same container unless you have a great reason to do so (ex: ViewPagers). + * + * @param container The ViewGroup that hosts the child Router + * @param tag The router's tag or {@code null} if none is needed + */ + @NonNull + public final Router getChildRouter(@NonNull ViewGroup container, @Nullable String tag) { + //noinspection ConstantConditions + return getChildRouter(container, tag, true); + } + + /** + * Retrieves the child {@link Router} for the given container/tag combination. Note that multiple + * routers should not exist in the same container unless a lot of care is taken to maintain order + * between them. Avoid using the same container unless you have a great reason to do so (ex: ViewPagers). + * The only time this method will return {@code null} is when the child router does not exist prior + * to calling this method and the createIfNeeded parameter is set to false. + * + * @param container The ViewGroup that hosts the child Router + * @param tag The router's tag or {@code null} if none is needed + * @param createIfNeeded If true, a router will be created if one does not yet exist. Else {@code null} will be returned in this case. + */ + @Nullable + public final Router getChildRouter(@NonNull ViewGroup container, @Nullable String tag, boolean createIfNeeded) { @IdRes final int containerId = container.getId(); ControllerHostedRouter childRouter = null; @@ -179,22 +207,28 @@ public final Router getChildRouter(@NonNull ViewGroup container, String tag) { } if (childRouter == null) { - childRouter = new ControllerHostedRouter(container.getId(), tag); - monitorChildRouter(childRouter); - childRouter.setHost(this, container); - childRouters.add(childRouter); + if (createIfNeeded) { + childRouter = new ControllerHostedRouter(container.getId(), tag); + childRouter.setHost(this, container); + childRouters.add(childRouter); + } } else if (!childRouter.hasHost()) { childRouter.setHost(this, container); - monitorChildRouter(childRouter); childRouter.rebindIfNeeded(); } return childRouter; } + /** + * Removes a child {@link Router} from this Controller. When removed, all Controllers currently managed by + * the {@link Router} will be destroyed. + * + * @param childRouter The router to be removed + */ public final void removeChildRouter(@NonNull Router childRouter) { if ((childRouter instanceof ControllerHostedRouter) && childRouters.remove(childRouter)) { - childRouter.destroy(); + childRouter.destroy(true); } } @@ -220,38 +254,48 @@ public final boolean isAttached() { } /** - * Return this Controller's View, if available. + * Return this Controller's View or {@code null} if it has not yet been created or has been + * destroyed. */ + @Nullable public final View getView() { return view; } /** - * Returns the host Activity of this Controller's {@link Router} + * Returns the host Activity of this Controller's {@link Router} or {@code null} if this + * Controller has not yet been attached to an Activity or if the Activity has been destroyed. */ + @Nullable public final Activity getActivity() { - return router.getActivity(); + return router != null ? router.getActivity() : null; } /** - * Returns the Resources from the host Activity + * Returns the Resources from the host Activity or {@code null} if this Controller has not + * yet been attached to an Activity or if the Activity has been destroyed. */ + @Nullable public final Resources getResources() { Activity activity = getActivity(); return activity != null ? activity.getResources() : null; } /** - * Returns the Application Context derived from the host Activity + * Returns the Application Context derived from the host Activity or {@code null} if this Controller + * has not yet been attached to an Activity or if the Activity has been destroyed. */ + @Nullable public final Context getApplicationContext() { Activity activity = getActivity(); return activity != null ? activity.getApplicationContext() : null; } /** - * Returns this Controller's parent Controller if it is a child Controller. + * Returns this Controller's parent Controller if it is a child Controller or {@code null} if + * it has no parent. */ + @Nullable public final Controller getParentController() { return parentController; } @@ -260,17 +304,19 @@ public final Controller getParentController() { * Returns this Controller's instance ID, which is generated when the instance is created and * retained across restarts. */ + @NonNull public final String getInstanceId() { return instanceId; } /** - * Returns the Controller with the given instance id, if available. - * May return the controller itself or a matching descendant + * Returns the Controller with the given instance id or {@code null} if no such Controller + * exists. May return the Controller itself or a matching descendant + * * @param instanceId The instance ID being searched for - * @return The matching Controller, if one exists */ - final Controller findController(String instanceId) { + @Nullable + final Controller findController(@NonNull String instanceId) { if (this.instanceId.equals(instanceId)) { return this; } @@ -287,6 +333,7 @@ final Controller findController(String instanceId) { /** * Returns all of this Controller's child Routers */ + @NonNull public final List getChildRouters() { List routers = new ArrayList<>(); for (Router router : childRouters) { @@ -302,7 +349,7 @@ public final List getChildRouters() { * * @param target The Controller that is the target of this one. */ - public void setTargetController(Controller target) { + public void setTargetController(@Nullable Controller target) { if (targetInstanceId != null) { throw new RuntimeException("Target controller already set. A controller's target may only be set once."); } @@ -311,10 +358,12 @@ public void setTargetController(Controller target) { } /** - * Returns the target Controller that was set with the {@link #setTargetController(Controller)} method + * Returns the target Controller that was set with the {@link #setTargetController(Controller)} + * method or {@code null} if this Controller has no target. * * @return This Controller's target */ + @Nullable public final Controller getTargetController() { if (targetInstanceId != null) { return router.getRootRouter().getControllerWithInstanceId(targetInstanceId); @@ -328,7 +377,7 @@ public final Controller getTargetController() { * * @param view The View to which this Controller should be bound. */ - protected void onDestroyView(View view) { } + protected void onDestroyView(@NonNull View view) { } /** * Called when this Controller begins the process of being swapped in or out of the host view. @@ -368,22 +417,22 @@ protected void onDestroy() { } /** * Called when this Controller's host Activity is started */ - protected void onActivityStarted(Activity activity) { } + protected void onActivityStarted(@NonNull Activity activity) { } /** * Called when this Controller's host Activity is resumed */ - protected void onActivityResumed(Activity activity) { } + protected void onActivityResumed(@NonNull Activity activity) { } /** * Called when this Controller's host Activity is paused */ - protected void onActivityPaused(Activity activity) { } + protected void onActivityPaused(@NonNull Activity activity) { } /** * Called when this Controller's host Activity is stopped */ - protected void onActivityStopped(Activity activity) { } + protected void onActivityStopped(@NonNull Activity activity) { } /** * Called to save this Controller's View state. As Views can be detached and destroyed as part of the @@ -422,7 +471,7 @@ protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { } /** * Calls startActivity(Intent) from this Controller's host Activity. */ - public final void startActivity(final Intent intent) { + public final void startActivity(@NonNull final Intent intent) { executeWithRouter(new RouterRequiringFunc() { @Override public void execute() { router.startActivity(intent); } }); @@ -431,7 +480,7 @@ public final void startActivity(final Intent intent) { /** * Calls startActivityForResult(Intent, int) from this Controller's host Activity. */ - public final void startActivityForResult(final Intent intent, final int requestCode) { + public final void startActivityForResult(@NonNull final Intent intent, final int requestCode) { executeWithRouter(new RouterRequiringFunc() { @Override public void execute() { router.startActivityForResult(instanceId, intent, requestCode); } }); @@ -440,12 +489,20 @@ public final void startActivityForResult(final Intent intent, final int requestC /** * Calls startActivityForResult(Intent, int, Bundle) from this Controller's host Activity. */ - public final void startActivityForResult(final Intent intent, final int requestCode, final Bundle options) { + public final void startActivityForResult(@NonNull final Intent intent, final int requestCode, @Nullable final Bundle options) { executeWithRouter(new RouterRequiringFunc() { @Override public void execute() { router.startActivityForResult(instanceId, intent, requestCode, options); } }); } + /** + * Calls startIntentSenderForResult(IntentSender, int, Intent, int, int, int, Bundle) from this Controller's host Activity. + */ + public final void startIntentSenderForResult(@NonNull final IntentSender intent, final int requestCode, @Nullable final Intent fillInIntent, final int flagsMask, + final int flagsValues, final int extraFlags, @Nullable final Bundle options) throws IntentSender.SendIntentException { + router.startIntentSenderForResult(instanceId, intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options); + } + /** * Registers this Controller to handle onActivityResult responses. Calling this method is NOT * necessary when calling {@link #startActivityForResult(Intent, int)} @@ -466,7 +523,7 @@ public final void registerForActivityResult(final int requestCode) { * @param resultCode The resultCode that was returned to the host Activity's onActivityResult method * @param data The data Intent that was returned to the host Activity's onActivityResult method */ - public void onActivityResult(int requestCode, int resultCode, Intent data) { } + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { } /** * Calls requestPermission(String[], int) from this Controller's host Activity. Results for this request, @@ -507,9 +564,22 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis * @return True if this Controller has consumed the back button press, otherwise false */ public boolean handleBack() { - Iterator childIterator = childBackstack.descendingIterator(); - while (childIterator.hasNext()) { - Controller childController = childIterator.next(); + List childTransactions = new ArrayList<>(); + + for (ControllerHostedRouter childRouter : childRouters) { + childTransactions.addAll(childRouter.getBackstack()); + } + + Collections.sort(childTransactions, new Comparator() { + @Override + public int compare(RouterTransaction o1, RouterTransaction o2) { + return o2.transactionIndex - o1.transactionIndex; + } + }); + + for (RouterTransaction transaction : childTransactions) { + Controller childController = transaction.controller; + if (childController.isAttached() && childController.getRouter().handleBack()) { return true; } @@ -523,7 +593,7 @@ public boolean handleBack() { * * @param lifecycleListener The listener */ - public void addLifecycleListener(LifecycleListener lifecycleListener) { + public void addLifecycleListener(@NonNull LifecycleListener lifecycleListener) { if (!lifecycleListeners.contains(lifecycleListener)) { lifecycleListeners.add(lifecycleListener); } @@ -534,13 +604,14 @@ public void addLifecycleListener(LifecycleListener lifecycleListener) { * * @param lifecycleListener The listener to be removed */ - public void removeLifecycleListener(LifecycleListener lifecycleListener) { + public void removeLifecycleListener(@NonNull LifecycleListener lifecycleListener) { lifecycleListeners.remove(lifecycleListener); } /** * Returns this Controller's {@link RetainViewMode}. Defaults to {@link RetainViewMode#RELEASE_DETACH}. */ + @NonNull public RetainViewMode getRetainViewMode() { return retainViewMode; } @@ -549,8 +620,8 @@ public RetainViewMode getRetainViewMode() { * Sets this Controller's {@link RetainViewMode}, which will influence when its view will be released. * This is useful when a Controller's view hierarchy is expensive to tear down and rebuild. */ - public void setRetainViewMode(RetainViewMode retainViewMode) { - this.retainViewMode = retainViewMode; + public void setRetainViewMode(@NonNull RetainViewMode retainViewMode) { + this.retainViewMode = retainViewMode != null ? retainViewMode : RetainViewMode.RELEASE_DETACH; if (this.retainViewMode == RetainViewMode.RELEASE_DETACH && !attached) { removeViewReference(); } @@ -560,6 +631,7 @@ public void setRetainViewMode(RetainViewMode retainViewMode) { * Returns the {@link ControllerChangeHandler} that should be used for pushing this Controller, or null * if the handler from the {@link RouterTransaction} should be used instead. */ + @Nullable public final ControllerChangeHandler getOverriddenPushHandler() { return overriddenPushHandler; } @@ -568,7 +640,7 @@ public final ControllerChangeHandler getOverriddenPushHandler() { * Overrides the {@link ControllerChangeHandler} that should be used for pushing this Controller. If this is a * non-null value, it will be used instead of the handler from the {@link RouterTransaction}. */ - public void overridePushHandler(ControllerChangeHandler overriddenPushHandler) { + public void overridePushHandler(@Nullable ControllerChangeHandler overriddenPushHandler) { this.overriddenPushHandler = overriddenPushHandler; } @@ -576,6 +648,7 @@ public void overridePushHandler(ControllerChangeHandler overriddenPushHandler) { * Returns the {@link ControllerChangeHandler} that should be used for popping this Controller, or null * if the handler from the {@link RouterTransaction} should be used instead. */ + @Nullable public ControllerChangeHandler getOverriddenPopHandler() { return overriddenPopHandler; } @@ -584,7 +657,7 @@ public ControllerChangeHandler getOverriddenPopHandler() { * Overrides the {@link ControllerChangeHandler} that should be used for popping this Controller. If this is a * non-null value, it will be used instead of the handler from the {@link RouterTransaction}. */ - public void overridePopHandler(ControllerChangeHandler overriddenPopHandler) { + public void overridePopHandler(@Nullable ControllerChangeHandler overriddenPopHandler) { this.overriddenPopHandler = overriddenPopHandler; } @@ -628,7 +701,7 @@ public final void setOptionsMenuHidden(boolean optionsMenuHidden) { * @param menu The menu into which your options should be placed. * @param inflater The inflater that can be used to inflate your menu items. */ - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { } + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { } /** * Prepare the screen's options menu to be displayed. This is called directly before showing the @@ -636,7 +709,7 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { } * * @param menu The menu that will be displayed */ - public void onPrepareOptionsMenu(Menu menu) { } + public void onPrepareOptionsMenu(@NonNull Menu menu) { } /** * Called when an option menu item has been selected by the user. @@ -644,7 +717,7 @@ public void onPrepareOptionsMenu(Menu menu) { } * @param item The selected item. * @return True if this event has been consumed, false if it has not. */ - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(@NonNull MenuItem item) { return false; } @@ -696,31 +769,39 @@ final void executeWithRouter(@NonNull RouterRequiringFunc listener) { } } - final void activityStarted(Activity activity) { + final void activityStarted(@NonNull Activity activity) { + if (viewAttachHandler != null) { + viewAttachHandler.onActivityStarted(); + } + onActivityStarted(activity); } - final void activityResumed(Activity activity) { + final void activityResumed(@NonNull Activity activity) { if (!attached && view != null && viewIsAttached) { attach(view); } else if (attached) { needsAttach = false; + hasSavedViewState = false; } onActivityResumed(activity); } - final void activityPaused(Activity activity) { + final void activityPaused(@NonNull Activity activity) { onActivityPaused(activity); } - final void activityStopped(Activity activity) { + final void activityStopped(@NonNull Activity activity) { + if (viewAttachHandler != null) { + viewAttachHandler.onActivityStopped(); + } onActivityStopped(activity); } final void activityDestroyed(boolean isChangingConfigurations) { if (isChangingConfigurations) { - detach(view, true); + detach(view, true, false); } else { destroy(true); } @@ -754,14 +835,14 @@ private void attach(@NonNull View view) { } } - void detach(@NonNull View view, boolean forceViewRefRemoval) { + void detach(@NonNull View view, boolean forceViewRefRemoval, boolean blockViewRefRemoval) { if (!attachedToUnownedParent) { for (ControllerHostedRouter router : childRouters) { router.prepareForHostDetach(); } } - final boolean removeViewRef = forceViewRefRemoval || retainViewMode == RetainViewMode.RELEASE_DETACH || isBeingDestroyed; + final boolean removeViewRef = !blockViewRefRemoval && (forceViewRefRemoval || retainViewMode == RetainViewMode.RELEASE_DETACH || isBeingDestroyed); if (attached) { List listeners = new ArrayList<>(lifecycleListeners); @@ -800,7 +881,8 @@ private void removeViewReference() { onDestroyView(view); - view.removeOnAttachStateChangeListener(onAttachStateChangeListener); + viewAttachHandler.unregisterAttachListener(view); + viewAttachHandler = null; viewIsAttached = false; if (isBeingDestroyed) { @@ -825,7 +907,7 @@ private void removeViewReference() { final View inflate(@NonNull ViewGroup parent) { if (view != null && view.getParent() != null && view.getParent() != parent) { - detach(view, true); + detach(view, true, false); removeViewReference(); } @@ -836,6 +918,9 @@ final View inflate(@NonNull ViewGroup parent) { } view = onCreateView(LayoutInflater.from(parent.getContext()), parent); + if (view == parent) { + throw new IllegalStateException("Controller's onCreateView method returned the parent ViewGroup. Perhaps you forgot to pass false for LayoutInflater.inflate's attachToRoot parameter?"); + } listeners = new ArrayList<>(lifecycleListeners); for (LifecycleListener lifecycleListener : listeners) { @@ -844,33 +929,52 @@ final View inflate(@NonNull ViewGroup parent) { restoreViewState(view); - onAttachStateChangeListener = new OnAttachStateChangeListener() { + viewAttachHandler = new ViewAttachHandler(new ViewAttachListener() { @Override - public void onViewAttachedToWindow(View v) { - if (v == view) { - viewIsAttached = true; - viewWasDetached = false; - } - attach(v); + public void onAttached() { + viewIsAttached = true; + viewWasDetached = false; + attach(view); } @Override - public void onViewDetachedFromWindow(View v) { + public void onDetached(boolean fromActivityStop) { viewIsAttached = false; viewWasDetached = true; if (!isDetachFrozen) { - detach(v, false); + detach(view, false, fromActivityStop); } } - }; - view.addOnAttachStateChangeListener(onAttachStateChangeListener); + @Override + public void onViewDetachAfterStop() { + if (!isDetachFrozen) { + detach(view, false, false); + } + } + }); + viewAttachHandler.listenForAttach(view); + } else if (retainViewMode == RetainViewMode.RETAIN_DETACH) { + restoreChildControllerHosts(); } return view; } + private void restoreChildControllerHosts() { + for (ControllerHostedRouter childRouter : childRouters) { + if (!childRouter.hasHost()) { + View containerView = view.findViewById(childRouter.getHostId()); + + if (containerView != null && containerView instanceof ViewGroup) { + childRouter.setHost(this, (ViewGroup)containerView); + childRouter.rebindIfNeeded(); + } + } + } + } + private void performDestroy() { if (!destroyed) { List listeners = new ArrayList<>(lifecycleListeners); @@ -903,20 +1007,20 @@ private void destroy(boolean removeViews) { } for (ControllerHostedRouter childRouter : childRouters) { - childRouter.destroy(); + childRouter.destroy(false); } if (!attached) { removeViewReference(); } else if (removeViews) { - detach(view, false); + detach(view, true, false); } } private void saveViewState(@NonNull View view) { hasSavedViewState = true; - viewState = new Bundle(); + viewState = new Bundle(getClass().getClassLoader()); SparseArray hierarchyState = new SparseArray<>(); view.saveHierarchyState(hierarchyState); @@ -937,17 +1041,7 @@ private void restoreViewState(@NonNull View view) { view.restoreHierarchyState(viewState.getSparseParcelableArray(KEY_VIEW_STATE_HIERARCHY)); onRestoreViewState(view, viewState.getBundle(KEY_VIEW_STATE_BUNDLE)); - for (ControllerHostedRouter childRouter : childRouters) { - if (!childRouter.hasHost()) { - View containerView = view.findViewById(childRouter.getHostId()); - - if (containerView != null && containerView instanceof ViewGroup) { - childRouter.setHost(this, (ViewGroup)containerView); - monitorChildRouter(childRouter); - childRouter.rebindIfNeeded(); - } - } - } + restoreChildControllerHosts(); List listeners = new ArrayList<>(lifecycleListeners); for (LifecycleListener lifecycleListener : listeners) { @@ -1001,6 +1095,10 @@ final Bundle saveInstanceState() { private void restoreInstanceState(@NonNull Bundle savedInstanceState) { viewState = savedInstanceState.getBundle(KEY_VIEW_STATE); + if (viewState != null) { + viewState.setClassLoader(getClass().getClassLoader()); + } + instanceId = savedInstanceState.getString(KEY_INSTANCE_ID); targetInstanceId = savedInstanceState.getString(KEY_TARGET_INSTANCE_ID); requestedPermissions.addAll(savedInstanceState.getStringArrayList(KEY_REQUESTED_PERMISSIONS)); @@ -1013,7 +1111,6 @@ private void restoreInstanceState(@NonNull Bundle savedInstanceState) { for (Bundle childBundle : childBundles) { ControllerHostedRouter childRouter = new ControllerHostedRouter(); childRouter.restoreInstanceState(childBundle); - monitorChildRouter(childRouter); childRouters.add(childRouter); } @@ -1034,7 +1131,7 @@ private void performOnRestoreInstanceState() { } } - final void changeStarted(ControllerChangeHandler changeHandler, ControllerChangeType changeType) { + final void changeStarted(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { if (!changeType.isEnter) { for (ControllerHostedRouter router : childRouters) { router.setDetachFrozen(true); @@ -1049,7 +1146,7 @@ final void changeStarted(ControllerChangeHandler changeHandler, ControllerChange } } - final void changeEnded(ControllerChangeHandler changeHandler, ControllerChangeType changeType) { + final void changeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { if (!changeType.isEnter) { for (ControllerHostedRouter router : childRouters) { router.setDetachFrozen(false); @@ -1081,44 +1178,28 @@ final void setDetachFrozen(boolean frozen) { } if (!frozen && view != null && viewWasDetached) { - detach(view, false); + detach(view, false, false); } } } - final void createOptionsMenu(Menu menu, MenuInflater inflater) { + final void createOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { if (attached && hasOptionsMenu && !optionsMenuHidden) { onCreateOptionsMenu(menu, inflater); } } - final void prepareOptionsMenu(Menu menu) { + final void prepareOptionsMenu(@NonNull Menu menu) { if (attached && hasOptionsMenu && !optionsMenuHidden) { onPrepareOptionsMenu(menu); } } - final boolean optionsItemSelected(MenuItem item) { + final boolean optionsItemSelected(@NonNull MenuItem item) { return attached && hasOptionsMenu && !optionsMenuHidden && onOptionsItemSelected(item); } - private void monitorChildRouter(ControllerHostedRouter childRouter) { - childRouter.addChangeListener(childRouterChangeListener); - } - - private void onChildControllerPushed(Controller controller) { - if (!childBackstack.contains(controller)) { - childBackstack.add(controller); - controller.addLifecycleListener(new LifecycleListener() { - @Override - public void postDestroy(@NonNull Controller controller) { - childBackstack.remove(controller); - } - }); - } - } - - final void setParentController(Controller controller) { + final void setParentController(@Nullable Controller controller) { parentController = controller; } @@ -1129,7 +1210,8 @@ private void ensureRequiredConstructor() { } } - private static Constructor getDefaultConstructor(Constructor[] constructors) { + @Nullable + private static Constructor getDefaultConstructor(@NonNull Constructor[] constructors) { for (Constructor constructor : constructors) { if (constructor.getParameterTypes().length == 0) { return constructor; @@ -1138,7 +1220,8 @@ private static Constructor getDefaultConstructor(Constructor[] constructors) { return null; } - private static Constructor getBundleConstructor(Constructor[] constructors) { + @Nullable + private static Constructor getBundleConstructor(@NonNull Constructor[] constructors) { for (Constructor constructor : constructors) { if (constructor.getParameterTypes().length == 1 && constructor.getParameterTypes()[0] == Bundle.class) { return constructor; diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/ControllerChangeHandler.java b/conductor/src/main/java/com/bluelinelabs/conductor/ControllerChangeHandler.java index 1e137e5b..ba73cfd8 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/ControllerChangeHandler.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/ControllerChangeHandler.java @@ -2,6 +2,7 @@ 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; @@ -9,7 +10,6 @@ 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; @@ -27,17 +27,18 @@ public abstract class ControllerChangeHandler { private static final Map 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 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(); @@ -61,10 +62,11 @@ 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 + * @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, Controller newTop) { } + public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) { } /** * Will be called on change handlers that push a controller if the controller being pushed is @@ -72,6 +74,27 @@ public void onAbortPush(@NonNull ControllerChangeHandler newHandler, Controller */ 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()); @@ -83,10 +106,6 @@ final Bundle toBundle() { return bundle; } - final ControllerChangeHandler copy() { - return fromBundle(toBundle()); - } - private void ensureDefaultConstructor() { try { getClass().getConstructor(); @@ -95,7 +114,8 @@ private void ensureDefaultConstructor() { } } - 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); @@ -107,7 +127,7 @@ public static ControllerChangeHandler fromBundle(Bundle bundle) { } } - static boolean completePushImmediately(String controllerInstanceId) { + static boolean completePushImmediately(@NonNull String controllerInstanceId) { ControllerChangeHandler changeHandler = inProgressPushHandlers.get(controllerInstanceId); if (changeHandler != null) { changeHandler.completeImmediately(); @@ -117,7 +137,7 @@ static boolean completePushImmediately(String controllerInstanceId) { return false; } - public static void abortPush(Controller toAbort, Controller newController, ControllerChangeHandler newChangeHandler) { + static void abortPush(@NonNull Controller toAbort, @Nullable Controller newController, @NonNull ControllerChangeHandler newChangeHandler) { ControllerChangeHandler handlerForPush = inProgressPushHandlers.get(toAbort.getInstanceId()); if (handlerForPush != null) { handlerForPush.onAbortPush(newChangeHandler, newController); @@ -125,22 +145,34 @@ public static void abortPush(Controller toAbort, Controller newController, Contr } } - public static void executeChange(final Controller to, final Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler inHandler) { - executeChange(to, from, isPush, container, inHandler, new ArrayList()); - } + 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 listeners) { + if (isPush && to != null && to.isDestroyed()) { + throw new IllegalStateException("Trying to push a controller that has already been destroyed. (" + to.getClass().getSimpleName() + ")"); + } - public static void executeChange(final Controller to, final Controller from, final boolean isPush, final ViewGroup container, final ControllerChangeHandler inHandler, @NonNull final List listeners) { if (container != null) { - final ControllerChangeHandler handler = inHandler != null ? inHandler : new SimpleSwapChangeHandler(); + 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; @@ -175,7 +207,7 @@ public void onChangeCompleted() { } for (ControllerChangeListener listener : listeners) { - listener.onChangeCompleted(to, from, isPush, container, inHandler); + listener.onChangeCompleted(to, from, isPush, container, handler); } if (handler.forceRemoveViewOnPush && fromView != null) { @@ -204,24 +236,24 @@ public interface ControllerChangeListener { /** * Called when a {@link ControllerChangeHandler} has started changing {@link Controller}s * - * @param to The new Controller - * @param from The old Controller + * @param to The new Controller or {@code null} if no Controller is being transitioned to + * @param from The old Controller or {@code null} if there was no Controller before this transition * @param isPush True if this is a push operation, or false if it's a pop. * @param container The containing ViewGroup * @param handler The change handler being used. */ - 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. */ - 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); } /** diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/ControllerHostedRouter.java b/conductor/src/main/java/com/bluelinelabs/conductor/ControllerHostedRouter.java index 50b42848..752e48c6 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/ControllerHostedRouter.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/ControllerHostedRouter.java @@ -2,12 +2,16 @@ import android.app.Activity; import android.content.Intent; +import android.content.IntentSender; +import android.content.IntentSender.SendIntentException; import android.os.Bundle; import android.support.annotation.IdRes; import android.support.annotation.NonNull; +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; @@ -24,7 +28,7 @@ class ControllerHostedRouter extends Router { ControllerHostedRouter() { } - ControllerHostedRouter(int hostId, String tag) { + ControllerHostedRouter(int hostId, @Nullable String tag) { this.hostId = hostId; this.tag = tag; } @@ -50,12 +54,12 @@ final void removeHost() { final List controllersToDestroy = new ArrayList<>(destroyingControllers); for (Controller controller : controllersToDestroy) { if (controller.getView() != null) { - controller.detach(controller.getView(), true); + controller.detach(controller.getView(), true, false); } } for (RouterTransaction transaction : backstack) { if (transaction.controller.getView() != null) { - transaction.controller.detach(transaction.controller.getView(), true); + transaction.controller.detach(transaction.controller.getView(), true, false); } } @@ -71,25 +75,25 @@ final void setDetachFrozen(boolean frozen) { } @Override - void destroy() { + void destroy(boolean popViews) { setDetachFrozen(false); - super.destroy(); + super.destroy(popViews); } - @Override + @Override @Nullable public Activity getActivity() { return hostController != null ? hostController.getActivity() : null; } @Override - public void onActivityDestroyed(Activity activity) { + public void onActivityDestroyed(@NonNull Activity activity) { super.onActivityDestroyed(activity); removeHost(); } @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (hostController != null && hostController.getRouter() != null) { hostController.getRouter().onActivityResult(requestCode, resultCode, data); } @@ -103,42 +107,49 @@ public void invalidateOptionsMenu() { } @Override - void startActivity(Intent intent) { + void startActivity(@NonNull Intent intent) { if (hostController != null && hostController.getRouter() != null) { hostController.getRouter().startActivity(intent); } } @Override - void startActivityForResult(String instanceId, Intent intent, int requestCode) { + void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) { if (hostController != null && hostController.getRouter() != null) { hostController.getRouter().startActivityForResult(instanceId, intent, requestCode); } } @Override - void startActivityForResult(String instanceId, Intent intent, int requestCode, Bundle options) { + void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options) { if (hostController != null && hostController.getRouter() != null) { hostController.getRouter().startActivityForResult(instanceId, intent, requestCode, options); } } @Override - void registerForActivityResult(String instanceId, int requestCode) { + void startIntentSenderForResult(@NonNull String instanceId, @NonNull IntentSender intent, int requestCode, @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, @Nullable Bundle options) throws SendIntentException { + if (hostController != null && hostController.getRouter() != null) { + hostController.getRouter().startIntentSenderForResult(instanceId, intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options); + } + } + + @Override + void registerForActivityResult(@NonNull String instanceId, int requestCode) { if (hostController != null && hostController.getRouter() != null) { hostController.getRouter().registerForActivityResult(instanceId, requestCode); } } @Override - void unregisterForActivityResults(String instanceId) { + void unregisterForActivityResults(@NonNull String instanceId) { if (hostController != null && hostController.getRouter() != null) { hostController.getRouter().unregisterForActivityResults(instanceId); } } @Override - void requestPermissions(String instanceId, String[] permissions, int requestCode) { + void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) { if (hostController != null && hostController.getRouter() != null) { hostController.getRouter().requestPermissions(instanceId, permissions, requestCode); } @@ -150,7 +161,7 @@ boolean hasHost() { } @Override - public void saveInstanceState(Bundle outState) { + public void saveInstanceState(@NonNull Bundle outState) { super.saveInstanceState(outState); outState.putInt(KEY_HOST_ID, hostId); @@ -158,7 +169,7 @@ public void saveInstanceState(Bundle outState) { } @Override - public void restoreInstanceState(Bundle savedInstanceState) { + public void restoreInstanceState(@NonNull Bundle savedInstanceState) { super.restoreInstanceState(savedInstanceState); hostId = savedInstanceState.getInt(KEY_HOST_ID); @@ -166,7 +177,7 @@ public void restoreInstanceState(Bundle savedInstanceState) { } @Override - void setControllerRouter(Controller controller) { + void setControllerRouter(@NonNull Controller controller) { super.setControllerRouter(controller); controller.setParentController(hostController); } @@ -175,11 +186,12 @@ int getHostId() { return hostId; } + @Nullable String getTag() { return tag; } - @Override + @Override @NonNull List getSiblingRouters() { List list = new ArrayList<>(); list.addAll(hostController.getChildRouters()); @@ -187,7 +199,7 @@ List getSiblingRouters() { return list; } - @Override + @Override @NonNull Router getRootRouter() { if (hostController != null && hostController.getRouter() != null) { return hostController.getRouter().getRootRouter(); @@ -195,4 +207,9 @@ Router getRootRouter() { return this; } } + + @Override @Nullable + TransactionIndexer getTransactionIndexer() { + return getRootRouter().getTransactionIndexer(); + } } diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/RestoreViewOnCreateController.java b/conductor/src/main/java/com/bluelinelabs/conductor/RestoreViewOnCreateController.java index 1d9a2aa8..4669a2f0 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/RestoreViewOnCreateController.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/RestoreViewOnCreateController.java @@ -26,12 +26,11 @@ protected RestoreViewOnCreateController() { * * @param args Any arguments that need to be retained. */ - protected RestoreViewOnCreateController(Bundle args) { + protected RestoreViewOnCreateController(@Nullable Bundle args) { super(args); } - @NonNull - @Override + @Override @NonNull protected final View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { return onCreateView(inflater, container, viewState); } @@ -46,7 +45,7 @@ protected final View onCreateView(@NonNull LayoutInflater inflater, @NonNull Vie * This Controller's view should NOT be added in this method. It is simply passed in * so that valid LayoutParams can be used during inflation. * @param savedViewState A bundle for the view's state, which would have been created in {@link #onSaveViewState(View, Bundle)}, - * or null if no saved state exists. + * or {@code null} if no saved state exists. */ @NonNull protected abstract View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState); diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/Router.java b/conductor/src/main/java/com/bluelinelabs/conductor/Router.java index 77aa2742..4be2def3 100755 --- a/conductor/src/main/java/com/bluelinelabs/conductor/Router.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/Router.java @@ -2,8 +2,11 @@ import android.app.Activity; import android.content.Intent; +import android.content.IntentSender; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -14,6 +17,7 @@ import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener; import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler; import com.bluelinelabs.conductor.internal.NoOpControllerChangeHandler; +import com.bluelinelabs.conductor.internal.TransactionIndexer; import java.util.ArrayList; import java.util.Collections; @@ -39,8 +43,10 @@ public abstract class Router { ViewGroup container; /** - * Returns this Router's host Activity + * Returns this Router's host Activity or {@code null} if it has either not yet been attached to + * an Activity or if the Activity has been destroyed. */ + @Nullable public abstract Activity getActivity(); /** @@ -48,21 +54,21 @@ public abstract class Router { * of the controller that called startActivityForResult is not known. * * @param requestCode The Activity's onActivityResult requestCode - * @param resultCode The Activity's onActivityResult resultCode - * @param data The Activity's onActivityResult data + * @param resultCode The Activity's onActivityResult resultCode + * @param data The Activity's onActivityResult data */ - public abstract void onActivityResult(int requestCode, int resultCode, Intent data); + public abstract void onActivityResult(int requestCode, int resultCode, @Nullable Intent data); /** * This should be called by the host Activity when its onRequestPermissionsResult method is called. The call will be forwarded * 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); @@ -75,8 +81,10 @@ public void onRequestPermissionsResult(String instanceId, int requestCode, Strin * * @return Whether or not a back action was handled by the Router */ + @UiThread public boolean handleBack() { if (!backstack.isEmpty()) { + //noinspection ConstantConditions if (backstack.peek().controller.handleBack()) { return true; } else if (popCurrentController()) { @@ -92,8 +100,14 @@ public boolean handleBack() { * * @return Whether or not this Router still has controllers remaining on it after popping. */ + @SuppressWarnings("WeakerAccess") + @UiThread public boolean popCurrentController() { - return popController(backstack.peek().controller); + RouterTransaction transaction = backstack.peek(); + if (transaction == null) { + throw new IllegalStateException("Trying to pop the current controller when there are none on the backstack."); + } + return popController(transaction.controller); } /** @@ -102,7 +116,8 @@ public boolean popCurrentController() { * @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) { + @UiThread + public boolean popController(@NonNull Controller controller) { RouterTransaction topController = backstack.peek(); boolean poppingTopController = topController != null && topController.controller == controller; @@ -134,6 +149,7 @@ public boolean popController(Controller controller) { * @param transaction The transaction detailing what should be pushed, including the {@link Controller}, * and its push and pop {@link ControllerChangeHandler}, and its tag. */ + @UiThread public void pushController(@NonNull RouterTransaction transaction) { RouterTransaction from = backstack.peek(); pushToBackstack(transaction); @@ -146,6 +162,8 @@ public void pushController(@NonNull RouterTransaction transaction) { * @param transaction The transaction detailing what should be pushed, including the {@link Controller}, * and its push and pop {@link ControllerChangeHandler}, and its tag. */ + @SuppressWarnings("WeakerAccess") + @UiThread public void replaceTopController(@NonNull RouterTransaction transaction) { RouterTransaction topTransaction = backstack.peek(); if (!backstack.isEmpty()) { @@ -153,11 +171,14 @@ public void replaceTopController(@NonNull RouterTransaction transaction) { } final ControllerChangeHandler handler = transaction.pushChangeHandler(); - final boolean oldHandlerRemovedViews = topTransaction.pushChangeHandler() == null || topTransaction.pushChangeHandler().removesFromViewOnPush(); - final boolean newHandlerRemovesViews = handler == null || handler.removesFromViewOnPush(); - if (!oldHandlerRemovedViews && newHandlerRemovesViews) { - for (RouterTransaction visibleTransaction : getVisibleTransactions(backstack.iterator())) { - performControllerChange(null, visibleTransaction.controller, true, handler != null ? handler.copy() : new SimpleSwapChangeHandler()); + if (topTransaction != null) { + //noinspection ConstantConditions + 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); + } } } @@ -169,14 +190,26 @@ public void replaceTopController(@NonNull RouterTransaction transaction) { performControllerChange(transaction.pushChangeHandler(handler), topTransaction, true); } - void destroy() { + void destroy(boolean popViews) { popsLastView = true; - List poppedControllers = backstack.popAll(); + final List poppedControllers = backstack.popAll(); + trackDestroyingControllers(poppedControllers); - if (poppedControllers.size() > 0) { - trackDestroyingControllers(poppedControllers); + if (popViews && poppedControllers.size() > 0) { + RouterTransaction topTransaction = poppedControllers.get(0); + topTransaction.controller().addLifecycleListener(new LifecycleListener() { + @Override + public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { + if (changeType == ControllerChangeType.POP_EXIT) { + for (int i = poppedControllers.size() - 1; i > 0; i--) { + RouterTransaction transaction = poppedControllers.get(i); + performControllerChange(null, transaction, true, new SimpleSwapChangeHandler()); + } + } + } + }); - performControllerChange(null, poppedControllers.get(0).controller, false, poppedControllers.get(0).popChangeHandler()); + performControllerChange(null, topTransaction, false, topTransaction.popChangeHandler()); } } @@ -189,6 +222,7 @@ public int getContainerId() { * 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; @@ -199,6 +233,7 @@ public Router setPopsLastView(boolean popsLastView) { * * @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); } @@ -209,8 +244,11 @@ public boolean popToRoot() { * @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) { + @SuppressWarnings("WeakerAccess") + @UiThread + public boolean popToRoot(@Nullable ControllerChangeHandler changeHandler) { if (backstack.size() > 1) { + //noinspection ConstantConditions popToTransaction(backstack.root(), changeHandler); return true; } else { @@ -224,6 +262,7 @@ public boolean popToRoot(ControllerChangeHandler changeHandler) { * @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); } @@ -231,11 +270,13 @@ public boolean popToTag(@NonNull String tag) { /** * 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) { + @SuppressWarnings("WeakerAccess") + @UiThread + public boolean popToTag(@NonNull String tag, @Nullable ControllerChangeHandler changeHandler) { for (RouterTransaction transaction : backstack) { if (tag.equals(transaction.tag())) { popToTransaction(transaction, changeHandler); @@ -251,39 +292,20 @@ public boolean popToTag(@NonNull String tag, ControllerChangeHandler changeHandl * @param transaction The transaction detailing what should be pushed, including the {@link Controller}, * and its push and pop {@link ControllerChangeHandler}, and its tag. */ + @UiThread public void setRoot(@NonNull RouterTransaction transaction) { - ControllerChangeHandler newHandler = transaction.pushChangeHandler() != null ? transaction.pushChangeHandler() : new SimpleSwapChangeHandler(); - - List visibleTransactions = getVisibleTransactions(backstack.iterator()); - RouterTransaction rootTransaction = visibleTransactions.size() > 0 ? visibleTransactions.get(0) : null; - - removeAllExceptVisibleAndUnowned(); - - trackDestroyingControllers(backstack.popAll()); - - pushToBackstack(transaction); - - for (int i = visibleTransactions.size() - 1; i > 0; i--) { - if (visibleTransactions.get(i).controller.getView() == null) { - ControllerChangeHandler.abortPush(visibleTransactions.get(i).controller, transaction.controller, newHandler); - } else { - performControllerChange(null, visibleTransactions.get(i).controller, true, newHandler); - } - } - - if (rootTransaction != null && rootTransaction.controller.getView() == null) { - ControllerChangeHandler.abortPush(rootTransaction.controller, transaction.controller, newHandler); - } - performControllerChange(transaction, rootTransaction, true); + List transactions = Collections.singletonList(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) { + @Nullable + public Controller getControllerWithInstanceId(@NonNull String instanceId) { for (RouterTransaction transaction : backstack) { Controller controllerWithId = transaction.controller.findController(instanceId); if (controllerWithId != null) { @@ -294,12 +316,13 @@ public Controller getControllerWithInstanceId(String instanceId) { } /** - * 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) { + @Nullable + public Controller getControllerWithTag(@NonNull String tag) { for (RouterTransaction transaction : backstack) { if (tag.equals(transaction.tag())) { return transaction.controller; @@ -311,6 +334,7 @@ public Controller getControllerWithTag(String tag) { /** * Returns the number of {@link Controller}s currently in the backstack */ + @SuppressWarnings("WeakerAccess") public int getBackstackSize() { return backstack.size(); } @@ -318,6 +342,7 @@ public int getBackstackSize() { /** * Returns the current backstack, ordered from root to most recently pushed. */ + @NonNull public List getBackstack() { List list = new ArrayList<>(); Iterator backstackIterator = backstack.reverseIterator(); @@ -331,55 +356,61 @@ public List getBackstack() { * Sets the backstack, transitioning from the current top controller to the top of the new stack (if different) * using the passed {@link ControllerChangeHandler} * - * @param newBackstack The new backstack + * @param newBackstack The new backstack * @param changeHandler An optional change handler to be used to handle the root view of transition */ - public void setBackstack(@NonNull List newBackstack, ControllerChangeHandler changeHandler) { + @SuppressWarnings("WeakerAccess") + @UiThread + public void setBackstack(@NonNull List newBackstack, @Nullable ControllerChangeHandler changeHandler) { List oldVisibleTransactions = getVisibleTransactions(backstack.iterator()); + boolean newRootRequiresPush = !(newBackstack.size() > 0 && backstack.contains(newBackstack.get(0))); + removeAllExceptVisibleAndUnowned(); + ensureOrderedTransactionIndices(newBackstack); + + backstack.setBackstack(newBackstack); + for (RouterTransaction transaction : backstack) { + transaction.onAttachedToRouter(); + } if (newBackstack.size() > 0) { List reverseNewBackstack = new ArrayList<>(newBackstack); Collections.reverse(reverseNewBackstack); List newVisibleTransactions = getVisibleTransactions(reverseNewBackstack.iterator()); - boolean visibleTransactionsChanged = newVisibleTransactions.size() != oldVisibleTransactions.size(); - if (!visibleTransactionsChanged) { - for (int i = 0; i < oldVisibleTransactions.size(); i++) { - if (oldVisibleTransactions.get(i).controller != newVisibleTransactions.get(i).controller) { - visibleTransactionsChanged = true; - break; - } - } - } - + boolean visibleTransactionsChanged = !backstacksAreEqual(newVisibleTransactions, oldVisibleTransactions); if (visibleTransactionsChanged) { - ControllerChangeHandler handler = changeHandler != null ? changeHandler : new SimpleSwapChangeHandler(); - - Controller rootController = oldVisibleTransactions.size() > 0 ? oldVisibleTransactions.get(0).controller : null; - performControllerChange(newVisibleTransactions.get(0).controller, rootController, true, handler.copy()); + RouterTransaction rootTransaction = oldVisibleTransactions.size() > 0 ? oldVisibleTransactions.get(0) : null; + // Replace the old root with the new one + if (rootTransaction == null || rootTransaction.controller != newVisibleTransactions.get(0).controller) { + performControllerChange(newVisibleTransactions.get(0), rootTransaction, newRootRequiresPush, changeHandler); + } + // Remove all visible controllers that were previously on the backstack for (int i = oldVisibleTransactions.size() - 1; i > 0; i--) { RouterTransaction transaction = oldVisibleTransactions.get(i); - ControllerChangeHandler localHandler = handler.copy(); - localHandler.setForceRemoveViewOnPush(true); - performControllerChange(null, transaction.controller, true, localHandler); + if (!newVisibleTransactions.contains(transaction)) { + ControllerChangeHandler localHandler = changeHandler != null ? changeHandler.copy() : new SimpleSwapChangeHandler(); + localHandler.setForceRemoveViewOnPush(true); + performControllerChange(null, transaction, newRootRequiresPush, localHandler); + } } + // Add any new controllers to the backstack for (int i = 1; i < newVisibleTransactions.size(); i++) { RouterTransaction transaction = newVisibleTransactions.get(i); - handler = transaction.pushChangeHandler() != null ? transaction.pushChangeHandler().copy() : new SimpleSwapChangeHandler(); - performControllerChange(transaction.controller, newVisibleTransactions.get(i - 1).controller, true, handler.copy()); + if (!oldVisibleTransactions.contains(transaction)) { + performControllerChange(transaction, newVisibleTransactions.get(i - 1), true, transaction.pushChangeHandler()); + } } } - } - for (RouterTransaction transaction : backstack) { - transaction.onAttachedToRouter(); + // Ensure all new controllers have a valid router set + for (RouterTransaction transaction : newBackstack) { + transaction.controller.setRouter(this); + } } - - backstack.setBackstack(newBackstack); } /** @@ -394,7 +425,8 @@ public boolean hasRootController() { * * @param changeListener The listener */ - public void addChangeListener(ControllerChangeListener changeListener) { + @SuppressWarnings("WeakerAccess") + public void addChangeListener(@NonNull ControllerChangeListener changeListener) { if (!changeListeners.contains(changeListener)) { changeListeners.add(changeListener); } @@ -405,32 +437,34 @@ public void addChangeListener(ControllerChangeListener changeListener) { * * @param changeListener The listener to be removed */ - public void removeChangeListener(ControllerChangeListener changeListener) { + @SuppressWarnings("WeakerAccess") + public void removeChangeListener(@NonNull ControllerChangeListener changeListener) { changeListeners.remove(changeListener); } /** * Attaches this Router's existing backstack to its container if one exists. */ + @UiThread public void rebindIfNeeded() { Iterator 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) { + public final void onActivityStarted(@NonNull Activity activity) { for (RouterTransaction transaction : backstack) { transaction.controller.activityStarted(activity); @@ -440,7 +474,7 @@ public final void onActivityStarted(Activity activity) { } } - public final void onActivityResumed(Activity activity) { + public final void onActivityResumed(@NonNull Activity activity) { for (RouterTransaction transaction : backstack) { transaction.controller.activityResumed(activity); @@ -450,7 +484,7 @@ public final void onActivityResumed(Activity activity) { } } - public final void onActivityPaused(Activity activity) { + public final void onActivityPaused(@NonNull Activity activity) { for (RouterTransaction transaction : backstack) { transaction.controller.activityPaused(activity); @@ -460,7 +494,7 @@ public final void onActivityPaused(Activity activity) { } } - public final void onActivityStopped(Activity activity) { + public final void onActivityStopped(@NonNull Activity activity) { for (RouterTransaction transaction : backstack) { transaction.controller.activityStopped(activity); @@ -470,7 +504,7 @@ public final void onActivityStopped(Activity activity) { } } - public void onActivityDestroyed(Activity activity) { + public void onActivityDestroyed(@NonNull Activity activity) { prepareForContainerRemoval(); changeListeners.clear(); @@ -494,7 +528,7 @@ public void onActivityDestroyed(Activity activity) { container = null; } - public void prepareForHostDetach() { + void prepareForHostDetach() { for (RouterTransaction transaction : backstack) { if (ControllerChangeHandler.completePushImmediately(transaction.controller.getInstanceId())) { transaction.controller.setNeedsAttach(); @@ -503,7 +537,7 @@ public void prepareForHostDetach() { } } - public void saveInstanceState(Bundle outState) { + public void saveInstanceState(@NonNull Bundle outState) { prepareForHostDetach(); Bundle backstackState = new Bundle(); @@ -513,8 +547,9 @@ public void saveInstanceState(Bundle outState) { outState.putBoolean(KEY_POPS_LAST_VIEW, popsLastView); } - public void restoreInstanceState(Bundle savedInstanceState) { + public void restoreInstanceState(@NonNull Bundle savedInstanceState) { Bundle backstackBundle = savedInstanceState.getParcelable(KEY_BACKSTACK); + //noinspection ConstantConditions backstack.restoreInstanceState(backstackBundle); popsLastView = savedInstanceState.getBoolean(KEY_POPS_LAST_VIEW); @@ -524,7 +559,7 @@ public void restoreInstanceState(Bundle savedInstanceState) { } } - public final void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + public final void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { for (RouterTransaction transaction : backstack) { transaction.controller.createOptionsMenu(menu, inflater); @@ -534,7 +569,7 @@ public final void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { } } - public final void onPrepareOptionsMenu(Menu menu) { + public final void onPrepareOptionsMenu(@NonNull Menu menu) { for (RouterTransaction transaction : backstack) { transaction.controller.prepareOptionsMenu(menu); @@ -544,7 +579,7 @@ public final void onPrepareOptionsMenu(Menu menu) { } } - public final boolean onOptionsItemSelected(MenuItem item) { + public final boolean onOptionsItemSelected(@NonNull MenuItem item) { for (RouterTransaction transaction : backstack) { if (transaction.controller.optionsItemSelected(item)) { return true; @@ -559,17 +594,26 @@ public final boolean onOptionsItemSelected(MenuItem item) { return false; } - private void popToTransaction(@NonNull RouterTransaction transaction, ControllerChangeHandler changeHandler) { - RouterTransaction topTransaction = backstack.peek(); - List poppedTransactions = backstack.popTo(transaction); - trackDestroyingControllers(poppedTransactions); + private void popToTransaction(@NonNull RouterTransaction transaction, @Nullable ControllerChangeHandler changeHandler) { + if (backstack.size() > 0) { + RouterTransaction topTransaction = backstack.peek(); + + List updatedBackstack = new ArrayList<>(); + Iterator backstackIterator = backstack.reverseIterator(); + while (backstackIterator.hasNext()) { + RouterTransaction existingTransaction = backstackIterator.next(); + updatedBackstack.add(existingTransaction); + if (existingTransaction == transaction) { + break; + } + } - if (poppedTransactions.size() > 0) { if (changeHandler == null) { + //noinspection ConstantConditions changeHandler = topTransaction.popChangeHandler(); } - performControllerChange(backstack.peek().controller, topTransaction.controller, false, changeHandler); + setBackstack(updatedBackstack, changeHandler); } } @@ -579,6 +623,7 @@ void prepareForContainerRemoval() { } } + @NonNull final List getControllers() { List controllers = new ArrayList<>(); @@ -590,6 +635,7 @@ final List getControllers() { return controllers; } + @Nullable public final Boolean handleRequestedPermission(@NonNull String permission) { for (RouterTransaction transaction : backstack) { if (transaction.controller.didRequestPermission(permission)) { @@ -599,7 +645,7 @@ public final Boolean handleRequestedPermission(@NonNull String permission) { 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(); } @@ -611,32 +657,33 @@ private void performControllerChange(RouterTransaction to, RouterTransaction fro } else if (from != null) { 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) { - setControllerRouter(to); + to.ensureValidIndex(getTransactionIndexer()); + setControllerRouter(toController); } else if (backstack.size() == 0 && !popsLastView) { // We're emptying out the backstack. Views get weird if you transition them out, so just no-op it. The hosting // Activity should be handling this by finishing or at least hiding this view. changeHandler = new NoOpControllerChangeHandler(); } - ControllerChangeHandler.executeChange(to, from, isPush, container, changeHandler, changeListeners); + ControllerChangeHandler.executeChange(toController, fromController, isPush, container, changeHandler, changeListeners); } private void pushToBackstack(@NonNull RouterTransaction entry) { backstack.push(entry); } - private void trackDestroyingController(RouterTransaction transaction) { + private void trackDestroyingController(@NonNull RouterTransaction transaction) { if (!transaction.controller.isDestroyed()) { destroyingControllers.add(transaction.controller); @@ -649,7 +696,7 @@ public void postDestroy(@NonNull Controller controller) { } } - private void trackDestroyingControllers(List transactions) { + private void trackDestroyingControllers(@NonNull List transactions) { for (RouterTransaction transaction : transactions) { trackDestroyingController(transaction); } @@ -679,7 +726,23 @@ private void removeAllExceptVisibleAndUnowned() { } } - private void addRouterViewsToList(Router router, List list) { + // 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 backstack) { + List 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 list) { for (Controller controller : router.getControllers()) { if (controller.getView() != null) { list.add(controller.getView()); @@ -691,12 +754,13 @@ private void addRouterViewsToList(Router router, List list) { } } - private List getVisibleTransactions(Iterator backstackIterator) { + private List getVisibleTransactions(@NonNull Iterator backstackIterator) { List transactions = new ArrayList<>(); while (backstackIterator.hasNext()) { RouterTransaction transaction = backstackIterator.next(); transactions.add(transaction); + //noinspection ConstantConditions if (transaction.pushChangeHandler() == null || transaction.pushChangeHandler().removesFromViewOnPush()) { break; } @@ -706,18 +770,36 @@ private List getVisibleTransactions(Iterator lhs, List 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(Intent intent); - abstract void startActivityForResult(String instanceId, Intent intent, int requestCode); - abstract void startActivityForResult(String instanceId, Intent intent, int requestCode, Bundle options); - abstract void registerForActivityResult(String instanceId, int requestCode); - abstract void unregisterForActivityResults(String instanceId); - abstract void requestPermissions(String instanceId, String[] permissions, int requestCode); + abstract void startActivity(@NonNull Intent intent); + abstract void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode); + abstract void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options); + abstract void startIntentSenderForResult(@NonNull String instanceId, @NonNull IntentSender intent, int requestCode, @Nullable Intent fillInIntent, int flagsMask, + int flagsValues, int extraFlags, @Nullable Bundle options) throws IntentSender.SendIntentException; + abstract void registerForActivityResult(@NonNull String instanceId, int requestCode); + abstract void unregisterForActivityResults(@NonNull String instanceId); + abstract void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode); abstract boolean hasHost(); - abstract List getSiblingRouters(); - abstract Router getRootRouter(); + @NonNull abstract List getSiblingRouters(); + @NonNull abstract Router getRootRouter(); + @Nullable abstract TransactionIndexer getTransactionIndexer(); + } diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/RouterTransaction.java b/conductor/src/main/java/com/bluelinelabs/conductor/RouterTransaction.java index d45cbbf4..a75759e3 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/RouterTransaction.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/RouterTransaction.java @@ -2,16 +2,22 @@ import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.bluelinelabs.conductor.internal.TransactionIndexer; /** * Metadata used for adding {@link Controller}s to a {@link Router}. */ public class RouterTransaction { + private static int INVALID_INDEX = -1; + private static final String KEY_VIEW_CONTROLLER_BUNDLE = "RouterTransaction.controller.bundle"; private static final String KEY_PUSH_TRANSITION = "RouterTransaction.pushControllerChangeHandler"; private static final String KEY_POP_TRANSITION = "RouterTransaction.popControllerChangeHandler"; private static final String KEY_TAG = "RouterTransaction.tag"; + private static final String KEY_INDEX = "RouterTransaction.transactionIndex"; private static final String KEY_ATTACHED_TO_ROUTER = "RouterTransaction.attachedToRouter"; @NonNull final Controller controller; @@ -20,7 +26,9 @@ public class RouterTransaction { 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); } @@ -34,6 +42,7 @@ private RouterTransaction(@NonNull Controller controller) { 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); } @@ -41,15 +50,18 @@ void onAttachedToRouter() { attachedToRouter = true; } + @NonNull public Controller controller() { return controller; } + @Nullable public String tag() { return tag; } - public RouterTransaction tag(String tag) { + @NonNull + public RouterTransaction tag(@Nullable String tag) { if (!attachedToRouter) { this.tag = tag; return this; @@ -58,6 +70,7 @@ public RouterTransaction tag(String tag) { } } + @Nullable public ControllerChangeHandler pushChangeHandler() { ControllerChangeHandler handler = controller.getOverriddenPushHandler(); if (handler == null) { @@ -66,7 +79,8 @@ public ControllerChangeHandler pushChangeHandler() { return handler; } - public RouterTransaction pushChangeHandler(ControllerChangeHandler handler) { + @NonNull + public RouterTransaction pushChangeHandler(@Nullable ControllerChangeHandler handler) { if (!attachedToRouter) { pushControllerChangeHandler = handler; return this; @@ -75,6 +89,7 @@ public RouterTransaction pushChangeHandler(ControllerChangeHandler handler) { } } + @Nullable public ControllerChangeHandler popChangeHandler() { ControllerChangeHandler handler = controller.getOverriddenPopHandler(); if (handler == null) { @@ -83,7 +98,8 @@ public ControllerChangeHandler popChangeHandler() { return handler; } - public RouterTransaction popChangeHandler(ControllerChangeHandler handler) { + @NonNull + public RouterTransaction popChangeHandler(@Nullable ControllerChangeHandler handler) { if (!attachedToRouter) { popControllerChangeHandler = handler; return this; @@ -92,9 +108,19 @@ public RouterTransaction popChangeHandler(ControllerChangeHandler handler) { } } + void ensureValidIndex(@Nullable TransactionIndexer indexer) { + if (indexer == null) { + throw new RuntimeException(); + } + if (transactionIndex == INVALID_INDEX && indexer != null) { + transactionIndex = indexer.nextIndex(); + } + } + /** * Used to serialize this transaction into a Bundle */ + @NonNull public Bundle saveInstanceState() { Bundle bundle = new Bundle(); @@ -108,6 +134,7 @@ public Bundle saveInstanceState() { } bundle.putString(KEY_TAG, tag); + bundle.putInt(KEY_INDEX, transactionIndex); bundle.putBoolean(KEY_ATTACHED_TO_ROUTER, attachedToRouter); return bundle; diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/AnimatorChangeHandler.java b/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/AnimatorChangeHandler.java index 9bb7328f..979b1006 100755 --- a/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/AnimatorChangeHandler.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/AnimatorChangeHandler.java @@ -5,6 +5,7 @@ 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; @@ -61,7 +62,7 @@ public void restoreFromBundle(@NonNull Bundle bundle) { } @Override - public void onAbortPush(@NonNull ControllerChangeHandler newHandler, Controller newTop) { + public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) { super.onAbortPush(newHandler, newTop); canceled = true; @@ -76,7 +77,7 @@ public void completeImmediately() { needsImmediateCompletion = true; if (animator != null) { - animator.cancel(); + animator.end(); } } @@ -93,12 +94,13 @@ public boolean 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. @@ -106,7 +108,7 @@ public boolean removesFromViewOnPush() { 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; @@ -138,7 +140,7 @@ public boolean onPreDraw() { } } - private void complete(ControllerChangeCompletedListener changeListener, AnimatorListener animatorListener) { + private void complete(@NonNull ControllerChangeCompletedListener changeListener, @Nullable AnimatorListener animatorListener) { if (!completed) { completed = true; changeListener.onChangeCompleted(); @@ -148,11 +150,12 @@ private void complete(ControllerChangeCompletedListener changeListener, Animator if (animatorListener != null) { animator.removeListener(animatorListener); } + animator.cancel(); animator = null; } } - private void performAnimation(@NonNull final ViewGroup container, final View from, final View to, final boolean isPush, final boolean toAddedToContainer, @NonNull final ControllerChangeCompletedListener changeListener) { + 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; diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/AutoTransitionChangeHandler.java b/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/AutoTransitionChangeHandler.java index f19ff60a..6584b842 100755 --- a/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/AutoTransitionChangeHandler.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/AutoTransitionChangeHandler.java @@ -3,21 +3,28 @@ 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(); + } + } diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/FadeChangeHandler.java b/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/FadeChangeHandler.java index e91c23d3..e4368f8d 100755 --- a/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/FadeChangeHandler.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/FadeChangeHandler.java @@ -4,9 +4,12 @@ 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,14 +29,15 @@ public FadeChangeHandler(long duration, boolean removesFromViewOnPush) { 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) { + if (from != null && removesFromViewOnPush()) { animator.play(ObjectAnimator.ofFloat(from, View.ALPHA, 0)); } @@ -44,4 +48,10 @@ protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, protected void resetFromView(@NonNull View from) { from.setAlpha(1); } + + @Override @NonNull + public ControllerChangeHandler copy() { + return new FadeChangeHandler(getAnimationDuration(), removesFromViewOnPush()); + } + } diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/HorizontalChangeHandler.java b/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/HorizontalChangeHandler.java index e8886acf..86f8e225 100755 --- a/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/HorizontalChangeHandler.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/HorizontalChangeHandler.java @@ -4,9 +4,12 @@ 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 HorizontalChangeHandler(long duration, boolean removesFromViewOnPush) { 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 @@ protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, 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)); } } @@ -53,4 +58,10 @@ protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, protected void resetFromView(@NonNull View from) { from.setTranslationX(0); } + + @Override @NonNull + public ControllerChangeHandler copy() { + return new HorizontalChangeHandler(getAnimationDuration(), removesFromViewOnPush()); + } + } diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/SimpleSwapChangeHandler.java b/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/SimpleSwapChangeHandler.java index f8b12c5c..5d845511 100755 --- a/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/SimpleSwapChangeHandler.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/SimpleSwapChangeHandler.java @@ -2,6 +2,7 @@ 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; @@ -44,7 +45,7 @@ public void restoreFromBundle(@NonNull Bundle bundle) { } @Override - public void onAbortPush(@NonNull ControllerChangeHandler newHandler, Controller newTop) { + public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) { super.onAbortPush(newHandler, newTop); canceled = true; @@ -62,7 +63,7 @@ public void completeImmediately() { } @Override - public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) { + 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); @@ -89,7 +90,7 @@ public boolean removesFromViewOnPush() { } @Override - public void onViewAttachedToWindow(View v) { + public void onViewAttachedToWindow(@NonNull View v) { v.removeOnAttachStateChangeListener(this); if (changeListener != null) { @@ -100,5 +101,15 @@ public void onViewAttachedToWindow(View v) { } @Override - public void onViewDetachedFromWindow(View v) { } + public void onViewDetachedFromWindow(@NonNull View v) { } + + @Override @NonNull + public ControllerChangeHandler copy() { + return new SimpleSwapChangeHandler(removesFromViewOnPush()); + } + + @Override + public boolean isReusable() { + return true; + } } diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/TransitionChangeHandler.java b/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/TransitionChangeHandler.java index 630dac7a..5efce348 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/TransitionChangeHandler.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/TransitionChangeHandler.java @@ -3,6 +3,7 @@ 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; @@ -18,34 +19,38 @@ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public abstract class TransitionChangeHandler extends ControllerChangeHandler { + public interface OnTransitionPreparedListener { + public void onPrepared(); + } + 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 onAbortPush(@NonNull ControllerChangeHandler newHandler, Controller newTop) { + public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) { super.onAbortPush(newHandler, newTop); canceled = true; } @Override - public void performChange(@NonNull final ViewGroup container, View from, View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) { + public void performChange(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) { if (canceled) { changeListener.onChangeCompleted(); return; } - Transition transition = getTransition(container, from, to, isPush); + final Transition transition = getTransition(container, from, to, isPush); transition.addListener(new TransitionListener() { @Override public void onTransitionStart(Transition transition) { } @@ -67,8 +72,48 @@ public void onTransitionPause(Transition transition) { } public void onTransitionResume(Transition transition) { } }); - TransitionManager.beginDelayedTransition(container, transition); - if (from != null) { + prepareForTransition(container, from, to, transition, isPush, new OnTransitionPreparedListener() { + @Override + public void onPrepared() { + if (!canceled) { + TransitionManager.beginDelayedTransition(container, transition); + executePropertyChanges(container, from, to, transition, isPush); + } + } + }); + } + + @Override + public boolean removesFromViewOnPush() { + return true; + } + + /** + * Called before a transition occurs. This can be used to reorder views, set their transition names, etc. The transition will begin + * when {@code onTransitionPreparedListener} is called. + * + * @param container The container these Views are hosted in + * @param from The previous View in the container or {@code null} if there was no Controller before this transition + * @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to + * @param transition The transition that is being prepared for + * @param isPush True if this is a push transaction, false if it's a pop + */ + public void prepareForTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @NonNull Transition transition, boolean isPush, @NonNull OnTransitionPreparedListener onTransitionPreparedListener) { + onTransitionPreparedListener.onPrepared(); + } + + /** + * This should set all view properties needed for the transition to work properly. By default it removes the "from" view + * and adds the "to" view. + * + * @param container The container these Views are hosted in + * @param from The previous View in the container or {@code null} if there was no Controller before this transition + * @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to + * @param transition The transition with which {@code TransitionManager.beginDelayedTransition} has been called + * @param isPush True if this is a push transaction, false if it's a pop + */ + public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @NonNull Transition transition, boolean isPush) { + if (from != null && (removesFromViewOnPush() || !isPush) && from.getParent() == container) { container.removeView(from); } if (to != null && to.getParent() == null) { @@ -76,8 +121,4 @@ public void onTransitionResume(Transition transition) { } } } - @Override - public final boolean removesFromViewOnPush() { - return true; - } } diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/TransitionChangeHandlerCompat.java b/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/TransitionChangeHandlerCompat.java index 39699dfe..07338e97 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/TransitionChangeHandlerCompat.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/TransitionChangeHandlerCompat.java @@ -3,6 +3,7 @@ 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 @@ */ public class TransitionChangeHandlerCompat extends ControllerChangeHandler { - private static final String KEY_TRANSITION_HANDLER_CLASS = "TransitionChangeHandlerCompat.transitionChangeHandler.class"; - private static final String KEY_FALLBACK_HANDLER_CLASS = "TransitionChangeHandlerCompat.fallbackChangeHandler.class"; - private static final String KEY_TRANSITION_HANDLER_STATE = "TransitionChangeHandlerCompat.transitionChangeHandler.state"; - private static final String KEY_FALLBACK_HANDLER_STATE = "TransitionChangeHandlerCompat.fallbackChangeHandler.state"; + private static final String KEY_CHANGE_HANDLER_CLASS = "TransitionChangeHandlerCompat.changeHandler.class"; + private static final String KEY_HANDLER_STATE = "TransitionChangeHandlerCompat.changeHandler.state"; - private TransitionChangeHandler transitionChangeHandler; - private ControllerChangeHandler fallbackChangeHandler; + private ControllerChangeHandler changeHandler; public TransitionChangeHandlerCompat() { } @@ -32,57 +30,52 @@ public TransitionChangeHandlerCompat() { } * @param transitionChangeHandler The change handler that will be used on API 21 and above * @param fallbackChangeHandler The change handler that will be used on APIs below 21 */ - public TransitionChangeHandlerCompat(TransitionChangeHandler transitionChangeHandler, ControllerChangeHandler fallbackChangeHandler) { - this.transitionChangeHandler = transitionChangeHandler; - this.fallbackChangeHandler = fallbackChangeHandler; - } - - @Override - public void performChange(@NonNull final ViewGroup container, View from, View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) { + public TransitionChangeHandlerCompat(@NonNull TransitionChangeHandler transitionChangeHandler, @NonNull ControllerChangeHandler fallbackChangeHandler) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - transitionChangeHandler.performChange(container, from, to, isPush, changeListener); + changeHandler = transitionChangeHandler; } else { - fallbackChangeHandler.performChange(container, from, to, isPush, changeListener); + changeHandler = fallbackChangeHandler; } } + @Override + public void performChange(@NonNull final ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) { + changeHandler.performChange(container, from, to, isPush, changeListener); + } + @Override public void saveToBundle(@NonNull Bundle bundle) { super.saveToBundle(bundle); - bundle.putString(KEY_TRANSITION_HANDLER_CLASS, transitionChangeHandler.getClass().getName()); - bundle.putString(KEY_FALLBACK_HANDLER_CLASS, fallbackChangeHandler.getClass().getName()); + bundle.putString(KEY_CHANGE_HANDLER_CLASS, changeHandler.getClass().getName()); - Bundle transitionBundle = new Bundle(); - transitionChangeHandler.saveToBundle(transitionBundle); - bundle.putBundle(KEY_TRANSITION_HANDLER_STATE, transitionBundle); - - Bundle fallbackBundle = new Bundle(); - fallbackChangeHandler.saveToBundle(fallbackBundle); - bundle.putBundle(KEY_FALLBACK_HANDLER_STATE, fallbackBundle); + Bundle stateBundle = new Bundle(); + changeHandler.saveToBundle(stateBundle); + bundle.putBundle(KEY_HANDLER_STATE, stateBundle); } @Override public void restoreFromBundle(@NonNull Bundle bundle) { super.restoreFromBundle(bundle); - String transitionClassName = bundle.getString(KEY_TRANSITION_HANDLER_CLASS); - transitionChangeHandler = ClassUtils.newInstance(transitionClassName); - //noinspection ConstantConditions - transitionChangeHandler.restoreFromBundle(bundle.getBundle(KEY_TRANSITION_HANDLER_STATE)); - - String fallbackClassName = bundle.getString(KEY_FALLBACK_HANDLER_CLASS); - fallbackChangeHandler = ClassUtils.newInstance(fallbackClassName); + String className = bundle.getString(KEY_CHANGE_HANDLER_CLASS); + changeHandler = ClassUtils.newInstance(className); //noinspection ConstantConditions - fallbackChangeHandler.restoreFromBundle(bundle.getBundle(KEY_FALLBACK_HANDLER_STATE)); + changeHandler.restoreFromBundle(bundle.getBundle(KEY_HANDLER_STATE)); } @Override public boolean removesFromViewOnPush() { + return changeHandler.removesFromViewOnPush(); + } + + @Override @NonNull + public ControllerChangeHandler copy() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - return transitionChangeHandler.removesFromViewOnPush(); + return new TransitionChangeHandlerCompat((TransitionChangeHandler)changeHandler.copy(), null); } else { - return fallbackChangeHandler.removesFromViewOnPush(); + return new TransitionChangeHandlerCompat(null, changeHandler.copy()); } } + } diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/VerticalChangeHandler.java b/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/VerticalChangeHandler.java index 4030cc4b..fd937c2e 100755 --- a/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/VerticalChangeHandler.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/changehandler/VerticalChangeHandler.java @@ -4,9 +4,12 @@ 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 VerticalChangeHandler(long duration, boolean removesFromViewOnPush) { 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 viewAnimators = new ArrayList<>(); @@ -48,4 +51,9 @@ protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, @Override protected void resetFromView(@NonNull View from) { } + @Override @NonNull + public ControllerChangeHandler copy() { + return new VerticalChangeHandler(getAnimationDuration(), removesFromViewOnPush()); + } + } diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/internal/ClassUtils.java b/conductor/src/main/java/com/bluelinelabs/conductor/internal/ClassUtils.java index 34ee53f6..de6dccc3 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/internal/ClassUtils.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/internal/ClassUtils.java @@ -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 Class classForName(String className) { - return classForName(className, true); - } - - @SuppressWarnings("unchecked") - public static Class classForName(String className, boolean allowEmptyName) { + @Nullable @SuppressWarnings("unchecked") + public static Class classForName(@NonNull String className, boolean allowEmptyName) { if (allowEmptyName && TextUtils.isEmpty(className)) { return null; } @@ -22,10 +19,10 @@ public static Class classForName(String className, boolean allo } } - @SuppressWarnings("unchecked") - public static T newInstance(String className) { + @Nullable @SuppressWarnings("unchecked") + public static T newInstance(@NonNull String className) { try { - Class cls = classForName(className); + Class 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()); diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/internal/LifecycleHandler.java b/conductor/src/main/java/com/bluelinelabs/conductor/internal/LifecycleHandler.java index 1c23da7c..7f02d9a8 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/internal/LifecycleHandler.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/internal/LifecycleHandler.java @@ -6,11 +6,13 @@ import android.app.Fragment; import android.content.Context; import android.content.Intent; +import android.content.IntentSender; import android.os.Build; import android.os.Bundle; import android.os.Parcel; 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; @@ -50,7 +52,8 @@ public LifecycleHandler() { 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); @@ -58,7 +61,8 @@ private static LifecycleHandler findInActivity(Activity activity) { 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(); @@ -68,7 +72,8 @@ public static LifecycleHandler install(Activity activity) { return lifecycleHandler; } - public Router getRouter(ViewGroup container, Bundle savedInstanceState) { + @NonNull + public Router getRouter(@NonNull ViewGroup container, @Nullable Bundle savedInstanceState) { ActivityHostedRouter router = routerMap.get(getRouterHashKey(container)); if (router == null) { router = new ActivityHostedRouter(); @@ -88,19 +93,21 @@ public Router getRouter(ViewGroup container, Bundle savedInstanceState) { return router; } + @NonNull public List getRouters() { return new ArrayList(routerMap.values()); } + @Nullable public Activity getLifecycleActivity() { return activity; } - private static int getRouterHashKey(ViewGroup viewGroup) { + private static int getRouterHashKey(@NonNull ViewGroup viewGroup) { return viewGroup.getId(); } - private void registerActivityListener(Activity activity) { + private void registerActivityListener(@NonNull Activity activity) { this.activity = activity; if (!hasRegisteredCallbacks) { @@ -255,11 +262,11 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } - public void registerForActivityResult(String instanceId, int requestCode) { + public void registerForActivityResult(@NonNull String instanceId, int requestCode) { activityRequestMap.put(requestCode, instanceId); } - public void unregisterForActivityResults(String instanceId) { + public void unregisterForActivityResults(@NonNull String instanceId) { for (int i = activityRequestMap.size() - 1; i >= 0; i--) { if (instanceId.equals(activityRequestMap.get(activityRequestMap.keyAt(i)))) { activityRequestMap.removeAt(i); @@ -267,18 +274,26 @@ public void unregisterForActivityResults(String instanceId) { } } - public void startActivityForResult(String instanceId, Intent intent, int requestCode) { + public void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) { registerForActivityResult(instanceId, requestCode); startActivityForResult(intent, requestCode); } - public void startActivityForResult(String instanceId, Intent intent, int requestCode, Bundle options) { + public void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options) { registerForActivityResult(instanceId, requestCode); startActivityForResult(intent, requestCode, options); } + @TargetApi(Build.VERSION_CODES.N) + public void startIntentSenderForResult(@NonNull String instanceId, @NonNull IntentSender intent, int requestCode, + @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, + @Nullable Bundle options) throws IntentSender.SendIntentException { + registerForActivityResult(instanceId, requestCode); + startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options); + } + @TargetApi(Build.VERSION_CODES.M) - public void requestPermissions(String instanceId, String[] permissions, int requestCode) { + public void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) { if (attached) { permissionRequestMap.put(requestCode, instanceId); requestPermissions(permissions, requestCode); @@ -349,7 +364,7 @@ private static class PendingPermissionRequest implements Parcelable { final String[] permissions; final int requestCode; - public PendingPermissionRequest(String instanceId, String[] permissions, int requestCode) { + PendingPermissionRequest(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) { this.instanceId = instanceId; this.permissions = permissions; this.requestCode = requestCode; @@ -361,10 +376,12 @@ private PendingPermissionRequest(Parcel in) { requestCode = in.readInt(); } + @Override public int describeContents() { return 0; } + @Override public void writeToParcel(Parcel out, int flags) { out.writeString(instanceId); out.writeStringArray(permissions); @@ -372,10 +389,12 @@ public void writeToParcel(Parcel out, int flags) { } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override public PendingPermissionRequest createFromParcel(Parcel in) { return new PendingPermissionRequest(in); } + @Override public PendingPermissionRequest[] newArray(int size) { return new PendingPermissionRequest[size]; } diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/internal/NoOpControllerChangeHandler.java b/conductor/src/main/java/com/bluelinelabs/conductor/internal/NoOpControllerChangeHandler.java index bc438b25..838174f1 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/internal/NoOpControllerChangeHandler.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/internal/NoOpControllerChangeHandler.java @@ -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 @@ 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; + } } \ No newline at end of file diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/internal/StringSparseArrayParceler.java b/conductor/src/main/java/com/bluelinelabs/conductor/internal/StringSparseArrayParceler.java index 6681df3e..9735f655 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/internal/StringSparseArrayParceler.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/internal/StringSparseArrayParceler.java @@ -2,17 +2,18 @@ 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 stringSparseArray; - public StringSparseArrayParceler(SparseArray stringSparseArray) { + public StringSparseArrayParceler(@NonNull SparseArray stringSparseArray) { this.stringSparseArray = stringSparseArray; } - private StringSparseArrayParceler(Parcel in) { + private StringSparseArrayParceler(@NonNull Parcel in) { stringSparseArray = new SparseArray<>(); final int size = in.readInt(); @@ -22,14 +23,17 @@ private StringSparseArrayParceler(Parcel in) { } } + @NonNull public SparseArray getStringSparseArray() { return stringSparseArray; } + @Override public int describeContents() { return 0; } + @Override public void writeToParcel(Parcel out, int flags) { final int size = stringSparseArray.size(); @@ -44,10 +48,12 @@ public void writeToParcel(Parcel out, int flags) { } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override public StringSparseArrayParceler createFromParcel(Parcel in) { return new StringSparseArrayParceler(in); } + @Override public StringSparseArrayParceler[] newArray(int size) { return new StringSparseArrayParceler[size]; } diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/internal/TransactionIndexer.java b/conductor/src/main/java/com/bluelinelabs/conductor/internal/TransactionIndexer.java new file mode 100644 index 00000000..5d507f7b --- /dev/null +++ b/conductor/src/main/java/com/bluelinelabs/conductor/internal/TransactionIndexer.java @@ -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); + } + +} diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/internal/ViewAttachHandler.java b/conductor/src/main/java/com/bluelinelabs/conductor/internal/ViewAttachHandler.java new file mode 100644 index 00000000..8e27662d --- /dev/null +++ b/conductor/src/main/java/com/bluelinelabs/conductor/internal/ViewAttachHandler.java @@ -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; + } + } + +} diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/BackstackTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/BackstackTests.java index 8d514227..9783f99b 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/BackstackTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/BackstackTests.java @@ -1,9 +1,12 @@ 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 backstack; @@ -15,20 +18,20 @@ public void setup() { @Test public void testPush() { - Assert.assertEquals(0, backstack.size()); + assertEquals(0, backstack.size()); backstack.push(RouterTransaction.with(new TestController())); - Assert.assertEquals(1, backstack.size()); + assertEquals(1, backstack.size()); } @Test public void testPop() { backstack.push(RouterTransaction.with(new TestController())); backstack.push(RouterTransaction.with(new TestController())); - Assert.assertEquals(2, backstack.size()); + assertEquals(2, backstack.size()); backstack.pop(); - Assert.assertEquals(1, backstack.size()); + assertEquals(1, backstack.size()); backstack.pop(); - Assert.assertEquals(0, backstack.size()); + assertEquals(0, backstack.size()); } @Test @@ -37,13 +40,13 @@ public void testPeek() { RouterTransaction transaction2 = RouterTransaction.with(new TestController()); backstack.push(transaction1); - Assert.assertEquals(transaction1, backstack.peek()); + assertEquals(transaction1, backstack.peek()); backstack.push(transaction2); - Assert.assertEquals(transaction2, backstack.peek()); + assertEquals(transaction2, backstack.peek()); backstack.pop(); - Assert.assertEquals(transaction1, backstack.peek()); + assertEquals(transaction1, backstack.peek()); } @Test @@ -56,11 +59,11 @@ public void testPopTo() { backstack.push(transaction2); backstack.push(transaction3); - Assert.assertEquals(3, backstack.size()); + assertEquals(3, backstack.size()); backstack.popTo(transaction1); - Assert.assertEquals(1, backstack.size()); - Assert.assertEquals(transaction1, backstack.peek()); + assertEquals(1, backstack.size()); + assertEquals(transaction1, backstack.peek()); } } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerChangeHandlerTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerChangeHandlerTests.java index 084cb91d..2009cf8a 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerChangeHandlerTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerChangeHandlerTests.java @@ -2,10 +2,12 @@ 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 @@ -21,17 +23,17 @@ public void testSaveRestore() { 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()); } } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleActivityReferenceTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleActivityReferenceTests.java new file mode 100644 index 00000000..cc97192f --- /dev/null +++ b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleActivityReferenceTests.java @@ -0,0 +1,269 @@ +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.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.Arrays; +import java.util.Collections; +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(Collections.singletonList(true), listener.changeEndReferences); + assertEquals(Collections.singletonList(true), listener.postCreateViewReferences); + assertEquals(Collections.singletonList(true), listener.postAttachReferences); + assertEquals(Collections.emptyList(), listener.postDetachReferences); + assertEquals(Collections.emptyList(), listener.postDestroyViewReferences); + assertEquals(Collections.emptyList(), listener.postDestroyReferences); + } + + @Test + 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(Collections.singletonList(true), listener.changeEndReferences); + assertEquals(Collections.singletonList(true), listener.postCreateViewReferences); + assertEquals(Collections.singletonList(true), listener.postAttachReferences); + assertEquals(Collections.emptyList(), listener.postDetachReferences); + assertEquals(Collections.emptyList(), listener.postDestroyViewReferences); + assertEquals(Collections.emptyList(), listener.postDestroyReferences); + } + + @Test + 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(Arrays.asList(true, true), listener.changeEndReferences); + assertEquals(Collections.singletonList(true), listener.postCreateViewReferences); + assertEquals(Collections.singletonList(true), listener.postAttachReferences); + assertEquals(Collections.singletonList(true), listener.postDetachReferences); + assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences); + assertEquals(Collections.singletonList(true), listener.postDestroyReferences); + } + + @Test + 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(Arrays.asList(true, true), listener.changeEndReferences); + assertEquals(Collections.singletonList(true), listener.postCreateViewReferences); + assertEquals(Collections.singletonList(true), listener.postAttachReferences); + assertEquals(Collections.singletonList(true), listener.postDetachReferences); + assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences); + assertEquals(Collections.singletonList(true), listener.postDestroyReferences); + } + + @Test + 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(Collections.singletonList(true), listener.changeEndReferences); + assertEquals(Collections.singletonList(true), listener.postCreateViewReferences); + assertEquals(Collections.singletonList(true), listener.postAttachReferences); + assertEquals(Collections.singletonList(true), listener.postDetachReferences); + assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences); + assertEquals(Collections.singletonList(true), listener.postDestroyReferences); + } + + @Test + 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(Collections.singletonList(true), listener.changeEndReferences); + assertEquals(Collections.singletonList(true), listener.postCreateViewReferences); + assertEquals(Collections.singletonList(true), listener.postAttachReferences); + assertEquals(Collections.singletonList(true), listener.postDetachReferences); + assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences); + assertEquals(Collections.singletonList(true), listener.postDestroyReferences); + } + + @Test + 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(Collections.singletonList(true), listener.changeEndReferences); + assertEquals(Collections.singletonList(true), listener.postCreateViewReferences); + assertEquals(Collections.singletonList(true), listener.postAttachReferences); + assertEquals(Collections.singletonList(true), listener.postDetachReferences); + assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences); + assertEquals(Collections.singletonList(true), listener.postDestroyReferences); + } + + static class ActivityReferencingLifecycleListener extends Controller.LifecycleListener { + final List changeEndReferences = new ArrayList<>(); + final List postCreateViewReferences = new ArrayList<>(); + final List postAttachReferences = new ArrayList<>(); + final List postDetachReferences = new ArrayList<>(); + final List postDestroyViewReferences = new ArrayList<>(); + final List 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); + } + } + +} diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleCallbacksTests.java similarity index 57% rename from conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleTests.java rename to conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleCallbacksTests.java index 8dbe5940..048ecff2 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleCallbacksTests.java @@ -6,18 +6,26 @@ import android.view.ViewGroup; import com.bluelinelabs.conductor.Controller.LifecycleListener; -import com.bluelinelabs.conductor.MockChangeHandler.ChangeHandlerListener; +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.Assert; 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 ControllerLifecycleTests { +public class ControllerLifecycleCallbacksTests { private Router router; @@ -60,8 +68,39 @@ public void testNormalLifecycle() { router.popCurrentController(); - Assert.assertNull(controller.getView()); + 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); } @@ -83,7 +122,7 @@ public void testLifecycleWithActivityDestroy() { assertCalls(expectedCallState, controller); - activityProxy.stop(); + activityProxy.stop(true); expectedCallState.saveViewStateCalls++; expectedCallState.detachCalls++; @@ -122,7 +161,7 @@ public void testLifecycleWithActivityConfigurationChange() { activityProxy.pause(); assertCalls(expectedCallState, controller); - activityProxy.stop(); + activityProxy.stop(true); expectedCallState.detachCalls++; expectedCallState.destroyViewCalls++; assertCalls(expectedCallState, controller); @@ -194,212 +233,212 @@ public void testLifecycleCallOrder() { @Override public void preCreateView(@NonNull Controller controller) { callState.createViewCalls++; - Assert.assertEquals(1, callState.createViewCalls); - Assert.assertEquals(0, testController.currentCallState.createViewCalls); + assertEquals(1, callState.createViewCalls); + assertEquals(0, testController.currentCallState.createViewCalls); - Assert.assertEquals(0, callState.attachCalls); - Assert.assertEquals(0, testController.currentCallState.attachCalls); + assertEquals(0, callState.attachCalls); + assertEquals(0, testController.currentCallState.attachCalls); - Assert.assertEquals(0, callState.detachCalls); - Assert.assertEquals(0, testController.currentCallState.detachCalls); + assertEquals(0, callState.detachCalls); + assertEquals(0, testController.currentCallState.detachCalls); - Assert.assertEquals(0, callState.destroyViewCalls); - Assert.assertEquals(0, testController.currentCallState.destroyViewCalls); + assertEquals(0, callState.destroyViewCalls); + assertEquals(0, testController.currentCallState.destroyViewCalls); - Assert.assertEquals(0, callState.destroyCalls); - Assert.assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, callState.destroyCalls); + 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); + assertEquals(2, callState.createViewCalls); + assertEquals(1, testController.currentCallState.createViewCalls); - Assert.assertEquals(0, callState.attachCalls); - Assert.assertEquals(0, testController.currentCallState.attachCalls); + assertEquals(0, callState.attachCalls); + assertEquals(0, testController.currentCallState.attachCalls); - Assert.assertEquals(0, callState.detachCalls); - Assert.assertEquals(0, testController.currentCallState.detachCalls); + assertEquals(0, callState.detachCalls); + assertEquals(0, testController.currentCallState.detachCalls); - Assert.assertEquals(0, callState.destroyViewCalls); - Assert.assertEquals(0, testController.currentCallState.destroyViewCalls); + assertEquals(0, callState.destroyViewCalls); + assertEquals(0, testController.currentCallState.destroyViewCalls); - Assert.assertEquals(0, callState.destroyCalls); - Assert.assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, callState.destroyCalls); + 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); + assertEquals(2, callState.createViewCalls); + assertEquals(1, testController.currentCallState.createViewCalls); - Assert.assertEquals(1, callState.attachCalls); - Assert.assertEquals(0, testController.currentCallState.attachCalls); + assertEquals(1, callState.attachCalls); + assertEquals(0, testController.currentCallState.attachCalls); - Assert.assertEquals(0, callState.detachCalls); - Assert.assertEquals(0, testController.currentCallState.detachCalls); + assertEquals(0, callState.detachCalls); + assertEquals(0, testController.currentCallState.detachCalls); - Assert.assertEquals(0, callState.destroyViewCalls); - Assert.assertEquals(0, testController.currentCallState.destroyViewCalls); + assertEquals(0, callState.destroyViewCalls); + assertEquals(0, testController.currentCallState.destroyViewCalls); - Assert.assertEquals(0, callState.destroyCalls); - Assert.assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, callState.destroyCalls); + 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); + assertEquals(2, callState.createViewCalls); + assertEquals(1, testController.currentCallState.createViewCalls); - Assert.assertEquals(2, callState.attachCalls); - Assert.assertEquals(1, testController.currentCallState.attachCalls); + assertEquals(2, callState.attachCalls); + assertEquals(1, testController.currentCallState.attachCalls); - Assert.assertEquals(0, callState.detachCalls); - Assert.assertEquals(0, testController.currentCallState.detachCalls); + assertEquals(0, callState.detachCalls); + assertEquals(0, testController.currentCallState.detachCalls); - Assert.assertEquals(0, callState.destroyViewCalls); - Assert.assertEquals(0, testController.currentCallState.destroyViewCalls); + assertEquals(0, callState.destroyViewCalls); + assertEquals(0, testController.currentCallState.destroyViewCalls); - Assert.assertEquals(0, callState.destroyCalls); - Assert.assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, callState.destroyCalls); + 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); + assertEquals(2, callState.createViewCalls); + assertEquals(1, testController.currentCallState.createViewCalls); - Assert.assertEquals(2, callState.attachCalls); - Assert.assertEquals(1, testController.currentCallState.attachCalls); + assertEquals(2, callState.attachCalls); + assertEquals(1, testController.currentCallState.attachCalls); - Assert.assertEquals(1, callState.detachCalls); - Assert.assertEquals(0, testController.currentCallState.detachCalls); + assertEquals(1, callState.detachCalls); + assertEquals(0, testController.currentCallState.detachCalls); - Assert.assertEquals(0, callState.destroyViewCalls); - Assert.assertEquals(0, testController.currentCallState.destroyViewCalls); + assertEquals(0, callState.destroyViewCalls); + assertEquals(0, testController.currentCallState.destroyViewCalls); - Assert.assertEquals(0, callState.destroyCalls); - Assert.assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, callState.destroyCalls); + 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); + assertEquals(2, callState.createViewCalls); + assertEquals(1, testController.currentCallState.createViewCalls); - Assert.assertEquals(2, callState.attachCalls); - Assert.assertEquals(1, testController.currentCallState.attachCalls); + assertEquals(2, callState.attachCalls); + assertEquals(1, testController.currentCallState.attachCalls); - Assert.assertEquals(2, callState.detachCalls); - Assert.assertEquals(1, testController.currentCallState.detachCalls); + assertEquals(2, callState.detachCalls); + assertEquals(1, testController.currentCallState.detachCalls); - Assert.assertEquals(0, callState.destroyViewCalls); - Assert.assertEquals(0, testController.currentCallState.destroyViewCalls); + assertEquals(0, callState.destroyViewCalls); + assertEquals(0, testController.currentCallState.destroyViewCalls); - Assert.assertEquals(0, callState.destroyCalls); - Assert.assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, callState.destroyCalls); + 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); + assertEquals(2, callState.createViewCalls); + assertEquals(1, testController.currentCallState.createViewCalls); - Assert.assertEquals(2, callState.attachCalls); - Assert.assertEquals(1, testController.currentCallState.attachCalls); + assertEquals(2, callState.attachCalls); + assertEquals(1, testController.currentCallState.attachCalls); - Assert.assertEquals(2, callState.detachCalls); - Assert.assertEquals(1, testController.currentCallState.detachCalls); + assertEquals(2, callState.detachCalls); + assertEquals(1, testController.currentCallState.detachCalls); - Assert.assertEquals(1, callState.destroyViewCalls); - Assert.assertEquals(0, testController.currentCallState.destroyViewCalls); + assertEquals(1, callState.destroyViewCalls); + assertEquals(0, testController.currentCallState.destroyViewCalls); - Assert.assertEquals(0, callState.destroyCalls); - Assert.assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, callState.destroyCalls); + 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); + assertEquals(2, callState.createViewCalls); + assertEquals(1, testController.currentCallState.createViewCalls); - Assert.assertEquals(2, callState.attachCalls); - Assert.assertEquals(1, testController.currentCallState.attachCalls); + assertEquals(2, callState.attachCalls); + assertEquals(1, testController.currentCallState.attachCalls); - Assert.assertEquals(2, callState.detachCalls); - Assert.assertEquals(1, testController.currentCallState.detachCalls); + assertEquals(2, callState.detachCalls); + assertEquals(1, testController.currentCallState.detachCalls); - Assert.assertEquals(2, callState.destroyViewCalls); - Assert.assertEquals(1, testController.currentCallState.destroyViewCalls); + assertEquals(2, callState.destroyViewCalls); + assertEquals(1, testController.currentCallState.destroyViewCalls); - Assert.assertEquals(0, callState.destroyCalls); - Assert.assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, callState.destroyCalls); + 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); + assertEquals(2, callState.createViewCalls); + assertEquals(1, testController.currentCallState.createViewCalls); - Assert.assertEquals(2, callState.attachCalls); - Assert.assertEquals(1, testController.currentCallState.attachCalls); + assertEquals(2, callState.attachCalls); + assertEquals(1, testController.currentCallState.attachCalls); - Assert.assertEquals(2, callState.detachCalls); - Assert.assertEquals(1, testController.currentCallState.detachCalls); + assertEquals(2, callState.detachCalls); + assertEquals(1, testController.currentCallState.detachCalls); - Assert.assertEquals(2, callState.destroyViewCalls); - Assert.assertEquals(1, testController.currentCallState.destroyViewCalls); + assertEquals(2, callState.destroyViewCalls); + assertEquals(1, testController.currentCallState.destroyViewCalls); - Assert.assertEquals(1, callState.destroyCalls); - Assert.assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(1, callState.destroyCalls); + 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); + assertEquals(2, callState.createViewCalls); + assertEquals(1, testController.currentCallState.createViewCalls); - Assert.assertEquals(2, callState.attachCalls); - Assert.assertEquals(1, testController.currentCallState.attachCalls); + assertEquals(2, callState.attachCalls); + assertEquals(1, testController.currentCallState.attachCalls); - Assert.assertEquals(2, callState.detachCalls); - Assert.assertEquals(1, testController.currentCallState.detachCalls); + assertEquals(2, callState.detachCalls); + assertEquals(1, testController.currentCallState.detachCalls); - Assert.assertEquals(2, callState.destroyViewCalls); - Assert.assertEquals(1, testController.currentCallState.destroyViewCalls); + assertEquals(2, callState.destroyViewCalls); + assertEquals(1, testController.currentCallState.destroyViewCalls); - Assert.assertEquals(2, callState.destroyCalls); - Assert.assertEquals(1, testController.currentCallState.destroyCalls); + assertEquals(2, callState.destroyCalls); + assertEquals(1, testController.currentCallState.destroyCalls); } }); router.pushController(RouterTransaction.with(testController) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); router.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); + 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(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler())); TestController child = new TestController(); attachLifecycleListener(child); @@ -408,7 +447,7 @@ public void testChildLifecycle() { assertCalls(expectedCallState, child); - Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null); + Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); childRouter .setRoot(RouterTransaction.with(child) .pushChangeHandler(getPushHandler(expectedCallState, child)) @@ -425,8 +464,8 @@ public void testChildLifecycle() { public void testChildLifecycle2() { Controller parent = new TestController(); router.pushController(RouterTransaction.with(parent) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); TestController child = new TestController(); attachLifecycleListener(child); @@ -435,7 +474,7 @@ public void testChildLifecycle2() { assertCalls(expectedCallState, child); - Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null); + Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); childRouter .setRoot(RouterTransaction.with(child) .pushChangeHandler(getPushHandler(expectedCallState, child)) @@ -445,26 +484,30 @@ public void testChildLifecycle2() { router.popCurrentController(); + expectedCallState.detachCalls++; + expectedCallState.destroyViewCalls++; + expectedCallState.destroyCalls++; + assertCalls(expectedCallState, child); } private MockChangeHandler getPushHandler(final CallState expectedCallState, final TestController controller) { - return new MockChangeHandler(new ChangeHandlerListener() { + return MockChangeHandler.listeningChangeHandler(new ChangeHandlerListener() { @Override - void willStartChange() { + public void willStartChange() { expectedCallState.changeStartCalls++; expectedCallState.createViewCalls++; assertCalls(expectedCallState, controller); } @Override - void didAttachOrDetach() { + public void didAttachOrDetach() { expectedCallState.attachCalls++; assertCalls(expectedCallState, controller); } @Override - void didEndChange() { + public void didEndChange() { expectedCallState.changeEndCalls++; assertCalls(expectedCallState, controller); } @@ -472,15 +515,15 @@ void didEndChange() { } private MockChangeHandler getPopHandler(final CallState expectedCallState, final TestController controller) { - return new MockChangeHandler(new ChangeHandlerListener() { + return MockChangeHandler.listeningChangeHandler(new ChangeHandlerListener() { @Override - void willStartChange() { + public void willStartChange() { expectedCallState.changeStartCalls++; assertCalls(expectedCallState, controller); } @Override - void didAttachOrDetach() { + public void didAttachOrDetach() { expectedCallState.destroyViewCalls++; expectedCallState.detachCalls++; expectedCallState.destroyCalls++; @@ -488,7 +531,7 @@ void didAttachOrDetach() { } @Override - void didEndChange() { + public void didEndChange() { expectedCallState.changeEndCalls++; assertCalls(expectedCallState, controller); } @@ -496,8 +539,8 @@ void didEndChange() { } 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, currentCallState); + 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) { diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerTests.java index 991452ac..25d51212 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerTests.java @@ -7,14 +7,22 @@ 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.RobolectricTestRunner; import org.robolectric.annotation.Config; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) public class ControllerTests { @@ -42,26 +50,26 @@ public void testViewRetention() { // Test View getting released w/ RELEASE_DETACH controller.setRetainViewMode(RetainViewMode.RELEASE_DETACH); - Assert.assertNull(controller.getView()); + assertNull(controller.getView()); View view = controller.inflate(router.container); - Assert.assertNotNull(controller.getView()); + assertNotNull(controller.getView()); ViewUtils.reportAttached(view, true); - Assert.assertNotNull(controller.getView()); + assertNotNull(controller.getView()); ViewUtils.reportAttached(view, false); - Assert.assertNull(controller.getView()); + assertNull(controller.getView()); // Test View getting retained w/ RETAIN_DETACH controller.setRetainViewMode(RetainViewMode.RETAIN_DETACH); view = controller.inflate(router.container); - Assert.assertNotNull(controller.getView()); + assertNotNull(controller.getView()); ViewUtils.reportAttached(view, true); - Assert.assertNotNull(controller.getView()); + assertNotNull(controller.getView()); ViewUtils.reportAttached(view, false); - Assert.assertNotNull(controller.getView()); + assertNotNull(controller.getView()); // Ensure re-setting RELEASE_DETACH releases controller.setRetainViewMode(RetainViewMode.RELEASE_DETACH); - Assert.assertNull(controller.getView()); + assertNull(controller.getView()); } @Test @@ -94,7 +102,7 @@ public void testActivityResultForChild() { TestController child = new TestController(); router.pushController(RouterTransaction.with(parent)); - parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null) + parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)) .setRoot(RouterTransaction.with(child)); CallState childExpectedCallState = new CallState(true); @@ -151,7 +159,7 @@ public void testPermissionResultForChild() { TestController child = new TestController(); router.pushController(RouterTransaction.with(parent)); - parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null) + parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)) .setRoot(RouterTransaction.with(child)); CallState childExpectedCallState = new CallState(true); @@ -215,7 +223,7 @@ public void testOptionsMenuForChild() { TestController child = new TestController(); router.pushController(RouterTransaction.with(parent)); - parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null) + parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)) .setRoot(RouterTransaction.with(child)); CallState childExpectedCallState = new CallState(true); @@ -263,48 +271,48 @@ public void testAddRemoveChildControllers() { router.pushController(RouterTransaction.with(parent)); - Assert.assertEquals(0, parent.getChildRouters().size()); - Assert.assertNull(child1.getParentController()); - Assert.assertNull(child2.getParentController()); + assertEquals(0, parent.getChildRouters().size()); + assertNull(child1.getParentController()); + assertNull(child2.getParentController()); - Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null); + Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); childRouter.setPopsLastView(true); childRouter.setRoot(RouterTransaction.with(child1)); - Assert.assertEquals(1, parent.getChildRouters().size()); - Assert.assertEquals(childRouter, parent.getChildRouters().get(0)); - Assert.assertEquals(1, childRouter.getBackstackSize()); - Assert.assertEquals(child1, childRouter.getControllers().get(0)); - Assert.assertEquals(parent, child1.getParentController()); - Assert.assertNull(child2.getParentController()); + 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), null); + childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); childRouter.pushController(RouterTransaction.with(child2)); - Assert.assertEquals(1, parent.getChildRouters().size()); - Assert.assertEquals(childRouter, parent.getChildRouters().get(0)); - Assert.assertEquals(2, childRouter.getBackstackSize()); - Assert.assertEquals(child1, childRouter.getControllers().get(0)); - Assert.assertEquals(child2, childRouter.getControllers().get(1)); - Assert.assertEquals(parent, child1.getParentController()); - Assert.assertEquals(parent, child2.getParentController()); + 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); - Assert.assertEquals(1, parent.getChildRouters().size()); - Assert.assertEquals(childRouter, parent.getChildRouters().get(0)); - Assert.assertEquals(1, childRouter.getBackstackSize()); - Assert.assertEquals(child1, childRouter.getControllers().get(0)); - Assert.assertEquals(parent, child1.getParentController()); - Assert.assertNull(child2.getParentController()); + 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); - Assert.assertEquals(1, parent.getChildRouters().size()); - Assert.assertEquals(childRouter, parent.getChildRouters().get(0)); - Assert.assertEquals(0, childRouter.getBackstackSize()); - Assert.assertNull(child1.getParentController()); - Assert.assertNull(child2.getParentController()); + assertEquals(1, parent.getChildRouters().size()); + assertEquals(childRouter, parent.getChildRouters().get(0)); + assertEquals(0, childRouter.getBackstackSize()); + assertNull(child1.getParentController()); + assertNull(child2.getParentController()); } @Test @@ -316,47 +324,89 @@ public void testAddRemoveChildRouters() { router.pushController(RouterTransaction.with(parent)); - Assert.assertEquals(0, parent.getChildRouters().size()); - Assert.assertNull(child1.getParentController()); - Assert.assertNull(child2.getParentController()); + assertEquals(0, parent.getChildRouters().size()); + assertNull(child1.getParentController()); + assertNull(child2.getParentController()); - Router childRouter1 = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1), null); - Router childRouter2 = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_2), null); + Router childRouter1 = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1)); + Router childRouter2 = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_2)); childRouter1.setRoot(RouterTransaction.with(child1)); childRouter2.setRoot(RouterTransaction.with(child2)); - Assert.assertEquals(2, parent.getChildRouters().size()); - Assert.assertEquals(childRouter1, parent.getChildRouters().get(0)); - Assert.assertEquals(childRouter2, parent.getChildRouters().get(1)); - Assert.assertEquals(1, childRouter1.getBackstackSize()); - Assert.assertEquals(1, childRouter2.getBackstackSize()); - Assert.assertEquals(child1, childRouter1.getControllers().get(0)); - Assert.assertEquals(child2, childRouter2.getControllers().get(0)); - Assert.assertEquals(parent, child1.getParentController()); - Assert.assertEquals(parent, child2.getParentController()); + 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); - Assert.assertEquals(1, parent.getChildRouters().size()); - Assert.assertEquals(childRouter1, parent.getChildRouters().get(0)); - Assert.assertEquals(1, childRouter1.getBackstackSize()); - Assert.assertEquals(0, childRouter2.getBackstackSize()); - Assert.assertEquals(child1, childRouter1.getControllers().get(0)); - Assert.assertEquals(parent, child1.getParentController()); - Assert.assertNull(child2.getParentController()); + 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); - Assert.assertEquals(0, parent.getChildRouters().size()); - Assert.assertEquals(0, childRouter1.getBackstackSize()); - Assert.assertEquals(0, childRouter2.getBackstackSize()); - Assert.assertNull(child1.getParentController()); - Assert.assertNull(child2.getParentController()); + 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); } } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerTransactionTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerTransactionTests.java index 37b39ad9..8cced734 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerTransactionTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerTransactionTests.java @@ -4,11 +4,12 @@ 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 @@ -22,10 +23,10 @@ public void testRouterSaveRestore() { RouterTransaction restoredTransaction = new RouterTransaction(bundle); - Assert.assertEquals(transaction.controller.getClass(), restoredTransaction.controller.getClass()); - Assert.assertEquals(transaction.pushChangeHandler().getClass(), restoredTransaction.pushChangeHandler().getClass()); - Assert.assertEquals(transaction.popChangeHandler().getClass(), restoredTransaction.popChangeHandler().getClass()); - Assert.assertEquals(transaction.tag(), restoredTransaction.tag()); + 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()); } } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/MockChangeHandler.java b/conductor/src/test/java/com/bluelinelabs/conductor/MockChangeHandler.java deleted file mode 100644 index 703bf7e5..00000000 --- a/conductor/src/test/java/com/bluelinelabs/conductor/MockChangeHandler.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.bluelinelabs.conductor; - - -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.view.View; -import android.view.ViewGroup; - -public class MockChangeHandler extends ControllerChangeHandler { - - private static final String KEY_REMOVES_FROM_VIEW_ON_PUSH = "MockChangeHandler.removesFromViewOnPush"; - - static class ChangeHandlerListener { - void willStartChange() { } - void didAttachOrDetach() { } - void didEndChange() { } - } - - final ChangeHandlerListener listener; - boolean removesFromViewOnPush; - - public MockChangeHandler() { - this(true, null); - } - - public MockChangeHandler(boolean removesViewOnPush) { - this(removesViewOnPush, null); - } - - public MockChangeHandler(@NonNull ChangeHandlerListener listener) { - this(true, listener); - } - - public MockChangeHandler(boolean removesFromViewOnPush, ChangeHandlerListener listener) { - this.removesFromViewOnPush = removesFromViewOnPush; - - if (listener == null) { - this.listener = new ChangeHandlerListener() { }; - } else { - this.listener = listener; - } - } - - @Override - public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) { - listener.willStartChange(); - - if (isPush) { - container.addView(to); - listener.didAttachOrDetach(); - - if (removesFromViewOnPush && from != null) { - container.removeView(from); - } - } else { - container.removeView(from); - listener.didAttachOrDetach(); - - if (to != null) { - container.addView(to); - } - - } - - changeListener.onChangeCompleted(); - listener.didEndChange(); - } - - @Override - public boolean removesFromViewOnPush() { - return removesFromViewOnPush; - } - - @Override - public void saveToBundle(@NonNull Bundle bundle) { - super.saveToBundle(bundle); - bundle.putBoolean(KEY_REMOVES_FROM_VIEW_ON_PUSH, removesFromViewOnPush); - } - - @Override - public void restoreFromBundle(@NonNull Bundle bundle) { - super.restoreFromBundle(bundle); - removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_VIEW_ON_PUSH); - } -} diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/ReattachCaseTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/ReattachCaseTests.java index 2c4439d6..1d00d21e 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/ReattachCaseTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/ReattachCaseTests.java @@ -3,13 +3,19 @@ import android.os.Bundle; import android.view.ViewGroup; -import org.junit.Assert; +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 { @@ -36,71 +42,71 @@ public void testNeedsAttachingOnPauseAndOrientation() { final TestController controllerB = new TestController(); router.pushController(RouterTransaction.with(controllerA) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); - Assert.assertTrue(controllerA.isAttached()); - Assert.assertFalse(controllerB.isAttached()); + assertTrue(controllerA.isAttached()); + assertFalse(controllerB.isAttached()); sleepWakeDevice(); - Assert.assertTrue(controllerA.isAttached()); - Assert.assertFalse(controllerB.isAttached()); + assertTrue(controllerA.isAttached()); + assertFalse(controllerB.isAttached()); router.pushController(RouterTransaction.with(controllerB) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); - Assert.assertFalse(controllerA.isAttached()); - Assert.assertTrue(controllerB.isAttached()); + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); activityProxy.rotate(); router.rebindIfNeeded(); - Assert.assertFalse(controllerA.isAttached()); - Assert.assertTrue(controllerB.isAttached()); + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); } @Test public void testChildNeedsAttachOnPauseAndOrientation() { - final TestController controllerA = new TestController(); - final TestController childController = new TestController(); - final TestController controllerB = new TestController(); + final Controller controllerA = new TestController(); + final Controller childController = new TestController(); + final Controller controllerB = new TestController(); router.pushController(RouterTransaction.with(controllerA) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); - Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID), null); + Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID)); childRouter.pushController(RouterTransaction.with(childController) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); - Assert.assertTrue(controllerA.isAttached()); - Assert.assertTrue(childController.isAttached()); - Assert.assertFalse(controllerB.isAttached()); + assertTrue(controllerA.isAttached()); + assertTrue(childController.isAttached()); + assertFalse(controllerB.isAttached()); sleepWakeDevice(); - Assert.assertTrue(controllerA.isAttached()); - Assert.assertTrue(childController.isAttached()); - Assert.assertFalse(controllerB.isAttached()); + assertTrue(controllerA.isAttached()); + assertTrue(childController.isAttached()); + assertFalse(controllerB.isAttached()); router.pushController(RouterTransaction.with(controllerB) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); - Assert.assertFalse(controllerA.isAttached()); - Assert.assertFalse(childController.isAttached()); - Assert.assertTrue(controllerB.isAttached()); + assertFalse(controllerA.isAttached()); + assertFalse(childController.isAttached()); + assertTrue(controllerB.isAttached()); activityProxy.rotate(); router.rebindIfNeeded(); - Assert.assertFalse(controllerA.isAttached()); - Assert.assertFalse(childController.isAttached()); - Assert.assertTrue(childController.getNeedsAttach()); - Assert.assertTrue(controllerB.isAttached()); + assertFalse(controllerA.isAttached()); + assertFalse(childController.isAttached()); + assertTrue(childController.getNeedsAttach()); + assertTrue(controllerB.isAttached()); } @Test @@ -110,45 +116,45 @@ public void testChildHandleBackOnOrientation() { final TestController childController = new TestController(); router.pushController(RouterTransaction.with(controllerA) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); - Assert.assertTrue(controllerA.isAttached()); - Assert.assertFalse(controllerB.isAttached()); - Assert.assertFalse(childController.isAttached()); + assertTrue(controllerA.isAttached()); + assertFalse(controllerB.isAttached()); + assertFalse(childController.isAttached()); router.pushController(RouterTransaction.with(controllerB) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); - Router childRouter = controllerB.getChildRouter((ViewGroup)controllerB.getView().findViewById(TestController.VIEW_ID), null); + Router childRouter = controllerB.getChildRouter((ViewGroup)controllerB.getView().findViewById(TestController.VIEW_ID)); childRouter.setPopsLastView(true); childRouter.pushController(RouterTransaction.with(childController) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); - Assert.assertFalse(controllerA.isAttached()); - Assert.assertTrue(controllerB.isAttached()); - Assert.assertTrue(childController.isAttached()); + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertTrue(childController.isAttached()); activityProxy.rotate(); router.rebindIfNeeded(); - Assert.assertFalse(controllerA.isAttached()); - Assert.assertTrue(controllerB.isAttached()); - Assert.assertTrue(childController.isAttached()); + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertTrue(childController.isAttached()); router.handleBack(); - Assert.assertFalse(controllerA.isAttached()); - Assert.assertTrue(controllerB.isAttached()); - Assert.assertFalse(childController.isAttached()); + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertFalse(childController.isAttached()); router.handleBack(); - Assert.assertTrue(controllerA.isAttached()); - Assert.assertFalse(controllerB.isAttached()); - Assert.assertFalse(childController.isAttached()); + assertTrue(controllerA.isAttached()); + assertFalse(controllerB.isAttached()); + assertFalse(childController.isAttached()); } // Attempt to test https://github.com/bluelinelabs/Conductor/issues/86#issuecomment-231381271 @@ -159,71 +165,71 @@ public void testReusedChildRouterHandleBackOnOrientation() { TestController childController = new TestController(); router.pushController(RouterTransaction.with(controllerA) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); - Assert.assertTrue(controllerA.isAttached()); - Assert.assertFalse(controllerB.isAttached()); - Assert.assertFalse(childController.isAttached()); + assertTrue(controllerA.isAttached()); + assertFalse(controllerB.isAttached()); + assertFalse(childController.isAttached()); router.pushController(RouterTransaction.with(controllerB) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); - Router childRouter = controllerB.getChildRouter((ViewGroup)controllerB.getView().findViewById(TestController.VIEW_ID), null); + Router childRouter = controllerB.getChildRouter((ViewGroup)controllerB.getView().findViewById(TestController.VIEW_ID)); childRouter.setPopsLastView(true); childRouter.pushController(RouterTransaction.with(childController) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); - Assert.assertFalse(controllerA.isAttached()); - Assert.assertTrue(controllerB.isAttached()); - Assert.assertTrue(childController.isAttached()); + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertTrue(childController.isAttached()); router.handleBack(); - Assert.assertFalse(controllerA.isAttached()); - Assert.assertTrue(controllerB.isAttached()); - Assert.assertFalse(childController.isAttached()); + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertFalse(childController.isAttached()); childController = new TestController(); childRouter.pushController(RouterTransaction.with(childController) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); - Assert.assertFalse(controllerA.isAttached()); - Assert.assertTrue(controllerB.isAttached()); - Assert.assertTrue(childController.isAttached()); + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertTrue(childController.isAttached()); activityProxy.rotate(); router.rebindIfNeeded(); - Assert.assertFalse(controllerA.isAttached()); - Assert.assertTrue(controllerB.isAttached()); - Assert.assertTrue(childController.isAttached()); + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertTrue(childController.isAttached()); router.handleBack(); childController = new TestController(); childRouter.pushController(RouterTransaction.with(childController) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); - Assert.assertFalse(controllerA.isAttached()); - Assert.assertTrue(controllerB.isAttached()); - Assert.assertTrue(childController.isAttached()); + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertTrue(childController.isAttached()); router.handleBack(); - Assert.assertFalse(controllerA.isAttached()); - Assert.assertTrue(controllerB.isAttached()); - Assert.assertFalse(childController.isAttached()); + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertFalse(childController.isAttached()); router.handleBack(); - Assert.assertTrue(controllerA.isAttached()); - Assert.assertFalse(controllerB.isAttached()); - Assert.assertFalse(childController.isAttached()); + assertTrue(controllerA.isAttached()); + assertFalse(controllerB.isAttached()); + assertFalse(childController.isAttached()); } private void sleepWakeDevice() { diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/RouterChangeHandlerTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/RouterChangeHandlerTests.java new file mode 100644 index 00000000..b75d7240 --- /dev/null +++ b/conductor/src/test/java/com/bluelinelabs/conductor/RouterChangeHandlerTests.java @@ -0,0 +1,239 @@ +package com.bluelinelabs.conductor; + +import android.view.View; + +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 java.util.Arrays; +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 newBackstack = Arrays.asList( + 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 newBackstack = Arrays.asList( + 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()); + } + +} diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/RouterTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/RouterTests.java index 4195b340..52eaef79 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/RouterTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/RouterTests.java @@ -1,15 +1,27 @@ package com.bluelinelabs.conductor; -import org.junit.Assert; +import android.view.ViewGroup; + +import com.bluelinelabs.conductor.changehandler.FadeChangeHandler; +import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler; +import com.bluelinelabs.conductor.util.ActivityProxy; +import com.bluelinelabs.conductor.util.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.Arrays; 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 { @@ -28,13 +40,13 @@ public void testSetRoot() { Controller rootController = new TestController(); - Assert.assertFalse(router.hasRootController()); + assertFalse(router.hasRootController()); router.setRoot(RouterTransaction.with(rootController).tag(rootTag)); - Assert.assertTrue(router.hasRootController()); + assertTrue(router.hasRootController()); - Assert.assertEquals(rootController, router.getControllerWithTag(rootTag)); + assertEquals(rootController, router.getControllerWithTag(rootTag)); } @Test @@ -48,8 +60,8 @@ public void testSetNewRoot() { router.setRoot(RouterTransaction.with(oldRootController).tag(oldRootTag)); router.setRoot(RouterTransaction.with(newRootController).tag(newRootTag)); - Assert.assertNull(router.getControllerWithTag(oldRootTag)); - Assert.assertEquals(newRootController, router.getControllerWithTag(newRootTag)); + assertNull(router.getControllerWithTag(oldRootTag)); + assertEquals(newRootController, router.getControllerWithTag(newRootTag)); } @Test @@ -58,8 +70,8 @@ public void testGetByInstanceId() { router.pushController(RouterTransaction.with(controller)); - Assert.assertEquals(controller, router.getControllerWithInstanceId(controller.getInstanceId())); - Assert.assertNull(router.getControllerWithInstanceId("fake id")); + assertEquals(controller, router.getControllerWithInstanceId(controller.getInstanceId())); + assertNull(router.getControllerWithInstanceId("fake id")); } @Test @@ -76,8 +88,8 @@ public void testGetByTag() { router.pushController(RouterTransaction.with(controller2) .tag(controller2Tag)); - Assert.assertEquals(controller1, router.getControllerWithTag(controller1Tag)); - Assert.assertEquals(controller2, router.getControllerWithTag(controller2Tag)); + assertEquals(controller1, router.getControllerWithTag(controller1Tag)); + assertEquals(controller2, router.getControllerWithTag(controller2Tag)); } @Test @@ -91,26 +103,26 @@ public void testPushPopControllers() { router.pushController(RouterTransaction.with(controller1) .tag(controller1Tag)); - Assert.assertEquals(1, router.getBackstackSize()); + assertEquals(1, router.getBackstackSize()); router.pushController(RouterTransaction.with(controller2) .tag(controller2Tag)); - Assert.assertEquals(2, router.getBackstackSize()); + assertEquals(2, router.getBackstackSize()); router.popCurrentController(); - Assert.assertEquals(1, router.getBackstackSize()); + assertEquals(1, router.getBackstackSize()); - Assert.assertEquals(controller1, router.getControllerWithTag(controller1Tag)); - Assert.assertNull(router.getControllerWithTag(controller2Tag)); + assertEquals(controller1, router.getControllerWithTag(controller1Tag)); + assertNull(router.getControllerWithTag(controller2Tag)); router.popCurrentController(); - Assert.assertEquals(0, router.getBackstackSize()); + assertEquals(0, router.getBackstackSize()); - Assert.assertNull(router.getControllerWithTag(controller1Tag)); - Assert.assertNull(router.getControllerWithTag(controller2Tag)); + assertNull(router.getControllerWithTag(controller1Tag)); + assertNull(router.getControllerWithTag(controller2Tag)); } @Test @@ -139,11 +151,11 @@ public void testPopToTag() { router.popToTag(controller2Tag); - Assert.assertEquals(2, router.getBackstackSize()); - Assert.assertEquals(controller1, router.getControllerWithTag(controller1Tag)); - Assert.assertEquals(controller2, router.getControllerWithTag(controller2Tag)); - Assert.assertNull(router.getControllerWithTag(controller3Tag)); - Assert.assertNull(router.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 @@ -167,10 +179,10 @@ public void testPopNonCurrent() { router.popController(controller2); - Assert.assertEquals(2, router.getBackstackSize()); - Assert.assertEquals(controller1, router.getControllerWithTag(controller1Tag)); - Assert.assertNull(router.getControllerWithTag(controller2Tag)); - Assert.assertEquals(controller3, router.getControllerWithTag(controller3Tag)); + assertEquals(2, router.getBackstackSize()); + assertEquals(controller1, router.getControllerWithTag(controller1Tag)); + assertNull(router.getControllerWithTag(controller2Tag)); + assertEquals(controller3, router.getControllerWithTag(controller3Tag)); } @Test @@ -179,81 +191,115 @@ public void testSetBackstack() { RouterTransaction middleTransaction = RouterTransaction.with(new TestController()); RouterTransaction topTransaction = RouterTransaction.with(new TestController()); - List backstack = new ArrayList<>(); - backstack.add(rootTransaction); - backstack.add(middleTransaction); - backstack.add(topTransaction); - + List backstack = Arrays.asList(rootTransaction, middleTransaction, topTransaction); router.setBackstack(backstack, null); - Assert.assertEquals(3, router.getBackstackSize()); + assertEquals(3, router.getBackstackSize()); List fetchedBackstack = router.getBackstack(); - Assert.assertEquals(rootTransaction, fetchedBackstack.get(0)); - Assert.assertEquals(middleTransaction, fetchedBackstack.get(1)); - Assert.assertEquals(topTransaction, fetchedBackstack.get(2)); + 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())); - Assert.assertEquals(1, router.getBackstackSize()); + assertEquals(1, router.getBackstackSize()); RouterTransaction rootTransaction = RouterTransaction.with(new TestController()); RouterTransaction middleTransaction = RouterTransaction.with(new TestController()); RouterTransaction topTransaction = RouterTransaction.with(new TestController()); - List backstack = new ArrayList<>(); - backstack.add(rootTransaction); - backstack.add(middleTransaction); - backstack.add(topTransaction); - + List backstack = Arrays.asList(rootTransaction, middleTransaction, topTransaction); router.setBackstack(backstack, null); - Assert.assertEquals(3, router.getBackstackSize()); + assertEquals(3, router.getBackstackSize()); List fetchedBackstack = router.getBackstack(); - Assert.assertEquals(rootTransaction, fetchedBackstack.get(0)); - Assert.assertEquals(middleTransaction, fetchedBackstack.get(1)); - Assert.assertEquals(topTransaction, fetchedBackstack.get(2)); + 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(new MockChangeHandler(false)); + RouterTransaction oldTopTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler()); router.setRoot(oldRootTransaction); router.pushController(oldTopTransaction); - Assert.assertEquals(2, router.getBackstackSize()); + assertEquals(2, router.getBackstackSize()); - Assert.assertTrue(oldRootTransaction.controller.isAttached()); - Assert.assertTrue(oldTopTransaction.controller.isAttached()); + assertTrue(oldRootTransaction.controller.isAttached()); + assertTrue(oldTopTransaction.controller.isAttached()); RouterTransaction rootTransaction = RouterTransaction.with(new TestController()); - RouterTransaction middleTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false)); - RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false)); - - List backstack = new ArrayList<>(); - backstack.add(rootTransaction); - backstack.add(middleTransaction); - backstack.add(topTransaction); + RouterTransaction middleTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler()); + RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler()); + List backstack = Arrays.asList(rootTransaction, middleTransaction, topTransaction); router.setBackstack(backstack, null); - Assert.assertEquals(3, router.getBackstackSize()); + assertEquals(3, router.getBackstackSize()); List fetchedBackstack = router.getBackstack(); - Assert.assertEquals(rootTransaction, fetchedBackstack.get(0)); - Assert.assertEquals(middleTransaction, fetchedBackstack.get(1)); - Assert.assertEquals(topTransaction, fetchedBackstack.get(2)); - - Assert.assertFalse(oldRootTransaction.controller.isAttached()); - Assert.assertFalse(oldTopTransaction.controller.isAttached()); - Assert.assertTrue(rootTransaction.controller.isAttached()); - Assert.assertTrue(middleTransaction.controller.isAttached()); - Assert.assertTrue(topTransaction.controller.isAttached()); + 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 testPopToRoot() { + RouterTransaction rootTransaction = RouterTransaction.with(new TestController()); + RouterTransaction transaction1 = RouterTransaction.with(new TestController()); + RouterTransaction transaction2 = RouterTransaction.with(new TestController()); + + List backstack = Arrays.asList(rootTransaction, transaction1, transaction2); + router.setBackstack(backstack, null); + + assertEquals(3, router.getBackstackSize()); + + router.popToRoot(); + + assertEquals(1, router.getBackstackSize()); + assertEquals(rootTransaction, router.getBackstack().get(0)); + + assertTrue(rootTransaction.controller.isAttached()); + assertFalse(transaction1.controller.isAttached()); + assertFalse(transaction2.controller.isAttached()); + } + + @Test + public void testPopToRootWithNoRemoveViewOnPush() { + RouterTransaction rootTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new HorizontalChangeHandler(false)); + RouterTransaction transaction1 = RouterTransaction.with(new TestController()).pushChangeHandler(new HorizontalChangeHandler(false)); + RouterTransaction transaction2 = RouterTransaction.with(new TestController()).pushChangeHandler(new HorizontalChangeHandler(false)); + + List backstack = Arrays.asList(rootTransaction, transaction1, transaction2); + router.setBackstack(backstack, null); + + assertEquals(3, router.getBackstackSize()); + + router.popToRoot(); + + assertEquals(1, router.getBackstackSize()); + assertEquals(rootTransaction, router.getBackstack().get(0)); + + assertTrue(rootTransaction.controller.isAttached()); + assertFalse(transaction1.controller.isAttached()); + assertFalse(transaction2.controller.isAttached()); } @Test @@ -261,61 +307,128 @@ public void testReplaceTopController() { RouterTransaction rootTransaction = RouterTransaction.with(new TestController()); RouterTransaction topTransaction = RouterTransaction.with(new TestController()); - List backstack = new ArrayList<>(); - backstack.add(rootTransaction); - backstack.add(topTransaction); - + List backstack = Arrays.asList(rootTransaction, topTransaction); router.setBackstack(backstack, null); - Assert.assertEquals(2, router.getBackstackSize()); + assertEquals(2, router.getBackstackSize()); List fetchedBackstack = router.getBackstack(); - Assert.assertEquals(rootTransaction, fetchedBackstack.get(0)); - Assert.assertEquals(topTransaction, fetchedBackstack.get(1)); + assertEquals(rootTransaction, fetchedBackstack.get(0)); + assertEquals(topTransaction, fetchedBackstack.get(1)); RouterTransaction newTopTransaction = RouterTransaction.with(new TestController()); router.replaceTopController(newTopTransaction); - Assert.assertEquals(2, router.getBackstackSize()); + assertEquals(2, router.getBackstackSize()); fetchedBackstack = router.getBackstack(); - Assert.assertEquals(rootTransaction, fetchedBackstack.get(0)); - Assert.assertEquals(newTopTransaction, fetchedBackstack.get(1)); + 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(new MockChangeHandler(false)); - - List backstack = new ArrayList<>(); - backstack.add(rootTransaction); - backstack.add(topTransaction); + RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler()); + List backstack = Arrays.asList(rootTransaction, topTransaction); router.setBackstack(backstack, null); - Assert.assertEquals(2, router.getBackstackSize()); + assertEquals(2, router.getBackstackSize()); - Assert.assertTrue(rootTransaction.controller.isAttached()); - Assert.assertTrue(topTransaction.controller.isAttached()); + assertTrue(rootTransaction.controller.isAttached()); + assertTrue(topTransaction.controller.isAttached()); List fetchedBackstack = router.getBackstack(); - Assert.assertEquals(rootTransaction, fetchedBackstack.get(0)); - Assert.assertEquals(topTransaction, fetchedBackstack.get(1)); + assertEquals(rootTransaction, fetchedBackstack.get(0)); + assertEquals(topTransaction, fetchedBackstack.get(1)); - RouterTransaction newTopTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false)); + RouterTransaction newTopTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler()); router.replaceTopController(newTopTransaction); newTopTransaction.pushChangeHandler().completeImmediately(); - Assert.assertEquals(2, router.getBackstackSize()); + assertEquals(2, router.getBackstackSize()); fetchedBackstack = router.getBackstack(); - Assert.assertEquals(rootTransaction, fetchedBackstack.get(0)); - Assert.assertEquals(newTopTransaction, fetchedBackstack.get(1)); + 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 backstack = Arrays.asList(transaction1, transaction2); + router.setBackstack(backstack, null); + + assertEquals(1, transaction1.transactionIndex); + assertEquals(2, transaction2.transactionIndex); + + backstack = Arrays.asList(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 backstack = Arrays.asList(transaction1, transaction2); + childRouter.setBackstack(backstack, null); + + assertEquals(2, transaction1.transactionIndex); + assertEquals(3, transaction2.transactionIndex); + + backstack = Arrays.asList(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()); + } + + @Test + public void testRemovesAllViewsOnDestroy() { + Controller controller1 = new TestController(); + Controller controller2 = new TestController(); + + router.setRoot(RouterTransaction.with(controller1)); + router.pushController(RouterTransaction.with(controller2) + .pushChangeHandler(new FadeChangeHandler(false))); + + assertEquals(2, router.container.getChildCount()); + + router.destroy(true); - Assert.assertTrue(rootTransaction.controller.isAttached()); - Assert.assertFalse(topTransaction.controller.isAttached()); - Assert.assertTrue(newTopTransaction.controller.isAttached()); + assertEquals(0, router.container.getChildCount()); } } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/TargetControllerTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/TargetControllerTests.java index 182c7884..9a91ca0c 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/TargetControllerTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/TargetControllerTests.java @@ -3,13 +3,19 @@ import android.os.Bundle; import android.view.ViewGroup; -import org.junit.Assert; +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 { @@ -34,21 +40,21 @@ public void testSiblingTarget() { final TestController controllerA = new TestController(); final TestController controllerB = new TestController(); - Assert.assertNull(controllerA.getTargetController()); - Assert.assertNull(controllerB.getTargetController()); + assertNull(controllerA.getTargetController()); + assertNull(controllerB.getTargetController()); router.pushController(RouterTransaction.with(controllerA) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); controllerB.setTargetController(controllerA); router.pushController(RouterTransaction.with(controllerB) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); - Assert.assertNull(controllerA.getTargetController()); - Assert.assertEquals(controllerA, controllerB.getTargetController()); + assertNull(controllerA.getTargetController()); + assertEquals(controllerA, controllerB.getTargetController()); } @Test @@ -56,22 +62,22 @@ public void testParentChildTarget() { final TestController controllerA = new TestController(); final TestController controllerB = new TestController(); - Assert.assertNull(controllerA.getTargetController()); - Assert.assertNull(controllerB.getTargetController()); + assertNull(controllerA.getTargetController()); + assertNull(controllerB.getTargetController()); router.pushController(RouterTransaction.with(controllerA) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); controllerB.setTargetController(controllerA); - Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID), null); + Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID)); childRouter.pushController(RouterTransaction.with(controllerB) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); - Assert.assertNull(controllerA.getTargetController()); - Assert.assertEquals(controllerA, controllerB.getTargetController()); + assertNull(controllerA.getTargetController()); + assertEquals(controllerA, controllerB.getTargetController()); } @Test @@ -79,22 +85,22 @@ public void testChildParentTarget() { final TestController controllerA = new TestController(); final TestController controllerB = new TestController(); - Assert.assertNull(controllerA.getTargetController()); - Assert.assertNull(controllerB.getTargetController()); + assertNull(controllerA.getTargetController()); + assertNull(controllerB.getTargetController()); router.pushController(RouterTransaction.with(controllerA) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); controllerA.setTargetController(controllerB); - Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID), null); + Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID)); childRouter.pushController(RouterTransaction.with(controllerB) - .pushChangeHandler(new MockChangeHandler()) - .popChangeHandler(new MockChangeHandler())); + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); - Assert.assertNull(controllerB.getTargetController()); - Assert.assertEquals(controllerB, controllerA.getTargetController()); + assertNull(controllerB.getTargetController()); + assertEquals(controllerB, controllerA.getTargetController()); } } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/ViewAttachHandlerTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/ViewAttachHandlerTests.java new file mode 100644 index 00000000..e08619e9 --- /dev/null +++ b/conductor/src/test/java/com/bluelinelabs/conductor/ViewAttachHandlerTests.java @@ -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++; + } + } + +} diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/ViewLeakTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/ViewLeakTests.java new file mode 100644 index 00000000..6a5ec483 --- /dev/null +++ b/conductor/src/test/java/com/bluelinelabs/conductor/ViewLeakTests.java @@ -0,0 +1,132 @@ +package com.bluelinelabs.conductor; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; +import android.view.ViewGroup; + +import com.bluelinelabs.conductor.util.ActivityProxy; +import com.bluelinelabs.conductor.util.TestController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class ViewLeakTests { + + private ActivityProxy activityProxy; + private Router router; + + public void createActivityController(Bundle savedInstanceState) { + activityProxy = new ActivityProxy().create(savedInstanceState).start().resume(); + router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState); + if (!router.hasRootController()) { + router.setRoot(RouterTransaction.with(new TestController())); + } + } + + @Before + public void setup() { + createActivityController(null); + } + + @Test + public void testPop() { + Controller controller = new TestController(); + router.pushController(RouterTransaction.with(controller)); + + assertNotNull(controller.getView()); + + router.popCurrentController(); + + assertNull(controller.getView()); + } + + @Test + public void testPopWhenPushNeverAdded() { + Controller controller = new TestController(); + router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverAddChangeHandler())); + + assertNotNull(controller.getView()); + + router.popCurrentController(); + + assertNull(controller.getView()); + } + + @Test + public void testPopWhenPushNeverCompleted() { + Controller controller = new TestController(); + router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverCompleteChangeHandler())); + + assertNotNull(controller.getView()); + + router.popCurrentController(); + + assertNull(controller.getView()); + } + + @Test + public void testActivityStop() { + Controller controller = new TestController(); + router.pushController(RouterTransaction.with(controller)); + + assertNotNull(controller.getView()); + + activityProxy.stop(true); + + assertNull(controller.getView()); + } + + @Test + public void testActivityStopWhenPushNeverAdded() { + Controller controller = new TestController(); + router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverAddChangeHandler())); + + assertNotNull(controller.getView()); + + activityProxy.stop(true); + + assertNull(controller.getView()); + } + + @Test + public void testActivityStopWhenPushNeverCompleted() { + Controller controller = new TestController(); + router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverCompleteChangeHandler())); + + assertNotNull(controller.getView()); + + activityProxy.stop(true); + + assertNull(controller.getView()); + } + + public static class NeverAddChangeHandler extends ControllerChangeHandler { + @Override + public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) { + if (from != null) { + container.removeView(from); + } + } + } + + public static class NeverCompleteChangeHandler extends ControllerChangeHandler { + @Override + public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) { + if (from != null) { + container.removeView(from); + } + container.addView(to); + } + } + +} diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/ActivityProxy.java b/conductor/src/test/java/com/bluelinelabs/conductor/util/ActivityProxy.java similarity index 90% rename from conductor/src/test/java/com/bluelinelabs/conductor/ActivityProxy.java rename to conductor/src/test/java/com/bluelinelabs/conductor/util/ActivityProxy.java index 81c4865c..be124911 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/ActivityProxy.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/util/ActivityProxy.java @@ -1,4 +1,4 @@ -package com.bluelinelabs.conductor; +package com.bluelinelabs.conductor.util; import android.os.Bundle; import android.support.annotation.IdRes; @@ -45,14 +45,19 @@ public ActivityProxy saveInstanceState(Bundle outState) { return this; } - public ActivityProxy stop() { + public ActivityProxy stop(boolean detachView) { activityController.stop(); - view.setAttached(false); + + if (detachView) { + view.setAttached(false); + } + return this; } public ActivityProxy destroy() { activityController.destroy(); + view.setAttached(false); return this; } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/AttachFakingFrameLayout.java b/conductor/src/test/java/com/bluelinelabs/conductor/util/AttachFakingFrameLayout.java similarity index 98% rename from conductor/src/test/java/com/bluelinelabs/conductor/AttachFakingFrameLayout.java rename to conductor/src/test/java/com/bluelinelabs/conductor/util/AttachFakingFrameLayout.java index 1d1afbc2..a5eeb99b 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/AttachFakingFrameLayout.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/util/AttachFakingFrameLayout.java @@ -1,4 +1,4 @@ -package com.bluelinelabs.conductor; +package com.bluelinelabs.conductor.util; import android.content.Context; import android.os.IBinder; diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/CallState.java b/conductor/src/test/java/com/bluelinelabs/conductor/util/CallState.java similarity index 99% rename from conductor/src/test/java/com/bluelinelabs/conductor/CallState.java rename to conductor/src/test/java/com/bluelinelabs/conductor/util/CallState.java index 72549d73..b77c96be 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/CallState.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/util/CallState.java @@ -1,4 +1,4 @@ -package com.bluelinelabs.conductor; +package com.bluelinelabs.conductor.util; import android.os.Parcel; import android.os.Parcelable; diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/util/ChangeHandlerHistory.java b/conductor/src/test/java/com/bluelinelabs/conductor/util/ChangeHandlerHistory.java new file mode 100644 index 00000000..96007e97 --- /dev/null +++ b/conductor/src/test/java/com/bluelinelabs/conductor/util/ChangeHandlerHistory.java @@ -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 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; + } + } + +} diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/util/MockChangeHandler.java b/conductor/src/test/java/com/bluelinelabs/conductor/util/MockChangeHandler.java new file mode 100644 index 00000000..168c3cb6 --- /dev/null +++ b/conductor/src/test/java/com/bluelinelabs/conductor/util/MockChangeHandler.java @@ -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; + } +} diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/TestActivity.java b/conductor/src/test/java/com/bluelinelabs/conductor/util/TestActivity.java similarity index 90% rename from conductor/src/test/java/com/bluelinelabs/conductor/TestActivity.java rename to conductor/src/test/java/com/bluelinelabs/conductor/util/TestActivity.java index 1a05326f..6278168a 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/TestActivity.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/util/TestActivity.java @@ -1,4 +1,4 @@ -package com.bluelinelabs.conductor; +package com.bluelinelabs.conductor.util; import android.app.Activity; diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/TestController.java b/conductor/src/test/java/com/bluelinelabs/conductor/util/TestController.java similarity index 80% rename from conductor/src/test/java/com/bluelinelabs/conductor/TestController.java rename to conductor/src/test/java/com/bluelinelabs/conductor/util/TestController.java index 87e26e23..779d78e5 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/TestController.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/util/TestController.java @@ -1,4 +1,4 @@ -package com.bluelinelabs.conductor; +package com.bluelinelabs.conductor.util; import android.content.Intent; import android.os.Bundle; @@ -11,6 +11,10 @@ import android.view.ViewGroup; import android.widget.FrameLayout; +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; @@ -19,11 +23,8 @@ public class TestController extends Controller { 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 @@ -32,11 +33,11 @@ protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup FrameLayout view = new AttachFakingFrameLayout(inflater.getContext()); view.setId(VIEW_ID); - FrameLayout childContainer1 = new FrameLayout(inflater.getContext()); + FrameLayout childContainer1 = new AttachFakingFrameLayout(inflater.getContext()); childContainer1.setId(CHILD_VIEW_ID_1); view.addView(childContainer1); - FrameLayout childContainer2 = new FrameLayout(inflater.getContext()); + FrameLayout childContainer2 = new AttachFakingFrameLayout(inflater.getContext()); childContainer2.setId(CHILD_VIEW_ID_2); view.addView(childContainer2); @@ -53,6 +54,13 @@ protected void onChangeStarted(@NonNull ControllerChangeHandler changeHandler, @ 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 @@ -68,7 +76,7 @@ protected void onDetach(@NonNull View view) { } @Override - protected void onDestroyView(View view) { + protected void onDestroyView(@NonNull View view) { super.onDestroyView(view); currentCallState.destroyViewCalls++; } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/ViewUtils.java b/conductor/src/test/java/com/bluelinelabs/conductor/util/ViewUtils.java similarity index 75% rename from conductor/src/test/java/com/bluelinelabs/conductor/ViewUtils.java rename to conductor/src/test/java/com/bluelinelabs/conductor/util/ViewUtils.java index dc0a3c49..abc8e669 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/ViewUtils.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/util/ViewUtils.java @@ -1,7 +1,8 @@ -package com.bluelinelabs.conductor; +package com.bluelinelabs.conductor.util; import android.view.View; import android.view.View.OnAttachStateChangeListener; +import android.view.ViewGroup; import org.robolectric.util.ReflectionHelpers; @@ -10,6 +11,10 @@ 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); } @@ -38,6 +43,14 @@ public void onViewDetachedFromWindow(View v) { } } } + 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 getAttachStateListeners(View view) { diff --git a/demo/build.gradle b/demo/build.gradle index 5c288407..c80a264f 100755 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -11,11 +11,12 @@ apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt' android { - compileSdkVersion 23 - buildToolsVersion "23.0.2" + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion lintOptions { - abortOnError false + abortOnError true + ignore 'UnusedResources' } compileOptions { @@ -25,10 +26,11 @@ android { defaultConfig { applicationId "com.bluelinelabs.conductor.demo" - minSdkVersion 16 - targetSdkVersion 23 + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0.0" + vectorDrawables.useSupportLibrary true } buildTypes { @@ -37,6 +39,10 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + packagingOptions { + exclude 'META-INF/rxjava.properties' + } } dependencies { @@ -49,6 +55,7 @@ dependencies { compile project(':conductor-support') compile project(':conductor-rxlifecycle') + compile project(':conductor-rxlifecycle2') debugCompile rootProject.ext.leakCanary releaseCompile rootProject.ext.leakCanaryNoOp diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index b9c7f043..e2426dfd 100755 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -25,6 +25,18 @@ + + + + + + + + + diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/MainActivity.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/MainActivity.java index 88b4d6c1..2d8349c5 100755 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/MainActivity.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/MainActivity.java @@ -1,8 +1,11 @@ package com.bluelinelabs.conductor.demo; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; +import android.util.Log; import android.view.ViewGroup; import com.bluelinelabs.conductor.Conductor; @@ -19,6 +22,8 @@ public final class MainActivity extends AppCompatActivity implements ActionBarPr @BindView(R.id.controller_container) ViewGroup container; private Router router; + private static final String TAG = MainActivity.class.getSimpleName(); + private static final String PATH_PREFIX = "/demo/"; @Override protected void onCreate(Bundle savedInstanceState) { @@ -31,7 +36,59 @@ protected void onCreate(Bundle savedInstanceState) { router = Conductor.attachRouter(this, container, savedInstanceState); if (!router.hasRootController()) { - router.setRoot(RouterTransaction.with(new HomeController())); + HomeController homeController = new HomeController(); + router.setRoot(RouterTransaction.with(homeController)); + handleIntentDataUri(homeController, getIntent()); + } + } + + private HomeController getHomeController() { + RouterTransaction rootRouterTransaction = router.getBackstack().get(0); + return (HomeController) rootRouterTransaction.controller(); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + HomeController homeController = getHomeController(); + handleIntentDataUri(homeController, intent); + } + + /** + * will handle the data URI of the intent and when it is valid, navigate to the matching + * controller + * + * EXAMPLE: to test this via command line + * + *
+     * adb shell am start -W -a android.intent.action.VIEW
+     *   -d "http://bluelinelabs.com/demo/NAVIGATION"
+     *   com.bluelinelabs.conductor.demo
+     * 
+ *
+ * + * @param homeController the {@link HomeController} will handle the actual navigation + */ + private void handleIntentDataUri(HomeController homeController, Intent intent) { + Uri intentDataUri = intent.getData(); + if (intentDataUri == null || intentDataUri.getPath() == null) return; + + String path = intentDataUri.getPath(); + if (!path.startsWith(PATH_PREFIX)) { + Log.w(TAG, "unexpected path: " + intentDataUri.getPath()); + return; + } + + /* Example: http://bluelinelabs.com/demo/MASTER_DETAIL?item=1 + + intentDataUri.getPath() = /demo/MASTER_DETAIL + intentDataUri.getQuery() = item=1 + */ + + // skip the expected prefix, so that the remaining part is e.g. MASTER_DETAIL + String navigationPath = path.substring(PATH_PREFIX.length()); + if (!homeController.navigateTo(navigationPath)) { + Log.w(TAG, "cannot navigate to: " + intentDataUri.getPath()); } } diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/CircularRevealChangeHandler.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/CircularRevealChangeHandler.java index 6c1151d2..78b48c57 100644 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/CircularRevealChangeHandler.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/CircularRevealChangeHandler.java @@ -120,7 +120,7 @@ public CircularRevealChangeHandler(int cx, int cy, long duration, boolean remove this.cy = cy; } - @Override + @Override @NonNull protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) { final float radius = (float) Math.hypot(cx, cy); Animator animator = null; diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/CircularRevealChangeHandlerCompat.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/CircularRevealChangeHandlerCompat.java index ab525bb2..347bdd92 100644 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/CircularRevealChangeHandlerCompat.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/CircularRevealChangeHandlerCompat.java @@ -16,14 +16,15 @@ public CircularRevealChangeHandlerCompat(@NonNull View fromView, @NonNull View c 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) { diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/FabToDialogTransitionChangeHandler.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/FabToDialogTransitionChangeHandler.java new file mode 100644 index 00000000..ebf3c009 --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/FabToDialogTransitionChangeHandler.java @@ -0,0 +1,101 @@ +package com.bluelinelabs.conductor.demo.changehandler; + +import android.annotation.TargetApi; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.transition.Fade; +import android.transition.Transition; +import android.transition.TransitionSet; +import android.view.View; +import android.view.ViewGroup; + +import com.bluelinelabs.conductor.changehandler.TransitionChangeHandler; +import com.bluelinelabs.conductor.demo.R; +import com.bluelinelabs.conductor.demo.changehandler.transitions.FabTransform; +import com.bluelinelabs.conductor.demo.util.AnimUtils; +import com.bluelinelabs.conductor.demo.util.AnimUtils.TransitionEndListener; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class FabToDialogTransitionChangeHandler extends TransitionChangeHandler { + + private View fab; + private View dialogBackground; + private ViewGroup fabParent; + + @NonNull @Override + protected Transition getTransition(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, boolean isPush) { + Transition backgroundFade = new Fade(); + backgroundFade.addTarget(R.id.dialog_background); + + Transition fabTransform = new FabTransform(ContextCompat.getColor(container.getContext(), R.color.colorAccent), R.drawable.ic_github_face); + + TransitionSet set = new TransitionSet(); + set.addTransition(backgroundFade); + set.addTransition(fabTransform); + + return set; + } + + @Override + public void prepareForTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @NonNull Transition transition, boolean isPush, @NonNull OnTransitionPreparedListener onTransitionPreparedListener) { + fab = isPush ? from.findViewById(R.id.fab) : to.findViewById(R.id.fab); + fabParent = (ViewGroup)fab.getParent(); + + if (!isPush) { + /* + * Before we transition back we want to remove the fab + * in order to add it again for the TransitionManager to be able to detect the change + */ + fabParent.removeView(fab); + fab.setVisibility(View.VISIBLE); + + /* + * Before we transition back we need to move the dialog's background to the new view + * so its fade won't take place over the fab transition + */ + dialogBackground = from.findViewById(R.id.dialog_background); + ((ViewGroup)dialogBackground.getParent()).removeView(dialogBackground); + fabParent.addView(dialogBackground); + } + + onTransitionPreparedListener.onPrepared(); + } + + @Override + public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @NonNull Transition transition, boolean isPush) { + if (isPush) { + fabParent.removeView(fab); + container.addView(to); + + /* + * After the transition is finished we have to add the fab back to the original container. + * Because otherwise we will be lost when trying to transition back. + * Set it to invisible because we don't want it to jump back after the transition + */ + transition.addListener(new AnimUtils.TransitionEndListener() { + @Override + public void onTransitionCompleted(Transition transition) { + fab.setVisibility(View.GONE); + fabParent.addView(fab); + fab = null; + fabParent = null; + } + }); + } else { + dialogBackground.setVisibility(View.INVISIBLE); + fabParent.addView(fab); + container.removeView(from); + + transition.addListener(new TransitionEndListener() { + @Override + public void onTransitionCompleted(Transition transition) { + fabParent.removeView(dialogBackground); + dialogBackground = null; + } + }); + } + } + +} diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/FlipChangeHandler.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/FlipChangeHandler.java index a61d3e43..cc6e4524 100755 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/FlipChangeHandler.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/FlipChangeHandler.java @@ -52,7 +52,7 @@ public FlipChangeHandler(FlipDirection flipDirection, long animationDuration) { this.animationDuration = animationDuration; } - @Override + @Override @NonNull protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) { AnimatorSet animatorSet = new AnimatorSet(); diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/ScaleFadeChangeHandler.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/ScaleFadeChangeHandler.java index 60632577..3dd8e4f0 100644 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/ScaleFadeChangeHandler.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/ScaleFadeChangeHandler.java @@ -15,11 +15,12 @@ public ScaleFadeChangeHandler() { 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) { diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/SharedElementDelayingChangeHandler.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/SharedElementDelayingChangeHandler.java new file mode 100644 index 00000000..ccefbb32 --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/SharedElementDelayingChangeHandler.java @@ -0,0 +1,163 @@ +package com.bluelinelabs.conductor.demo.changehandler; + +import android.annotation.TargetApi; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.transition.Transition; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnPreDrawListener; + +import com.bluelinelabs.conductor.Controller; +import com.bluelinelabs.conductor.ControllerChangeHandler; + +import java.util.ArrayList; +import java.util.List; + +/** + * A TransitionChangeHandler that will wait for views with the passed transition names to be fully laid out + * before executing. An OnPreDrawListener will be added to the "to" view, then to all of its subviews that + * match the transaction names we're interested in. Once all of the views are fully ready, the "to" view + * is set to invisible so that it'll fade in nicely, and the views that we want to use as shared elements + * are removed from their containers, then immediately re-added within the beginDelayedTransition call so + * the system picks them up as shared elements. + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class SharedElementDelayingChangeHandler extends ArcFadeMoveChangeHandler { + + private static final String KEY_WAIT_FOR_TRANSITION_NAMES = "SharedElementDelayingChangeHandler.waitForTransitionNames"; + + private final ArrayList waitForTransitionNames; + private final ArrayList removedViews = new ArrayList<>(); + private OnPreDrawListener onPreDrawListener; + + public SharedElementDelayingChangeHandler() { + waitForTransitionNames = new ArrayList<>(); + } + + public SharedElementDelayingChangeHandler(@NonNull List waitForTransitionNames) { + this.waitForTransitionNames = new ArrayList<>(waitForTransitionNames); + } + + @Override + public void prepareForTransition(@NonNull final ViewGroup container, @Nullable View from, @Nullable final View to, @NonNull Transition transition, boolean isPush, @NonNull final OnTransitionPreparedListener onTransitionPreparedListener) { + if (to != null && to.getParent() == null && waitForTransitionNames.size() > 0) { + onPreDrawListener = new OnPreDrawListener() { + boolean addedSubviewListeners; + + @Override + public boolean onPreDraw() { + List foundViews = new ArrayList<>(); + for (String transitionName : waitForTransitionNames) { + foundViews.add(getViewWithTransitionName(to, transitionName)); + } + + if (!foundViews.contains(null) && !addedSubviewListeners) { + addedSubviewListeners = true; + + for (final View view : foundViews) { + view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { + @Override + public boolean onPreDraw() { + view.getViewTreeObserver().removeOnPreDrawListener(this); + waitForTransitionNames.remove(view.getTransitionName()); + + ViewGroup parent = (ViewGroup)view.getParent(); + removedViews.add(new ViewParentPair(view, parent)); + parent.removeView(view); + + if (waitForTransitionNames.size() == 0) { + to.getViewTreeObserver().removeOnPreDrawListener(onPreDrawListener); + + to.setVisibility(View.INVISIBLE); + + onTransitionPreparedListener.onPrepared(); + } + return true; + } + }); + } + } + + + return false; + } + }; + + to.getViewTreeObserver().addOnPreDrawListener(onPreDrawListener); + + container.addView(to); + } else { + onTransitionPreparedListener.onPrepared(); + } + } + + @Override + public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @NonNull Transition transition, boolean isPush) { + if (to != null) { + to.setVisibility(View.VISIBLE); + + for (ViewParentPair removedView : removedViews) { + removedView.parent.addView(removedView.view); + } + + removedViews.clear(); + } + + super.executePropertyChanges(container, from, to, transition, isPush); + } + + @Override + public void saveToBundle(@NonNull Bundle bundle) { + bundle.putStringArrayList(KEY_WAIT_FOR_TRANSITION_NAMES, waitForTransitionNames); + } + + @Override + public void restoreFromBundle(@NonNull Bundle bundle) { + List savedNames = bundle.getStringArrayList(KEY_WAIT_FOR_TRANSITION_NAMES); + if (savedNames != null) { + waitForTransitionNames.addAll(savedNames); + } + } + + @Override + public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) { + super.onAbortPush(newHandler, newTop); + + removedViews.clear(); + } + + @Nullable + View getViewWithTransitionName(@NonNull View view, @NonNull String transitionName) { + if (transitionName.equals(view.getTransitionName())) { + return view; + } + + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup)view; + int childCount = viewGroup.getChildCount(); + + for (int i = 0; i < childCount; i++) { + View viewWithTransitionName = getViewWithTransitionName(viewGroup.getChildAt(i), transitionName); + if (viewWithTransitionName != null) { + return viewWithTransitionName; + } + } + } + + return null; + } + + private static class ViewParentPair { + View view; + ViewGroup parent; + + public ViewParentPair(View view, ViewGroup parent) { + this.view = view; + this.parent = parent; + } + } + +} diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/transitions/FabTransform.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/transitions/FabTransform.java new file mode 100644 index 00000000..cf3aee0a --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/transitions/FabTransform.java @@ -0,0 +1,295 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example from https://github.com/nickbutcher/plaid + */ +package com.bluelinelabs.conductor.demo.changehandler.transitions; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.graphics.Outline; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.ColorInt; +import android.support.annotation.DrawableRes; +import android.support.v4.content.ContextCompat; +import android.transition.Transition; +import android.transition.TransitionValues; +import android.view.View; +import android.view.ViewAnimationUtils; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.view.ViewTreeObserver.OnPreDrawListener; +import android.view.animation.Interpolator; + +import com.bluelinelabs.conductor.demo.util.AnimUtils; + +import java.util.ArrayList; +import java.util.List; + +import static android.view.View.MeasureSpec.makeMeasureSpec; + +/** + * A transition between a FAB & another surface using a circular reveal moving along an arc. + *

+ * See: https://www.google.com/design/spec/motion/transforming-material.html#transforming-material-radial-transformation + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class FabTransform extends Transition { + + private static final long DEFAULT_DURATION = 240L; + private static final String PROP_BOUNDS = "plaid:fabTransform:bounds"; + private static final String[] TRANSITION_PROPERTIES = { + PROP_BOUNDS + }; + + private final int color; + private final int icon; + + public FabTransform(@ColorInt int fabColor, @DrawableRes int fabIconResId) { + color = fabColor; + icon = fabIconResId; + setPathMotion(new GravityArcMotion()); + setDuration(DEFAULT_DURATION); + } + + @Override + public String[] getTransitionProperties() { + return TRANSITION_PROPERTIES; + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public Animator createAnimator(final ViewGroup sceneRoot, + final TransitionValues startValues, + final TransitionValues endValues) { + if (startValues == null || endValues == null) return null; + + final Rect startBounds = (Rect) startValues.values.get(PROP_BOUNDS); + final Rect endBounds = (Rect) endValues.values.get(PROP_BOUNDS); + + final boolean fromFab = endBounds.width() > startBounds.width(); + final View view = endValues.view; + final Rect dialogBounds = fromFab ? endBounds : startBounds; + final Interpolator fastOutSlowInInterpolator = + AnimUtils.getFastOutSlowInInterpolator(); + final long duration = getDuration(); + final long halfDuration = duration / 2; + final long twoThirdsDuration = duration * 2 / 3; + + if (!fromFab) { + // Force measure / layout the dialog back to it's original bounds + view.measure( + makeMeasureSpec(startBounds.width(), View.MeasureSpec.EXACTLY), + makeMeasureSpec(startBounds.height(), View.MeasureSpec.EXACTLY)); + view.layout(startBounds.left, startBounds.top, startBounds.right, startBounds.bottom); + } + + final int translationX = startBounds.centerX() - endBounds.centerX(); + final int translationY = startBounds.centerY() - endBounds.centerY(); + if (fromFab) { + view.setTranslationX(translationX); + view.setTranslationY(translationY); + } + + // Add a color overlay to fake appearance of the FAB + final ColorDrawable fabColor = new ColorDrawable(color); + fabColor.setBounds(0, 0, dialogBounds.width(), dialogBounds.height()); + if (!fromFab) fabColor.setAlpha(0); + view.getOverlay().add(fabColor); + + // Add an icon overlay again to fake the appearance of the FAB + final Drawable fabIcon = + ContextCompat.getDrawable(sceneRoot.getContext(), icon).mutate(); + final int iconLeft = (dialogBounds.width() - fabIcon.getIntrinsicWidth()) / 2; + final int iconTop = (dialogBounds.height() - fabIcon.getIntrinsicHeight()) / 2; + fabIcon.setBounds(iconLeft, iconTop, + iconLeft + fabIcon.getIntrinsicWidth(), + iconTop + fabIcon.getIntrinsicHeight()); + if (!fromFab) fabIcon.setAlpha(0); + view.getOverlay().add(fabIcon); + + // Since the view that's being transition to always seems to be on the top (z-order), we have + // to make a copy of the "from" view and put it in the "to" view's overlay, then fade it out. + // There has to be another way to do this, right? + Drawable dialogView = null; + if (!fromFab) { + startValues.view.setDrawingCacheEnabled(true); + startValues.view.buildDrawingCache(); + Bitmap viewBitmap = startValues.view.getDrawingCache(); + dialogView = new BitmapDrawable(view.getResources(), viewBitmap); + dialogView.setBounds(0, 0, dialogBounds.width(), dialogBounds.height()); + view.getOverlay().add(dialogView); + } + + // Circular clip from/to the FAB size + final Animator circularReveal; + if (fromFab) { + circularReveal = ViewAnimationUtils.createCircularReveal(view, + view.getWidth() / 2, + view.getHeight() / 2, + startBounds.width() / 2, + (float) Math.hypot(endBounds.width() / 2, endBounds.height() / 2)); + circularReveal.setInterpolator( + AnimUtils.getFastOutLinearInInterpolator()); + } else { + circularReveal = ViewAnimationUtils.createCircularReveal(view, + view.getWidth() / 2, + view.getHeight() / 2, + (float) Math.hypot(startBounds.width() / 2, startBounds.height() / 2), + endBounds.width() / 2); + circularReveal.setInterpolator( + AnimUtils.getLinearOutSlowInInterpolator()); + + // Persist the end clip i.e. stay at FAB size after the reveal has run + circularReveal.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + final ViewOutlineProvider fabOutlineProvider = view.getOutlineProvider(); + + view.setOutlineProvider(new ViewOutlineProvider() { + boolean hasRun = false; + + @Override + public void getOutline(final View view, Outline outline) { + final int left = (view.getWidth() - endBounds.width()) / 2; + final int top = (view.getHeight() - endBounds.height()) / 2; + + outline.setOval( + left, top, left + endBounds.width(), top + endBounds.height()); + + if (!hasRun) { + hasRun = true; + view.setClipToOutline(true); + + // We have to remove this as soon as it's laid out so we can get the shadow back + view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { + @Override + public boolean onPreDraw() { + if (view.getWidth() == endBounds.width() && view.getHeight() == endBounds.height()) { + view.setOutlineProvider(fabOutlineProvider); + view.setClipToOutline(false); + view.getViewTreeObserver().removeOnPreDrawListener(this); + return true; + } + + return true; + } + }); + } + } + }); + } + }); + } + circularReveal.setDuration(duration); + + // Translate to end position along an arc + final Animator translate = ObjectAnimator.ofFloat( + view, + View.TRANSLATION_X, + View.TRANSLATION_Y, + fromFab ? getPathMotion().getPath(translationX, translationY, 0, 0) + : getPathMotion().getPath(0, 0, -translationX, -translationY)); + translate.setDuration(duration); + translate.setInterpolator(fastOutSlowInInterpolator); + + // Fade contents of non-FAB view in/out + List fadeContents = null; + if (view instanceof ViewGroup) { + final ViewGroup vg = ((ViewGroup) view); + fadeContents = new ArrayList<>(vg.getChildCount()); + for (int i = vg.getChildCount() - 1; i >= 0; i--) { + final View child = vg.getChildAt(i); + final Animator fade = + ObjectAnimator.ofFloat(child, View.ALPHA, fromFab ? 1f : 0f); + if (fromFab) { + child.setAlpha(0f); + } + fade.setDuration(twoThirdsDuration); + fade.setInterpolator(fastOutSlowInInterpolator); + fadeContents.add(fade); + } + } + + // Fade in/out the fab color & icon overlays + final Animator colorFade = ObjectAnimator.ofInt(fabColor, "alpha", fromFab ? 0 : 255); + final Animator iconFade = ObjectAnimator.ofInt(fabIcon, "alpha", fromFab ? 0 : 255); + if (!fromFab) { + colorFade.setStartDelay(halfDuration); + iconFade.setStartDelay(halfDuration); + } + colorFade.setDuration(halfDuration); + iconFade.setDuration(halfDuration); + colorFade.setInterpolator(fastOutSlowInInterpolator); + iconFade.setInterpolator(fastOutSlowInInterpolator); + + // Run all animations together + final AnimatorSet transition = new AnimatorSet(); + transition.playTogether(circularReveal, translate, colorFade, iconFade); + transition.playTogether(fadeContents); + if (dialogView != null) { + final Animator dialogViewFade = ObjectAnimator.ofInt(dialogView, "alpha", 0).setDuration(twoThirdsDuration); + dialogViewFade.setInterpolator(fastOutSlowInInterpolator); + transition.playTogether(dialogViewFade); + } + transition.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Clean up + view.getOverlay().clear(); + + if (!fromFab) { + view.setTranslationX(0); + view.setTranslationY(0); + view.setTranslationZ(0); + + view.measure( + makeMeasureSpec(endBounds.width(), View.MeasureSpec.EXACTLY), + makeMeasureSpec(endBounds.height(), View.MeasureSpec.EXACTLY)); + view.layout(endBounds.left, endBounds.top, endBounds.right, endBounds.bottom); + } + + } + }); + return new AnimUtils.NoPauseAnimator(transition); + } + + private void captureValues(TransitionValues transitionValues) { + final View view = transitionValues.view; + if (view == null || view.getWidth() <= 0 || view.getHeight() <= 0) return; + + transitionValues.values.put(PROP_BOUNDS, new Rect(view.getLeft(), view.getTop(), + view.getRight(), view.getBottom())); + } +} \ No newline at end of file diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/transitions/GravityArcMotion.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/transitions/GravityArcMotion.java new file mode 100644 index 00000000..48bd611b --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/transitions/GravityArcMotion.java @@ -0,0 +1,217 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bluelinelabs.conductor.demo.changehandler.transitions; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Path; +import android.os.Build; +import android.transition.ArcMotion; +import android.util.AttributeSet; + +/** + * A tweak to {@link ArcMotion} which slightly alters the path calculation. In the real world + * gravity slows upward motion and accelerates downward motion. This class emulates this behavior + * to make motion paths appear more natural. + *

+ * See https://www.google.com/design/spec/motion/movement.html#movement-movement-within-screen-bounds + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class GravityArcMotion extends ArcMotion { + + private static final float DEFAULT_MIN_ANGLE_DEGREES = 0; + private static final float DEFAULT_MAX_ANGLE_DEGREES = 70; + private static final float DEFAULT_MAX_TANGENT = (float) + Math.tan(Math.toRadians(DEFAULT_MAX_ANGLE_DEGREES/2)); + + private float mMinimumHorizontalAngle = 0; + private float mMinimumVerticalAngle = 0; + private float mMaximumAngle = DEFAULT_MAX_ANGLE_DEGREES; + private float mMinimumHorizontalTangent = 0; + private float mMinimumVerticalTangent = 0; + private float mMaximumTangent = DEFAULT_MAX_TANGENT; + + public GravityArcMotion() {} + + public GravityArcMotion(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * @inheritDoc + */ + @Override + public void setMinimumHorizontalAngle(float angleInDegrees) { + mMinimumHorizontalAngle = angleInDegrees; + mMinimumHorizontalTangent = toTangent(angleInDegrees); + } + + /** + * @inheritDoc + */ + @Override + public float getMinimumHorizontalAngle() { + return mMinimumHorizontalAngle; + } + + /** + * @inheritDoc + */ + @Override + public void setMinimumVerticalAngle(float angleInDegrees) { + mMinimumVerticalAngle = angleInDegrees; + mMinimumVerticalTangent = toTangent(angleInDegrees); + } + + /** + * @inheritDoc + */ + @Override + public float getMinimumVerticalAngle() { + return mMinimumVerticalAngle; + } + + /** + * @inheritDoc + */ + @Override + public void setMaximumAngle(float angleInDegrees) { + mMaximumAngle = angleInDegrees; + mMaximumTangent = toTangent(angleInDegrees); + } + + /** + * @inheritDoc + */ + @Override + public float getMaximumAngle() { + return mMaximumAngle; + } + + private static float toTangent(float arcInDegrees) { + if (arcInDegrees < 0 || arcInDegrees > 90) { + throw new IllegalArgumentException("Arc must be between 0 and 90 degrees"); + } + return (float) Math.tan(Math.toRadians(arcInDegrees / 2)); + } + + @Override + public Path getPath(float startX, float startY, float endX, float endY) { + // Here's a little ascii art to show how this is calculated: + // c---------- b + // \ / | + // \ d | + // \ / e + // a----f + // This diagram assumes that the horizontal distance is less than the vertical + // distance between The start point (a) and end point (b). + // d is the midpoint between a and b. c is the center point of the circle with + // This path is formed by assuming that start and end points are in + // an arc on a circle. The end point is centered in the circle vertically + // and start is a point on the circle. + + // Triangles bfa and bde form similar right triangles. The control points + // for the cubic Bezier arc path are the midpoints between a and e and e and b. + + Path path = new Path(); + path.moveTo(startX, startY); + + float ex; + float ey; + if (startY == endY) { + ex = (startX + endX) / 2; + ey = startY + mMinimumHorizontalTangent * Math.abs(endX - startX) / 2; + } else if (startX == endX) { + ex = startX + mMinimumVerticalTangent * Math.abs(endY - startY) / 2; + ey = (startY + endY) / 2; + } else { + float deltaX = endX - startX; + + /** + * This is the only change to ArcMotion + */ + float deltaY; + if (endY < startY) { + deltaY = startY - endY; // Y is inverted compared to diagram above. + } else { + deltaY = endY - startY; + } + /** + * End changes + */ + + // hypotenuse squared. + float h2 = deltaX * deltaX + deltaY * deltaY; + + // Midpoint between start and end + float dx = (startX + endX) / 2; + float dy = (startY + endY) / 2; + + // Distance squared between end point and mid point is (1/2 hypotenuse)^2 + float midDist2 = h2 * 0.25f; + + float minimumArcDist2 = 0; + + if (Math.abs(deltaX) < Math.abs(deltaY)) { + // Similar triangles bfa and bde mean that (ab/fb = eb/bd) + // Therefore, eb = ab * bd / fb + // ab = hypotenuse + // bd = hypotenuse/2 + // fb = deltaY + float eDistY = h2 / (2 * deltaY); + ey = endY + eDistY; + ex = endX; + + minimumArcDist2 = midDist2 * mMinimumVerticalTangent + * mMinimumVerticalTangent; + } else { + // Same as above, but flip X & Y + float eDistX = h2 / (2 * deltaX); + ex = endX + eDistX; + ey = endY; + + minimumArcDist2 = midDist2 * mMinimumHorizontalTangent + * mMinimumHorizontalTangent; + } + float arcDistX = dx - ex; + float arcDistY = dy - ey; + float arcDist2 = arcDistX * arcDistX + arcDistY * arcDistY; + + float maximumArcDist2 = midDist2 * mMaximumTangent * mMaximumTangent; + + float newArcDistance2 = 0; + if (arcDist2 < minimumArcDist2) { + newArcDistance2 = minimumArcDist2; + } else if (arcDist2 > maximumArcDist2) { + newArcDistance2 = maximumArcDist2; + } + if (newArcDistance2 != 0) { + float ratio2 = newArcDistance2 / arcDist2; + float ratio = (float) Math.sqrt(ratio2); + ex = dx + (ratio * (ex - dx)); + ey = dy + (ratio * (ey - dy)); + } + } + float controlX1 = (startX + ex) / 2; + float controlY1 = (startY + ey) / 2; + float controlX2 = (ex + endX) / 2; + float controlY2 = (ey + endY) / 2; + path.cubicTo(controlX1, controlY1, controlX2, controlY2, endX, endY); + return path; + } + +} \ No newline at end of file diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/CityDetailController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/CityDetailController.java new file mode 100644 index 00000000..41a4c504 --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/CityDetailController.java @@ -0,0 +1,169 @@ +package com.bluelinelabs.conductor.demo.controllers; + +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.bluelinelabs.conductor.demo.R; +import com.bluelinelabs.conductor.demo.controllers.base.BaseController; +import com.bluelinelabs.conductor.demo.util.BundleBuilder; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class CityDetailController extends BaseController { + + private static final String KEY_TITLE = "CityDetailController.title"; + private static final String KEY_IMAGE = "CityDetailController.image"; + + private static final String[] LIST_ROWS = new String[] { + "• This is a city.", + "• There's some cool stuff about it.", + "• But really this is just a demo, not a city guide app.", + "• This demo is meant to show some nice transitions, as long as you're on Lollipop or later.", + "• You should have seen some sweet shared element transitions using the ImageView and the TextView in the \"header\" above.", + "• This transition utilized some callbacks to ensure all the necessary rows in the RecyclerView were laid about before the transition occurred.", + "• Just adding some more lines so it scrolls now...\n\n\n\n\n\n\nThe end." + }; + + @BindView(R.id.recycler_view) RecyclerView recyclerView; + + @DrawableRes private int imageDrawableRes; + private String title; + + public CityDetailController(@DrawableRes int imageDrawableRes, String title) { + this(new BundleBuilder(new Bundle()) + .putInt(KEY_IMAGE, imageDrawableRes) + .putString(KEY_TITLE, title) + .build()); + } + + public CityDetailController(Bundle args) { + super(args); + imageDrawableRes = getArgs().getInt(KEY_IMAGE); + title = getArgs().getString(KEY_TITLE); + } + + @NonNull + @Override + protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { + return inflater.inflate(R.layout.controller_city_detail, container, false); + } + + @Override + protected void onViewBound(@NonNull View view) { + super.onViewBound(view); + + recyclerView.setHasFixedSize(true); + recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext())); + recyclerView.setAdapter(new CityDetailAdapter(LayoutInflater.from(view.getContext()), title, imageDrawableRes, LIST_ROWS, title)); + } + + @Override + protected String getTitle() { + return title; + } + + static class CityDetailAdapter extends RecyclerView.Adapter { + + private static final int VIEW_TYPE_HEADER = 0; + private static final int VIEW_TYPE_DETAIL = 1; + + private final LayoutInflater inflater; + private final String title; + @DrawableRes private final int imageDrawableRes; + private final String imageViewTransitionName; + private final String textViewTransitionName; + private final String[] details; + + public CityDetailAdapter(LayoutInflater inflater, @DrawableRes String title, int imageDrawableRes, String[] details, String transitionNameBase) { + this.inflater = inflater; + this.title = title; + this.imageDrawableRes = imageDrawableRes; + this.details = details; + imageViewTransitionName = inflater.getContext().getResources().getString(R.string.transition_tag_image_named, transitionNameBase); + textViewTransitionName = inflater.getContext().getResources().getString(R.string.transition_tag_title_named, transitionNameBase); + } + + @Override + public int getItemViewType(int position) { + if (position == 0) { + return VIEW_TYPE_HEADER; + } else { + return VIEW_TYPE_DETAIL; + } + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == VIEW_TYPE_HEADER) { + return new HeaderViewHolder(inflater.inflate(R.layout.row_city_header, parent, false)); + } else { + return new DetailViewHolder(inflater.inflate(R.layout.row_city_detail, parent, false)); + } + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + if (getItemViewType(position) == VIEW_TYPE_HEADER) { + ((HeaderViewHolder)holder).bind(imageDrawableRes, title, imageViewTransitionName, textViewTransitionName); + } else { + ((DetailViewHolder)holder).bind(details[position - 1]); + } + } + + @Override + public int getItemCount() { + return 1 + details.length; + } + + static class ViewHolder extends RecyclerView.ViewHolder { + ViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + } + + static class HeaderViewHolder extends ViewHolder { + + @BindView(R.id.image_view) ImageView imageView; + @BindView(R.id.text_view) TextView textView; + + public HeaderViewHolder(View itemView) { + super(itemView); + } + + void bind(@DrawableRes int imageDrawableRes, String title, String imageTransitionName, String textViewTransitionName) { + imageView.setImageResource(imageDrawableRes); + textView.setText(title); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + imageView.setTransitionName(imageTransitionName); + textView.setTransitionName(textViewTransitionName); + } + } + } + + static class DetailViewHolder extends ViewHolder { + + @BindView(R.id.text_view) TextView textView; + + public DetailViewHolder(View itemView) { + super(itemView); + } + + void bind(String detail) { + textView.setText(detail); + } + + } + } +} diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/CityGridController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/CityGridController.java new file mode 100644 index 00000000..addd10dc --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/CityGridController.java @@ -0,0 +1,174 @@ +package com.bluelinelabs.conductor.demo.controllers; + +import android.graphics.PorterDuff.Mode; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.bluelinelabs.conductor.RouterTransaction; +import com.bluelinelabs.conductor.changehandler.FadeChangeHandler; +import com.bluelinelabs.conductor.changehandler.TransitionChangeHandlerCompat; +import com.bluelinelabs.conductor.demo.R; +import com.bluelinelabs.conductor.demo.changehandler.SharedElementDelayingChangeHandler; +import com.bluelinelabs.conductor.demo.controllers.base.BaseController; +import com.bluelinelabs.conductor.demo.util.BundleBuilder; + +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; + +public class CityGridController extends BaseController { + + private static final String KEY_TITLE = "CityGridController.title"; + private static final String KEY_DOT_COLOR = "CityGridController.dotColor"; + private static final String KEY_FROM_POSITION = "CityGridController.position"; + + private static final CityModel[] CITY_MODELS = new CityModel[] { + new CityModel(R.drawable.chicago, "Chicago"), + new CityModel(R.drawable.jakarta, "Jakarta"), + new CityModel(R.drawable.london, "London"), + new CityModel(R.drawable.sao_paulo, "Sao Paulo"), + new CityModel(R.drawable.tokyo, "Tokyo") + }; + + @BindView(R.id.tv_title) TextView tvTitle; + @BindView(R.id.img_dot) ImageView imgDot; + @BindView(R.id.recycler_view) RecyclerView recyclerView; + + private String title; + private int dotColor; + private int fromPosition; + + public CityGridController(String title, int dotColor, int fromPosition) { + this(new BundleBuilder(new Bundle()) + .putString(KEY_TITLE, title) + .putInt(KEY_DOT_COLOR, dotColor) + .putInt(KEY_FROM_POSITION, fromPosition) + .build()); + } + + public CityGridController(Bundle args) { + super(args); + title = getArgs().getString(KEY_TITLE); + dotColor = getArgs().getInt(KEY_DOT_COLOR); + fromPosition = getArgs().getInt(KEY_FROM_POSITION); + } + + @NonNull + @Override + protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { + return inflater.inflate(R.layout.controller_city_grid, container, false); + } + + @Override + protected void onViewBound(@NonNull View view) { + super.onViewBound(view); + + tvTitle.setText(title); + imgDot.getDrawable().setColorFilter(ContextCompat.getColor(getActivity(), dotColor), Mode.SRC_ATOP); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + tvTitle.setTransitionName(getResources().getString(R.string.transition_tag_title_indexed, fromPosition)); + imgDot.setTransitionName(getResources().getString(R.string.transition_tag_dot_indexed, fromPosition)); + } + + recyclerView.setHasFixedSize(true); + recyclerView.setLayoutManager(new GridLayoutManager(view.getContext(), 2)); + recyclerView.setAdapter(new CityGridAdapter(LayoutInflater.from(view.getContext()), CITY_MODELS)); + } + + @Override + protected String getTitle() { + return "Shared Element Demos"; + } + + void onModelRowClick(CityModel model) { + String imageTransitionName = getResources().getString(R.string.transition_tag_image_named, model.title); + String titleTransitionName = getResources().getString(R.string.transition_tag_title_named, model.title); + + List names = new ArrayList<>(); + names.add(imageTransitionName); + names.add(titleTransitionName); + + getRouter().pushController(RouterTransaction.with(new CityDetailController(model.drawableRes, model.title)) + .pushChangeHandler(new TransitionChangeHandlerCompat(new SharedElementDelayingChangeHandler(names), new FadeChangeHandler())) + .popChangeHandler(new TransitionChangeHandlerCompat(new SharedElementDelayingChangeHandler(names), new FadeChangeHandler()))); + } + + class CityGridAdapter extends RecyclerView.Adapter { + + private final LayoutInflater inflater; + private final CityModel[] items; + + public CityGridAdapter(LayoutInflater inflater, CityModel[] items) { + this.inflater = inflater; + this.items = items; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new ViewHolder(inflater.inflate(R.layout.row_city_grid, parent, false)); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + holder.bind(items[position]); + } + + @Override + public int getItemCount() { + return items.length; + } + + class ViewHolder extends RecyclerView.ViewHolder { + + @BindView(R.id.tv_title) TextView textView; + @BindView(R.id.img_city) ImageView imageView; + private CityModel model; + + public ViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + + void bind(CityModel item) { + model = item; + imageView.setImageResource(item.drawableRes); + textView.setText(item.title); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + textView.setTransitionName(getResources().getString(R.string.transition_tag_title_named, model.title)); + imageView.setTransitionName(getResources().getString(R.string.transition_tag_image_named, model.title)); + } + } + + @OnClick(R.id.row_root) + void onRowClick() { + onModelRowClick(model); + } + + } + } + + private static class CityModel { + @DrawableRes int drawableRes; + String title; + + public CityModel(@DrawableRes int drawableRes, String title) { + this.drawableRes = drawableRes; + this.title = title; + } + } +} diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/DialogController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/DialogController.java new file mode 100644 index 00000000..699fc08e --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/DialogController.java @@ -0,0 +1,60 @@ +package com.bluelinelabs.conductor.demo.controllers; + + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.text.method.LinkMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.bluelinelabs.conductor.demo.R; +import com.bluelinelabs.conductor.demo.controllers.base.BaseController; +import com.bluelinelabs.conductor.demo.util.BundleBuilder; + +import butterknife.BindView; +import butterknife.OnClick; + +public class DialogController extends BaseController { + + private static final String KEY_TITLE = "DialogController.title"; + private static final String KEY_DESCRIPTION = "DialogController.description"; + + @BindView(R.id.tv_title) TextView tvTitle; + @BindView(R.id.tv_description) TextView tvDescription; + + public DialogController(CharSequence title, CharSequence description) { + this(new BundleBuilder(new Bundle()) + .putCharSequence(KEY_TITLE, title) + .putCharSequence(KEY_DESCRIPTION, description) + .build()); + } + + public DialogController(Bundle args) { + super(args); + } + + @Override + protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { + return inflater.inflate(R.layout.controller_dialog, container, false); + } + + @Override + public void onViewBound(@NonNull View view) { + super.onViewBound(view); + tvTitle.setText(getArgs().getCharSequence(KEY_TITLE)); + tvDescription.setText(getArgs().getCharSequence(KEY_DESCRIPTION)); + tvDescription.setMovementMethod(LinkMovementMethod.getInstance()); + } + + @OnClick({R.id.dismiss, R.id.dialog_window}) + public void dismissDialog() { + getRouter().popController(this); + } + + @Override + public boolean handleBack() { + return super.handleBack(); + } +} diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/DragDismissController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/DragDismissController.java index 63dcf0ec..6eae6243 100644 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/DragDismissController.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/DragDismissController.java @@ -6,7 +6,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import com.bluelinelabs.conductor.demo.R; import com.bluelinelabs.conductor.demo.changehandler.ScaleFadeChangeHandler; @@ -14,13 +13,9 @@ import com.bluelinelabs.conductor.demo.widget.ElasticDragDismissFrameLayout; import com.bluelinelabs.conductor.demo.widget.ElasticDragDismissFrameLayout.ElasticDragDismissCallback; -import butterknife.BindView; - @TargetApi(VERSION_CODES.LOLLIPOP) public class DragDismissController extends BaseController { - @BindView(R.id.tv_lorem_ipsum) TextView tvLoremIpsum; - private final ElasticDragDismissCallback dragDismissListener = new ElasticDragDismissCallback() { @Override public void onDragDismissed() { @@ -38,15 +33,10 @@ protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup @Override protected void onViewBound(@NonNull View view) { ((ElasticDragDismissFrameLayout)view).addListener(dragDismissListener); - - tvLoremIpsum.setText("Lorem ipsum dolor sit amet, volutpat lacus egestas integer vitae, tempus potenti posuere dolore, elit cras ut vulputate pede eros. Pharetra curabitur, cum ultrices nisi nulla, non a est diamlorem in pede. Feugiat vivamus id, leo massa, pede ligula libero wisi, posuere nec interdum risus. Mauris eros. Scelerisque etiam dignissim, sem odio magna posuere libero in. Eget non posuere, rutrum nunc ut, ipsum ornare, vestibulum nisl turpis, urna interdum. Arcu mi velit. Sem dolor amet sed hymenaeos tempor. Cras felis.\n" + - "Tempus risus vehicula, mauris nulla interdum purus sodales suspendisse, morbi ultrices tempus vitae vestibulum porttitor, vel mus enim tellus non massa in, quis nec fermentum scelerisque sem congue dolores. Accumsan lacinia urna eu feugiat, habitasse wisi cras id nonummy sapien, a pede. Turpis ac donec, adipiscing ut faucibus, odio ut morbi, habitant volutpat lacinia. At vitae ipsum, porttitor wisi hendrerit pellentesque sapien, suspendisse pellentesque praesent sit tellus varius. At in ligula neque imperdiet eget, viverra lectus risus est sem feugiat. Diam amet non phasellus, sed enim ante etiam lorem id at, feugiat eu urna, urna posuere. Amet vel, vehicula ac in fermentum id, elit mauris dolor, eget orci in nec non in vestibulum. Tortor risus mattis, massa vel condimentum non ornare, pede nunc curabitur dui, eu dolorum luctus duis pellentesque. Nec in imperdiet ac tortor pulvinar bibendum. Parturient lacus luctus posuere, quis luctus sociis tellus iaculis.\n" + - "Eu nec, nulla ut neque ac suspendisse, ac purus mattis pretium orci orci, penatibus sollicitudin pellentesque massa, felis et molestie natoque justo vitae. Laoreet nunc nulla augue semper, nulla ridiculus elit in quis bibendum ultrices, integer duis. Dolor ut donec ligula vehicula odio, in lacinia mattis ut quos, semper libero nulla, et euismod ut, nec curabitur turpis aliquet mauris sit a. Tellus interdum dignissim felis. Ultrices dignissim ut, enim urna adipiscing, nunc accumsan justo odio fringilla magna penatibus. Amet integer metus sollicitudin tristique libero dolor, augue nulla pellentesque, massa in suspendisse, adipiscing donec neque nunc. Malesuada non, luctus vestibulum, et at taciti orci felis. Aliquam dolor nibh erat in tincidunt in, risus suscipit integer, quis dolor quis quam. Ultrices dictum, dignissim interdum aenean pede ornare pretium, vehicula adipiscing enim nec magna mollis duis. Imperdiet dolor velit phasellus, laoreet sed ridiculus sollicitudin sit viverra metus, integer sit nunc fusce nonummy augue. Maecenas adipiscing porttitor eu risus nunc malesuada, quo vitae blandit amet feugiat nunc. Pede hac duis in.\n" + - "Lorem posuere ridiculus donec, volutpat vehicula erat nunc ut, justo occaecati vivamus aliquam massa, felis etiam, sed feugiat molestie eu. Et leo non dignissim nam. Fringilla pretium suspendisse vitae nascetur massa hymenaeos, lectus ut senectus amet, a enim. Pellentesque integer erat, mauris morbi pellentesque, sodales phasellus turpis purus nulla porta, massa vulputate consectetuer habitasse malesuada pulvinar, vehicula in elit eros interdum ut. Et enim vulputate, aliquam donec ullamcorper et, vel consequat tincidunt. Ipsum quam sed ante quis at, ultrices eget. Rutrum qui et velit possimus in, odio amet ut adipiscing sed nec, a tellus ut, quam molestie. Nisl adipiscing euismod nec, eget facilisis ac sit. Suspendisse elit amet consequat dolor senectus vivamus, at scelerisque erat, odio doloribus velit et felis neque, turpis adipiscing, arcu varius placerat leo."); } @Override - protected void onDestroyView(View view) { + protected void onDestroyView(@NonNull View view) { super.onDestroyView(view); ((ElasticDragDismissFrameLayout)view).removeListener(dragDismissListener); diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/HomeController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/HomeController.java index 5b9f3b1f..f060813c 100644 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/HomeController.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/HomeController.java @@ -130,6 +130,23 @@ protected String getTitle() { return "Conductor Demos"; } + /** + * will navigate to the controller represented by the @{code enumIdParam} parameter. + * + * @param enumIdParam can be any Id of the HomeDemoModel enumerations (case does not matter) + * @return @{code false} when we did not find a matching controller, @{code true} otherwise + */ + public boolean navigateTo(@NonNull String enumIdParam) { + String enumIdentifier = enumIdParam.toUpperCase(); + try { + HomeDemoModel hdm = HomeDemoModel.valueOf(enumIdentifier); + onModelRowClick(hdm); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + void onModelRowClick(HomeDemoModel model) { switch (model) { case NAVIGATION: diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/MasterDetailListController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/MasterDetailListController.java index 1336a47f..f410c5ef 100644 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/MasterDetailListController.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/MasterDetailListController.java @@ -91,7 +91,7 @@ void onRowSelected(int index) { ChildController controller = new ChildController(model.detail, model.backgroundColor, true); if (twoPaneView) { - getChildRouter(detailContainer, null).setRoot(RouterTransaction.with(controller)); + getChildRouter(detailContainer).setRoot(RouterTransaction.with(controller)); } else { getRouter().pushController(RouterTransaction.with(controller) .pushChangeHandler(new HorizontalChangeHandler()) diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/MultipleChildRouterController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/MultipleChildRouterController.java index b504b5c2..22982674 100644 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/MultipleChildRouterController.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/MultipleChildRouterController.java @@ -8,6 +8,7 @@ import com.bluelinelabs.conductor.Router; import com.bluelinelabs.conductor.RouterTransaction; import com.bluelinelabs.conductor.demo.R; +import com.bluelinelabs.conductor.demo.controllers.NavigationDemoController.DisplayUpMode; import com.bluelinelabs.conductor.demo.controllers.base.BaseController; import butterknife.BindViews; @@ -26,9 +27,9 @@ protected void onViewBound(@NonNull View view) { super.onViewBound(view); for (ViewGroup childContainer : childContainers) { - Router childRouter = getChildRouter(childContainer, null).setPopsLastView(false); + Router childRouter = getChildRouter(childContainer).setPopsLastView(false); if (!childRouter.hasRootController()) { - childRouter.setRoot(RouterTransaction.with(new NavigationDemoController(0, false))); + childRouter.setRoot(RouterTransaction.with(new NavigationDemoController(0, DisplayUpMode.HIDE))); } } } diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/NavigationDemoController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/NavigationDemoController.java index 61351529..09bffb9d 100644 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/NavigationDemoController.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/NavigationDemoController.java @@ -7,6 +7,8 @@ import android.view.ViewGroup; import android.widget.TextView; +import com.bluelinelabs.conductor.ControllerChangeHandler; +import com.bluelinelabs.conductor.ControllerChangeType; import com.bluelinelabs.conductor.RouterTransaction; import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler; import com.bluelinelabs.conductor.demo.R; @@ -19,27 +21,42 @@ public class NavigationDemoController extends BaseController { + public enum DisplayUpMode { + SHOW, + SHOW_FOR_CHILDREN_ONLY, + HIDE; + + private DisplayUpMode getDisplayUpModeForChild() { + switch (this) { + case HIDE: + return HIDE; + default: + return SHOW; + } + } + } + public static final String TAG_UP_TRANSACTION = "NavigationDemoController.up"; private static final String KEY_INDEX = "NavigationDemoController.index"; - private static final String KEY_DISPLAY_UP = "NavigationDemoController.displayUp"; + private static final String KEY_DISPLAY_UP_MODE = "NavigationDemoController.displayUpMode"; @BindView(R.id.tv_title) TextView tvTitle; private int index; - private boolean displayUp; + private DisplayUpMode displayUpMode; - public NavigationDemoController(int index, boolean displayUpButton) { + public NavigationDemoController(int index, DisplayUpMode displayUpMode) { this(new BundleBuilder(new Bundle()) .putInt(KEY_INDEX, index) - .putBoolean(KEY_DISPLAY_UP, displayUpButton) + .putInt(KEY_DISPLAY_UP_MODE, displayUpMode.ordinal()) .build()); } public NavigationDemoController(Bundle args) { super(args); index = args.getInt(KEY_INDEX); - displayUp = args.getBoolean(KEY_DISPLAY_UP); + displayUpMode = DisplayUpMode.values()[args.getInt(KEY_DISPLAY_UP_MODE)]; } @NonNull @@ -52,7 +69,7 @@ protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup protected void onViewBound(@NonNull View view) { super.onViewBound(view); - if (!displayUp) { + if (displayUpMode != DisplayUpMode.SHOW) { view.findViewById(R.id.btn_up).setVisibility(View.GONE); } @@ -60,13 +77,36 @@ protected void onViewBound(@NonNull View view) { tvTitle.setText(getResources().getString(R.string.navigation_title, index)); } + @Override + protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { + super.onChangeEnded(changeHandler, changeType); + + setButtonsEnabled(true); + } + + @Override + protected void onChangeStarted(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { + super.onChangeStarted(changeHandler, changeType); + + setButtonsEnabled(false); + } + @Override protected String getTitle() { return "Navigation Demos"; } + private void setButtonsEnabled(boolean enabled) { + final View view = getView(); + if (view != null) { + view.findViewById(R.id.btn_next).setEnabled(enabled); + view.findViewById(R.id.btn_up).setEnabled(enabled); + view.findViewById(R.id.btn_pop_to_root).setEnabled(enabled); + } + } + @OnClick(R.id.btn_next) void onNextClicked() { - getRouter().pushController(RouterTransaction.with(new NavigationDemoController(index + 1, displayUp)) + getRouter().pushController(RouterTransaction.with(new NavigationDemoController(index + 1, displayUpMode.getDisplayUpModeForChild())) .pushChangeHandler(new HorizontalChangeHandler()) .popChangeHandler(new HorizontalChangeHandler())); } diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/OverlayController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/OverlayController.java deleted file mode 100755 index b2d99aa8..00000000 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/OverlayController.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.bluelinelabs.conductor.demo.controllers; - -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.text.method.LinkMovementMethod; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import com.bluelinelabs.conductor.demo.R; -import com.bluelinelabs.conductor.demo.controllers.base.BaseController; -import com.bluelinelabs.conductor.demo.util.BundleBuilder; - -import butterknife.BindView; - -public class OverlayController extends BaseController { - - private static final String KEY_TEXT = "OverlayController.text"; - - @BindView(R.id.text_view) TextView textView; - - public OverlayController(CharSequence text) { - this(new BundleBuilder(new Bundle()) - .putCharSequence(KEY_TEXT, text) - .build()); - } - - public OverlayController(Bundle args) { - super(args); - } - - @NonNull - @Override - protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { - return inflater.inflate(R.layout.controller_overlay, container, false); - } - - @Override - public void onViewBound(@NonNull View view) { - super.onViewBound(view); - textView.setText(getArgs().getCharSequence(KEY_TEXT)); - textView.setMovementMethod(LinkMovementMethod.getInstance()); - } - -} diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/PagerController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/PagerController.java index fae5a1bb..2f34de4a 100644 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/PagerController.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/PagerController.java @@ -8,9 +8,13 @@ import android.view.ViewGroup; import com.bluelinelabs.conductor.Controller; +import com.bluelinelabs.conductor.Router; +import com.bluelinelabs.conductor.RouterTransaction; import com.bluelinelabs.conductor.demo.R; import com.bluelinelabs.conductor.demo.controllers.base.BaseController; -import com.bluelinelabs.conductor.support.ControllerPagerAdapter; +import com.bluelinelabs.conductor.support.RouterPagerAdapter; + +import java.util.Locale; import butterknife.BindView; @@ -21,13 +25,16 @@ public class PagerController extends BaseController { @BindView(R.id.tab_layout) TabLayout tabLayout; @BindView(R.id.view_pager) ViewPager viewPager; - private final ControllerPagerAdapter pagerAdapter; + private final RouterPagerAdapter pagerAdapter; public PagerController() { - pagerAdapter = new ControllerPagerAdapter(this, false) { + pagerAdapter = new RouterPagerAdapter(this) { @Override - public Controller getItem(int position) { - return new ChildController(String.format("Child #%d (Swipe to see more)", position), PAGE_COLORS[position], true); + public void configureRouter(@NonNull Router router, int position) { + if (!router.hasRootController()) { + Controller page = new ChildController(String.format(Locale.getDefault(), "Child #%d (Swipe to see more)", position), PAGE_COLORS[position], true); + router.setRoot(RouterTransaction.with(page)); + } } @Override @@ -50,7 +57,7 @@ protected void onViewBound(@NonNull View view) { } @Override - protected void onDestroyView(View view) { + protected void onDestroyView(@NonNull View view) { viewPager.setAdapter(null); super.onDestroyView(view); } diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/ParentController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/ParentController.java index 1a05b16d..5a72ad4a 100644 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/ParentController.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/ParentController.java @@ -42,7 +42,7 @@ protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @No private void addChild(final int index) { @IdRes final int frameId = getResources().getIdentifier("child_content_" + (index + 1), "id", getActivity().getPackageName()); final ViewGroup container = (ViewGroup)getView().findViewById(frameId); - final Router childRouter = getChildRouter(container, null).setPopsLastView(true); + final Router childRouter = getChildRouter(container).setPopsLastView(true); if (!childRouter.hasRootController()) { ChildController childController = new ChildController("Child Controller #" + index, ColorUtil.getMaterialColor(getResources(), index), false); @@ -50,17 +50,19 @@ private void addChild(final int index) { childController.addLifecycleListener(new LifecycleListener() { @Override public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { - 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); + 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); + } } } } diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/RxLifecycle2Controller.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/RxLifecycle2Controller.java new file mode 100755 index 00000000..8eb6b060 --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/RxLifecycle2Controller.java @@ -0,0 +1,165 @@ +package com.bluelinelabs.conductor.demo.controllers; + +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.bluelinelabs.conductor.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.bindUntilEvent(ControllerEvent.DESTROY)) + .subscribe(new Consumer() { + @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); + view.setBackgroundColor(ContextCompat.getColor(container.getContext(), R.color.brown_300)); + unbinder = ButterKnife.bind(this, view); + + tvTitle.setText(getResources().getString(R.string.rxlifecycle_title, TAG)); + + Observable.interval(1, TimeUnit.SECONDS) + .doOnDispose(new Action() { + @Override + public void run() { + Log.i(TAG, "Disposing from onCreateView)"); + } + }) + .compose(this.bindUntilEvent(ControllerEvent.DESTROY_VIEW)) + .subscribe(new Consumer() { + @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.bindUntilEvent(ControllerEvent.DETACH)) + .subscribe(new Consumer() { + @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())); + } +} diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/RxLifecycleController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/RxLifecycleController.java index 6329edf0..ac46f7ff 100755 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/RxLifecycleController.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/RxLifecycleController.java @@ -1,36 +1,45 @@ package com.bluelinelabs.conductor.demo.controllers; import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import com.bluelinelabs.conductor.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.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"; @BindView(R.id.tv_title) TextView tvTitle; - public RxLifecycleController() { + private Unbinder unbinder; + private boolean hasExited; + public RxLifecycleController() { Observable.interval(1, TimeUnit.SECONDS) .doOnUnsubscribe(new Action0() { @Override @@ -47,10 +56,15 @@ public void call(Long num) { }); } + @NonNull @Override - public void onViewBound(@NonNull View view) { + protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { Log.i(TAG, "onCreateView() called"); + View view = inflater.inflate(R.layout.controller_rxlifecycle, container, false); + view.setBackgroundColor(ContextCompat.getColor(container.getContext(), R.color.teal_300)); + unbinder = ButterKnife.bind(this, view); + tvTitle.setText(getResources().getString(R.string.rxlifecycle_title, TAG)); Observable.interval(1, TimeUnit.SECONDS) @@ -67,6 +81,8 @@ public void call(Long num) { Log.i(TAG, "Started in onCreateView(), running until onDestroyView(): " + num); } }); + + return view; } @Override @@ -75,6 +91,8 @@ protected void onAttach(@NonNull View view) { Log.i(TAG, "onAttach() called"); + (((ActionBarProvider)getActivity()).getSupportActionBar()).setTitle("RxLifecycle Demo"); + Observable.interval(1, TimeUnit.SECONDS) .doOnUnsubscribe(new Action0() { @Override @@ -92,10 +110,13 @@ public void call(Long num) { } @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,17 +131,20 @@ public void onDestroy() { super.onDestroy(); Log.i(TAG, "onDestroy() called"); - } - @Override - protected String getTitle() { - return "RxLifecycle Demo"; + if (hasExited) { + DemoApplication.refWatcher.watch(this); + } } - @NonNull @Override - protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { - return inflater.inflate(R.layout.controller_rxlifecycle, container, false); + 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() { diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/base/ButterKnifeController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/base/ButterKnifeController.java index b56e873b..f30a9603 100644 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/base/ButterKnifeController.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/base/ButterKnifeController.java @@ -6,12 +6,12 @@ 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; @@ -34,7 +34,7 @@ protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup protected void onViewBound(@NonNull View view) { } @Override - protected void onDestroyView(View view) { + protected void onDestroyView(@NonNull View view) { super.onDestroyView(view); unbinder.unbind(); unbinder = null; diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/util/AnimUtils.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/util/AnimUtils.java new file mode 100644 index 00000000..bbb8349f --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/util/AnimUtils.java @@ -0,0 +1,377 @@ +/* + * Copyright 2015 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bluelinelabs.conductor.demo.util; + +import android.animation.Animator; +import android.animation.TimeInterpolator; +import android.annotation.TargetApi; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.v4.view.animation.FastOutLinearInInterpolator; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.support.v4.view.animation.LinearOutSlowInInterpolator; +import android.transition.Transition; +import android.util.ArrayMap; +import android.util.FloatProperty; +import android.util.IntProperty; +import android.util.Property; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; + +/** + * Utility methods for working with animations. + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class AnimUtils { + + private AnimUtils() { } + + private static Interpolator fastOutSlowIn; + private static Interpolator fastOutLinearIn; + private static Interpolator linearOutSlowIn; + private static Interpolator linear; + + @NonNull + public static Interpolator getFastOutSlowInInterpolator() { + if (fastOutSlowIn == null) { + fastOutSlowIn = new FastOutSlowInInterpolator(); + } + return fastOutSlowIn; + } + + @NonNull + public static Interpolator getFastOutLinearInInterpolator() { + if (fastOutLinearIn == null) { + fastOutLinearIn = new FastOutLinearInInterpolator(); + } + return fastOutLinearIn; + } + + @NonNull + public static Interpolator getLinearOutSlowInInterpolator() { + if (linearOutSlowIn == null) { + linearOutSlowIn = new LinearOutSlowInInterpolator(); + } + return linearOutSlowIn; + } + + @NonNull + public static Interpolator getLinearInterpolator() { + if (linear == null) { + linear = new LinearInterpolator(); + } + return linear; + } + + /** + * Linear interpolate between a and b with parameter t. + */ + public static float lerp(float a, float b, float t) { + return a + (b - a) * t; + } + + /** + * A delegate for creating a {@link Property} of int type. + */ + public static abstract class IntProp { + + public final String name; + + public IntProp(String name) { + this.name = name; + } + + public abstract void set(T object, int value); + public abstract int get(T object); + } + + /** + * The animation framework has an optimization for Properties of type + * int but it was only made public in API24, so wrap the impl in our own type + * and conditionally create the appropriate type, delegating the implementation. + */ + public static Property createIntProperty(final IntProp impl) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return new IntProperty(impl.name) { + @Override + public Integer get(T object) { + return impl.get(object); + } + + @Override + public void setValue(T object, int value) { + impl.set(object, value); + } + }; + } else { + return new Property(Integer.class, impl.name) { + @Override + public Integer get(T object) { + return impl.get(object); + } + + @Override + public void set(T object, Integer value) { + impl.set(object, value); + } + }; + } + } + + /** + * A delegate for creating a {@link Property} of float type. + */ + public static abstract class FloatProp { + + public final String name; + + protected FloatProp(String name) { + this.name = name; + } + + public abstract void set(T object, float value); + public abstract float get(T object); + } + + /** + * The animation framework has an optimization for Properties of type + * float but it was only made public in API24, so wrap the impl in our own type + * and conditionally create the appropriate type, delegating the implementation. + */ + public static Property createFloatProperty(final FloatProp impl) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return new FloatProperty(impl.name) { + @Override + public Float get(T object) { + return impl.get(object); + } + + @Override + public void setValue(T object, float value) { + impl.set(object, value); + } + }; + } else { + return new Property(Float.class, impl.name) { + @Override + public Float get(T object) { + return impl.get(object); + } + + @Override + public void set(T object, Float value) { + impl.set(object, value); + } + }; + } + } + + /** + * https://halfthought.wordpress.com/2014/11/07/reveal-transition/ + *

+ * Interrupting Activity transitions can yield an OperationNotSupportedException when the + * transition tries to pause the animator. Yikes! We can fix this by wrapping the Animator: + */ + @TargetApi(Build.VERSION_CODES.KITKAT) + public static class NoPauseAnimator extends Animator { + private final Animator mAnimator; + private final ArrayMap mListeners = new ArrayMap<>(); + + public NoPauseAnimator(Animator animator) { + mAnimator = animator; + } + + @Override + public void addListener(AnimatorListener listener) { + AnimatorListener wrapper = new AnimatorListenerWrapper(this, listener); + if (!mListeners.containsKey(listener)) { + mListeners.put(listener, wrapper); + mAnimator.addListener(wrapper); + } + } + + @Override + public void cancel() { + mAnimator.cancel(); + } + + @Override + public void end() { + mAnimator.end(); + } + + @Override + public long getDuration() { + return mAnimator.getDuration(); + } + + @Override + public TimeInterpolator getInterpolator() { + return mAnimator.getInterpolator(); + } + + @Override + public void setInterpolator(TimeInterpolator timeInterpolator) { + mAnimator.setInterpolator(timeInterpolator); + } + + @Override + public ArrayList getListeners() { + return new ArrayList<>(mListeners.keySet()); + } + + @Override + public long getStartDelay() { + return mAnimator.getStartDelay(); + } + + @Override + public void setStartDelay(long delayMS) { + mAnimator.setStartDelay(delayMS); + } + + @Override + public boolean isPaused() { + return mAnimator.isPaused(); + } + + @Override + public boolean isRunning() { + return mAnimator.isRunning(); + } + + @Override + public boolean isStarted() { + return mAnimator.isStarted(); + } + + /* We don't want to override pause or resume methods because we don't want them + * to affect mAnimator. + public void pause(); + + public void resume(); + + public void addPauseListener(AnimatorPauseListener listener); + + public void removePauseListener(AnimatorPauseListener listener); + */ + + @Override + public void removeAllListeners() { + mListeners.clear(); + mAnimator.removeAllListeners(); + } + + @Override + public void removeListener(AnimatorListener listener) { + AnimatorListener wrapper = mListeners.get(listener); + if (wrapper != null) { + mListeners.remove(listener); + mAnimator.removeListener(wrapper); + } + } + + @Override + public Animator setDuration(long durationMS) { + mAnimator.setDuration(durationMS); + return this; + } + + @Override + public void setTarget(Object target) { + mAnimator.setTarget(target); + } + + @Override + public void setupEndValues() { + mAnimator.setupEndValues(); + } + + @Override + public void setupStartValues() { + mAnimator.setupStartValues(); + } + + @Override + public void start() { + mAnimator.start(); + } + } + + public static class AnimatorListenerWrapper implements Animator.AnimatorListener { + private final Animator mAnimator; + private final Animator.AnimatorListener mListener; + + AnimatorListenerWrapper(Animator animator, Animator.AnimatorListener listener) { + mAnimator = animator; + mListener = listener; + } + + @Override + public void onAnimationStart(Animator animator) { + mListener.onAnimationStart(mAnimator); + } + + @Override + public void onAnimationEnd(Animator animator) { + mListener.onAnimationEnd(mAnimator); + } + + @Override + public void onAnimationCancel(Animator animator) { + mListener.onAnimationCancel(mAnimator); + } + + @Override + public void onAnimationRepeat(Animator animator) { + mListener.onAnimationRepeat(mAnimator); + } + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static abstract class TransitionEndListener implements Transition.TransitionListener { + public abstract void onTransitionCompleted(Transition transition); + + @Override + public void onTransitionStart(Transition transition) { + + } + + @Override + public void onTransitionEnd(Transition transition) { + onTransitionCompleted(transition); + } + + @Override + public void onTransitionCancel(Transition transition) { + onTransitionCompleted(transition); + } + + @Override + public void onTransitionPause(Transition transition) { + + } + + @Override + public void onTransitionResume(Transition transition) { + + } + } + +} diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/widget/ElasticDragDismissFrameLayout.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/widget/ElasticDragDismissFrameLayout.java index 65865b8f..c1b1271d 100644 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/widget/ElasticDragDismissFrameLayout.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/widget/ElasticDragDismissFrameLayout.java @@ -16,6 +16,7 @@ package com.bluelinelabs.conductor.demo.widget; +import android.annotation.TargetApi; import android.content.Context; import android.support.v4.view.NestedScrollingParent; import android.support.v4.view.animation.FastOutSlowInInterpolator; @@ -32,6 +33,7 @@ * Applies an elasticity factor to reduce movement as you approach the given dismiss distance. * Optionally also scales down content during drag. */ +@TargetApi(21) public class ElasticDragDismissFrameLayout extends FrameLayout implements NestedScrollingParent { public static abstract class ElasticDragDismissCallback { diff --git a/demo/src/main/res/animator/raise.xml b/demo/src/main/res/animator/raise.xml new file mode 100644 index 00000000..265f8431 --- /dev/null +++ b/demo/src/main/res/animator/raise.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/demo/src/main/res/drawable-hdpi/ic_arrow_forward_white_36dp.png b/demo/src/main/res/drawable-hdpi/ic_arrow_forward_white_36dp.png deleted file mode 100644 index 7dd23b7f..00000000 Binary files a/demo/src/main/res/drawable-hdpi/ic_arrow_forward_white_36dp.png and /dev/null differ diff --git a/demo/src/main/res/drawable-mdpi/ic_arrow_forward_white_36dp.png b/demo/src/main/res/drawable-mdpi/ic_arrow_forward_white_36dp.png deleted file mode 100644 index b8c16a38..00000000 Binary files a/demo/src/main/res/drawable-mdpi/ic_arrow_forward_white_36dp.png and /dev/null differ diff --git a/demo/src/main/res/drawable-xhdpi/chicago.jpg b/demo/src/main/res/drawable-xhdpi/chicago.jpg new file mode 100644 index 00000000..17190197 Binary files /dev/null and b/demo/src/main/res/drawable-xhdpi/chicago.jpg differ diff --git a/demo/src/main/res/drawable-xhdpi/ic_arrow_forward_white_36dp.png b/demo/src/main/res/drawable-xhdpi/ic_arrow_forward_white_36dp.png deleted file mode 100644 index 8c4c394e..00000000 Binary files a/demo/src/main/res/drawable-xhdpi/ic_arrow_forward_white_36dp.png and /dev/null differ diff --git a/demo/src/main/res/drawable-xhdpi/jakarta.jpg b/demo/src/main/res/drawable-xhdpi/jakarta.jpg new file mode 100644 index 00000000..77bda03b Binary files /dev/null and b/demo/src/main/res/drawable-xhdpi/jakarta.jpg differ diff --git a/demo/src/main/res/drawable-xhdpi/london.jpg b/demo/src/main/res/drawable-xhdpi/london.jpg new file mode 100644 index 00000000..65e1d78a Binary files /dev/null and b/demo/src/main/res/drawable-xhdpi/london.jpg differ diff --git a/demo/src/main/res/drawable-xhdpi/sao_paulo.jpg b/demo/src/main/res/drawable-xhdpi/sao_paulo.jpg new file mode 100644 index 00000000..27cc87e9 Binary files /dev/null and b/demo/src/main/res/drawable-xhdpi/sao_paulo.jpg differ diff --git a/demo/src/main/res/drawable-xhdpi/tokyo.jpg b/demo/src/main/res/drawable-xhdpi/tokyo.jpg new file mode 100644 index 00000000..189fbd94 Binary files /dev/null and b/demo/src/main/res/drawable-xhdpi/tokyo.jpg differ diff --git a/demo/src/main/res/drawable-xxhdpi/ic_arrow_forward_white_36dp.png b/demo/src/main/res/drawable-xxhdpi/ic_arrow_forward_white_36dp.png deleted file mode 100644 index e14d1c0a..00000000 Binary files a/demo/src/main/res/drawable-xxhdpi/ic_arrow_forward_white_36dp.png and /dev/null differ diff --git a/demo/src/main/res/drawable-xxxhdpi/ic_arrow_forward_white_36dp.png b/demo/src/main/res/drawable-xxxhdpi/ic_arrow_forward_white_36dp.png deleted file mode 100644 index f8cf79f9..00000000 Binary files a/demo/src/main/res/drawable-xxxhdpi/ic_arrow_forward_white_36dp.png and /dev/null differ diff --git a/demo/src/main/res/drawable/dialog_bg.xml b/demo/src/main/res/drawable/dialog_bg.xml new file mode 100644 index 00000000..00c9b0c1 --- /dev/null +++ b/demo/src/main/res/drawable/dialog_bg.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/demo/src/main/res/drawable/ic_arrow_forward_white_36dp.xml b/demo/src/main/res/drawable/ic_arrow_forward_white_36dp.xml new file mode 100644 index 00000000..de398fca --- /dev/null +++ b/demo/src/main/res/drawable/ic_arrow_forward_white_36dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/demo/src/main/res/drawable/ic_github_face.xml b/demo/src/main/res/drawable/ic_github_face.xml new file mode 100644 index 00000000..56580b93 --- /dev/null +++ b/demo/src/main/res/drawable/ic_github_face.xml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/demo/src/main/res/layout-land/controller_master_detail_list.xml b/demo/src/main/res/layout-land/controller_master_detail_list.xml index 5c602077..6225634b 100644 --- a/demo/src/main/res/layout-land/controller_master_detail_list.xml +++ b/demo/src/main/res/layout-land/controller_master_detail_list.xml @@ -1,9 +1,11 @@ + android:layout_height="match_parent" + android:orientation="horizontal" + tools:ignore="InconsistentLayout"> + \ No newline at end of file diff --git a/demo/src/main/res/layout/controller_city_grid.xml b/demo/src/main/res/layout/controller_city_grid.xml new file mode 100644 index 00000000..73f7f3c9 --- /dev/null +++ b/demo/src/main/res/layout/controller_city_grid.xml @@ -0,0 +1,35 @@ + + + + + + + + + + \ No newline at end of file diff --git a/demo/src/main/res/layout/controller_dialog.xml b/demo/src/main/res/layout/controller_dialog.xml new file mode 100644 index 00000000..027b51e3 --- /dev/null +++ b/demo/src/main/res/layout/controller_dialog.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + +