Compare commits

...

12 Commits

Author SHA1 Message Date
EricKuck cd11ac9d6b Version bump for 3.0.0 release 2020-11-05 14:40:15 -06:00
EricKuck e78347709b Revert to Java 7 - we'll be kotlinizing soon anyway 2020-11-05 14:12:34 -06:00
EricKuck 341debc5b9 Bump back kotlin version 2020-11-04 16:59:35 -06:00
EricKuck 2346e48154 Fix errant onAttach calls for children whose parents were popped on attach
Fixes #481
Fixes #426
Fixes #407
2020-11-04 16:58:04 -06:00
EricKuck 4174e12958 Fix bug where old views could remain in the hierarchy after being popped
Fixes #608
2020-11-04 16:06:55 -06:00
EricKuck c0abed0813 Fix popping a controller during view creation
Fixes #348
2020-11-04 12:58:50 -06:00
Eric Kuck 6fdb1d6ed3 Adds ViewPager2 Adapter (#594) 2020-11-03 16:17:30 -06:00
Eric Kuck 3334b8e21f Don't remove host references on configuration changes (#629)
Should fix #567
2020-11-03 16:13:17 -06:00
Paul Woitaschek 240424dc63 Lint changehandler subclass fix (#627)
* Updated gradle to 6.7

* Updated AGP to 4.1.0 and infer lint from the agp version.

* Suppress the UnstableApiUsage on the lint tests.

* Added a test case where private change handler extends a base class that extends from the ControllerChangeHandler.

* Fixed the test case by checking the all super classes.

* Added a controller stub for the controleller detector tests.

* Added a testcase for when a controller has a base class.

* Added a testcase for when a controller has a base class.

* Accept licenses before installing for travis.

* Attempt to not add the android components.

* Removed the licenses command.

* Added the preview license too.
2020-10-27 08:23:55 +01:00
Eric Kuck f768e9ab00 Pushing to child router while unattached fixed. Closes #606 (#607) 2020-05-13 14:24:41 -05:00
Islom Matnazarov 4c89124683 Update CircularRevealChangeHandler to not ignore removesFromViewOnPush (#600) 2020-05-11 11:17:09 -05:00
Eric Kuck e0bbd48935 Remove platform transition module 2020-05-04 20:14:33 -05:00
54 changed files with 1045 additions and 1338 deletions
+1 -3
View File
@@ -35,6 +35,4 @@ pom.xml.*
local.properties
*.prefs
# The keystore file
app/spothero-release.keystore
.DS_Store
+5 -8
View File
@@ -1,13 +1,10 @@
language: android
android:
components:
- tools
- build-tools-28.0.3
- android-28
- extra-android-m2repository
licenses:
- '.+'
before_install:
- mkdir "$ANDROID_HOME/licenses" || true
- echo "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_HOME/licenses/android-sdk-license"
- echo "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
- yes | sdkmanager --update
script:
- ./gradlew test
+9 -6
View File
@@ -20,22 +20,25 @@ Conductor is architecture-agnostic and does not try to force any design decision
## Installation
```gradle
implementation 'com.bluelinelabs:conductor:3.0.0-rc5'
implementation 'com.bluelinelabs:conductor:3.0.0'
// AndroidX Transition change handlers:
implementation 'com.bluelinelabs:conductor-androidx-transition:3.0.0-rc5'
implementation 'com.bluelinelabs:conductor-androidx-transition:3.0.0'
// ViewPager PagerAdapter:
implementation 'com.bluelinelabs:conductor-viewpager:3.0.0-rc5'
implementation 'com.bluelinelabs:conductor-viewpager:3.0.0'
// ViewPager2 Adapter:
implementation 'com.bluelinelabs:conductor-viewpager2:3.0.0'
// RxJava2 lifecycle support:
implementation 'com.bluelinelabs:conductor-rxlifecycle2:3.0.0-rc5'
implementation 'com.bluelinelabs:conductor-rxlifecycle2:3.0.0'
// RxJava2 Autodispose support:
implementation 'com.bluelinelabs:conductor-autodispose:3.0.0-rc5'
implementation 'com.bluelinelabs:conductor-autodispose:3.0.0'
// Lifecycle-aware Controllers (architecture components):
implementation 'com.bluelinelabs:conductor-archlifecycle:3.0.0-rc5'
implementation 'com.bluelinelabs:conductor-archlifecycle:3.0.0'
```
**SNAPSHOT**
+2 -20
View File
@@ -6,7 +6,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3'
classpath "com.android.tools.build:gradle:$agpVersion"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "com.vanniktech:gradle-maven-publish-plugin:$mvnPublishVersion"
}
@@ -15,27 +15,9 @@ buildscript {
allprojects {
repositories {
mavenCentral()
maven { url 'https://maven.google.com' }
google()
jcenter()
}
plugins.withType(com.android.build.gradle.BasePlugin).configureEach { plugin ->
plugin.extension.compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
tasks.withType(JavaCompile).configureEach { task ->
task.sourceCompatibility = JavaVersion.VERSION_1_8
task.targetCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile).configureEach { task ->
task.kotlinOptions {
jvmTarget = "1.8"
}
}
}
apply from: rootProject.file('dependencies.gradle')
@@ -14,7 +14,6 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UTypeReferenceExpression;
import java.util.Collections;
import java.util.List;
@@ -48,13 +47,7 @@ public final class ControllerChangeHandlerIssueDetector extends Detector impleme
return;
}
boolean hasSuperType = false;
for (UTypeReferenceExpression superType : node.getUastSuperTypes()) {
if (CLASS_NAME.equals(superType.asRenderString())) {
hasSuperType = true;
break;
}
}
final boolean hasSuperType = evaluator.extendsClass(node.getPsi(), CLASS_NAME, true);
if (!hasSuperType) {
return;
}
@@ -16,7 +16,6 @@ import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UParameter;
import org.jetbrains.uast.UTypeReferenceExpression;
import java.util.Collections;
import java.util.List;
@@ -35,7 +34,7 @@ public final class ControllerIssueDetector extends Detector implements Detector.
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.<Class<? extends UElement>>singletonList(UClass.class);
return Collections.singletonList(UClass.class);
}
@Override
@@ -49,13 +48,7 @@ public final class ControllerIssueDetector extends Detector implements Detector.
return;
}
boolean hasSuperType = false;
for (UTypeReferenceExpression superType : node.getUastSuperTypes()) {
if (CLASS_NAME.equals(superType.asRenderString())) {
hasSuperType = true;
break;
}
}
final boolean hasSuperType = evaluator.extendsClass(node.getPsi(), CLASS_NAME, true);
if (!hasSuperType) {
return;
}
@@ -1,24 +1,27 @@
package com.bluelinelabs.conductor.lint;
import com.android.tools.lint.checks.infrastructure.LintDetectorTest;
import org.intellij.lang.annotations.Language;
import org.junit.Test;
import static com.android.tools.lint.checks.infrastructure.TestFiles.java;
import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint;
@SuppressWarnings("UnstableApiUsage")
public class ControllerChangeHandlerDetectorTest {
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"
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ "^\n"
+ "1 errors, 0 warnings\n";
private final LintDetectorTest.TestFile controllerChangeHandlerStub = java(
"package com.bluelinelabs.conductor;\n"
+ "abstract class ControllerChangeHandler {}"
);
@Test
public void testWithNoConstructor() {
@Language("JAVA") String source = ""
@@ -27,7 +30,7 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
@@ -42,7 +45,7 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
@@ -57,7 +60,7 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(CONSTRUCTOR);
@@ -73,7 +76,7 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
@@ -88,7 +91,7 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(CONSTRUCTOR);
@@ -103,10 +106,32 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(PRIVATE_CLASS_ERROR);
.expect("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");
}
@Test
public void testWithPrivateClassOfBaseClass() {
@Language("JAVA") String baseClass = ""
+ "package test;\n"
+ "abstract class BaseChangeHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {}";
@Language("JAVA") String source = ""
+ "package test;\n"
+ "private class SampleHandler extends test.BaseChangeHandler {}";
lint()
.files(controllerChangeHandlerStub, java(baseClass), java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect("src/test/SampleHandler.java:2: Error: This ControllerChangeHandler class should be public (test.SampleHandler) [ValidControllerChangeHandler]\n" +
"private class SampleHandler extends test.BaseChangeHandler {}\n" +
"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" +
"1 errors, 0 warnings");
}
}
@@ -1,24 +1,33 @@
package com.bluelinelabs.conductor.lint;
import com.android.tools.lint.checks.infrastructure.LintDetectorTest;
import org.intellij.lang.annotations.Language;
import org.junit.Test;
import static com.android.tools.lint.checks.infrastructure.TestFiles.java;
import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint;
@SuppressWarnings("UnstableApiUsage")
public class ControllerDetectorTest {
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";
+ "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";
private final LintDetectorTest.TestFile controllerStub = java(
"package com.bluelinelabs.conductor;\n"
+ "abstract class Controller {}"
);
@Test
public void testWithNoConstructor() {
@Language("JAVA") String source = ""
@@ -27,7 +36,7 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
@@ -42,7 +51,7 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
@@ -57,7 +66,7 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(CONSTRUCTOR_ERROR);
@@ -73,12 +82,38 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
}
@Test
public void testWithBaseClassAndPrivateConstructor() {
@Language("JAVA")
String baseClass = ""
+ "package test;\n"
+ "public class BaseController extends com.bluelinelabs.conductor.Controller {}";
@Language("JAVA")
String source = ""
+ "package test;\n"
+ "public class SampleController extends BaseController {\n"
+ " private SampleController() { }\n"
+ "}";
lint()
.files(controllerStub, java(baseClass), java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(
"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 BaseController {\n" +
"^\n" +
"1 errors, 0 warnings"
);
}
@Test
public void testWithPrivateConstructor() {
@Language("JAVA") String source = ""
@@ -88,7 +123,7 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(CONSTRUCTOR_ERROR);
@@ -103,7 +138,7 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(CLASS_ERROR);
@@ -1,3 +1,3 @@
POM_NAME=Conductor AndroidX Transition Extensions
POM_ARTIFACT_ID=conductor-transition-androidx
POM_ARTIFACT_ID=conductor-androidx-transition
POM_PACKAGING=aar
@@ -1,3 +0,0 @@
POM_NAME=Conductor Platform Transition Extensions
POM_ARTIFACT_ID=conductor-transition-platform
POM_PACKAGING=aar
@@ -1,3 +0,0 @@
<manifest package="com.bluelinelabs.conductor.platformtransition">
<application />
</manifest>
@@ -1,647 +0,0 @@
package com.bluelinelabs.conductor.changehandler.platformtransition;
import android.annotation.TargetApi;
import android.app.SharedElementCallback;
import android.graphics.Rect;
import android.os.Build;
import android.transition.Transition;
import android.transition.Transition.TransitionListener;
import android.transition.TransitionSet;
import android.util.ArrayMap;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnPreDrawListener;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.internal.TransitionUtils;
import java.util.ArrayList;
import java.util.List;
/**
* A TransitionChangeHandler that facilitates using different Transitions for the entering view, the exiting view,
* and shared elements between the two.
* <p/>
* Note that this class uses Android's <b>platform</b> {@link Transition}. If you're using androidx transitions, consider
* using the {@code SharedElementTransitionChangeHandler} provided by the {@code androidx-transitions} Conductor module.
*/
// Much of this class is based on FragmentTransition.java and FragmentTransitionCompat21.java from the Android support library
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class SharedElementTransitionChangeHandler extends TransitionChangeHandler {
// A map of from -> to names. Generally these will be the same.
@NonNull final ArrayMap<String, String> sharedElementNames = new ArrayMap<>();
@NonNull final List<String> waitForTransitionNames = new ArrayList<>();
@NonNull final List<ViewParentPair> removedViews = new ArrayList<>();
@Nullable Transition exitTransition;
@Nullable Transition enterTransition;
@Nullable Transition sharedElementTransition;
@Nullable private SharedElementCallback exitTransitionCallback;
@Nullable private SharedElementCallback enterTransitionCallback;
@NonNull
@Override
protected final Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
exitTransition = getExitTransition(container, from, to, isPush);
enterTransition = getEnterTransition(container, from, to, isPush);
sharedElementTransition = getSharedElementTransition(container, from, to, isPush);
exitTransitionCallback = getExitTransitionCallback(container, from, to, isPush);
enterTransitionCallback = getEnterTransitionCallback(container, from, to, isPush);
if (enterTransition == null && sharedElementTransition == null && exitTransition == null) {
throw new IllegalStateException("SharedElementTransitionChangeHandler must have at least one transaction.");
}
return mergeTransitions(isPush);
}
@Override
public void prepareForTransition(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, @NonNull final Transition transition, final boolean isPush, @NonNull final OnTransitionPreparedListener onTransitionPreparedListener) {
OnTransitionPreparedListener listener = () -> {
configureTransition(container, from, to, transition, isPush);
onTransitionPreparedListener.onPrepared();
};
configureSharedElements(container, from, to, isPush);
if (to != null && to.getParent() == null && waitForTransitionNames.size() > 0) {
waitOnAllTransitionNames(to, listener);
container.addView(to);
} else {
listener.onPrepared();
}
}
@Override
public final void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @Nullable Transition transition, boolean isPush) {
if (to != null && removedViews.size() > 0) {
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 onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
removedViews.clear();
}
@Override
protected void onEnd() {
exitTransition = null;
enterTransition = null;
sharedElementTransition = null;
}
void configureTransition(@NonNull final ViewGroup container, @Nullable View from, @Nullable View to, @NonNull final Transition transition, boolean isPush) {
final View nonExistentView = new View(container.getContext());
List<View> fromSharedElements = new ArrayList<>();
List<View> toSharedElements = new ArrayList<>();
configureSharedElements(container, nonExistentView, to, from, isPush, fromSharedElements, toSharedElements);
List<View> exitingViews = exitTransition != null ? configureEnteringExitingViews(exitTransition, from, fromSharedElements, nonExistentView) : null;
if (exitingViews == null || exitingViews.isEmpty()) {
exitTransition = null;
}
if (enterTransition != null) {
enterTransition.addTarget(nonExistentView);
}
final List<View> enteringViews = new ArrayList<>();
scheduleRemoveTargets(transition, enterTransition, enteringViews, exitTransition, exitingViews, sharedElementTransition, toSharedElements);
scheduleTargetChange(container, to, nonExistentView, toSharedElements, enteringViews, exitingViews);
setNameOverrides(container, toSharedElements);
scheduleNameReset(container, toSharedElements);
}
private void waitOnAllTransitionNames(@NonNull final View to, @NonNull final OnTransitionPreparedListener onTransitionPreparedListener) {
OnPreDrawListener onPreDrawListener = new OnPreDrawListener() {
boolean addedSubviewListeners;
@Override
public boolean onPreDraw() {
List<View> foundViews = new ArrayList<>();
boolean allViewsFound = true;
for (String transitionName : waitForTransitionNames) {
View namedView = TransitionUtils.findNamedView(to, transitionName);
if (namedView != null) {
foundViews.add(TransitionUtils.findNamedView(to, transitionName));
} else {
allViewsFound = false;
break;
}
}
if (allViewsFound && !addedSubviewListeners) {
addedSubviewListeners = true;
waitOnChildTransitionNames(to, foundViews, this, onTransitionPreparedListener);
}
return false;
}
};
to.getViewTreeObserver().addOnPreDrawListener(onPreDrawListener);
}
void waitOnChildTransitionNames(@NonNull final View to, @NonNull List<View> foundViews, @NonNull final OnPreDrawListener parentPreDrawListener, @NonNull final OnTransitionPreparedListener onTransitionPreparedListener) {
for (final View view : foundViews) {
OneShotPreDrawListener.add(true, view, () -> {
waitForTransitionNames.remove(view.getTransitionName());
removedViews.add(new ViewParentPair(view, (ViewGroup) view.getParent()));
((ViewGroup) view.getParent()).removeView(view);
if (waitForTransitionNames.size() == 0) {
to.getViewTreeObserver().removeOnPreDrawListener(parentPreDrawListener);
to.setVisibility(View.INVISIBLE);
onTransitionPreparedListener.onPrepared();
}
});
}
}
private void scheduleTargetChange(@NonNull final ViewGroup container, @Nullable final View to, @NonNull final View nonExistentView,
@NonNull final List<View> toSharedElements, @NonNull final List<View> enteringViews, @Nullable final List<View> exitingViews) {
OneShotPreDrawListener.add(true, container, () -> {
if (enterTransition != null) {
enterTransition.removeTarget(nonExistentView);
List<View> views = configureEnteringExitingViews(enterTransition, to, toSharedElements, nonExistentView);
enteringViews.addAll(views);
}
if (exitingViews != null) {
if (exitTransition != null) {
List<View> tempExiting = new ArrayList<>();
tempExiting.add(nonExistentView);
TransitionUtils.replaceTargets(exitTransition, exitingViews, tempExiting);
}
exitingViews.clear();
exitingViews.add(nonExistentView);
}
});
}
private Transition mergeTransitions(boolean isPush) {
boolean overlap = enterTransition == null || exitTransition == null || allowTransitionOverlap(isPush);
if (overlap) {
return TransitionUtils.mergeTransitions(TransitionSet.ORDERING_TOGETHER, exitTransition, enterTransition, sharedElementTransition);
} else {
Transition staggered = TransitionUtils.mergeTransitions(TransitionSet.ORDERING_SEQUENTIAL, exitTransition, enterTransition);
return TransitionUtils.mergeTransitions(TransitionSet.ORDERING_TOGETHER, staggered, sharedElementTransition);
}
}
@NonNull List<View> configureEnteringExitingViews(@NonNull Transition transition, @Nullable View view, @NonNull List<View> sharedElements, @NonNull View nonExistentView) {
List<View> viewList = new ArrayList<>();
if (view != null) {
captureTransitioningViews(viewList, view);
}
viewList.removeAll(sharedElements);
if (!viewList.isEmpty()) {
viewList.add(nonExistentView);
TransitionUtils.addTargets(transition, viewList);
}
return viewList;
}
private void configureSharedElements(@NonNull ViewGroup container, @NonNull final View nonExistentView, @Nullable final View to, @Nullable View from,
final boolean isPush, @NonNull final List<View> fromSharedElements, @NonNull final List<View> toSharedElements) {
if (to == null || from == null) {
return;
}
ArrayMap<String, View> capturedFromSharedElements = captureFromSharedElements(from);
if (sharedElementNames.isEmpty()) {
sharedElementTransition = null;
} else if (capturedFromSharedElements != null) {
fromSharedElements.addAll(capturedFromSharedElements.values());
}
if (enterTransition == null && exitTransition == null && sharedElementTransition == null) {
return;
}
callSharedElementStartEnd(capturedFromSharedElements, true);
final Rect toEpicenter;
if (sharedElementTransition != null) {
toEpicenter = new Rect();
TransitionUtils.setTargets(sharedElementTransition, nonExistentView, fromSharedElements);
setFromEpicenter(capturedFromSharedElements);
if (enterTransition != null) {
enterTransition.setEpicenterCallback(new Transition.EpicenterCallback() {
@Override
public Rect onGetEpicenter(Transition transition) {
if (toEpicenter.isEmpty()) {
return null;
}
return toEpicenter;
}
});
}
} else {
toEpicenter = null;
}
OneShotPreDrawListener.add(true, container, () -> {
ArrayMap<String, View> capturedToSharedElements = captureToSharedElements(to, isPush);
if (capturedToSharedElements != null) {
toSharedElements.addAll(capturedToSharedElements.values());
toSharedElements.add(nonExistentView);
}
callSharedElementStartEnd(capturedToSharedElements, false);
if (sharedElementTransition != null) {
sharedElementTransition.getTargets().clear();
sharedElementTransition.getTargets().addAll(toSharedElements);
TransitionUtils.replaceTargets(sharedElementTransition, fromSharedElements, toSharedElements);
final View toEpicenterView = getToEpicenterView(capturedToSharedElements);
if (toEpicenterView != null && toEpicenter != null) {
TransitionUtils.getBoundsOnScreen(toEpicenterView, toEpicenter);
}
}
});
}
@Nullable View getToEpicenterView(@Nullable ArrayMap<String, View> toSharedElements) {
if (enterTransition != null && sharedElementNames.size() > 0 && toSharedElements != null) {
return toSharedElements.get(sharedElementNames.valueAt(0));
}
return null;
}
private void setFromEpicenter(@Nullable ArrayMap<String, View> fromSharedElements) {
if (sharedElementNames.size() > 0 && fromSharedElements != null) {
final View fromEpicenterView = fromSharedElements.get(sharedElementNames.keyAt(0));
if (sharedElementTransition != null) {
TransitionUtils.setEpicenter(sharedElementTransition, fromEpicenterView);
}
if (exitTransition != null) {
TransitionUtils.setEpicenter(exitTransition, fromEpicenterView);
}
}
}
@Nullable ArrayMap<String, View> captureToSharedElements(@Nullable final View to, boolean isPush) {
if (sharedElementNames.isEmpty() || sharedElementTransition == null || to == null) {
sharedElementNames.clear();
return null;
}
final ArrayMap<String, View> toSharedElements = new ArrayMap<>();
TransitionUtils.findNamedViews(toSharedElements, to);
for (ViewParentPair removedView : removedViews) {
toSharedElements.put(removedView.view.getTransitionName(), removedView.view);
}
final List<String> names = new ArrayList<>(sharedElementNames.values());
toSharedElements.retainAll(names);
if (enterTransitionCallback != null) {
enterTransitionCallback.onMapSharedElements(names, toSharedElements);
for (int i = names.size() - 1; i >= 0; i--) {
String name = names.get(i);
View view = toSharedElements.get(name);
if (view == null) {
String key = findKeyForValue(sharedElementNames, name);
if (key != null) {
sharedElementNames.remove(key);
}
} else if (!name.equals(view.getTransitionName())) {
String key = findKeyForValue(sharedElementNames, name);
if (key != null) {
sharedElementNames.put(key, view.getTransitionName());
}
}
}
} else {
for (int i = sharedElementNames.size() - 1; i >= 0; i--) {
final String targetName = sharedElementNames.valueAt(i);
if (!toSharedElements.containsKey(targetName)) {
sharedElementNames.removeAt(i);
}
}
}
return toSharedElements;
}
@Nullable String findKeyForValue(@NonNull ArrayMap<String, String> map, @NonNull String value) {
final int numElements = map.size();
for (int i = 0; i < numElements; i++) {
if (value.equals(map.valueAt(i))) {
return map.keyAt(i);
}
}
return null;
}
@Nullable
private ArrayMap<String, View> captureFromSharedElements(@NonNull View from) {
if (sharedElementNames.isEmpty() || sharedElementTransition == null) {
sharedElementNames.clear();
return null;
}
final ArrayMap<String, View> fromSharedElements = new ArrayMap<>();
TransitionUtils.findNamedViews(fromSharedElements, from);
final List<String> names = new ArrayList<>(sharedElementNames.keySet());
fromSharedElements.retainAll(names);
if (exitTransitionCallback != null) {
exitTransitionCallback.onMapSharedElements(names, fromSharedElements);
for (int i = names.size() - 1; i >= 0; i--) {
String name = names.get(i);
View view = fromSharedElements.get(name);
if (view == null) {
sharedElementNames.remove(name);
} else if (!name.equals(view.getTransitionName())) {
String targetValue = sharedElementNames.remove(name);
sharedElementNames.put(view.getTransitionName(), targetValue);
}
}
} else {
sharedElementNames.retainAll(fromSharedElements.keySet());
}
return fromSharedElements;
}
void callSharedElementStartEnd(@Nullable ArrayMap<String, View> sharedElements, boolean isStart) {
if (enterTransitionCallback != null) {
final int count = sharedElements == null ? 0 : sharedElements.size();
List<View> views = new ArrayList<>(count);
List<String> names = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
names.add(sharedElements.keyAt(i));
views.add(sharedElements.valueAt(i));
}
if (isStart) {
enterTransitionCallback.onSharedElementStart(names, views, null);
} else {
enterTransitionCallback.onSharedElementEnd(names, views, null);
}
}
}
private void captureTransitioningViews(@NonNull List<View> transitioningViews, @NonNull View view) {
if (view.getVisibility() == View.VISIBLE) {
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
if (viewGroup.isTransitionGroup()) {
transitioningViews.add(viewGroup);
} else {
int count = viewGroup.getChildCount();
for (int i = 0; i < count; i++) {
View child = viewGroup.getChildAt(i);
captureTransitioningViews(transitioningViews, child);
}
}
} else {
transitioningViews.add(view);
}
}
}
private void scheduleRemoveTargets(@NonNull final Transition overallTransition,
@Nullable final Transition enterTransition, @Nullable final List<View> enteringViews,
@Nullable final Transition exitTransition, @Nullable final List<View> exitingViews,
@Nullable final Transition sharedElementTransition, @Nullable final List<View> toSharedElements) {
overallTransition.addListener(new TransitionListener() {
@Override
public void onTransitionStart(Transition transition) {
if (enterTransition != null && enteringViews != null) {
TransitionUtils.replaceTargets(enterTransition, enteringViews, null);
}
if (exitTransition != null && exitingViews != null) {
TransitionUtils.replaceTargets(exitTransition, exitingViews, null);
}
if (sharedElementTransition != null && toSharedElements != null) {
TransitionUtils.replaceTargets(sharedElementTransition, toSharedElements, null);
}
}
@Override
public void onTransitionEnd(Transition transition) { }
@Override
public void onTransitionCancel(Transition transition) { }
@Override
public void onTransitionPause(Transition transition) { }
@Override
public void onTransitionResume(Transition transition) { }
});
}
private void setNameOverrides(@NonNull final View container, @NonNull final List<View> toSharedElements) {
OneShotPreDrawListener.add(true, container, () -> {
final int numSharedElements = toSharedElements.size();
for (int i = 0; i < numSharedElements; i++) {
View view = toSharedElements.get(i);
String name = view.getTransitionName();
if (name != null) {
String inName = findKeyForValue(sharedElementNames, name);
view.setTransitionName(inName);
}
}
});
}
private void scheduleNameReset(@NonNull final ViewGroup container, @NonNull final List<View> toSharedElements) {
OneShotPreDrawListener.add(true, container, () -> {
final int numSharedElements = toSharedElements.size();
for (int i = 0; i < numSharedElements; i++) {
final View view = toSharedElements.get(i);
final String name = view.getTransitionName();
final String inName = sharedElementNames.get(name);
view.setTransitionName(inName);
}
});
}
/**
* Will be called when views are ready to have their shared elements configured. Within this method one of the addSharedElement methods
* should be called for each shared element that will be used. If one or more of these shared elements will not instantly be available in
* the incoming view (for ex, in a RecyclerView), waitOnSharedElementNamed can be called to delay the transition until everything is available.
*/
public abstract void configureSharedElements(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
/**
* Should return the transition that will be used on the exiting ("from") view, if one is desired.
*/
@Nullable
public abstract Transition getExitTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
/**
* Should return the transition that will be used on shared elements between the from and to views.
*/
@Nullable
public abstract Transition getSharedElementTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
/**
* Should return the transition that will be used on the entering ("to") view, if one is desired.
*/
@Nullable
public abstract Transition getEnterTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
/**
* Should return a callback that can be used to customize transition behavior of the shared element transition for the "from" view.
*/
@Nullable
public SharedElementCallback getExitTransitionCallback(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
return null;
}
/**
* Should return a callback that can be used to customize transition behavior of the shared element transition for the "to" view.
*/
@Nullable
public SharedElementCallback getEnterTransitionCallback(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
return null;
}
/**
* Should return whether or not the the exit transition and enter transition should overlap. If true,
* the enter transition will start as soon as possible. Otherwise, the enter transition will wait until the
* completion of the exit transition. Defaults to true.
*/
public boolean allowTransitionOverlap(boolean isPush) {
return true;
}
/**
* Used to register an element that will take part in the shared element transition.
*
* @param name The transition name that is used for both the entering and exiting views.
*/
protected final void addSharedElement(@NonNull String name) {
sharedElementNames.put(name, name);
}
/**
* Used to register an element that will take part in the shared element transition. Maps the name used in the
* "from" view to the name used in the "to" view if they are not the same.
*
* @param fromName The transition name used in the "from" view
* @param toName The transition name used in the "to" view
*/
protected final void addSharedElement(@NonNull String fromName, @NonNull String toName) {
sharedElementNames.put(fromName, toName);
}
/**
* Used to register an element that will take part in the shared element transition. Maps the name used in the
* "from" view to the name used in the "to" view if they are not the same.
*
* @param sharedElement The view from the "from" view that will take part in the shared element transition
* @param toName The transition name used in the "to" view
*/
protected final void addSharedElement(@NonNull View sharedElement, @NonNull String toName) {
String transitionName = sharedElement.getTransitionName();
if (transitionName == null) {
throw new IllegalArgumentException("Unique transitionNames are required for all sharedElements");
}
sharedElementNames.put(transitionName, toName);
}
/**
* The transition will be delayed until the view with the name passed in is available in the "to" hierarchy. This is
* particularly useful for views that don't load instantly, like RecyclerViews. Note that using this method can
* potentially lock up your app indefinitely if the view never loads!
*/
protected final void waitOnSharedElementNamed(@NonNull String name) {
if (!sharedElementNames.values().contains(name)) {
throw new IllegalStateException("Can't wait on a shared element that hasn't been registered using addSharedElement");
}
waitForTransitionNames.add(name);
}
private static class OneShotPreDrawListener implements OnPreDrawListener, View.OnAttachStateChangeListener {
private final View view;
private ViewTreeObserver viewTreeObserver;
private final Runnable runnable;
private final boolean preDrawReturnValue;
private OneShotPreDrawListener(boolean preDrawReturnValue, @NonNull View view, @NonNull Runnable runnable) {
this.preDrawReturnValue = preDrawReturnValue;
this.view = view;
viewTreeObserver = view.getViewTreeObserver();
this.runnable = runnable;
}
@NonNull
public static OneShotPreDrawListener add(boolean preDrawReturnValue, @NonNull View view, @NonNull Runnable runnable) {
OneShotPreDrawListener listener = new OneShotPreDrawListener(preDrawReturnValue, view, runnable);
view.getViewTreeObserver().addOnPreDrawListener(listener);
view.addOnAttachStateChangeListener(listener);
return listener;
}
@Override
public boolean onPreDraw() {
removeListener();
runnable.run();
return preDrawReturnValue;
}
private void removeListener() {
if (viewTreeObserver.isAlive()) {
viewTreeObserver.removeOnPreDrawListener(this);
} else {
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
view.removeOnAttachStateChangeListener(this);
}
@Override
public void onViewAttachedToWindow(View v) {
viewTreeObserver = v.getViewTreeObserver();
}
@Override
public void onViewDetachedFromWindow(View v) {
removeListener();
}
}
private static class ViewParentPair {
@NonNull final View view;
@NonNull final ViewGroup parent;
ViewParentPair(@NonNull View view, ViewGroup parent) {
this.view = view;
this.parent = parent;
}
}
}
@@ -1,149 +0,0 @@
package com.bluelinelabs.conductor.changehandler.platformtransition;
import android.annotation.TargetApi;
import android.os.Build;
import android.transition.Transition;
import android.transition.Transition.TransitionListener;
import android.transition.TransitionManager;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* A base {@link ControllerChangeHandler} that facilitates using {@link Transition}s to replace Controller Views.
* <p/>
* Note that this class uses Android's <b>platform</b> {@link Transition}. If you're using androidx transitions, consider
* using the {@code TransitionChangeHandler} provided by the {@code androidx-transitions} Conductor module.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class TransitionChangeHandler extends ControllerChangeHandler {
public interface OnTransitionPreparedListener {
void onPrepared();
}
boolean canceled;
private boolean needsImmediateCompletion;
/**
* Should be overridden to return the Transition to use while replacing Views.
*
* @param container The container these Views are hosted in
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop
*/
@NonNull
protected abstract Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
canceled = true;
}
@Override
public void completeImmediately() {
super.completeImmediately();
needsImmediateCompletion = true;
}
@Nullable
private ControllerChangeCompletedListener listener;
@Override
public void performChange(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
listener = changeListener;
if (canceled) {
changeListener.onChangeCompleted();
return;
}
if (needsImmediateCompletion) {
executePropertyChanges(container, from, to, null, isPush);
changeListener.onChangeCompleted();
return;
}
final Runnable onTransitionNotStarted = changeListener::onChangeCompleted;
final Transition transition = getTransition(container, from, to, isPush);
transition.addListener(new TransitionListener() {
@Override
public void onTransitionStart(Transition transition) {
container.removeCallbacks(onTransitionNotStarted);
}
@Override
public void onTransitionEnd(Transition transition) {
listener.onChangeCompleted();
listener = null;
}
@Override
public void onTransitionCancel(Transition transition) {
listener.onChangeCompleted();
listener = null;
}
@Override
public void onTransitionPause(Transition transition) { }
@Override
public void onTransitionResume(Transition transition) { }
});
prepareForTransition(container, from, to, transition, isPush, () -> {
if (!canceled) {
TransitionManager.beginDelayedTransition(container, transition);
executePropertyChanges(container, from, to, transition, isPush);
container.post(onTransitionNotStarted);
}
});
}
@Override
public boolean removesFromViewOnPush() {
return true;
}
/**
* Called before a transition occurs. This can be used to reorder views, set their transition names, etc. The transition will begin
* when {@code onTransitionPreparedListener} is called.
*
* @param container The container these Views are hosted in
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param transition The transition that is being prepared for
* @param isPush True if this is a push transaction, false if it's a pop
*/
public void prepareForTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @NonNull Transition transition, boolean isPush, @NonNull OnTransitionPreparedListener onTransitionPreparedListener) {
onTransitionPreparedListener.onPrepared();
}
/**
* This should set all view properties needed for the transition to work properly. By default it removes the "from" view
* and adds the "to" view.
*
* @param container The container these Views are hosted in
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param transition The transition with which {@code TransitionManager.beginDelayedTransition} has been called. This will be null only if another ControllerChangeHandler immediately overrides this one.
* @param isPush True if this is a push transaction, false if it's a pop
*/
public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @Nullable Transition transition, boolean isPush) {
if (from != null && (removesFromViewOnPush() || !isPush) && from.getParent() == container) {
container.removeView(from);
}
if (to != null && to.getParent() == null) {
container.addView(to);
}
}
}
@@ -1,184 +0,0 @@
package com.bluelinelabs.conductor.internal;
import android.annotation.TargetApi;
import android.graphics.Rect;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.transition.Transition;
import android.transition.TransitionSet;
import android.view.View;
import android.view.ViewGroup;
import java.util.List;
import java.util.Map;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class TransitionUtils {
public static void findNamedViews(@NonNull Map<String, View> namedViews, View view) {
if (view.getVisibility() == View.VISIBLE) {
String transitionName = view.getTransitionName();
if (transitionName != null) {
namedViews.put(transitionName, view);
}
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = viewGroup.getChildAt(i);
findNamedViews(namedViews, child);
}
}
}
}
@Nullable
public static View findNamedView(@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 = findNamedView(viewGroup.getChildAt(i), transitionName);
if (viewWithTransitionName != null) {
return viewWithTransitionName;
}
}
}
return null;
}
public static void setEpicenter(@NonNull Transition transition, @Nullable View view) {
if (view != null) {
final Rect epicenter = new Rect();
getBoundsOnScreen(view, epicenter);
transition.setEpicenterCallback(new Transition.EpicenterCallback() {
@Override
public Rect onGetEpicenter(Transition transition) {
return epicenter;
}
});
}
}
public static void getBoundsOnScreen(@NonNull View view, @NonNull Rect epicenter) {
int[] loc = new int[2];
view.getLocationOnScreen(loc);
epicenter.set(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight());
}
public static void setTargets(@NonNull Transition transition, @NonNull View nonExistentView, @NonNull List<View> sharedViews) {
final List<View> views = transition.getTargets();
views.clear();
final int count = sharedViews.size();
for (int i = 0; i < count; i++) {
final View view = sharedViews.get(i);
bfsAddViewChildren(views, view);
}
views.add(nonExistentView);
sharedViews.add(nonExistentView);
addTargets(transition, sharedViews);
}
public static void addTargets(@Nullable Transition transition, @NonNull List<View> views) {
if (transition == null) {
return;
}
if (transition instanceof TransitionSet) {
TransitionSet set = (TransitionSet) transition;
int numTransitions = set.getTransitionCount();
for (int i = 0; i < numTransitions; i++) {
Transition child = set.getTransitionAt(i);
addTargets(child, views);
}
} else if (!hasSimpleTarget(transition)) {
List<View> targets = transition.getTargets();
if (isNullOrEmpty(targets)) {
int numViews = views.size();
for (int i = 0; i < numViews; i++) {
transition.addTarget(views.get(i));
}
}
}
}
public static void replaceTargets(@NonNull Transition transition, @NonNull List<View> oldTargets, @Nullable List<View> newTargets) {
if (transition instanceof TransitionSet) {
TransitionSet set = (TransitionSet) transition;
int numTransitions = set.getTransitionCount();
for (int i = 0; i < numTransitions; i++) {
Transition child = set.getTransitionAt(i);
replaceTargets(child, oldTargets, newTargets);
}
} else if (!hasSimpleTarget(transition)) {
List<View> targets = transition.getTargets();
if (targets != null && targets.size() == oldTargets.size() && targets.containsAll(oldTargets)) {
final int targetCount = newTargets == null ? 0 : newTargets.size();
for (int i = 0; i < targetCount; i++) {
transition.addTarget(newTargets.get(i));
}
for (int i = oldTargets.size() - 1; i >= 0; i--) {
transition.removeTarget(oldTargets.get(i));
}
}
}
}
private static void bfsAddViewChildren(@NonNull final List<View> views, @NonNull final View startView) {
final int startIndex = views.size();
if (containedBeforeIndex(views, startView, startIndex)) {
return; // This child is already in the list, so all its children are also.
}
views.add(startView);
for (int index = startIndex; index < views.size(); index++) {
final View view = views.get(index);
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
final int childCount = viewGroup.getChildCount();
for (int childIndex = 0; childIndex < childCount; childIndex++) {
final View child = viewGroup.getChildAt(childIndex);
if (!containedBeforeIndex(views, child, startIndex)) {
views.add(child);
}
}
}
}
}
private static boolean containedBeforeIndex(@NonNull List<View> views, View view, int maxIndex) {
for (int i = 0; i < maxIndex; i++) {
if (views.get(i) == view) {
return true;
}
}
return false;
}
public static boolean hasSimpleTarget(@NonNull Transition transition) {
return !isNullOrEmpty(transition.getTargetIds())
|| !isNullOrEmpty(transition.getTargetNames())
|| !isNullOrEmpty(transition.getTargetTypes());
}
private static boolean isNullOrEmpty(@Nullable List list) {
return list == null || list.isEmpty();
}
@NonNull
public static TransitionSet mergeTransitions(int ordering, Transition... transitions) {
TransitionSet transitionSet = new TransitionSet();
for (Transition transition : transitions) {
if (transition != null) {
transitionSet.addTransition(transition);
}
}
transitionSet.setOrdering(ordering);
return transitionSet;
}
}
@@ -6,6 +6,10 @@ import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager.widget.PagerAdapter;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
@@ -15,12 +19,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager.widget.PagerAdapter;
/**
* An adapter for ViewPagers that uses Routers as pages
* An ViewPager adapter that uses Routers as pages
*/
public abstract class RouterPagerAdapter extends PagerAdapter {
@@ -1,35 +1,42 @@
package com.bluelinelabs.conductor.viewpager
import android.app.Activity
import android.os.Looper.getMainLooper
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.ViewCompat
import androidx.viewpager.widget.ViewPager
import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
import com.bluelinelabs.conductor.viewpager.util.FakePager
import com.bluelinelabs.conductor.viewpager.util.TestController
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class StateSaveTests {
private val pager: FakePager
private val pager: ViewPager
private val pagerAdapter: RouterPagerAdapter
private val destroyedItems = mutableListOf<Int>()
init {
val activityController = Robolectric.buildActivity(Activity::class.java).setup()
val layout = FrameLayout(activityController.get())
activityController.get().setContentView(layout)
val router = Conductor.attachRouter(activityController.get(), FrameLayout(activityController.get()), null)
val controller = TestController()
router.setRoot(with(controller))
pager = FakePager(FrameLayout(activityController.get()).also {
pager = ViewPager(activityController.get()).also {
it.id = ViewCompat.generateViewId()
})
}
layout.addView(pager)
pager.offscreenPageLimit = 1
pagerAdapter = object : RouterPagerAdapter(controller) {
override fun configureRouter(router: Router, position: Int) {
@@ -41,21 +48,27 @@ class StateSaveTests {
override fun getCount(): Int {
return 20
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
super.destroyItem(container, position, `object`)
destroyedItems.add(position)
}
}
pager.setAdapter(pagerAdapter)
pager.adapter = pagerAdapter
shadowOf(getMainLooper()).idle()
}
@Test
fun testNoMaxSaves() {
// Load all pages
for (i in 0 until pagerAdapter.count) {
pager.pageTo(i)
pager.currentItem = i
shadowOf(getMainLooper()).idle()
}
pager.pageTo(pagerAdapter.count / 2)
// Ensure all non-visible pages are saved
assertEquals(
pagerAdapter.count - 1 - (pager.offscreenPageLimit * 2),
destroyedItems.size,
pagerAdapter.savedPages.size()
)
}
@@ -67,38 +80,40 @@ class StateSaveTests {
// Load all pages
for (i in 0 until pagerAdapter.count) {
pager.pageTo(i)
pager.currentItem = i
shadowOf(getMainLooper()).idle()
}
val firstSelectedItem = pagerAdapter.count / 2
pager.pageTo(firstSelectedItem)
for (i in pagerAdapter.count downTo firstSelectedItem) {
pager.currentItem = i
shadowOf(getMainLooper()).idle()
}
var savedPages = pagerAdapter.savedPages
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size())
// Ensure correct pages are saved
assertEquals(
pagerAdapter.count - 3,
savedPages.keyAt(0)
)
assertEquals(
pagerAdapter.count - 2,
savedPages.keyAt(1)
)
assertEquals(
pagerAdapter.count - 1,
savedPages.keyAt(2)
)
assertEquals(destroyedItems[destroyedItems.lastIndex], savedPages.keyAt(0))
assertEquals(destroyedItems[destroyedItems.lastIndex - 1], savedPages.keyAt(1))
assertEquals(destroyedItems[destroyedItems.lastIndex - 2], savedPages.keyAt(2))
val secondSelectedItem = 1
pager.pageTo(secondSelectedItem)
for (i in firstSelectedItem downTo secondSelectedItem) {
pager.currentItem = i
shadowOf(getMainLooper()).idle()
}
savedPages = pagerAdapter.savedPages
// 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))
assertEquals(destroyedItems[destroyedItems.lastIndex], savedPages.keyAt(0))
assertEquals(destroyedItems[destroyedItems.lastIndex - 1], savedPages.keyAt(1))
assertEquals(destroyedItems[destroyedItems.lastIndex - 2], savedPages.keyAt(2))
}
}
@@ -1,61 +0,0 @@
package com.bluelinelabs.conductor.viewpager.util;
import android.util.SparseArray;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.viewpager.RouterPagerAdapter;
import java.util.ArrayList;
import java.util.List;
public class FakePager {
private ViewGroup container;
private int offscreenPageLimit;
private final SparseArray<Object> pages = new SparseArray<>();
private RouterPagerAdapter adapter;
public FakePager(ViewGroup container) {
this.container = container;
}
public void setAdapter(RouterPagerAdapter adapter) {
this.adapter = adapter;
}
public void pageTo(int page) {
int firstPage = Math.max(0, page - offscreenPageLimit);
int lastPage = Math.min(adapter.getCount() - 1, page + offscreenPageLimit);
List<Integer> pagesI = new ArrayList<>();
for (int i = 0; i < pages.size(); i++) {
pagesI.add(pages.keyAt(i));
}
for (int i = pages.size() - 1; i >= 0; i--) {
int key = pages.keyAt(i);
if (key < firstPage || key > lastPage) {
adapter.destroyItem(container, key, pages.get(key));
pages.remove(key);
}
}
for (int key = firstPage; key <= lastPage; key++) {
if (pages.get(key) == null) {
pages.put(key, adapter.instantiateItem(container, key));
}
}
adapter.setPrimaryItem(container, page, pages.get(page));
}
public int getOffscreenPageLimit() {
return offscreenPageLimit;
}
public void setOffscreenPageLimit(int offscreenPageLimit) {
this.offscreenPageLimit = offscreenPageLimit;
}
}
@@ -1,4 +1,6 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
@@ -9,13 +11,24 @@ android {
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.roboelectric
implementation rootProject.ext.androidxAppCompat
implementation rootProject.ext.androidxViewPager2
implementation project(':conductor')
}
ext.artifactId = 'conductor-android-transition'
ext.artifactId = 'conductor-viewpager2'
apply from: rootProject.file('dependencies.gradle')
apply plugin: "com.vanniktech.maven.publish"
@@ -0,0 +1,3 @@
POM_NAME=Conductor ViewPager2 Adapter
POM_ARTIFACT_ID=conductor-viewpager2
POM_PACKAGING=aar
@@ -0,0 +1,3 @@
<manifest package="com.bluelinelabs.conductor.viewpager2">
<application />
</manifest>
@@ -0,0 +1,227 @@
package com.bluelinelabs.conductor.viewpager2
import android.os.Bundle
import android.os.Parcelable
import android.util.LongSparseArray
import android.util.SparseArray
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.StatefulAdapter
import androidx.viewpager2.widget.ViewPager2
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router
import kotlinx.android.parcel.Parcelize
/**
* An ViewPager2 adapter that uses Routers as pages
*/
abstract class RouterStateAdapter(private val host: Controller) :
RecyclerView.Adapter<RouterViewHolder>(), StatefulAdapter {
private var savedPages = LongSparseArray<Bundle>()
internal var savedPageHistory = mutableListOf<Long>()
private var maxPagesToStateSave = Int.MAX_VALUE
private val visibleRouters = SparseArray<Router>()
private var currentPrimaryRouterPosition = 0
private var primaryItemCallback: PrimaryItemCallback? = null
init {
super.setHasStableIds(true)
}
/**
* 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.
*/
abstract fun configureRouter(router: Router, position: Int)
/**
* 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.
*/
open fun setMaxPagesToStateSave(maxPagesToStateSave: Int) {
require(maxPagesToStateSave >= 0) { "Only positive integers may be passed for maxPagesToStateSave." }
this.maxPagesToStateSave = maxPagesToStateSave
ensurePagesSaved()
}
private fun inferViewPager(recyclerView: RecyclerView): ViewPager2 {
return recyclerView.parent as? ViewPager2 ?:
error("Expected ViewPager2 instance. Got: ${recyclerView.parent}")
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
val viewPager = inferViewPager(recyclerView)
primaryItemCallback = PrimaryItemCallback().also {
viewPager.registerOnPageChangeCallback(it)
}
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
val viewPager = inferViewPager(recyclerView)
primaryItemCallback?.let {
viewPager.unregisterOnPageChangeCallback(it)
}
primaryItemCallback = null
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RouterViewHolder {
return RouterViewHolder(parent)
}
override fun onBindViewHolder(holder: RouterViewHolder, position: Int) {
holder.currentItemPosition = position
attachRouter(holder, position)
}
override fun onViewAttachedToWindow(holder: RouterViewHolder) {
super.onViewAttachedToWindow(holder)
if (!holder.attached) {
attachRouter(holder, holder.currentItemPosition)
}
}
override fun onViewDetachedFromWindow(holder: RouterViewHolder) {
super.onViewDetachedFromWindow(holder)
detachRouter(holder)
}
override fun onViewRecycled(holder: RouterViewHolder) {
super.onViewRecycled(holder)
detachRouter(holder)
holder.currentRouter?.let { router ->
host.removeChildRouter(router)
holder.currentRouter = null
}
}
override fun onFailedToRecycleView(holder: RouterViewHolder): Boolean {
return true
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun setHasStableIds(hasStableIds: Boolean) {
throw UnsupportedOperationException("Stable Ids are required for the adapter to function properly")
}
override fun saveState(): Parcelable {
return SavedState(
savedPagesKeys = (0 until savedPages.size()).map { savedPages.keyAt(it) },
savedPagesValues = (0 until savedPages.size()).map { savedPages.valueAt(it) },
savedPageHistory = savedPageHistory,
maxPagesToStateSave = maxPagesToStateSave
)
}
override fun restoreState(state: Parcelable) {
if (state !is SavedState) return
savedPages = LongSparseArray()
state.savedPagesKeys.indices.forEach { index ->
savedPages.put(state.savedPagesKeys[index], state.savedPagesValues[index])
}
savedPageHistory = state.savedPageHistory.toMutableList()
maxPagesToStateSave = state.maxPagesToStateSave
}
private fun attachRouter(holder: RouterViewHolder, position: Int) {
val itemId = getItemId(position)
val router = host.getChildRouter(holder.container, "$itemId")
holder.currentRouter = router
holder.currentItemId = itemId
if (!router.hasRootController()) {
val routerSavedState = savedPages[position.toLong()]
if (routerSavedState != null) {
router.restoreInstanceState(routerSavedState)
savedPages.remove(itemId)
savedPageHistory.remove(itemId)
}
}
router.rebindIfNeeded()
configureRouter(router, position)
if (position != currentPrimaryRouterPosition) {
for (transaction in router.backstack) {
transaction.controller.setOptionsMenuHidden(true)
}
}
visibleRouters.put(position, router)
holder.attached = true
}
private fun detachRouter(holder: RouterViewHolder) {
if (!holder.attached) {
return
}
holder.currentRouter?.let { router ->
router.prepareForHostDetach()
val savedState = Bundle()
router.saveInstanceState(savedState)
savedPages.put(holder.currentItemId, savedState)
savedPageHistory.remove(holder.currentItemId)
savedPageHistory.add(holder.currentItemId)
ensurePagesSaved()
if (visibleRouters[holder.currentItemPosition] == router) {
visibleRouters.remove(holder.currentItemPosition)
}
}
holder.attached = false
}
private fun ensurePagesSaved() {
while (savedPages.size() > maxPagesToStateSave) {
val positionToRemove = savedPageHistory.removeAt(0)
savedPages.remove(positionToRemove)
}
}
/**
* Returns the already instantiated Router in the specified position or `null` if there
* is no router associated with this position.
*/
fun getRouter(position: Int): Router? {
return visibleRouters[position]
}
inner class PrimaryItemCallback : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
val router = visibleRouters[position]
if (position != currentPrimaryRouterPosition) {
val previousRouter = visibleRouters[currentPrimaryRouterPosition]
previousRouter?.backstack?.forEach { it.controller.setOptionsMenuHidden(true) }
router?.backstack?.forEach { it.controller.setOptionsMenuHidden(false) }
currentPrimaryRouterPosition = position
}
}
}
@Parcelize
private data class SavedState(
val savedPagesKeys: List<Long>,
val savedPagesValues: List<Bundle>,
val savedPageHistory: List<Long>,
val maxPagesToStateSave: Int
) : Parcelable
}
@@ -0,0 +1,27 @@
package com.bluelinelabs.conductor.viewpager2
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.bluelinelabs.conductor.ChangeHandlerFrameLayout
import com.bluelinelabs.conductor.Router
class RouterViewHolder private constructor(val container: ChangeHandlerFrameLayout) : ViewHolder(container) {
var currentRouter: Router? = null
var currentItemPosition = 0
var currentItemId = 0L
var attached = false
companion object {
operator fun invoke(parent: ViewGroup): RouterViewHolder {
val container = ChangeHandlerFrameLayout(parent.context)
container.id = ViewCompat.generateViewId()
container.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
container.isSaveEnabled = false
return RouterViewHolder(container)
}
}
}
@@ -0,0 +1,119 @@
package com.bluelinelabs.conductor.viewpager2
import android.app.Activity
import android.os.Looper.getMainLooper
import android.widget.FrameLayout
import androidx.core.view.ViewCompat
import androidx.viewpager2.widget.ViewPager2
import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
import com.bluelinelabs.conductor.viewpager2.util.TestController
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class StateSaveTests {
private val pager: ViewPager2
private val adapter: RouterStateAdapter
private val destroyedItems = mutableListOf<Int>()
init {
val activityController = Robolectric.buildActivity(Activity::class.java).setup()
val layout = FrameLayout(activityController.get())
activityController.get().setContentView(layout)
val router = Conductor.attachRouter(activityController.get(), FrameLayout(activityController.get()), null)
val controller = TestController()
router.setRoot(with(controller))
pager = ViewPager2(activityController.get()).also {
it.id = ViewCompat.generateViewId()
}
layout.addView(pager)
pager.offscreenPageLimit = 1
adapter = object : RouterStateAdapter(controller) {
override fun configureRouter(router: Router, position: Int) {
if (!router.hasRootController()) {
router.setRoot(with(TestController()))
}
}
override fun getItemCount(): Int {
return 20
}
override fun onViewDetachedFromWindow(holder: RouterViewHolder) {
super.onViewDetachedFromWindow(holder)
destroyedItems.add(holder.currentItemPosition)
}
}
pager.adapter = adapter
shadowOf(getMainLooper()).idle()
}
@Test
fun testNoMaxSaves() {
// Load all pages
for (i in 0 until adapter.itemCount) {
pager.setCurrentItem(i, false)
shadowOf(getMainLooper()).idle()
}
// Ensure all non-visible pages are saved
assertEquals(
destroyedItems.size,
adapter.savedPageHistory.size
)
}
@Test
fun testMaxSavedSet() {
val maxPages = 3
adapter.setMaxPagesToStateSave(maxPages)
// Load all pages
for (i in 0 until adapter.itemCount) {
pager.setCurrentItem(i, false)
shadowOf(getMainLooper()).idle()
}
val firstSelectedItem = adapter.itemCount / 2
for (i in adapter.itemCount downTo firstSelectedItem) {
pager.setCurrentItem(i, false)
shadowOf(getMainLooper()).idle()
}
var savedPages = adapter.savedPageHistory
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size)
// Ensure correct pages are saved
assertEquals(destroyedItems[destroyedItems.lastIndex], savedPages[savedPages.lastIndex].toInt())
assertEquals(destroyedItems[destroyedItems.lastIndex - 1], savedPages[savedPages.lastIndex - 1].toInt())
assertEquals(destroyedItems[destroyedItems.lastIndex - 2], savedPages[savedPages.lastIndex - 2].toInt())
val secondSelectedItem = 1
for (i in adapter.itemCount downTo secondSelectedItem) {
pager.setCurrentItem(i, false)
shadowOf(getMainLooper()).idle()
}
savedPages = adapter.savedPageHistory
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size)
// Ensure correct pages are saved
assertEquals(destroyedItems[destroyedItems.lastIndex], savedPages[savedPages.lastIndex].toInt())
assertEquals(destroyedItems[destroyedItems.lastIndex - 1], savedPages[savedPages.lastIndex - 1].toInt())
assertEquals(destroyedItems[destroyedItems.lastIndex - 2], savedPages[savedPages.lastIndex - 2].toInt())
}
}
@@ -0,0 +1,21 @@
package com.bluelinelabs.conductor.viewpager2.util;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
public class TestController extends Controller {
@Override @NonNull
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) {
return new FrameLayout(inflater.getContext());
}
}
@@ -5,9 +5,10 @@ import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.internal.LifecycleHandler;
@@ -57,9 +58,12 @@ public class ActivityHostedRouter extends Router {
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
super.onActivityDestroyed(activity);
lifecycleHandler = null;
public void onActivityDestroyed(@NonNull Activity activity, boolean isConfigurationChange) {
super.onActivityDestroyed(activity, isConfigurationChange);
if (!isConfigurationChange) {
lifecycleHandler = null;
}
}
@Override
@@ -32,6 +32,7 @@ import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
@@ -228,7 +229,7 @@ public abstract class Controller {
if (childRouter == null) {
if (createIfNeeded) {
childRouter = new ControllerHostedRouter(container.getId(), tag);
childRouter.setHost(this, container);
childRouter.setHostContainer(this, container);
childRouters.add(childRouter);
if (isPerformingExitTransition) {
@@ -236,7 +237,7 @@ public abstract class Controller {
}
}
} else if (!childRouter.hasHost()) {
childRouter.setHost(this, container);
childRouter.setHostContainer(this, container);
childRouter.rebindIfNeeded();
}
@@ -522,21 +523,36 @@ public abstract class Controller {
* Calls startActivity(Intent) from this Controller's host Activity.
*/
public final void startActivity(@NonNull final Intent intent) {
executeWithRouter(() -> router.startActivity(intent));
executeWithRouter(new RouterRequiringFunc() {
@Override
public void execute() {
router.startActivity(intent);
}
});
}
/**
* Calls startActivityForResult(Intent, int) from this Controller's host Activity.
*/
public final void startActivityForResult(@NonNull final Intent intent, final int requestCode) {
executeWithRouter(() -> router.startActivityForResult(instanceId, intent, requestCode));
executeWithRouter(new RouterRequiringFunc() {
@Override
public void execute() {
router.startActivityForResult(instanceId, intent, requestCode);
}
});
}
/**
* Calls startActivityForResult(Intent, int, Bundle) from this Controller's host Activity.
*/
public final void startActivityForResult(@NonNull final Intent intent, final int requestCode, @Nullable final Bundle options) {
executeWithRouter(() -> router.startActivityForResult(instanceId, intent, requestCode, options));
executeWithRouter(new RouterRequiringFunc() {
@Override
public void execute() {
router.startActivityForResult(instanceId, intent, requestCode, options);
}
});
}
/**
@@ -554,7 +570,12 @@ public abstract class Controller {
* @param requestCode The request code being registered for.
*/
public final void registerForActivityResult(final int requestCode) {
executeWithRouter(() -> router.registerForActivityResult(instanceId, requestCode));
executeWithRouter(new RouterRequiringFunc() {
@Override
public void execute() {
router.registerForActivityResult(instanceId, requestCode);
}
});
}
/**
@@ -577,7 +598,12 @@ public abstract class Controller {
public final void requestPermissions(@NonNull final String[] permissions, final int requestCode) {
requestedPermissions.addAll(Arrays.asList(permissions));
executeWithRouter(() -> router.requestPermissions(instanceId, permissions, requestCode));
executeWithRouter(new RouterRequiringFunc() {
@Override
public void execute() {
router.requestPermissions(instanceId, permissions, requestCode);
}
});
}
/**
@@ -612,7 +638,12 @@ public abstract class Controller {
childTransactions.addAll(childRouter.getBackstack());
}
Collections.sort(childTransactions, (o1, o2) -> o2.getTransactionIndex() - o1.getTransactionIndex());
Collections.sort(childTransactions, new Comparator<RouterTransaction>() {
@Override
public int compare(RouterTransaction o1, RouterTransaction o2) {
return o2.getTransactionIndex() - o1.getTransactionIndex();
}
});
for (RouterTransaction transaction : childTransactions) {
Controller childController = transaction.controller();
@@ -987,7 +1018,11 @@ public abstract class Controller {
onDestroyView(view);
viewAttachHandler.unregisterAttachListener(view);
// viewAttachHandler may be null iff the controller was popped before we got here
if (viewAttachHandler != null) {
viewAttachHandler.unregisterAttachListener(view);
}
viewAttachHandler = null;
viewIsAttached = false;
@@ -1036,32 +1071,34 @@ public abstract class Controller {
restoreViewState(view);
viewAttachHandler = new ViewAttachHandler(new ViewAttachListener() {
@Override
public void onAttached() {
viewIsAttached = true;
viewWasDetached = false;
attach(view);
}
@Override
public void onDetached(boolean fromActivityStop) {
viewIsAttached = false;
viewWasDetached = true;
if (!isDetachFrozen) {
detach(view, false, fromActivityStop);
if (!isBeingDestroyed) {
viewAttachHandler = new ViewAttachHandler(new ViewAttachListener() {
@Override
public void onAttached() {
viewIsAttached = true;
viewWasDetached = false;
attach(view);
}
}
@Override
public void onViewDetachAfterStop() {
if (!isDetachFrozen) {
detach(view, false, false);
@Override
public void onDetached(boolean fromActivityStop) {
viewIsAttached = false;
viewWasDetached = true;
if (!isDetachFrozen) {
detach(view, false, fromActivityStop);
}
}
}
});
viewAttachHandler.listenForAttach(view);
@Override
public void onViewDetachAfterStop() {
if (!isDetachFrozen) {
detach(view, false, false);
}
}
});
viewAttachHandler.listenForAttach(view);
}
} else if (retainViewMode == RetainViewMode.RETAIN_DETACH) {
restoreChildControllerHosts();
}
@@ -1075,7 +1112,7 @@ public abstract class Controller {
View containerView = view.findViewById(childRouter.getHostId());
if (containerView != null && containerView instanceof ViewGroup) {
childRouter.setHost(this, (ViewGroup) containerView);
childRouter.setHostContainer(this, (ViewGroup) containerView);
childRouter.rebindIfNeeded();
}
}
@@ -1234,6 +1271,7 @@ public abstract class Controller {
List<Bundle> childBundles = savedInstanceState.getParcelableArrayList(KEY_CHILD_ROUTERS);
for (Bundle childBundle : childBundles) {
ControllerHostedRouter childRouter = new ControllerHostedRouter();
childRouter.setHostController(this);
childRouter.restoreInstanceState(childBundle);
childRouters.add(childRouter);
}
@@ -1382,66 +1420,66 @@ public abstract class Controller {
/**
* Allows external classes to listen for lifecycle events in a Controller
*/
public interface LifecycleListener {
public static abstract class LifecycleListener {
default void onChangeStart(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
public void onChangeStart(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
}
default void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
}
default void preCreateView(@NonNull Controller controller) {
public void preCreateView(@NonNull Controller controller) {
}
default void postCreateView(@NonNull Controller controller, @NonNull View view) {
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
}
default void preAttach(@NonNull Controller controller, @NonNull View view) {
public void preAttach(@NonNull Controller controller, @NonNull View view) {
}
default void postAttach(@NonNull Controller controller, @NonNull View view) {
public void postAttach(@NonNull Controller controller, @NonNull View view) {
}
default void preDetach(@NonNull Controller controller, @NonNull View view) {
public void preDetach(@NonNull Controller controller, @NonNull View view) {
}
default void postDetach(@NonNull Controller controller, @NonNull View view) {
public void postDetach(@NonNull Controller controller, @NonNull View view) {
}
default void preDestroyView(@NonNull Controller controller, @NonNull View view) {
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
}
default void postDestroyView(@NonNull Controller controller) {
public void postDestroyView(@NonNull Controller controller) {
}
default void preDestroy(@NonNull Controller controller) {
public void preDestroy(@NonNull Controller controller) {
}
default void postDestroy(@NonNull Controller controller) {
public void postDestroy(@NonNull Controller controller) {
}
default void preContextAvailable(@NonNull Controller controller) {
public void preContextAvailable(@NonNull Controller controller) {
}
default void postContextAvailable(@NonNull Controller controller, @NonNull Context context) {
public void postContextAvailable(@NonNull Controller controller, @NonNull Context context) {
}
default void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
}
default void postContextUnavailable(@NonNull Controller controller) {
public void postContextUnavailable(@NonNull Controller controller) {
}
default void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
}
default void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
}
default void onSaveViewState(@NonNull Controller controller, @NonNull Bundle outState) {
public void onSaveViewState(@NonNull Controller controller, @NonNull Bundle outState) {
}
default void onRestoreViewState(@NonNull Controller controller, @NonNull Bundle savedViewState) {
public void onRestoreViewState(@NonNull Controller controller, @NonNull Bundle savedViewState) {
}
}
@@ -206,29 +206,32 @@ public abstract class ControllerChangeHandler {
fromView = null;
}
handler.performChange(container, fromView, toView, isPush, () -> {
if (from != null) {
from.changeEnded(handler, fromChangeType);
}
if (to != null) {
inProgressChangeHandlers.remove(to.getInstanceId());
to.changeEnded(handler, toChangeType);
}
for (ControllerChangeListener listener : listeners) {
listener.onChangeCompleted(to, from, isPush, container, handler);
}
if (handler.forceRemoveViewOnPush && fromView != null) {
ViewParent fromParent = fromView.getParent();
if (fromParent != null && fromParent instanceof ViewGroup) {
((ViewGroup) fromParent).removeView(fromView);
handler.performChange(container, fromView, toView, isPush, new ControllerChangeCompletedListener() {
@Override
public void onChangeCompleted() {
if (from != null) {
from.changeEnded(handler, fromChangeType);
}
}
if (handler.removesFromViewOnPush() && from != null) {
from.setNeedsAttach(false);
if (to != null) {
inProgressChangeHandlers.remove(to.getInstanceId());
to.changeEnded(handler, toChangeType);
}
for (ControllerChangeListener listener : listeners) {
listener.onChangeCompleted(to, from, isPush, container, handler);
}
if (handler.forceRemoveViewOnPush && fromView != null) {
ViewParent fromParent = fromView.getParent();
if (fromParent != null && fromParent instanceof ViewGroup) {
((ViewGroup) fromParent).removeView(fromView);
}
}
if (handler.removesFromViewOnPush() && from != null) {
from.setNeedsAttach(false);
}
}
});
}
@@ -258,8 +261,7 @@ public abstract class ControllerChangeHandler {
* @param container The containing ViewGroup
* @param handler The change handler being used.
*/
default void onChangeStarted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull 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
@@ -270,8 +272,7 @@ public abstract class ControllerChangeHandler {
* @param container The containing ViewGroup
* @param handler The change handler that was used.
*/
default void onChangeCompleted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) {
}
void onChangeCompleted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler);
}
static class ChangeTransaction {
@@ -36,12 +36,18 @@ class ControllerHostedRouter extends Router {
this.tag = tag;
}
final void setHost(@NonNull Controller controller, @NonNull ViewGroup container) {
final void setHostController(@NonNull Controller controller) {
if (hostController == null) {
hostController = controller;
}
}
final void setHostContainer(@NonNull Controller controller, @NonNull ViewGroup container) {
if (hostController != controller || this.container != container) {
removeHost();
if (container instanceof ControllerChangeListener) {
addChangeListener((ControllerChangeListener)container);
addChangeListener((ControllerChangeListener) container);
}
hostController = controller;
@@ -57,7 +63,7 @@ class ControllerHostedRouter extends Router {
final void removeHost() {
if (container != null && container instanceof ControllerChangeListener) {
removeChangeListener((ControllerChangeListener)container);
removeChangeListener((ControllerChangeListener) container);
}
final List<Controller> controllersToDestroy = new ArrayList<>(destroyingControllers);
@@ -73,7 +79,6 @@ class ControllerHostedRouter extends Router {
}
prepareForContainerRemoval();
hostController = null;
container = null;
}
@@ -108,14 +113,29 @@ class ControllerHostedRouter extends Router {
super.setBackstack(newBackstack, changeHandler);
}
@Override
void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush) {
super.performControllerChange(to, from, isPush);
// If we're pushing a transaction that will detach controllers to an unattached child
// router, we need mark all other controllers as NOT needing to be reattached.
if (to != null && !hostController.isAttached()) {
if (to.pushChangeHandler() == null || to.pushChangeHandler().removesFromViewOnPush()) {
for (RouterTransaction transaction : backstack) {
transaction.controller().setNeedsAttach(false);
}
}
}
}
@Override @Nullable
public Activity getActivity() {
return hostController != null ? hostController.getActivity() : null;
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
super.onActivityDestroyed(activity);
public void onActivityDestroyed(@NonNull Activity activity, boolean isConfigurationChange) {
super.onActivityDestroyed(activity, isConfigurationChange);
removeHost();
}
@@ -185,7 +205,7 @@ class ControllerHostedRouter extends Router {
@Override
boolean hasHost() {
return hostController != null;
return hostController != null && container != null;
}
@Override
@@ -205,9 +225,9 @@ class ControllerHostedRouter extends Router {
}
@Override
void setControllerRouter(@NonNull Controller controller) {
void setRouterOnController(@NonNull Controller controller) {
controller.setParentController(hostController);
super.setControllerRouter(controller);
super.setRouterOnController(controller);
}
int getHostId() {
@@ -205,7 +205,7 @@ public abstract class Router {
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())) {
for (RouterTransaction visibleTransaction : getVisibleTransactions(backstack.iterator(), true)) {
performControllerChange(null, visibleTransaction, true, handler);
}
}
@@ -404,7 +404,7 @@ public abstract class Router {
ThreadUtils.ensureMainThread();
List<RouterTransaction> oldTransactions = getBackstack();
List<RouterTransaction> oldVisibleTransactions = getVisibleTransactions(backstack.iterator());
List<RouterTransaction> oldVisibleTransactions = getVisibleTransactions(backstack.iterator(), false);
removeAllExceptVisibleAndUnowned();
ensureOrderedTransactionIndices(newBackstack);
@@ -434,13 +434,13 @@ public abstract class Router {
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
transaction.onAttachedToRouter();
setControllerRouter(transaction.controller());
setRouterOnController(transaction.controller());
}
if (newBackstack.size() > 0) {
List<RouterTransaction> reverseNewBackstack = new ArrayList<>(newBackstack);
Collections.reverse(reverseNewBackstack);
List<RouterTransaction> newVisibleTransactions = getVisibleTransactions(reverseNewBackstack.iterator());
List<RouterTransaction> newVisibleTransactions = getVisibleTransactions(reverseNewBackstack.iterator(), false);
boolean newRootRequiresPush = !(newVisibleTransactions.size() > 0 && oldTransactions.contains(newVisibleTransactions.get(0)));
boolean visibleTransactionsChanged = !backstacksAreEqual(newVisibleTransactions, oldVisibleTransactions);
@@ -464,7 +464,10 @@ public abstract class Router {
ControllerChangeHandler localHandler = changeHandler != null ? changeHandler.copy() : new SimpleSwapChangeHandler();
localHandler.setForceRemoveViewOnPush(true);
ControllerChangeHandler.completeHandlerImmediately(transaction.controller().getInstanceId());
performControllerChange(null, transaction, newRootRequiresPush, localHandler);
if (transaction.controller().view != null) {
performControllerChange(null, transaction, newRootRequiresPush, localHandler);
}
}
}
@@ -549,7 +552,7 @@ public abstract class Router {
if (transaction.controller().getNeedsAttach()) {
performControllerChange(transaction, null, true, new SimpleSwapChangeHandler(false));
} else {
setControllerRouter(transaction.controller());
setRouterOnController(transaction.controller());
}
}
}
@@ -605,7 +608,7 @@ public abstract class Router {
isActivityStopped = true;
}
public void onActivityDestroyed(@NonNull Activity activity) {
public void onActivityDestroyed(@NonNull Activity activity, boolean isConfigurationChange) {
prepareForContainerRemoval();
changeListeners.clear();
@@ -613,7 +616,7 @@ public abstract class Router {
transaction.controller().activityDestroyed(activity);
for (Router childRouter : transaction.controller().getChildRouters()) {
childRouter.onActivityDestroyed(activity);
childRouter.onActivityDestroyed(activity, isConfigurationChange);
}
}
@@ -622,7 +625,7 @@ public abstract class Router {
controller.activityDestroyed(activity);
for (Router childRouter : controller.getChildRouters()) {
childRouter.onActivityDestroyed(activity);
childRouter.onActivityDestroyed(activity, isConfigurationChange);
}
}
@@ -657,7 +660,7 @@ public abstract class Router {
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
setControllerRouter(backstackIterator.next().controller());
setRouterOnController(backstackIterator.next().controller());
}
}
@@ -720,7 +723,12 @@ public abstract class Router {
}
void watchContainerAttach() {
container.post(() -> containerFullyAttached = true);
container.post(new Runnable() {
@Override
public void run() {
containerFullyAttached = true;
}
});
}
void prepareForContainerRemoval() {
@@ -759,7 +767,7 @@ public abstract class Router {
return null;
}
private void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush) {
void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush) {
if (isPush && to != null) {
to.onAttachedToRouter();
}
@@ -784,7 +792,7 @@ public abstract class Router {
if (to != null) {
to.ensureValidIndex(getTransactionIndexer());
setControllerRouter(toController);
setRouterOnController(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 host
// Activity or controller should be handling this by finishing or at least hiding this view.
@@ -802,7 +810,7 @@ public abstract class Router {
if (fromController.getView() != null) {
fromController.detach(fromController.getView(), true, false);
} else {
from.controller().destroy();
fromController.destroy();
}
}
}
@@ -829,7 +837,12 @@ public abstract class Router {
to.setNeedsAttach(true);
}
pendingControllerChanges.add(transaction);
container.post(this::performPendingControllerChanges);
container.post(new Runnable() {
@Override
public void run() {
performPendingControllerChanges();
}
});
} else {
ControllerChangeHandler.executeChange(transaction);
}
@@ -873,7 +886,7 @@ public abstract class Router {
private void removeAllExceptVisibleAndUnowned() {
List<View> views = new ArrayList<>();
for (RouterTransaction transaction : getVisibleTransactions(backstack.iterator())) {
for (RouterTransaction transaction : getVisibleTransactions(backstack.iterator(), false)) {
if (transaction.controller().getView() != null) {
views.add(transaction.controller().getView());
}
@@ -933,14 +946,20 @@ public abstract class Router {
}
}
private List<RouterTransaction> getVisibleTransactions(@NonNull Iterator<RouterTransaction> backstackIterator) {
private List<RouterTransaction> getVisibleTransactions(@NonNull Iterator<RouterTransaction> backstackIterator, boolean onlyTop) {
boolean visible = true;
List<RouterTransaction> transactions = new ArrayList<>();
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
transactions.add(transaction);
//noinspection ConstantConditions
if (transaction.pushChangeHandler() == null || transaction.pushChangeHandler().removesFromViewOnPush()) {
if (visible) {
transactions.add(transaction);
}
visible = transaction.pushChangeHandler() != null && !transaction.pushChangeHandler().removesFromViewOnPush();
if (onlyTop && !visible) {
break;
}
}
@@ -963,7 +982,7 @@ public abstract class Router {
return true;
}
void setControllerRouter(@NonNull Controller controller) {
void setRouterOnController(@NonNull Controller controller) {
controller.setRouter(this);
controller.onContextAvailable();
}
@@ -101,7 +101,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
@NonNull
public List<Router> getRouters() {
return new ArrayList<>(routerMap.values());
return new ArrayList<Router>(routerMap.values());
}
@Nullable
@@ -133,13 +133,13 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
if (savedInstanceState != null) {
StringSparseArrayParceler permissionParcel = savedInstanceState.getParcelable(KEY_PERMISSION_REQUEST_CODES);
permissionRequestMap = permissionParcel != null ? permissionParcel.getStringSparseArray() : new SparseArray<>();
permissionRequestMap = permissionParcel != null ? permissionParcel.getStringSparseArray() : new SparseArray<String>();
StringSparseArrayParceler activityParcel = savedInstanceState.getParcelable(KEY_ACTIVITY_REQUEST_CODES);
activityRequestMap = activityParcel != null ? activityParcel.getStringSparseArray() : new SparseArray<>();
activityRequestMap = activityParcel != null ? activityParcel.getStringSparseArray() : new SparseArray<String>();
ArrayList<PendingPermissionRequest> pendingRequests = savedInstanceState.getParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS);
pendingPermissionRequests = pendingRequests != null ? pendingRequests : new ArrayList<>();
pendingPermissionRequests = pendingRequests != null ? pendingRequests : new ArrayList<PendingPermissionRequest>();
}
}
@@ -159,13 +159,16 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
if (activity != null) {
activity.getApplication().unregisterActivityLifecycleCallbacks(this);
activeLifecycleHandlers.remove(activity);
destroyRouters();
destroyRouters(false);
activity = null;
}
routerMap.clear();
}
@Override
public void onAttach(Activity activity) {
this.activity = activity;
super.onAttach(activity);
destroyed = false;
setAttached();
@@ -173,6 +176,10 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
@Override
public void onAttach(Context context) {
if (context instanceof Activity) {
this.activity = (Activity) context;
}
super.onAttach(context);
destroyed = false;
setAttached();
@@ -183,7 +190,10 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
super.onDetach();
attached = false;
destroyRouters();
if (activity != null) {
destroyRouters(activity.isChangingConfigurations());
}
}
private void setAttached() {
@@ -194,20 +204,20 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
PendingPermissionRequest request = pendingPermissionRequests.remove(i);
requestPermissions(request.instanceId, request.permissions, request.requestCode);
}
}
for (ActivityHostedRouter router : new ArrayList<>(routerMap.values())) {
router.onContextAvailable();
for (ActivityHostedRouter router : new ArrayList<>(routerMap.values())) {
router.onContextAvailable();
}
}
}
private void destroyRouters() {
private void destroyRouters(boolean configurationChange) {
if (!destroyed) {
destroyed = true;
if (activity != null) {
for (Router router : getRouters()) {
router.onActivityDestroyed(activity);
router.onActivityDestroyed(activity, configurationChange);
}
}
}
@@ -319,7 +329,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (this.activity == null && findInActivity(activity) == LifecycleHandler.this) {
if (findInActivity(activity) == LifecycleHandler.this) {
this.activity = activity;
for (ActivityHostedRouter router : new ArrayList<>(routerMap.values())) {
@@ -40,10 +40,12 @@ public class ViewAttachHandler implements OnAttachStateChangeListener {
}
rootAttached = true;
listenForDeepestChildAttach(v, () -> {
childrenAttached = true;
reportAttached();
listenForDeepestChildAttach(v, new ChildAttachListener() {
@Override
public void onAttached() {
childrenAttached = true;
reportAttached();
}
});
}
@@ -65,6 +67,7 @@ public class ViewAttachHandler implements OnAttachStateChangeListener {
if (childOnAttachStateChangeListener != null && view instanceof ViewGroup) {
findDeepestChild((ViewGroup)view).removeOnAttachStateChangeListener(childOnAttachStateChangeListener);
childOnAttachStateChangeListener = null;
}
}
@@ -118,7 +121,7 @@ public class ViewAttachHandler implements OnAttachStateChangeListener {
@Override
public void onViewAttachedToWindow(View v) {
if (!attached) {
if (!attached && childOnAttachStateChangeListener != null) {
attached = true;
attachListener.onAttached();
v.removeOnAttachStateChangeListener(this);
@@ -2,6 +2,7 @@ package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
@@ -85,7 +86,7 @@ public class ControllerLifecycleActivityReferenceTests {
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter(parent.getView().findViewById(TestController.VIEW_ID));
Router childRouter = parent.getChildRouter((ViewGroup) parent.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
@@ -132,7 +133,7 @@ public class ControllerLifecycleActivityReferenceTests {
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter(parent.getView().findViewById(TestController.VIEW_ID));
Router childRouter = parent.getChildRouter((ViewGroup) parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
@@ -161,7 +162,7 @@ public class ControllerLifecycleActivityReferenceTests {
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter(parent.getView().findViewById(TestController.VIEW_ID));
Router childRouter = parent.getChildRouter((ViewGroup) parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
@@ -211,7 +212,7 @@ public class ControllerLifecycleActivityReferenceTests {
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter(parent.getView().findViewById(TestController.VIEW_ID));
Router childRouter = parent.getChildRouter((ViewGroup) parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
@@ -227,7 +228,7 @@ public class ControllerLifecycleActivityReferenceTests {
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
static class ActivityReferencingLifecycleListener implements Controller.LifecycleListener {
static class ActivityReferencingLifecycleListener extends Controller.LifecycleListener {
final List<Boolean> changeEndReferences = new ArrayList<>();
final List<Boolean> postCreateViewReferences = new ArrayList<>();
final List<Boolean> postAttachReferences = new ArrayList<>();
@@ -2,10 +2,11 @@ package com.bluelinelabs.conductor;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.bluelinelabs.conductor.Controller.RetainViewMode;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
@@ -590,6 +591,34 @@ public class ControllerLifecycleCallbacksTests {
assertTrue(child.isAttached());
}
@Test
public void testChildLifecycleAfterPushPopPush() {
Controller parent = new TestController();
parent.setRetainViewMode(RetainViewMode.RETAIN_DETACH);
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter
.setRoot(RouterTransaction.with(child)
.pushChangeHandler(new SimpleSwapChangeHandler())
.popChangeHandler(new SimpleSwapChangeHandler()));
Controller nextController = new TestController();
router.pushController(RouterTransaction.with(nextController));
TestController child2 = new TestController();
childRouter.pushController(RouterTransaction.with(child2));
router.popCurrentController();
assertTrue(parent.isAttached());
assertFalse(child.isAttached());
assertTrue(child2.isAttached());
}
private MockChangeHandler getPushHandler(final CallState expectedCallState, final TestController controller) {
return MockChangeHandler.listeningChangeHandler(new ChangeHandlerListener() {
@Override
@@ -8,7 +8,6 @@ import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.AttachFakingFrameLayout;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import com.bluelinelabs.conductor.util.ViewUtils;
import org.junit.Before;
import org.junit.Test;
@@ -17,6 +16,7 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@@ -80,7 +80,7 @@ public class ReattachCaseTests {
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID));
Router childRouter = controllerA.getChildRouter((ViewGroup) controllerA.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
@@ -350,6 +350,54 @@ public class ReattachCaseTests {
assertTrue(controller2.isAttached());
}
@Test
public void testHostAvailableDuringRotation() {
final Controller controllerA = new TestController();
final Controller childControllerA = new TestController();
final Controller controllerB = new TestController();
final Controller childControllerB = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Router childRouterA = controllerA.getChildRouter((ViewGroup) controllerA.getView().findViewById(TestController.VIEW_ID));
childRouterA.pushController(RouterTransaction.with(childControllerA)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertNotNull(controllerA.getActivity());
assertNotNull(childControllerA.getActivity());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Router childRouterB = controllerB.getChildRouter((ViewGroup) controllerB.getView().findViewById(TestController.VIEW_ID));
childRouterB.pushController(RouterTransaction.with(childControllerB)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertNotNull(controllerA.getActivity());
assertNotNull(childControllerA.getActivity());
assertNotNull(controllerB.getActivity());
assertNotNull(childControllerB.getActivity());
activityProxy.rotate();
assertNotNull(controllerA.getActivity());
assertNotNull(childControllerA.getActivity());
assertNotNull(controllerB.getActivity());
assertNotNull(childControllerB.getActivity());
router.rebindIfNeeded();
assertNotNull(controllerA.getActivity());
assertNotNull(childControllerA.getActivity());
assertNotNull(controllerB.getActivity());
assertNotNull(childControllerB.getActivity());
}
private void sleepWakeDevice() {
activityProxy.saveInstanceState(new Bundle()).pause();
activityProxy.resume();
@@ -1,11 +1,13 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.TestController;
@@ -110,6 +112,25 @@ public class ViewLeakTests {
assertNull(controller.getView());
}
@Test
public void testViewRemovedIfLayeredNotRemovesFromViewOnPush() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller));
router.pushController(RouterTransaction.with(new TestController()).pushChangeHandler(new SimpleSwapChangeHandler(false)));
View view = controller.view;
assertNotNull(view.getParent());
router.pushController(RouterTransaction.with(new TestController()));
assertNotNull(view.getParent());
router.popToRoot();
assertNull(view.getParent());
}
public static class NeverAddChangeHandler extends ControllerChangeHandler {
@Override
public void performChange(@NonNull final ViewGroup container, @Nullable View from, @Nullable final View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
+5 -3
View File
@@ -4,8 +4,8 @@ android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
@@ -31,6 +31,7 @@ android {
dependencies {
implementation rootProject.ext.androidxAppCompat
implementation rootProject.ext.androidxViewPager2
implementation rootProject.ext.material
implementation rootProject.ext.archComponentsLiveDataCore // Fix duplicate classes
@@ -41,10 +42,11 @@ dependencies {
implementation project(':conductor')
implementation project(':conductor-modules:viewpager')
implementation project(':conductor-modules:viewpager2')
implementation project(':conductor-modules:rxlifecycle2')
implementation project(':conductor-modules:autodispose')
implementation project(':conductor-modules:arch-components-lifecycle')
implementation project(':conductor-modules:transition-androidx')
implementation project(':conductor-modules:androidx-transition')
debugImplementation rootProject.ext.leakCanary
releaseImplementation rootProject.ext.leakCanaryNoOp
@@ -51,7 +51,7 @@ public class CircularRevealChangeHandler extends AnimatorChangeHandler {
* @param removesFromViewOnPush If true, the view being replaced will be removed from the view hierarchy on pushes
*/
public CircularRevealChangeHandler(@NonNull View fromView, @NonNull View containerView, boolean removesFromViewOnPush) {
this(fromView, containerView, DEFAULT_ANIMATION_DURATION, true);
this(fromView, containerView, DEFAULT_ANIMATION_DURATION, removesFromViewOnPush);
}
/**
@@ -1,13 +1,14 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.controllers.base.BaseController;
import com.bluelinelabs.conductor.demo.util.BundleBuilder;
@@ -48,11 +48,12 @@ public class HomeController extends BaseController {
SHARED_ELEMENT_TRANSITIONS("Shared Element Demos", R.color.purple_300),
CHILD_CONTROLLERS("Child Controllers", R.color.orange_300),
VIEW_PAGER("ViewPager", R.color.green_300),
TARGET_CONTROLLER("Target Controller", R.color.pink_300),
MULTIPLE_CHILD_ROUTERS("Multiple Child Routers", R.color.deep_orange_300),
MASTER_DETAIL("Master Detail", R.color.grey_300),
DRAG_DISMISS("Drag Dismiss", R.color.lime_300),
EXTERNAL_MODULES("Bonus Modules", R.color.teal_300);
VIEW_PAGER_2("ViewPager2", R.color.pink_300),
TARGET_CONTROLLER("Target Controller", R.color.deep_orange_300),
MULTIPLE_CHILD_ROUTERS("Multiple Child Routers", R.color.grey_300),
MASTER_DETAIL("Master Detail", R.color.lime_300),
DRAG_DISMISS("Drag Dismiss", R.color.teal_300),
EXTERNAL_MODULES("Bonus Modules", R.color.deep_purple_300);
String title;
@ColorRes int color;
@@ -182,7 +183,12 @@ public class HomeController extends BaseController {
.popChangeHandler(new FadeChangeHandler()));
break;
case VIEW_PAGER:
getRouter().pushController(RouterTransaction.with(new PagerController())
getRouter().pushController(RouterTransaction.with(new ViewPagerController())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
case VIEW_PAGER_2:
getRouter().pushController(RouterTransaction.with(new ViewPager2Controller())
.pushChangeHandler(new FadeChangeHandler())
.popChangeHandler(new FadeChangeHandler()));
break;
@@ -0,0 +1,76 @@
package com.bluelinelabs.conductor.demo.controllers;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.viewpager2.widget.ViewPager2;
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.viewpager2.RouterStateAdapter;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import java.util.Locale;
import butterknife.BindView;
public class ViewPager2Controller extends BaseController {
private int[] PAGE_COLORS = new int[]{R.color.green_300, R.color.cyan_300, R.color.deep_purple_300, R.color.lime_300, R.color.red_300};
@BindView(R.id.tab_layout) TabLayout tabLayout;
@BindView(R.id.view_pager) ViewPager2 viewPager;
private final RouterStateAdapter pagerAdapter;
public ViewPager2Controller() {
pagerAdapter = new RouterStateAdapter(this) {
@Override
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
public int getItemCount() {
return PAGE_COLORS.length;
}
};
}
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_view_pager2, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
viewPager.setAdapter(pagerAdapter);
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> tab.setText("Page " + position)).attach();
}
@Override
protected void onDestroyView(@NonNull View view) {
if (!getActivity().isChangingConfigurations()) {
viewPager.setAdapter(null);
}
tabLayout.setupWithViewPager(null);
super.onDestroyView(view);
}
@Override
protected String getTitle() {
return "ViewPager2 Demo";
}
}
@@ -1,24 +1,25 @@
package com.bluelinelabs.conductor.demo.controllers;
import androidx.annotation.NonNull;
import com.google.android.material.tabs.TabLayout;
import androidx.viewpager.widget.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.viewpager.widget.ViewPager;
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.viewpager.RouterPagerAdapter;
import com.google.android.material.tabs.TabLayout;
import java.util.Locale;
import butterknife.BindView;
public class PagerController extends BaseController {
public class ViewPagerController extends BaseController {
private int[] PAGE_COLORS = new int[]{R.color.green_300, R.color.cyan_300, R.color.deep_purple_300, R.color.lime_300, R.color.red_300};
@@ -27,7 +28,7 @@ public class PagerController extends BaseController {
private final RouterPagerAdapter pagerAdapter;
public PagerController() {
public ViewPagerController() {
pagerAdapter = new RouterPagerAdapter(this) {
@Override
public void configureRouter(@NonNull Router router, int position) {
@@ -52,7 +53,7 @@ public class PagerController extends BaseController {
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_pager, container, false);
return inflater.inflate(R.layout.controller_view_pager, container, false);
}
@Override
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:elevation="6dp"
android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
tools:targetApi="lollipop"
/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
+9 -3
View File
@@ -13,17 +13,23 @@ ext {
autodisposeVersion = '1.0.0'
archComponentsVersion = '2.0.0'
junitVersion = '4.12'
lintVersion = '26.6.1'
mvnPublishVersion = '0.11.1'
kotlinVersion = '1.3.71'
agpVersion = "4.1.0"
lintVersion = agpVersion.replaceFirst(~/\d*/) { version ->
// the major version of lint is always 23 version higher than the major version of agp
version.toInteger() + 23
}
kotlinVersion = '1.3.72'
kotlinStd = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
material = "com.google.android.material:material:1.0.0"
material = "com.google.android.material:material:1.1.0"
androidxAnnotations = "androidx.annotation:annotation:1.1.0"
androidxAppCompat = "androidx.appcompat:appcompat:1.0.0"
androidxTransition = "androidx.transition:transition:1.3.1"
androidxCollection = "androidx.collection:collection:1.1.0"
androidxViewPager2 = "androidx.viewpager2:viewpager2:1.0.0"
butterknife = "com.jakewharton:butterknife:$butterknifeVersion"
butterknifeCompiler = "com.jakewharton:butterknife-compiler:$butterknifeVersion"
Binary file not shown.
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+2
View File
@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
Vendored
+4 -18
View File
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -54,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -64,28 +64,14 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
+2 -2
View File
@@ -1,9 +1,9 @@
include ':conductor'
include ':conductor-lint'
include ':conductor-modules:viewpager'
include ':conductor-modules:viewpager2'
include ':conductor-modules:rxlifecycle2'
include ':conductor-modules:autodispose'
include ':conductor-modules:arch-components-lifecycle'
include ':conductor-modules:transition-platform'
include ':conductor-modules:transition-androidx'
include ':conductor-modules:androidx-transition'
include ':demo'