Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd11ac9d6b | |||
| e78347709b | |||
| 341debc5b9 | |||
| 2346e48154 | |||
| 4174e12958 | |||
| c0abed0813 | |||
| 6fdb1d6ed3 | |||
| 3334b8e21f | |||
| 240424dc63 | |||
| f768e9ab00 | |||
| 4c89124683 | |||
| e0bbd48935 |
+1
-3
@@ -35,6 +35,4 @@ pom.xml.*
|
||||
local.properties
|
||||
|
||||
*.prefs
|
||||
|
||||
# The keystore file
|
||||
app/spothero-release.keystore
|
||||
.DS_Store
|
||||
|
||||
+5
-8
@@ -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
|
||||
|
||||
@@ -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
@@ -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')
|
||||
|
||||
+1
-8
@@ -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;
|
||||
}
|
||||
|
||||
+2
-9
@@ -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;
|
||||
}
|
||||
|
||||
+38
-13
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+44
-9
@@ -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
-1
@@ -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>
|
||||
-647
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-149
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-184
@@ -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;
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -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 {
|
||||
|
||||
|
||||
+41
-26
@@ -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))
|
||||
}
|
||||
}
|
||||
-61
@@ -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;
|
||||
}
|
||||
}
|
||||
+14
-1
@@ -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>
|
||||
+227
@@ -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
|
||||
}
|
||||
+27
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+119
@@ -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())
|
||||
}
|
||||
}
|
||||
+21
@@ -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);
|
||||
|
||||
+6
-5
@@ -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<>();
|
||||
|
||||
+30
-1
@@ -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
@@ -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
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+76
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
+7
-6
@@ -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
@@ -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"
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user