Compare commits

...

51 Commits

Author SHA1 Message Date
Eric Kuck 76074d1e3d Update mvn publish plugin, version bump 2020-05-04 18:07:12 -05:00
Eric Kuck 623ed03df8 Revert transition module package name change 2020-05-04 17:56:53 -05:00
Eric Kuck 28b8810e56 Fixes tests 2020-05-04 17:55:11 -05:00
Eric Kuck 5c397404ce ChangeHandlerFrameLayout now open 2020-05-04 17:05:48 -05:00
Eric Kuck 3b81a962b1 Revert attachRouter as an extension 2020-05-04 17:05:07 -05:00
Eric Kuck 4f3662bbf6 Fixes a potential state restoration NPE
Could occur if backing containers for child controllers are not immediately available when the app is restored after being killed.
2020-04-27 15:59:31 -05:00
Paul Woitaschek c53b8d9d49 Migrated the StateSaveTests to kotlin and set an id on the frame layout. 2020-04-12 08:58:15 +02:00
Paul Woitaschek c8f6e552e4 Require setting an id in getChildRouter. Fixes #515 2020-04-12 08:43:53 +02:00
Paul Woitaschek c1cc2e2bca Updated robolectric. 2020-04-12 08:22:41 +02:00
Paul Woitaschek 9ace20b88b Updated AGP 2020-04-12 08:22:31 +02:00
Paul Woitaschek d8606498b0 For rotating, change the requested orientation. 2020-04-12 08:21:31 +02:00
Paul Woitaschek f35768393d Migrated the AcitvityProxy to kotlin. 2020-04-12 08:11:51 +02:00
Chris Horner bbb4e8c066 Add platform transition module (#591) 2020-04-04 21:40:02 -05:00
Eric Kuck 12e66867f1 Version bumps 2020-03-30 12:27:21 -05:00
Paul Woitaschek 8eee79dcea Updated lint. (#587) 2020-03-30 11:37:34 -05:00
Eric Kuck 5ebd8c9a5d Minor lambda cleanup 2020-03-30 11:22:38 -05:00
Eric Kuck 4c9f2e9f30 RestoreViewOnCreateController functionality merged into Controller 2020-03-30 11:19:04 -05:00
Eric Kuck b0340d4c67 Removes platform TransitionChangeHandler 2020-03-30 11:12:07 -05:00
Eric Kuck f6b396f679 Removes RxLifecycle for RxJava 1 2020-03-30 11:10:54 -05:00
Paul Woitaschek 589cb91fff Introduce Kotlin (and Java 8) (#585)
* Introduce kotlin and converted the ControllerChangeType to kotlin.

* Migrated the ThreadUtils to kotlin.

* Migrated the TransactionIndexer to kotlin.

* Migrated the ChangeHandlerFrameLayout to kotlin.

* Migrated Conductor to kotlin.

* Added RestrictTo annotations.

* Migrated the RouterRequiringFunc to kotlin.

* Migrated the RouterTransaction to kotlin

* Make use of diamonds.

* Make use of lambdas and method references.

* Use an interface with default implementations for the LifecycleListener.

* Simplify the attachRouter function.

* Updated the code style and checkin the idea folder.

* Use a Controller extension function for the router transaction.

* Use let's instead of forced !! casts.

* Added default implementations of ControllerChangeListener

* Migrated the NoOpControllerChangeHandler to kotlin.

* Use a top level extension function for the asTransaction function.

* Add JvmField annotations to the ControllerChangeType fields.

* Use the local variables instead of calling the controller again.
2020-03-30 16:37:13 +02:00
Paul Woitaschek a51f4192cb Updated AGP to 3.6.1 2020-03-28 21:29:19 +01:00
Paul Woitaschek 266d6a0fd6 Updated the androidx annotations. 2020-03-28 21:24:34 +01:00
Paul Woitaschek 7410f7d123 Disabled the jetifier as it's no longer needed. 2020-03-28 21:24:13 +01:00
Paul Woitaschek 56b5ce3b33 Updated gradle to 6.3 2020-03-28 21:23:21 +01:00
Paul Woitaschek a20ee6477a Replaced unmock with robolectric. 2020-03-28 21:22:55 +01:00
Eric Kuck 41f14f6ae5 support package renamed to viewpager 2020-03-28 14:44:15 -05:00
Alexey Glushkov a170942ae1 fix of removing/adding child view controller views when the retain mode of a parent controller is RETAIN_DETACH (#571) 2020-03-28 14:36:11 -05:00
Alexey Glushkov 6df6ce8de7 Fix Conductor pending changes lock, related to #287 discussion (#570) 2020-03-28 14:33:44 -05:00
Chris Horner 7ddc115918 Fix memory leak in SharedElementTransitionChangeHandler. (#528) 2020-03-28 14:26:41 -05:00
Alexey Glushkov 5f8f3ad98e Fix #563 bug: Controller in the middle of a backstack isn't destroyed after calling popController (#569) 2020-03-28 14:19:42 -05:00
Eric Kuck 5205033cc6 Renamed support->viewpager, removed old deprecated code 2020-03-28 14:07:19 -05:00
Steven Schoen bbd26995c5 Add androidx-transition module, update demos (#584) 2020-03-26 21:54:25 -05:00
Eric Kuck 94e4112ece Travis updates 2020-03-24 11:32:37 -05:00
Eric Kuck 45db55c678 Fixes RouterPagerAdapter state restoration when removing pages.
Fixes #582
2020-03-24 11:17:06 -05:00
dtrabo 7a00a6777e Fixed outdated docs (#576) 2019-12-06 16:04:40 -06:00
Eric Kuck 7a50f12740 Build tools updates 2019-08-11 11:14:36 -05:00
Steven Schoen 530bd551e2 Fix TransitionChangeHandlers getting stuck when two are run simultaneously (#547)
* Fix TransitionChangeHandlers getting stuck when two are run simultaneously

* Don't unnecessarily re-execute changes
2019-08-11 11:09:58 -05:00
Steven Schoen e40cdf2fb0 Keep ControllerChangeHandler default constructor in Proguard rules (#550) 2019-08-11 10:46:56 -05:00
Eric Kuck 94c9121bb1 Fixed logic error that could result in undestroyed controllers 2019-04-06 14:58:54 -04:00
Eric Kuck f72b5bed65 Fixes #524 - onContextAvailable not being called on all child controllers 2019-03-26 18:24:30 -05:00
Eric Kuck 7cb681f6f1 Fixed an edge case that could result in View being null in change handlers when setting backstack before view has been fully restored 2019-03-01 16:48:36 +02:00
Isuru Kusumal Rajapakse f80d78858d Fix MainActivity from leaking by releasing the listeners kept by handlers (#502) 2019-02-27 15:29:09 +02:00
Eric Kuck 6f856453c1 Version bump 2018-12-18 14:41:18 -06:00
Eric Kuck a6c50fccca Fix for onAttach potentially being called after destroy 2018-12-18 14:21:48 -06:00
Shaishav Gandhi 9ad140d00f Upgrade AutoDispose to 1.0.0 (#497)
* Upgrade AutoDispose to 1.0.0
2018-11-04 07:36:53 +01:00
Paul Woitaschek bb173b7cdc Migrate to AndroidX. Fixes #490 (#492)
* Gradle 4.10.2
* Android Build tools 3.2.1
* compileSdk 27, targetSdk 27, support libraries 28.0.0
* Migrated to androidX
2018-10-22 20:10:50 +02:00
Andy Byrnes 5f68328131 Fix README typos (#479) 2018-09-18 15:49:47 -05:00
Eric Kuck 61826fb42b Added a list of 3rd party modules to the readme 2018-09-04 11:35:09 -07:00
Shaishav Gandhi 6b5495a13b Update AutoDispose to 1.0.0-RC2 (#469)
* Upgrade AutoDispose to 1.0.0-RC2

Signed-off-by: shaishavgandhi05 <shaishgandhi@gmail.com>

* Remove fully qualified reference

Signed-off-by: shaishavgandhi05 <shaishgandhi@gmail.com>
2018-08-22 09:26:50 -05:00
Eric Kuck 7a24f77b59 No longer re-attaches controllers that shouldn't be attached when popController(controller) is called. Fixes #463. 2018-08-09 14:40:15 -05:00
Paul Woitaschek 562ed171c3 Specify the current api for the IssueRegistry. (#460) 2018-08-02 09:18:56 +02:00
162 changed files with 2858 additions and 2454 deletions
+1 -1
View File
@@ -5,10 +5,10 @@
.checkstyle
# IntelliJ IDEA
.idea
*.iml
*.ipr
*.iws
*.idea/dictionaries
classes
gen-external-apklibs
+8
View File
@@ -0,0 +1,8 @@
/libraries
/runConfigurations.xml
/misc.xml
/vcs.xml
/workspace.xml
/caches
/gradle.xml
/modules.xml
+129
View File
@@ -0,0 +1,129 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
+6
View File
@@ -0,0 +1,6 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default (1)" />
</state>
</component>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>
+2 -2
View File
@@ -3,8 +3,8 @@ language: android
android:
components:
- tools
- build-tools-27.0.3
- android-27
- build-tools-28.0.3
- android-28
- extra-android-m2repository
licenses:
- '.+'
+25 -18
View File
@@ -1,4 +1,4 @@
[![Travis Build](https://travis-ci.org/bluelinelabs/Conductor.svg)](https://travis-ci.org/bluelinelabs/Conductor) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Conductor-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/3361) [![Javadocs](http://javadoc.io/badge/com.bluelinelabs/conductor.svg)](http://javadoc.io/doc/com.bluelinelabs/conductor)
[![Travis Build](https://travis-ci.com/bluelinelabs/Conductor.svg)](https://travis-ci.com/bluelinelabs/Conductor) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Conductor-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/3361) [![Javadocs](http://javadoc.io/badge/com.bluelinelabs/conductor.svg)](http://javadoc.io/doc/com.bluelinelabs/conductor)
# Conductor
@@ -13,35 +13,34 @@ A small, yet full-featured framework that allows building View-based Android app
:twisted_rightwards_arrows: | Beautiful transitions between views
:floppy_disk: | State persistence
:phone: | Callbacks for onActivityResult, onRequestPermissionsResult, etc
:european_post_office: | MVP / MVVM / VIPER / MVC ready
:european_post_office: | MVP / MVVM / MVI / VIPER / MVC ready
Conductor is architecture-agnostic and does not try to force any design decisions on the developer. We here at BlueLine Labs tend to use either MVP or MVVM, but it would work equally well with standard MVC or whatever else you want to throw at it.
## Installation
```gradle
implementation 'com.bluelinelabs:conductor:2.1.5'
implementation 'com.bluelinelabs:conductor:3.0.0-rc5'
// If you want the components that go along with
// Android's support libraries (currently just a PagerAdapter):
implementation 'com.bluelinelabs:conductor-support:2.1.5'
// AndroidX Transition change handlers:
implementation 'com.bluelinelabs:conductor-androidx-transition:3.0.0-rc5'
// If you want RxJava lifecycle support:
implementation 'com.bluelinelabs:conductor-rxlifecycle:2.1.5'
// ViewPager PagerAdapter:
implementation 'com.bluelinelabs:conductor-viewpager:3.0.0-rc5'
// If you want RxJava2 lifecycle support:
implementation 'com.bluelinelabs:conductor-rxlifecycle2:2.1.5'
// RxJava2 lifecycle support:
implementation 'com.bluelinelabs:conductor-rxlifecycle2:3.0.0-rc5'
// If you want RxJava2 Autodispose support:
implementation 'com.bluelinelabs:conductor-autodispose:2.1.5'
// RxJava2 Autodispose support:
implementation 'com.bluelinelabs:conductor-autodispose:3.0.0-rc5'
// If you want Controllers that are Lifecycle-aware (architecture components):
implementation 'com.bluelinelabs:conductor-archlifecycle:2.1.5'
// Lifecycle-aware Controllers (architecture components):
implementation 'com.bluelinelabs:conductor-archlifecycle:3.0.0-rc5'
```
**SNAPSHOT**
Just use `2.1.6-SNAPSHOT` as your version number in any of the dependencies above and add the url to the snapshot repository:
Just use `3.0.1-SNAPSHOT` as your version number in any of the dependencies above and add the url to the snapshot repository:
```gradle
allprojects {
@@ -58,7 +57,7 @@ allprojects {
__Controller__ | The Controller is the View wrapper that will give you all of your lifecycle management features. Think of it as a lighter-weight and more predictable Fragment alternative with an easier to manage lifecycle.
__Router__ | A Router implements navigation and backstack handling for Controllers. Router objects are attached to Activity/containing ViewGroup pairs. Routers do not directly render or push Views to the container ViewGroup, but instead defer this responsibility to the ControllerChangeHandler specified in a given transaction.
__ControllerChangeHandler__ | ControllerChangeHandlers are responsible for swapping the View for one Controller to the View of another. They can be useful for performing animations and transitions between Controllers. Several default ControllerChangeHandlers are included.
__ControllerTransaction__ | Transactions are used to define data about adding Controllers. RouterControllerTransactions are used to push a Controller to a Router with specified ControllerChangeHandlers, while ChildControllerTransactions are used to add child Controllers.
__RouterTransaction__ | Transactions are used to define data about adding Controllers. RouterTransactions are used to push a Controller to a Router with specified ControllerChangeHandlers, while ChildControllerTransactions are used to add child Controllers.
## Getting Started
@@ -99,7 +98,7 @@ public class MainActivity extends Activity {
public class HomeController extends Controller {
@Override
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) {
View view = inflater.inflate(R.layout.controller_home, container, false);
((TextView) view.findViewById(R.id.tv_title)).setText("Hello World");
return view;
@@ -132,9 +131,17 @@ The lifecycle of a Controller is significantly simpler to understand than that o
### RxJava Lifecycle
If the AutoDispose dependency has been added, there is a `ControllerScopeProvider` available that can be used along with the standard [AutoDispose library](https://github.com/uber/AutoDispose).
## Community Projects
The community has provided several helpful modules to make developing apps with Conductor even easier. Here's a collection of helpful libraries:
* [ConductorGlide](https://github.com/MkhytarMkhoian/ConductorGlide) - Adds Glide lifecycle support to Controllers
* [ConductorDialog](https://github.com/MkhytarMkhoian/ConductorDialog) - Adds a helpful DialogController (a Conductor version of DialogFragment)
* [Mosby-Conductor](https://github.com/sockeqwe/mosby-conductor) - A plugin to integrate Mosby, an MVP/MVI library
## License
```
Copyright 2016 BlueLine Labs, Inc.
Copyright 2020 BlueLine Labs, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+23 -1
View File
@@ -1,10 +1,14 @@
buildscript {
apply from: rootProject.file('dependencies.gradle')
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.3'
classpath 'com.android.tools.build:gradle:3.6.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "com.vanniktech:gradle-maven-publish-plugin:$mvnPublishVersion"
}
}
@@ -14,6 +18,24 @@ allprojects {
maven { url 'https://maven.google.com' }
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')
+4 -9
View File
@@ -1,25 +1,20 @@
apply plugin: 'java'
targetCompatibility = JavaVersion.VERSION_1_7
sourceCompatibility = JavaVersion.VERSION_1_7
apply plugin: 'java-library'
configurations {
lintChecks
}
dependencies {
implementation rootProject.ext.lintapi
implementation rootProject.ext.lintchecks
compileOnly rootProject.ext.lintapi
compileOnly rootProject.ext.lintchecks
testImplementation rootProject.ext.lint
testImplementation rootProject.ext.lintTests
lintChecks files(jar)
}
jar {
manifest {
attributes('Lint-Registry': 'com.bluelinelabs.conductor.lint.IssueRegistry')
attributes('Lint-Registry-v2': 'com.bluelinelabs.conductor.lint.IssueRegistry')
}
}
@@ -10,6 +10,7 @@ import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UMethod;
@@ -18,9 +19,10 @@ import org.jetbrains.uast.UTypeReferenceExpression;
import java.util.Collections;
import java.util.List;
@SuppressWarnings("UnstableApiUsage")
public final class ControllerChangeHandlerIssueDetector extends Detector implements Detector.UastScanner {
public static final Issue ISSUE =
static final Issue ISSUE =
Issue.create("ValidControllerChangeHandler", "ControllerChangeHandler not instantiatable",
"Non-abstract ControllerChangeHandler instances must have a default constructor for the"
+ " system to re-create them in the case of the process being killed.",
@@ -41,7 +43,7 @@ public final class ControllerChangeHandlerIssueDetector extends Detector impleme
return new UElementHandler() {
@Override
public void visitClass(UClass node) {
public void visitClass(@NotNull UClass node) {
if (evaluator.isAbstract(node)) {
return;
}
@@ -11,6 +11,7 @@ import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UMethod;
@@ -20,9 +21,10 @@ import org.jetbrains.uast.UTypeReferenceExpression;
import java.util.Collections;
import java.util.List;
@SuppressWarnings("UnstableApiUsage")
public final class ControllerIssueDetector extends Detector implements Detector.UastScanner {
public static final Issue ISSUE =
static final Issue ISSUE =
Issue.create("ValidController", "Controller not instantiatable",
"Non-abstract Controller instances must have a default or single-argument constructor"
+ " that takes a Bundle in order for the system to re-create them in the"
@@ -42,7 +44,7 @@ public final class ControllerIssueDetector extends Detector implements Detector.
return new UElementHandler() {
@Override
public void visitClass(UClass node) {
public void visitClass(@NotNull UClass node) {
if (evaluator.isAbstract(node)) {
return;
}
@@ -100,5 +102,5 @@ public final class ControllerIssueDetector extends Detector implements Detector.
}
};
}
}
@@ -5,10 +5,21 @@ import com.android.tools.lint.detector.api.Issue;
import java.util.Arrays;
import java.util.List;
import static com.android.tools.lint.detector.api.ApiKt.CURRENT_API;
@SuppressWarnings({"unused", "UnstableApiUsage"})
public final class IssueRegistry extends com.android.tools.lint.client.api.IssueRegistry {
@Override public List<Issue> getIssues() {
@Override
public List<Issue> getIssues() {
return Arrays.asList(
ControllerIssueDetector.ISSUE,
ControllerChangeHandlerIssueDetector.ISSUE);
ControllerChangeHandlerIssueDetector.ISSUE
);
}
@Override
public int getApi() {
return CURRENT_API;
}
}
@@ -1,16 +1,8 @@
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
@@ -26,3 +18,6 @@ dependencies {
}
ext.artifactId = 'conductor-arch-components-lifecycle'
apply from: rootProject.file('dependencies.gradle')
apply plugin: "com.vanniktech.maven.publish"
@@ -1,11 +1,11 @@
package com.bluelinelabs.conductor.archlifecycle;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.Lifecycle.Event;
import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.LifecycleRegistry;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.Lifecycle.Event;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
import android.content.Context;
import android.support.annotation.NonNull;
import androidx.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
@@ -1,10 +1,10 @@
package com.bluelinelabs.conductor.archlifecycle;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleOwner;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
@@ -1,28 +0,0 @@
package com.bluelinelabs.conductor.archlifecycle;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleOwner;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bluelinelabs.conductor.RestoreViewOnCreateController;
public abstract class LifecycleRestoreViewOnCreateController extends RestoreViewOnCreateController implements LifecycleOwner {
private final ControllerLifecycleOwner mLifecycleOwner = new ControllerLifecycleOwner(this);
public LifecycleRestoreViewOnCreateController() {
super();
}
public LifecycleRestoreViewOnCreateController(@Nullable Bundle args) {
super(args);
}
@Override @NonNull
public Lifecycle getLifecycle() {
return mLifecycleOwner.getLifecycle();
}
}
+2 -6
View File
@@ -1,16 +1,11 @@
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
apply plugin: 'com.android.library'
apply plugin: "com.vanniktech.maven.publish"
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
@@ -22,6 +17,7 @@ android {
dependencies {
api rootProject.ext.rxJava2
api rootProject.ext.autodispose
api rootProject.ext.autodisposeLifecycle
implementation project(':conductor')
}
@@ -1,11 +1,11 @@
package com.bluelinelabs.conductor.autodispose;
import android.content.Context;
import android.support.annotation.NonNull;
import androidx.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.uber.autodispose.OutsideLifecycleException;
import com.uber.autodispose.OutsideScopeException;
import io.reactivex.subjects.BehaviorSubject;
@@ -16,7 +16,7 @@ public class ControllerLifecycleSubjectHelper {
public static BehaviorSubject<ControllerEvent> create(@NonNull Controller controller) {
ControllerEvent initialState;
if (controller.isBeingDestroyed() || controller.isDestroyed()) {
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
throw new OutsideScopeException("Cannot bind to Controller lifecycle when outside of it.");
} else if (controller.isAttached()) {
initialState = ControllerEvent.ATTACH;
} else if (controller.getView() != null) {
@@ -1,20 +1,22 @@
package com.bluelinelabs.conductor.autodispose;
import android.support.annotation.NonNull;
import androidx.annotation.NonNull;
import com.bluelinelabs.conductor.Controller;
import com.uber.autodispose.LifecycleScopeProvider;
import com.uber.autodispose.OutsideLifecycleException;
import com.uber.autodispose.OutsideScopeException;
import com.uber.autodispose.lifecycle.LifecycleScopeProvider;
import com.uber.autodispose.lifecycle.LifecycleScopes;
import com.uber.autodispose.lifecycle.CorrespondingEventsFunction;
import io.reactivex.CompletableSource;
import io.reactivex.Observable;
import io.reactivex.functions.Function;
import io.reactivex.subjects.BehaviorSubject;
public class ControllerScopeProvider implements LifecycleScopeProvider<ControllerEvent> {
private static final CorrespondingEventsFunction CORRESPONDING_EVENTS =
new CorrespondingEventsFunction() {
private static final CorrespondingEventsFunction<ControllerEvent> CORRESPONDING_EVENTS =
new CorrespondingEventsFunction<ControllerEvent>() {
@Override
public ControllerEvent apply(ControllerEvent lastEvent) throws OutsideLifecycleException {
public ControllerEvent apply(ControllerEvent lastEvent) throws OutsideScopeException {
switch (lastEvent) {
case CREATE:
return ControllerEvent.DESTROY;
@@ -27,20 +29,20 @@ public class ControllerScopeProvider implements LifecycleScopeProvider<Controlle
case DETACH:
return ControllerEvent.DESTROY;
default:
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
throw new OutsideScopeException("Cannot bind to Controller lifecycle when outside of it.");
}
}
};
@NonNull private final BehaviorSubject<ControllerEvent> lifecycleSubject;
@NonNull private final Function<ControllerEvent, ControllerEvent> correspondingEventsFunction;
@NonNull private final CorrespondingEventsFunction<ControllerEvent> correspondingEventsFunction;
public static ControllerScopeProvider from(@NonNull Controller controller) {
return new ControllerScopeProvider(controller, CORRESPONDING_EVENTS);
}
public static ControllerScopeProvider from(@NonNull Controller controller, @NonNull final ControllerEvent untilEvent) {
return new ControllerScopeProvider(controller, new CorrespondingEventsFunction() {
return new ControllerScopeProvider(controller, new CorrespondingEventsFunction<ControllerEvent>() {
@Override
public ControllerEvent apply(ControllerEvent controllerEvent) {
return untilEvent;
@@ -48,11 +50,11 @@ public class ControllerScopeProvider implements LifecycleScopeProvider<Controlle
});
}
public static ControllerScopeProvider from(@NonNull Controller controller, @NonNull final CorrespondingEventsFunction correspondingEventsFunction) {
public static ControllerScopeProvider from(@NonNull Controller controller, @NonNull final CorrespondingEventsFunction<ControllerEvent> correspondingEventsFunction) {
return new ControllerScopeProvider(controller, correspondingEventsFunction);
}
private ControllerScopeProvider(@NonNull Controller controller, @NonNull CorrespondingEventsFunction correspondingEventsFunction) {
private ControllerScopeProvider(@NonNull Controller controller, @NonNull CorrespondingEventsFunction<ControllerEvent> correspondingEventsFunction) {
lifecycleSubject = ControllerLifecycleSubjectHelper.create(controller);
this.correspondingEventsFunction = correspondingEventsFunction;
}
@@ -63,7 +65,7 @@ public class ControllerScopeProvider implements LifecycleScopeProvider<Controlle
}
@Override
public Function<ControllerEvent, ControllerEvent> correspondingEvents() {
public CorrespondingEventsFunction<ControllerEvent> correspondingEvents() {
return correspondingEventsFunction;
}
@@ -71,4 +73,9 @@ public class ControllerScopeProvider implements LifecycleScopeProvider<Controlle
public ControllerEvent peekLifecycle() {
return lifecycleSubject.getValue();
}
@Override
public CompletableSource requestScope() throws Exception {
return LifecycleScopes.resolveScopeFromLifecycle(this);
}
}
@@ -1,24 +0,0 @@
package com.bluelinelabs.conductor.autodispose;
import com.uber.autodispose.OutsideLifecycleException;
import io.reactivex.functions.Function;
/**
* Based on https://github.com/uber/AutoDispose/blob/master/lifecycle/autodispose-lifecycle/src/main/java/com/uber/autodispose/lifecycle/CorrespondingEventsFunction.java
*
* A corresponding events function that acts as a normal {@link Function} but ensures ControllerEvent event
* types are used in the generic and tightens the possible exception thrown to {@link OutsideLifecycleException}.
*/
public interface CorrespondingEventsFunction extends Function<ControllerEvent, ControllerEvent> {
/**
* Given an event {@code event}, returns the next corresponding event that this lifecycle should
* dispose on.
*
* @param event the source or start event.
* @return the target event that should signal disposal.
* @throws OutsideLifecycleException if the lifecycle exceeds its scope boundaries.
*/
@Override ControllerEvent apply(ControllerEvent event) throws OutsideLifecycleException;
}
@@ -1,3 +0,0 @@
POM_NAME=Conductor RxLifecycle Extensions
POM_ARTIFACT_ID=conductor-rxlifecycle
POM_PACKAGING=aar
@@ -1,3 +0,0 @@
<manifest package="com.bluelinelabs.conductor.rxlifecycle">
<application />
</manifest>
@@ -1,14 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle;
public enum ControllerEvent {
CREATE,
CONTEXT_AVAILABLE,
CREATE_VIEW,
ATTACH,
DETACH,
DESTROY_VIEW,
CONTEXT_UNAVAILABLE,
DESTROY
}
@@ -1,77 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.trello.rxlifecycle.OutsideLifecycleException;
import rx.subjects.BehaviorSubject;
/**
* A simple utility class that will create a {@link BehaviorSubject} that calls onNext when events
* occur in your {@link Controller}
*/
public class ControllerLifecycleSubjectHelper {
private ControllerLifecycleSubjectHelper() { }
public static BehaviorSubject<ControllerEvent> create(Controller controller) {
ControllerEvent initialState;
if (controller.isBeingDestroyed() || controller.isDestroyed()) {
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
} else if (controller.isAttached()) {
initialState = ControllerEvent.ATTACH;
} else if (controller.getView() != null) {
initialState = ControllerEvent.CREATE_VIEW;
} else if (controller.getActivity() != null) {
initialState = ControllerEvent.CONTEXT_AVAILABLE;
} else {
initialState = ControllerEvent.CREATE;
}
final BehaviorSubject<ControllerEvent> subject = BehaviorSubject.create(initialState);
controller.addLifecycleListener(new LifecycleListener() {
@Override
public void preContextAvailable(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CONTEXT_AVAILABLE);
}
@Override
public void preCreateView(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CREATE_VIEW);
}
@Override
public void preAttach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.ATTACH);
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DETACH);
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DESTROY_VIEW);
}
@Override
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
subject.onNext(ControllerEvent.CONTEXT_UNAVAILABLE);
}
@Override
public void preDestroy(@NonNull Controller controller) {
subject.onNext(ControllerEvent.DESTROY);
}
});
return subject;
}
}
@@ -1,52 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.Controller;
import com.trello.rxlifecycle.LifecycleProvider;
import com.trello.rxlifecycle.LifecycleTransformer;
import com.trello.rxlifecycle.RxLifecycle;
import rx.Observable;
import rx.subjects.BehaviorSubject;
/**
* A base {@link Controller} that can be used to expose lifecycle events using RxJava
*/
public abstract class RxController extends Controller implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxController() {
this(null);
}
public RxController(Bundle args) {
super(args);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.asObservable();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
@@ -1,48 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import com.trello.rxlifecycle.LifecycleTransformer;
import com.trello.rxlifecycle.OutsideLifecycleException;
import com.trello.rxlifecycle.RxLifecycle;
import rx.Observable;
import rx.functions.Func1;
public class RxControllerLifecycle {
/**
* Binds the given source to a Controller lifecycle. This is the Controller version of
* {@link com.trello.rxlifecycle.android.RxLifecycleAndroid#bindFragment(Observable)}.
*
* @param lifecycle the lifecycle sequence of a Controller
* @return a reusable {@link rx.Observable.Transformer} that unsubscribes the source during the Controller lifecycle
*/
@NonNull
@CheckResult
public static <T> LifecycleTransformer<T> bindController(@NonNull final Observable<ControllerEvent> lifecycle) {
return RxLifecycle.bind(lifecycle, CONTROLLER_LIFECYCLE);
}
private static final Func1<ControllerEvent, ControllerEvent> CONTROLLER_LIFECYCLE =
new Func1<ControllerEvent, ControllerEvent>() {
@Override
public ControllerEvent call(ControllerEvent lastEvent) {
switch (lastEvent) {
case CREATE:
return ControllerEvent.DESTROY;
case CONTEXT_AVAILABLE:
return ControllerEvent.CONTEXT_UNAVAILABLE;
case ATTACH:
return ControllerEvent.DETACH;
case CREATE_VIEW:
return ControllerEvent.DESTROY_VIEW;
case DETACH:
return ControllerEvent.DESTROY;
default:
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
}
}
};
}
@@ -1,52 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.RestoreViewOnCreateController;
import com.trello.rxlifecycle.LifecycleProvider;
import com.trello.rxlifecycle.LifecycleTransformer;
import com.trello.rxlifecycle.RxLifecycle;
import rx.Observable;
import rx.subjects.BehaviorSubject;
/**
* A base {@link RestoreViewOnCreateController} that can be used to expose lifecycle events using RxJava
*/
public abstract class RxRestoreViewOnCreateController extends RestoreViewOnCreateController implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxRestoreViewOnCreateController() {
this(null);
}
public RxRestoreViewOnCreateController(Bundle args) {
super(args);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.asObservable();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
+1 -6
View File
@@ -1,16 +1,11 @@
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
apply plugin: 'com.android.library'
apply plugin: "com.vanniktech.maven.publish"
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
@@ -1,7 +1,7 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.content.Context;
import android.support.annotation.NonNull;
import androidx.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
@@ -1,9 +1,9 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import androidx.annotation.CheckResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
import com.trello.rxlifecycle2.LifecycleProvider;
import com.trello.rxlifecycle2.LifecycleTransformer;
@@ -1,6 +1,6 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.support.annotation.NonNull;
import androidx.annotation.NonNull;
import com.trello.rxlifecycle2.LifecycleTransformer;
import com.trello.rxlifecycle2.OutsideLifecycleException;
import com.trello.rxlifecycle2.RxLifecycle;
@@ -1,45 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.RestoreViewOnCreateController;
import com.trello.rxlifecycle2.LifecycleProvider;
import com.trello.rxlifecycle2.LifecycleTransformer;
import com.trello.rxlifecycle2.RxLifecycle;
import io.reactivex.Observable;
import io.reactivex.subjects.BehaviorSubject;
public abstract class RxRestoreViewOnCreateController extends RestoreViewOnCreateController implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxRestoreViewOnCreateController() {
this(null);
}
public RxRestoreViewOnCreateController(Bundle args) {
super(args);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
-40
View File
@@ -1,40 +0,0 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'de.mobilej.unmock:UnMockPlugin:0.6.5'
}
}
apply plugin: 'com.android.library'
apply plugin: 'de.mobilej.unmock'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.roboelectric
implementation rootProject.ext.supportV4
implementation project(':conductor')
}
ext.artifactId = 'conductor-support'
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
@@ -1,3 +0,0 @@
POM_NAME=Conductor Support Extensions
POM_ARTIFACT_ID=conductor-support
POM_PACKAGING=aar
@@ -1,3 +0,0 @@
<manifest package="com.bluelinelabs.conductor.support">
<application />
</manifest>
@@ -1,154 +0,0 @@
package com.bluelinelabs.conductor.support;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
/**
* @deprecated Use RouterPagerAdapter instead! This implementation was too limited and had too many
* gotchas associated with it.
*
* An adapter for ViewPagers that will handle adding and removing Controllers
*/
@Deprecated
public abstract class ControllerPagerAdapter extends PagerAdapter {
private static final String KEY_SAVED_PAGES = "ControllerPagerAdapter.savedStates";
private static final String KEY_SAVES_STATE = "ControllerPagerAdapter.savesState";
private static final String KEY_VISIBLE_PAGE_IDS_KEYS = "ControllerPagerAdapter.visiblePageIds.keys";
private static final String KEY_VISIBLE_PAGE_IDS_VALUES = "ControllerPagerAdapter.visiblePageIds.values";
private final Controller host;
private boolean savesState;
private SparseArray<Bundle> savedPages = new SparseArray<>();
private SparseArray<String> visiblePageIds = new SparseArray<>();
/**
* Creates a new ControllerPagerAdapter using the passed host.
*/
public ControllerPagerAdapter(@NonNull Controller host, boolean saveControllerState) {
this.host = host;
savesState = saveControllerState;
}
/**
* Return the Controller associated with a specified position.
*/
@NonNull
public abstract Controller getItem(int position);
@Override
public Object instantiateItem(ViewGroup container, int position) {
final String name = makeControllerName(container.getId(), getItemId(position));
Router router = host.getChildRouter(container, name);
if (savesState && !router.hasRootController()) {
Bundle routerSavedState = savedPages.get(position);
if (routerSavedState != null) {
router.restoreInstanceState(routerSavedState);
}
}
final Controller controller;
if (!router.hasRootController()) {
controller = getItem(position);
router.setRoot(RouterTransaction.with(controller).tag(name));
} else {
router.rebindIfNeeded();
controller = router.getControllerWithTag(name);
}
if (controller != null) {
visiblePageIds.put(position, controller.getInstanceId());
}
return router.getControllerWithTag(name);
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Router router = ((Controller)object).getRouter();
if (savesState) {
Bundle savedState = new Bundle();
router.saveInstanceState(savedState);
savedPages.put(position, savedState);
}
visiblePageIds.remove(position);
host.removeChildRouter(router);
}
@Override
public boolean isViewFromObject(View view, Object object) {
return ((Controller)object).getView() == view;
}
@Override
public Parcelable saveState() {
Bundle bundle = new Bundle();
bundle.putBoolean(KEY_SAVES_STATE, savesState);
bundle.putSparseParcelableArray(KEY_SAVED_PAGES, savedPages);
int[] visiblePageIdsKeys = new int[visiblePageIds.size()];
String[] visiblePageIdsValues = new String[visiblePageIds.size()];
for (int i = 0; i < visiblePageIds.size(); i++) {
visiblePageIdsKeys[i] = visiblePageIds.keyAt(i);
visiblePageIdsValues[i] = visiblePageIds.valueAt(i);
}
bundle.putIntArray(KEY_VISIBLE_PAGE_IDS_KEYS, visiblePageIdsKeys);
bundle.putStringArray(KEY_VISIBLE_PAGE_IDS_VALUES, visiblePageIdsValues);
return bundle;
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
Bundle bundle = (Bundle)state;
if (state != null) {
savesState = bundle.getBoolean(KEY_SAVES_STATE, false);
savedPages = bundle.getSparseParcelableArray(KEY_SAVED_PAGES);
int[] visiblePageIdsKeys = bundle.getIntArray(KEY_VISIBLE_PAGE_IDS_KEYS);
String[] visiblePageIdsValues = bundle.getStringArray(KEY_VISIBLE_PAGE_IDS_VALUES);
visiblePageIds = new SparseArray<>(visiblePageIdsKeys.length);
for (int i = 0; i < visiblePageIdsKeys.length; i++) {
visiblePageIds.put(visiblePageIdsKeys[i], visiblePageIdsValues[i]);
}
}
}
/**
* Returns the already instantiated Controller in the specified position or {@code null} if
* this position does not yet have a controller.
*/
@Nullable
public Controller getController(int position) {
String instanceId = visiblePageIds.get(position);
if (instanceId != null) {
return host.getRouter().getControllerWithInstanceId(instanceId);
} else {
return null;
}
}
public long getItemId(int position) {
return position;
}
private static String makeControllerName(int viewId, long id) {
return viewId + ":" + id;
}
}
@@ -1,113 +0,0 @@
package com.bluelinelabs.conductor.support;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.util.SparseArray;
import android.widget.FrameLayout;
import com.bluelinelabs.conductor.Conductor;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.support.util.FakePager;
import com.bluelinelabs.conductor.support.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class StateSaveTests {
private FakePager pager;
private RouterPagerAdapter pagerAdapter;
public void createActivityController(Bundle savedInstanceState) {
ActivityController<Activity> activityController = Robolectric.buildActivity(Activity.class).create().start().resume();
Router router = Conductor.attachRouter(activityController.get(), new FrameLayout(activityController.get()), savedInstanceState);
TestController controller = new TestController();
router.setRoot(RouterTransaction.with(controller));
pager = new FakePager(new FrameLayout(activityController.get()));
pager.setOffscreenPageLimit(1);
pagerAdapter = new RouterPagerAdapter(controller) {
@Override
public void configureRouter(@NonNull Router router, int position) {
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Override
public int getCount() {
return 20;
}
};
pager.setAdapter(pagerAdapter);
}
@Before
public void setup() {
createActivityController(null);
}
@Test
public void testNoMaxSaves() {
// Load all pages
for (int i = 0; i < pagerAdapter.getCount(); i++) {
pager.pageTo(i);
}
pager.pageTo(pagerAdapter.getCount() / 2);
// Ensure all non-visible pages are saved
assertEquals(pagerAdapter.getCount() - 1 - pager.getOffscreenPageLimit() * 2, pagerAdapter.getSavedPages().size());
}
@Test
public void testMaxSavedSet() {
final int maxPages = 3;
pagerAdapter.setMaxPagesToStateSave(maxPages);
// Load all pages
for (int i = 0; i < pagerAdapter.getCount(); i++) {
pager.pageTo(i);
}
final int firstSelectedItem = pagerAdapter.getCount() / 2;
pager.pageTo(firstSelectedItem);
SparseArray<Bundle> savedPages = pagerAdapter.getSavedPages();
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size());
// Ensure correct pages are saved
assertEquals(pagerAdapter.getCount() - 3, savedPages.keyAt(0));
assertEquals(pagerAdapter.getCount() - 2, savedPages.keyAt(1));
assertEquals(pagerAdapter.getCount() - 1, savedPages.keyAt(2));
final int secondSelectedItem = 1;
pager.pageTo(secondSelectedItem);
savedPages = pagerAdapter.getSavedPages();
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size());
// Ensure correct pages are saved
assertEquals(firstSelectedItem - 1, savedPages.keyAt(0));
assertEquals(firstSelectedItem, savedPages.keyAt(1));
assertEquals(firstSelectedItem + 1, savedPages.keyAt(2));
}
}
@@ -0,0 +1,24 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
implementation rootProject.ext.androidxAppCompat
implementation rootProject.ext.androidxCollection
api rootProject.ext.androidxTransition
implementation project(':conductor')
}
ext.artifactId = 'conductor-androidx-transition'
apply from: rootProject.file('dependencies.gradle')
apply plugin: "com.vanniktech.maven.publish"
@@ -0,0 +1,3 @@
POM_NAME=Conductor AndroidX Transition Extensions
POM_ARTIFACT_ID=conductor-transition-androidx
POM_PACKAGING=aar
@@ -0,0 +1,3 @@
<manifest package="com.bluelinelabs.conductor.androidxtransition">
<application />
</manifest>
@@ -1,20 +1,20 @@
package com.bluelinelabs.conductor.changehandler;
package com.bluelinelabs.conductor.changehandler.androidxtransition;
import android.annotation.TargetApi;
import android.app.SharedElementCallback;
import android.graphics.Rect;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.Transition;
import android.transition.Transition.TransitionListener;
import android.transition.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 androidx.collection.ArrayMap;
import androidx.core.app.SharedElementCallback;
import androidx.core.view.ViewCompat;
import androidx.core.view.ViewGroupCompat;
import androidx.transition.Transition;
import androidx.transition.TransitionSet;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.internal.TransitionUtils;
@@ -25,9 +25,11 @@ 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 the <b>androidx</b> {@link Transition}. If you're using Android's platform transitions,
* consider using the {@code SharedElementTransitionChangeHandler} provided by the {@code android-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.
@@ -160,7 +162,7 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
OneShotPreDrawListener.add(true, view, new Runnable() {
@Override
public void run() {
waitForTransitionNames.remove(view.getTransitionName());
waitForTransitionNames.remove(ViewCompat.getTransitionName(view));
removedViews.add(new ViewParentPair(view, (ViewGroup)view.getParent()));
((ViewGroup)view.getParent()).removeView(view);
@@ -319,7 +321,7 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
final ArrayMap<String, View> toSharedElements = new ArrayMap<>();
TransitionUtils.findNamedViews(toSharedElements, to);
for (ViewParentPair removedView : removedViews) {
toSharedElements.put(removedView.view.getTransitionName(), removedView.view);
toSharedElements.put(ViewCompat.getTransitionName(removedView.view), removedView.view);
}
final List<String> names = new ArrayList<>(sharedElementNames.values());
@@ -335,10 +337,10 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
if (key != null) {
sharedElementNames.remove(key);
}
} else if (!name.equals(view.getTransitionName())) {
} else if (!name.equals(ViewCompat.getTransitionName(view))) {
String key = findKeyForValue(sharedElementNames, name);
if (key != null) {
sharedElementNames.put(key, view.getTransitionName());
sharedElementNames.put(key, ViewCompat.getTransitionName(view));
}
}
}
@@ -383,9 +385,9 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
View view = fromSharedElements.get(name);
if (view == null) {
sharedElementNames.remove(name);
} else if (!name.equals(view.getTransitionName())) {
} else if (!name.equals(ViewCompat.getTransitionName(view))) {
String targetValue = sharedElementNames.remove(name);
sharedElementNames.put(view.getTransitionName(), targetValue);
sharedElementNames.put(ViewCompat.getTransitionName(view), targetValue);
}
}
} else {
@@ -415,7 +417,7 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
if (view.getVisibility() == View.VISIBLE) {
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
if (viewGroup.isTransitionGroup()) {
if (ViewGroupCompat.isTransitionGroup(viewGroup)) {
transitioningViews.add(viewGroup);
} else {
int count = viewGroup.getChildCount();
@@ -434,7 +436,7 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
@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() {
overallTransition.addListener(new Transition.TransitionListener() {
@Override
public void onTransitionStart(Transition transition) {
if (enterTransition != null && enteringViews != null) {
@@ -469,10 +471,10 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
final int numSharedElements = toSharedElements.size();
for (int i = 0; i < numSharedElements; i++) {
View view = toSharedElements.get(i);
String name = view.getTransitionName();
String name = ViewCompat.getTransitionName(view);
if (name != null) {
String inName = findKeyForValue(sharedElementNames, name);
view.setTransitionName(inName);
ViewCompat.setTransitionName(view, inName);
}
}
}
@@ -486,9 +488,9 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
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 name = ViewCompat.getTransitionName(view);
final String inName = sharedElementNames.get(name);
view.setTransitionName(inName);
ViewCompat.setTransitionName(view, inName);
}
}
});
@@ -572,7 +574,7 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
* @param toName The transition name used in the "to" view
*/
protected final void addSharedElement(@NonNull View sharedElement, @NonNull String toName) {
String transitionName = sharedElement.getTransitionName();
String transitionName = ViewCompat.getTransitionName(sharedElement);
if (transitionName == null) {
throw new IllegalArgumentException("Unique transitionNames are required for all sharedElements");
}
@@ -0,0 +1,153 @@
package com.bluelinelabs.conductor.changehandler.androidxtransition;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.transition.Transition;
import androidx.transition.TransitionManager;
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 the <b>androidx</b> {@link Transition}. If you're using Android's platform transitions,
* consider using the {@code TransitionChangeHandler} provided by the {@code android-transitions} Conductor module.
*/
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 = new Runnable() {
@Override
public void run() {
changeListener.onChangeCompleted();
}
};
final Transition transition = getTransition(container, from, to, isPush);
transition.addListener(new Transition.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, new OnTransitionPreparedListener() {
@Override
public void onPrepared() {
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);
}
}
}
@@ -0,0 +1,184 @@
package com.bluelinelabs.conductor.internal;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.transition.Transition;
import androidx.transition.TransitionSet;
import java.util.List;
import java.util.Map;
public class TransitionUtils {
public static void findNamedViews(@NonNull Map<String, View> namedViews, View view) {
if (view.getVisibility() == View.VISIBLE) {
String transitionName = ViewCompat.getTransitionName(view);
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(ViewCompat.getTransitionName(view))) {
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 (!TransitionUtils.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;
}
}
@@ -0,0 +1,21 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
implementation project(':conductor')
}
ext.artifactId = 'conductor-android-transition'
apply from: rootProject.file('dependencies.gradle')
apply plugin: "com.vanniktech.maven.publish"
@@ -0,0 +1,3 @@
POM_NAME=Conductor Platform Transition Extensions
POM_ARTIFACT_ID=conductor-transition-platform
POM_PACKAGING=aar
@@ -0,0 +1,3 @@
<manifest package="com.bluelinelabs.conductor.platformtransition">
<application />
</manifest>
@@ -0,0 +1,647 @@
package com.bluelinelabs.conductor.changehandler.platformtransition;
import android.annotation.TargetApi;
import android.app.SharedElementCallback;
import android.graphics.Rect;
import android.os.Build;
import android.transition.Transition;
import android.transition.Transition.TransitionListener;
import android.transition.TransitionSet;
import android.util.ArrayMap;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnPreDrawListener;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.internal.TransitionUtils;
import java.util.ArrayList;
import java.util.List;
/**
* A TransitionChangeHandler that facilitates using different Transitions for the entering view, the exiting view,
* and shared elements between the two.
* <p/>
* Note that this class uses Android's <b>platform</b> {@link Transition}. If you're using androidx transitions, consider
* using the {@code SharedElementTransitionChangeHandler} provided by the {@code androidx-transitions} Conductor module.
*/
// Much of this class is based on FragmentTransition.java and FragmentTransitionCompat21.java from the Android support library
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class SharedElementTransitionChangeHandler extends TransitionChangeHandler {
// A map of from -> to names. Generally these will be the same.
@NonNull final ArrayMap<String, String> sharedElementNames = new ArrayMap<>();
@NonNull final List<String> waitForTransitionNames = new ArrayList<>();
@NonNull final List<ViewParentPair> removedViews = new ArrayList<>();
@Nullable Transition exitTransition;
@Nullable Transition enterTransition;
@Nullable Transition sharedElementTransition;
@Nullable private SharedElementCallback exitTransitionCallback;
@Nullable private SharedElementCallback enterTransitionCallback;
@NonNull
@Override
protected final Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
exitTransition = getExitTransition(container, from, to, isPush);
enterTransition = getEnterTransition(container, from, to, isPush);
sharedElementTransition = getSharedElementTransition(container, from, to, isPush);
exitTransitionCallback = getExitTransitionCallback(container, from, to, isPush);
enterTransitionCallback = getEnterTransitionCallback(container, from, to, isPush);
if (enterTransition == null && sharedElementTransition == null && exitTransition == null) {
throw new IllegalStateException("SharedElementTransitionChangeHandler must have at least one transaction.");
}
return mergeTransitions(isPush);
}
@Override
public void prepareForTransition(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, @NonNull final Transition transition, final boolean isPush, @NonNull final OnTransitionPreparedListener onTransitionPreparedListener) {
OnTransitionPreparedListener listener = () -> {
configureTransition(container, from, to, transition, isPush);
onTransitionPreparedListener.onPrepared();
};
configureSharedElements(container, from, to, isPush);
if (to != null && to.getParent() == null && waitForTransitionNames.size() > 0) {
waitOnAllTransitionNames(to, listener);
container.addView(to);
} else {
listener.onPrepared();
}
}
@Override
public final void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @Nullable Transition transition, boolean isPush) {
if (to != null && removedViews.size() > 0) {
to.setVisibility(View.VISIBLE);
for (ViewParentPair removedView : removedViews) {
removedView.parent.addView(removedView.view);
}
removedViews.clear();
}
super.executePropertyChanges(container, from, to, transition, isPush);
}
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
removedViews.clear();
}
@Override
protected void onEnd() {
exitTransition = null;
enterTransition = null;
sharedElementTransition = null;
}
void configureTransition(@NonNull final ViewGroup container, @Nullable View from, @Nullable View to, @NonNull final Transition transition, boolean isPush) {
final View nonExistentView = new View(container.getContext());
List<View> fromSharedElements = new ArrayList<>();
List<View> toSharedElements = new ArrayList<>();
configureSharedElements(container, nonExistentView, to, from, isPush, fromSharedElements, toSharedElements);
List<View> exitingViews = exitTransition != null ? configureEnteringExitingViews(exitTransition, from, fromSharedElements, nonExistentView) : null;
if (exitingViews == null || exitingViews.isEmpty()) {
exitTransition = null;
}
if (enterTransition != null) {
enterTransition.addTarget(nonExistentView);
}
final List<View> enteringViews = new ArrayList<>();
scheduleRemoveTargets(transition, enterTransition, enteringViews, exitTransition, exitingViews, sharedElementTransition, toSharedElements);
scheduleTargetChange(container, to, nonExistentView, toSharedElements, enteringViews, exitingViews);
setNameOverrides(container, toSharedElements);
scheduleNameReset(container, toSharedElements);
}
private void waitOnAllTransitionNames(@NonNull final View to, @NonNull final OnTransitionPreparedListener onTransitionPreparedListener) {
OnPreDrawListener onPreDrawListener = new OnPreDrawListener() {
boolean addedSubviewListeners;
@Override
public boolean onPreDraw() {
List<View> foundViews = new ArrayList<>();
boolean allViewsFound = true;
for (String transitionName : waitForTransitionNames) {
View namedView = TransitionUtils.findNamedView(to, transitionName);
if (namedView != null) {
foundViews.add(TransitionUtils.findNamedView(to, transitionName));
} else {
allViewsFound = false;
break;
}
}
if (allViewsFound && !addedSubviewListeners) {
addedSubviewListeners = true;
waitOnChildTransitionNames(to, foundViews, this, onTransitionPreparedListener);
}
return false;
}
};
to.getViewTreeObserver().addOnPreDrawListener(onPreDrawListener);
}
void waitOnChildTransitionNames(@NonNull final View to, @NonNull List<View> foundViews, @NonNull final OnPreDrawListener parentPreDrawListener, @NonNull final OnTransitionPreparedListener onTransitionPreparedListener) {
for (final View view : foundViews) {
OneShotPreDrawListener.add(true, view, () -> {
waitForTransitionNames.remove(view.getTransitionName());
removedViews.add(new ViewParentPair(view, (ViewGroup) view.getParent()));
((ViewGroup) view.getParent()).removeView(view);
if (waitForTransitionNames.size() == 0) {
to.getViewTreeObserver().removeOnPreDrawListener(parentPreDrawListener);
to.setVisibility(View.INVISIBLE);
onTransitionPreparedListener.onPrepared();
}
});
}
}
private void scheduleTargetChange(@NonNull final ViewGroup container, @Nullable final View to, @NonNull final View nonExistentView,
@NonNull final List<View> toSharedElements, @NonNull final List<View> enteringViews, @Nullable final List<View> exitingViews) {
OneShotPreDrawListener.add(true, container, () -> {
if (enterTransition != null) {
enterTransition.removeTarget(nonExistentView);
List<View> views = configureEnteringExitingViews(enterTransition, to, toSharedElements, nonExistentView);
enteringViews.addAll(views);
}
if (exitingViews != null) {
if (exitTransition != null) {
List<View> tempExiting = new ArrayList<>();
tempExiting.add(nonExistentView);
TransitionUtils.replaceTargets(exitTransition, exitingViews, tempExiting);
}
exitingViews.clear();
exitingViews.add(nonExistentView);
}
});
}
private Transition mergeTransitions(boolean isPush) {
boolean overlap = enterTransition == null || exitTransition == null || allowTransitionOverlap(isPush);
if (overlap) {
return TransitionUtils.mergeTransitions(TransitionSet.ORDERING_TOGETHER, exitTransition, enterTransition, sharedElementTransition);
} else {
Transition staggered = TransitionUtils.mergeTransitions(TransitionSet.ORDERING_SEQUENTIAL, exitTransition, enterTransition);
return TransitionUtils.mergeTransitions(TransitionSet.ORDERING_TOGETHER, staggered, sharedElementTransition);
}
}
@NonNull List<View> configureEnteringExitingViews(@NonNull Transition transition, @Nullable View view, @NonNull List<View> sharedElements, @NonNull View nonExistentView) {
List<View> viewList = new ArrayList<>();
if (view != null) {
captureTransitioningViews(viewList, view);
}
viewList.removeAll(sharedElements);
if (!viewList.isEmpty()) {
viewList.add(nonExistentView);
TransitionUtils.addTargets(transition, viewList);
}
return viewList;
}
private void configureSharedElements(@NonNull ViewGroup container, @NonNull final View nonExistentView, @Nullable final View to, @Nullable View from,
final boolean isPush, @NonNull final List<View> fromSharedElements, @NonNull final List<View> toSharedElements) {
if (to == null || from == null) {
return;
}
ArrayMap<String, View> capturedFromSharedElements = captureFromSharedElements(from);
if (sharedElementNames.isEmpty()) {
sharedElementTransition = null;
} else if (capturedFromSharedElements != null) {
fromSharedElements.addAll(capturedFromSharedElements.values());
}
if (enterTransition == null && exitTransition == null && sharedElementTransition == null) {
return;
}
callSharedElementStartEnd(capturedFromSharedElements, true);
final Rect toEpicenter;
if (sharedElementTransition != null) {
toEpicenter = new Rect();
TransitionUtils.setTargets(sharedElementTransition, nonExistentView, fromSharedElements);
setFromEpicenter(capturedFromSharedElements);
if (enterTransition != null) {
enterTransition.setEpicenterCallback(new Transition.EpicenterCallback() {
@Override
public Rect onGetEpicenter(Transition transition) {
if (toEpicenter.isEmpty()) {
return null;
}
return toEpicenter;
}
});
}
} else {
toEpicenter = null;
}
OneShotPreDrawListener.add(true, container, () -> {
ArrayMap<String, View> capturedToSharedElements = captureToSharedElements(to, isPush);
if (capturedToSharedElements != null) {
toSharedElements.addAll(capturedToSharedElements.values());
toSharedElements.add(nonExistentView);
}
callSharedElementStartEnd(capturedToSharedElements, false);
if (sharedElementTransition != null) {
sharedElementTransition.getTargets().clear();
sharedElementTransition.getTargets().addAll(toSharedElements);
TransitionUtils.replaceTargets(sharedElementTransition, fromSharedElements, toSharedElements);
final View toEpicenterView = getToEpicenterView(capturedToSharedElements);
if (toEpicenterView != null && toEpicenter != null) {
TransitionUtils.getBoundsOnScreen(toEpicenterView, toEpicenter);
}
}
});
}
@Nullable View getToEpicenterView(@Nullable ArrayMap<String, View> toSharedElements) {
if (enterTransition != null && sharedElementNames.size() > 0 && toSharedElements != null) {
return toSharedElements.get(sharedElementNames.valueAt(0));
}
return null;
}
private void setFromEpicenter(@Nullable ArrayMap<String, View> fromSharedElements) {
if (sharedElementNames.size() > 0 && fromSharedElements != null) {
final View fromEpicenterView = fromSharedElements.get(sharedElementNames.keyAt(0));
if (sharedElementTransition != null) {
TransitionUtils.setEpicenter(sharedElementTransition, fromEpicenterView);
}
if (exitTransition != null) {
TransitionUtils.setEpicenter(exitTransition, fromEpicenterView);
}
}
}
@Nullable ArrayMap<String, View> captureToSharedElements(@Nullable final View to, boolean isPush) {
if (sharedElementNames.isEmpty() || sharedElementTransition == null || to == null) {
sharedElementNames.clear();
return null;
}
final ArrayMap<String, View> toSharedElements = new ArrayMap<>();
TransitionUtils.findNamedViews(toSharedElements, to);
for (ViewParentPair removedView : removedViews) {
toSharedElements.put(removedView.view.getTransitionName(), removedView.view);
}
final List<String> names = new ArrayList<>(sharedElementNames.values());
toSharedElements.retainAll(names);
if (enterTransitionCallback != null) {
enterTransitionCallback.onMapSharedElements(names, toSharedElements);
for (int i = names.size() - 1; i >= 0; i--) {
String name = names.get(i);
View view = toSharedElements.get(name);
if (view == null) {
String key = findKeyForValue(sharedElementNames, name);
if (key != null) {
sharedElementNames.remove(key);
}
} else if (!name.equals(view.getTransitionName())) {
String key = findKeyForValue(sharedElementNames, name);
if (key != null) {
sharedElementNames.put(key, view.getTransitionName());
}
}
}
} else {
for (int i = sharedElementNames.size() - 1; i >= 0; i--) {
final String targetName = sharedElementNames.valueAt(i);
if (!toSharedElements.containsKey(targetName)) {
sharedElementNames.removeAt(i);
}
}
}
return toSharedElements;
}
@Nullable String findKeyForValue(@NonNull ArrayMap<String, String> map, @NonNull String value) {
final int numElements = map.size();
for (int i = 0; i < numElements; i++) {
if (value.equals(map.valueAt(i))) {
return map.keyAt(i);
}
}
return null;
}
@Nullable
private ArrayMap<String, View> captureFromSharedElements(@NonNull View from) {
if (sharedElementNames.isEmpty() || sharedElementTransition == null) {
sharedElementNames.clear();
return null;
}
final ArrayMap<String, View> fromSharedElements = new ArrayMap<>();
TransitionUtils.findNamedViews(fromSharedElements, from);
final List<String> names = new ArrayList<>(sharedElementNames.keySet());
fromSharedElements.retainAll(names);
if (exitTransitionCallback != null) {
exitTransitionCallback.onMapSharedElements(names, fromSharedElements);
for (int i = names.size() - 1; i >= 0; i--) {
String name = names.get(i);
View view = fromSharedElements.get(name);
if (view == null) {
sharedElementNames.remove(name);
} else if (!name.equals(view.getTransitionName())) {
String targetValue = sharedElementNames.remove(name);
sharedElementNames.put(view.getTransitionName(), targetValue);
}
}
} else {
sharedElementNames.retainAll(fromSharedElements.keySet());
}
return fromSharedElements;
}
void callSharedElementStartEnd(@Nullable ArrayMap<String, View> sharedElements, boolean isStart) {
if (enterTransitionCallback != null) {
final int count = sharedElements == null ? 0 : sharedElements.size();
List<View> views = new ArrayList<>(count);
List<String> names = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
names.add(sharedElements.keyAt(i));
views.add(sharedElements.valueAt(i));
}
if (isStart) {
enterTransitionCallback.onSharedElementStart(names, views, null);
} else {
enterTransitionCallback.onSharedElementEnd(names, views, null);
}
}
}
private void captureTransitioningViews(@NonNull List<View> transitioningViews, @NonNull View view) {
if (view.getVisibility() == View.VISIBLE) {
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
if (viewGroup.isTransitionGroup()) {
transitioningViews.add(viewGroup);
} else {
int count = viewGroup.getChildCount();
for (int i = 0; i < count; i++) {
View child = viewGroup.getChildAt(i);
captureTransitioningViews(transitioningViews, child);
}
}
} else {
transitioningViews.add(view);
}
}
}
private void scheduleRemoveTargets(@NonNull final Transition overallTransition,
@Nullable final Transition enterTransition, @Nullable final List<View> enteringViews,
@Nullable final Transition exitTransition, @Nullable final List<View> exitingViews,
@Nullable final Transition sharedElementTransition, @Nullable final List<View> toSharedElements) {
overallTransition.addListener(new TransitionListener() {
@Override
public void onTransitionStart(Transition transition) {
if (enterTransition != null && enteringViews != null) {
TransitionUtils.replaceTargets(enterTransition, enteringViews, null);
}
if (exitTransition != null && exitingViews != null) {
TransitionUtils.replaceTargets(exitTransition, exitingViews, null);
}
if (sharedElementTransition != null && toSharedElements != null) {
TransitionUtils.replaceTargets(sharedElementTransition, toSharedElements, null);
}
}
@Override
public void onTransitionEnd(Transition transition) { }
@Override
public void onTransitionCancel(Transition transition) { }
@Override
public void onTransitionPause(Transition transition) { }
@Override
public void onTransitionResume(Transition transition) { }
});
}
private void setNameOverrides(@NonNull final View container, @NonNull final List<View> toSharedElements) {
OneShotPreDrawListener.add(true, container, () -> {
final int numSharedElements = toSharedElements.size();
for (int i = 0; i < numSharedElements; i++) {
View view = toSharedElements.get(i);
String name = view.getTransitionName();
if (name != null) {
String inName = findKeyForValue(sharedElementNames, name);
view.setTransitionName(inName);
}
}
});
}
private void scheduleNameReset(@NonNull final ViewGroup container, @NonNull final List<View> toSharedElements) {
OneShotPreDrawListener.add(true, container, () -> {
final int numSharedElements = toSharedElements.size();
for (int i = 0; i < numSharedElements; i++) {
final View view = toSharedElements.get(i);
final String name = view.getTransitionName();
final String inName = sharedElementNames.get(name);
view.setTransitionName(inName);
}
});
}
/**
* Will be called when views are ready to have their shared elements configured. Within this method one of the addSharedElement methods
* should be called for each shared element that will be used. If one or more of these shared elements will not instantly be available in
* the incoming view (for ex, in a RecyclerView), waitOnSharedElementNamed can be called to delay the transition until everything is available.
*/
public abstract void configureSharedElements(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
/**
* Should return the transition that will be used on the exiting ("from") view, if one is desired.
*/
@Nullable
public abstract Transition getExitTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
/**
* Should return the transition that will be used on shared elements between the from and to views.
*/
@Nullable
public abstract Transition getSharedElementTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
/**
* Should return the transition that will be used on the entering ("to") view, if one is desired.
*/
@Nullable
public abstract Transition getEnterTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
/**
* Should return a callback that can be used to customize transition behavior of the shared element transition for the "from" view.
*/
@Nullable
public SharedElementCallback getExitTransitionCallback(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
return null;
}
/**
* Should return a callback that can be used to customize transition behavior of the shared element transition for the "to" view.
*/
@Nullable
public SharedElementCallback getEnterTransitionCallback(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
return null;
}
/**
* Should return whether or not the the exit transition and enter transition should overlap. If true,
* the enter transition will start as soon as possible. Otherwise, the enter transition will wait until the
* completion of the exit transition. Defaults to true.
*/
public boolean allowTransitionOverlap(boolean isPush) {
return true;
}
/**
* Used to register an element that will take part in the shared element transition.
*
* @param name The transition name that is used for both the entering and exiting views.
*/
protected final void addSharedElement(@NonNull String name) {
sharedElementNames.put(name, name);
}
/**
* Used to register an element that will take part in the shared element transition. Maps the name used in the
* "from" view to the name used in the "to" view if they are not the same.
*
* @param fromName The transition name used in the "from" view
* @param toName The transition name used in the "to" view
*/
protected final void addSharedElement(@NonNull String fromName, @NonNull String toName) {
sharedElementNames.put(fromName, toName);
}
/**
* Used to register an element that will take part in the shared element transition. Maps the name used in the
* "from" view to the name used in the "to" view if they are not the same.
*
* @param sharedElement The view from the "from" view that will take part in the shared element transition
* @param toName The transition name used in the "to" view
*/
protected final void addSharedElement(@NonNull View sharedElement, @NonNull String toName) {
String transitionName = sharedElement.getTransitionName();
if (transitionName == null) {
throw new IllegalArgumentException("Unique transitionNames are required for all sharedElements");
}
sharedElementNames.put(transitionName, toName);
}
/**
* The transition will be delayed until the view with the name passed in is available in the "to" hierarchy. This is
* particularly useful for views that don't load instantly, like RecyclerViews. Note that using this method can
* potentially lock up your app indefinitely if the view never loads!
*/
protected final void waitOnSharedElementNamed(@NonNull String name) {
if (!sharedElementNames.values().contains(name)) {
throw new IllegalStateException("Can't wait on a shared element that hasn't been registered using addSharedElement");
}
waitForTransitionNames.add(name);
}
private static class OneShotPreDrawListener implements OnPreDrawListener, View.OnAttachStateChangeListener {
private final View view;
private ViewTreeObserver viewTreeObserver;
private final Runnable runnable;
private final boolean preDrawReturnValue;
private OneShotPreDrawListener(boolean preDrawReturnValue, @NonNull View view, @NonNull Runnable runnable) {
this.preDrawReturnValue = preDrawReturnValue;
this.view = view;
viewTreeObserver = view.getViewTreeObserver();
this.runnable = runnable;
}
@NonNull
public static OneShotPreDrawListener add(boolean preDrawReturnValue, @NonNull View view, @NonNull Runnable runnable) {
OneShotPreDrawListener listener = new OneShotPreDrawListener(preDrawReturnValue, view, runnable);
view.getViewTreeObserver().addOnPreDrawListener(listener);
view.addOnAttachStateChangeListener(listener);
return listener;
}
@Override
public boolean onPreDraw() {
removeListener();
runnable.run();
return preDrawReturnValue;
}
private void removeListener() {
if (viewTreeObserver.isAlive()) {
viewTreeObserver.removeOnPreDrawListener(this);
} else {
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
view.removeOnAttachStateChangeListener(this);
}
@Override
public void onViewAttachedToWindow(View v) {
viewTreeObserver = v.getViewTreeObserver();
}
@Override
public void onViewDetachedFromWindow(View v) {
removeListener();
}
}
private static class ViewParentPair {
@NonNull final View view;
@NonNull final ViewGroup parent;
ViewParentPair(@NonNull View view, ViewGroup parent) {
this.view = view;
this.parent = parent;
}
}
}
@@ -1,20 +1,24 @@
package com.bluelinelabs.conductor.changehandler;
package com.bluelinelabs.conductor.changehandler.platformtransition;
import android.annotation.TargetApi;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.Transition;
import android.transition.Transition.TransitionListener;
import android.transition.TransitionManager;
import android.view.View;
import android.view.ViewGroup;
import 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 android.transition.Transition}s to replace Controller Views.
* 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 {
@@ -51,8 +55,12 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
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;
@@ -63,19 +71,25 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
return;
}
final Runnable onTransitionNotStarted = changeListener::onChangeCompleted;
final Transition transition = getTransition(container, from, to, isPush);
transition.addListener(new TransitionListener() {
@Override
public void onTransitionStart(Transition transition) { }
public void onTransitionStart(Transition transition) {
container.removeCallbacks(onTransitionNotStarted);
}
@Override
public void onTransitionEnd(Transition transition) {
changeListener.onChangeCompleted();
listener.onChangeCompleted();
listener = null;
}
@Override
public void onTransitionCancel(Transition transition) {
changeListener.onChangeCompleted();
listener.onChangeCompleted();
listener = null;
}
@Override
@@ -85,13 +99,11 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
public void onTransitionResume(Transition transition) { }
});
prepareForTransition(container, from, to, transition, isPush, new OnTransitionPreparedListener() {
@Override
public void onPrepared() {
if (!canceled) {
TransitionManager.beginDelayedTransition(container, transition);
executePropertyChanges(container, from, to, transition, isPush);
}
prepareForTransition(container, from, to, transition, isPush, () -> {
if (!canceled) {
TransitionManager.beginDelayedTransition(container, transition);
executePropertyChanges(container, from, to, transition, isPush);
container.post(onTransitionNotStarted);
}
});
}
@@ -3,8 +3,8 @@ package com.bluelinelabs.conductor.internal;
import android.annotation.TargetApi;
import android.graphics.Rect;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.transition.Transition;
import android.transition.TransitionSet;
import android.view.View;
@@ -116,7 +116,7 @@ public class TransitionUtils {
Transition child = set.getTransitionAt(i);
replaceTargets(child, oldTargets, newTargets);
}
} else if (!TransitionUtils.hasSimpleTarget(transition)) {
} 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();
@@ -181,5 +181,4 @@ public class TransitionUtils {
transitionSet.setOrdering(ordering);
return transitionSet;
}
}
@@ -1,16 +1,9 @@
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
@@ -20,11 +13,14 @@ android {
}
dependencies {
api rootProject.ext.rxJava
api rootProject.ext.rxLifecycle
api rootProject.ext.rxLifecycleAndroid
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.roboelectric
implementation rootProject.ext.androidxAppCompat
implementation project(':conductor')
}
ext.artifactId = 'conductor-rxlifecycle'
ext.artifactId = 'conductor-viewpager'
apply from: rootProject.file('dependencies.gradle')
apply plugin: "com.vanniktech.maven.publish"
@@ -0,0 +1,3 @@
POM_NAME=Conductor PagerAdapter
POM_ARTIFACT_ID=conductor-viewpager
POM_PACKAGING=aar
@@ -0,0 +1,3 @@
<manifest package="com.bluelinelabs.conductor.viewpager">
<application />
</manifest>
@@ -1,10 +1,7 @@
package com.bluelinelabs.conductor.support;
package com.bluelinelabs.conductor.viewpager;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
@@ -14,7 +11,13 @@ import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import java.util.ArrayList;
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
@@ -22,11 +25,14 @@ import java.util.List;
public abstract class RouterPagerAdapter extends PagerAdapter {
private static final String KEY_SAVED_PAGES = "RouterPagerAdapter.savedStates";
private static final String KEY_TAGS_KEYS = "RouterPagerAdapter.tags.keys";
private static final String KEY_TAGS_VALUES = "RouterPagerAdapter.tags.values";
private static final String KEY_MAX_PAGES_TO_STATE_SAVE = "RouterPagerAdapter.maxPagesToStateSave";
private static final String KEY_SAVE_PAGE_HISTORY = "RouterPagerAdapter.savedPageHistory";
private final Controller host;
private int maxPagesToStateSave = Integer.MAX_VALUE;
private Map<Integer, String> tags = new HashMap<>();
private SparseArray<Bundle> savedPages = new SparseArray<>();
private SparseArray<Router> visibleRouters = new SparseArray<>();
private ArrayList<Integer> savedPageHistory = new ArrayList<>();
@@ -65,6 +71,12 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
public Object instantiateItem(ViewGroup container, int position) {
final String name = makeRouterName(container.getId(), getItemId(position));
// Ensure we don't try to restore state for a router with a different ID just because
// the position was reused. Fixes https://github.com/bluelinelabs/Conductor/issues/582
if (tags.get(position) != null && !tags.get(position).equals(name)) {
savedPages.remove(position);
}
Router router = host.getChildRouter(container, name);
if (!router.hasRootController()) {
Bundle routerSavedState = savedPages.get(position);
@@ -85,6 +97,7 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
}
}
tags.put(position, name);
visibleRouters.put(position, router);
return router;
}
@@ -141,6 +154,8 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
public Parcelable saveState() {
Bundle bundle = new Bundle();
bundle.putSparseParcelableArray(KEY_SAVED_PAGES, savedPages);
bundle.putIntegerArrayList(KEY_TAGS_KEYS, new ArrayList<>(tags.keySet()));
bundle.putStringArrayList(KEY_TAGS_VALUES, new ArrayList<>(tags.values()));
bundle.putInt(KEY_MAX_PAGES_TO_STATE_SAVE, maxPagesToStateSave);
bundle.putIntegerArrayList(KEY_SAVE_PAGE_HISTORY, savedPageHistory);
return bundle;
@@ -153,6 +168,14 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
savedPages = bundle.getSparseParcelableArray(KEY_SAVED_PAGES);
maxPagesToStateSave = bundle.getInt(KEY_MAX_PAGES_TO_STATE_SAVE);
savedPageHistory = bundle.getIntegerArrayList(KEY_SAVE_PAGE_HISTORY);
List<Integer> tagsKeys = bundle.getIntegerArrayList(KEY_TAGS_KEYS);
List<String> tagsValues = bundle.getStringArrayList(KEY_TAGS_VALUES);
if (tagsKeys != null && tagsValues != null && tagsKeys.size() == tagsValues.size()) {
for (int i = 0; i < tagsKeys.size(); i++) {
tags.put(tagsKeys.get(i), tagsValues.get(i));
}
}
}
}
@@ -0,0 +1,104 @@
package com.bluelinelabs.conductor.viewpager
import android.app.Activity
import android.widget.FrameLayout
import androidx.core.view.ViewCompat
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.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class StateSaveTests {
private val pager: FakePager
private val pagerAdapter: RouterPagerAdapter
init {
val activityController = Robolectric.buildActivity(Activity::class.java).setup()
val router = Conductor.attachRouter(activityController.get(), FrameLayout(activityController.get()), null)
val controller = TestController()
router.setRoot(with(controller))
pager = FakePager(FrameLayout(activityController.get()).also {
it.id = ViewCompat.generateViewId()
})
pager.offscreenPageLimit = 1
pagerAdapter = object : RouterPagerAdapter(controller) {
override fun configureRouter(router: Router, position: Int) {
if (!router.hasRootController()) {
router.setRoot(with(TestController()))
}
}
override fun getCount(): Int {
return 20
}
}
pager.setAdapter(pagerAdapter)
}
@Test
fun testNoMaxSaves() {
// Load all pages
for (i in 0 until pagerAdapter.count) {
pager.pageTo(i)
}
pager.pageTo(pagerAdapter.count / 2)
// Ensure all non-visible pages are saved
assertEquals(
pagerAdapter.count - 1 - (pager.offscreenPageLimit * 2),
pagerAdapter.savedPages.size()
)
}
@Test
fun testMaxSavedSet() {
val maxPages = 3
pagerAdapter.setMaxPagesToStateSave(maxPages)
// Load all pages
for (i in 0 until pagerAdapter.count) {
pager.pageTo(i)
}
val firstSelectedItem = pagerAdapter.count / 2
pager.pageTo(firstSelectedItem)
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)
)
val secondSelectedItem = 1
pager.pageTo(secondSelectedItem)
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))
}
}
@@ -1,9 +1,9 @@
package com.bluelinelabs.conductor.support.util;
package com.bluelinelabs.conductor.viewpager.util;
import android.util.SparseArray;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.support.RouterPagerAdapter;
import com.bluelinelabs.conductor.viewpager.RouterPagerAdapter;
import java.util.ArrayList;
import java.util.List;
@@ -1,17 +1,20 @@
package com.bluelinelabs.conductor.support.util;
package com.bluelinelabs.conductor.viewpager.util;
import android.support.annotation.NonNull;
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) {
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) {
return new FrameLayout(inflater.getContext());
}
+5 -34
View File
@@ -1,23 +1,9 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'de.mobilej.unmock:UnMockPlugin:0.6.5'
}
}
apply plugin: 'com.android.library'
apply plugin: 'de.mobilej.unmock'
apply plugin: 'kotlin-android'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
@@ -27,32 +13,17 @@ android {
}
}
configurations {
lintChecks
}
dependencies {
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.roboelectric
api rootProject.ext.supportAnnotations
api rootProject.ext.androidxAnnotations
api kotlinStd
lintChecks project(path: ':conductor-lint', configuration: 'lintChecks')
}
task copyLintJar(type: Copy) {
from(configurations.lintChecks) {
rename { 'lint.jar' }
}
into 'build/intermediates/lint/'
}
project.afterEvaluate {
def compileLintTask = project.tasks.find { it.name == 'compileLint' }
compileLintTask.dependsOn(copyLintJar)
lintPublish project(':conductor-lint')
}
ext.artifactId = 'conductor'
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
apply plugin: "com.vanniktech.maven.publish"
+3
View File
@@ -3,3 +3,6 @@
public <init>();
public <init>(android.os.Bundle);
}
-keepclassmembers public class * extends com.bluelinelabs.conductor.ControllerChangeHandler {
public <init>();
}
@@ -5,8 +5,8 @@ import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
@@ -1,8 +1,9 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -58,7 +59,7 @@ class Backstack implements Iterable<RouterTransaction> {
@NonNull
RouterTransaction pop() {
RouterTransaction popped = backstack.pop();
popped.controller.destroy();
popped.controller().destroy();
return popped;
}
@@ -89,7 +90,7 @@ class Backstack implements Iterable<RouterTransaction> {
boolean contains(@NonNull Controller controller) {
for (RouterTransaction transaction : backstack) {
if (controller == transaction.controller) {
if (controller == transaction.controller()) {
return true;
}
}
@@ -1,56 +0,0 @@
package com.bluelinelabs.conductor;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
/**
* A FrameLayout implementation that can be used to block user interactions while
* {@link ControllerChangeHandler}s are performing changes. It is not required to use this
* ViewGroup, but it can be helpful.
*/
public class ChangeHandlerFrameLayout extends FrameLayout implements ControllerChangeListener {
private int inProgressTransactionCount;
public ChangeHandlerFrameLayout(Context context) {
super(context);
}
public ChangeHandlerFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ChangeHandlerFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ChangeHandlerFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return (inProgressTransactionCount > 0) || super.onInterceptTouchEvent(ev);
}
@Override
public void onChangeStarted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) {
inProgressTransactionCount++;
}
@Override
public void onChangeCompleted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) {
inProgressTransactionCount--;
}
}
@@ -0,0 +1,60 @@
package com.bluelinelabs.conductor
import android.annotation.TargetApi
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.ViewGroup
import android.widget.FrameLayout
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener
/**
* A FrameLayout implementation that can be used to block user interactions while
* [ControllerChangeHandler]s are performing changes. It is not required to use this
* ViewGroup, but it can be helpful.
*/
open class ChangeHandlerFrameLayout : FrameLayout, ControllerChangeListener {
private var inProgressTransactionCount = 0
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(
context,
attrs,
defStyleAttr,
defStyleRes
)
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
return inProgressTransactionCount > 0 || super.onInterceptTouchEvent(ev)
}
override fun onChangeStarted(
to: Controller?,
from: Controller?,
isPush: Boolean,
container: ViewGroup,
handler: ControllerChangeHandler
) {
inProgressTransactionCount++
}
override fun onChangeCompleted(
to: Controller?,
from: Controller?,
isPush: Boolean,
container: ViewGroup,
handler: ControllerChangeHandler
) {
inProgressTransactionCount--
}
}
@@ -1,44 +0,0 @@
package com.bluelinelabs.conductor;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.internal.LifecycleHandler;
import com.bluelinelabs.conductor.internal.ThreadUtils;
/**
* Point of initial interaction with Conductor. Used to attach a {@link Router} to your Activity.
*/
public final class Conductor {
private Conductor() {}
/**
* Conductor will create a {@link Router} that has been initialized for your Activity and containing ViewGroup.
* If an existing {@link Router} is already associated with this Activity/ViewGroup pair, either in memory
* or in the savedInstanceState, that router will be used and rebound instead of creating a new one with
* an empty backstack.
*
* @param activity The Activity that will host the {@link Router} being attached.
* @param container The ViewGroup in which the {@link Router}'s {@link Controller} views will be hosted
* @param savedInstanceState The savedInstanceState passed into the hosting Activity's onCreate method. Used
* for restoring the Router's state if possible.
* @return A fully configured {@link Router} instance for use with this Activity/ViewGroup pair.
*/
@NonNull @UiThread
public static Router attachRouter(@NonNull Activity activity, @NonNull ViewGroup container, @Nullable Bundle savedInstanceState) {
ThreadUtils.ensureMainThread();
LifecycleHandler lifecycleHandler = LifecycleHandler.install(activity);
Router router = lifecycleHandler.getRouter(container, savedInstanceState);
router.rebindIfNeeded();
return router;
}
}
@@ -0,0 +1,32 @@
package com.bluelinelabs.conductor
import android.app.Activity
import android.os.Bundle
import android.view.ViewGroup
import androidx.annotation.UiThread
import com.bluelinelabs.conductor.internal.LifecycleHandler
import com.bluelinelabs.conductor.internal.ensureMainThread
/**
* Conductor will create a [Router] that has been initialized for your Activity and containing ViewGroup.
* If an existing [Router] is already associated with this Activity/ViewGroup pair, either in memory
* or in the savedInstanceState, that router will be used and rebound instead of creating a new one with
* an empty backstack.
*
* @param activity The Activity that will host the [Router] being attached.
* @param container The ViewGroup in which the [Router]'s [Controller] views will be hosted
* @param savedInstanceState The savedInstanceState passed into the hosting Activity's onCreate method. Used
* for restoring the Router's state if possible.
* @return A fully configured [Router] instance for use with this Activity/ViewGroup pair.
*/
@UiThread
object Conductor {
@JvmStatic
fun attachRouter(activity: Activity, container: ViewGroup, savedInstanceState: Bundle?): Router {
ensureMainThread()
return LifecycleHandler.install(activity)
.getRouter(container, savedInstanceState)
.also { it.rebindIfNeeded() }
}
}
@@ -9,9 +9,6 @@ import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.SparseArray;
import android.view.LayoutInflater;
@@ -21,6 +18,10 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.internal.ClassUtils;
import com.bluelinelabs.conductor.internal.RouterRequiringFunc;
import com.bluelinelabs.conductor.internal.ViewAttachHandler;
@@ -31,7 +32,6 @@ 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;
@@ -107,10 +107,10 @@ public abstract class Controller {
Controller controller;
try {
if (bundleConstructor != null) {
controller = (Controller)bundleConstructor.newInstance(args);
controller = (Controller) bundleConstructor.newInstance(args);
} else {
//noinspection ConstantConditions
controller = (Controller)getDefaultConstructor(constructors).newInstance();
controller = (Controller) getDefaultConstructor(constructors).newInstance();
// Restore the args that existed before the last process death
if (args != null) {
@@ -148,13 +148,15 @@ public abstract class Controller {
* for this method will be {@code return inflater.inflate(R.layout.my_layout, container, false);}, plus
* any binding code.
*
* @param inflater The LayoutInflater that should be used to inflate views
* @param container The parent view that this Controller's view will eventually be attached to.
* This Controller's view should NOT be added in this method. It is simply passed in
* so that valid LayoutParams can be used during inflation.
* @param inflater The LayoutInflater that should be used to inflate views
* @param container The parent view that this Controller's view will eventually be attached to.
* This Controller's view should NOT be added in this method. It is simply passed in
* so that valid LayoutParams can be used during inflation.
* @param savedViewState A bundle for the view's state, which would have been created in {@link #onSaveViewState(View, Bundle)},
* or {@code null} if no saved state exists.
*/
@NonNull
protected abstract View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container);
protected abstract View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState);
/**
* Returns the {@link Router} object that can be used for pushing or popping other Controllers
@@ -189,7 +191,7 @@ public abstract class Controller {
* the same container unless you have a great reason to do so (ex: ViewPagers).
*
* @param container The ViewGroup that hosts the child Router
* @param tag The router's tag or {@code null} if none is needed
* @param tag The router's tag or {@code null} if none is needed
*/
@NonNull
public final Router getChildRouter(@NonNull ViewGroup container, @Nullable String tag) {
@@ -204,13 +206,16 @@ public abstract class Controller {
* The only time this method will return {@code null} is when the child router does not exist prior
* to calling this method and the createIfNeeded parameter is set to false.
*
* @param container The ViewGroup that hosts the child Router
* @param tag The router's tag or {@code null} if none is needed
* @param container The ViewGroup that hosts the child Router
* @param tag The router's tag or {@code null} if none is needed
* @param createIfNeeded If true, a router will be created if one does not yet exist. Else {@code null} will be returned in this case.
*/
@Nullable
public final Router getChildRouter(@NonNull ViewGroup container, @Nullable String tag, boolean createIfNeeded) {
@IdRes final int containerId = container.getId();
if (containerId == View.NO_ID) {
throw new IllegalStateException("You must set an id on your container.");
}
ControllerHostedRouter childRouter = null;
for (ControllerHostedRouter router : childRouters) {
@@ -393,7 +398,8 @@ public abstract class Controller {
*
* @param view The View to which this Controller should be bound.
*/
protected void onDestroyView(@NonNull View view) { }
protected void onDestroyView(@NonNull View view) {
}
/**
* Called when this Controller begins the process of being swapped in or out of the host view.
@@ -401,7 +407,8 @@ public abstract class Controller {
* @param changeHandler The {@link ControllerChangeHandler} that's managing the swap
* @param changeType The type of change that's occurring
*/
protected void onChangeStarted(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { }
protected void onChangeStarted(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
}
/**
* Called when this Controller completes the process of being swapped in or out of the host view.
@@ -409,59 +416,69 @@ public abstract class Controller {
* @param changeHandler The {@link ControllerChangeHandler} that's managing the swap
* @param changeType The type of change that occurred
*/
protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { }
protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
}
/**
* Called when this Controller has a Context available to it. This will happen very early on in the lifecycle
* (before a view is created). If the host activity is re-created (ex: for orientation change), this will be
* called again when the new context is available.
*/
protected void onContextAvailable(@NonNull Context context) { }
protected void onContextAvailable(@NonNull Context context) {
}
/**
* Called when this Controller's Context is no longer available. This can happen when the Controller is
* destroyed or when the host Activity is destroyed.
*/
protected void onContextUnavailable() { }
protected void onContextUnavailable() {
}
/**
* Called when this Controller is attached to its host ViewGroup
*
* @param view The View for this Controller (passed for convenience)
*/
protected void onAttach(@NonNull View view) { }
protected void onAttach(@NonNull View view) {
}
/**
* Called when this Controller is detached from its host ViewGroup
*
* @param view The View for this Controller (passed for convenience)
*/
protected void onDetach(@NonNull View view) { }
protected void onDetach(@NonNull View view) {
}
/**
* Called when this Controller has been destroyed.
*/
protected void onDestroy() { }
protected void onDestroy() {
}
/**
* Called when this Controller's host Activity is started
*/
protected void onActivityStarted(@NonNull Activity activity) { }
protected void onActivityStarted(@NonNull Activity activity) {
}
/**
* Called when this Controller's host Activity is resumed
*/
protected void onActivityResumed(@NonNull Activity activity) { }
protected void onActivityResumed(@NonNull Activity activity) {
}
/**
* Called when this Controller's host Activity is paused
*/
protected void onActivityPaused(@NonNull Activity activity) { }
protected void onActivityPaused(@NonNull Activity activity) {
}
/**
* Called when this Controller's host Activity is stopped
*/
protected void onActivityStopped(@NonNull Activity activity) { }
protected void onActivityStopped(@NonNull Activity activity) {
}
/**
* Called to save this Controller's View state. As Views can be detached and destroyed as part of the
@@ -471,7 +488,8 @@ public abstract class Controller {
* @param view This Controller's View, passed for convenience
* @param outState The Bundle into which the View state should be saved
*/
protected void onSaveViewState(@NonNull View view, @NonNull Bundle outState) { }
protected void onSaveViewState(@NonNull View view, @NonNull Bundle outState) {
}
/**
* Restores data that was saved in the {@link #onSaveViewState(View, Bundle)} method. This should be overridden
@@ -480,14 +498,16 @@ public abstract class Controller {
* @param view This Controller's View, passed for convenience
* @param savedViewState The bundle that has data to be restored
*/
protected void onRestoreViewState(@NonNull View view, @NonNull Bundle savedViewState) { }
protected void onRestoreViewState(@NonNull View view, @NonNull Bundle savedViewState) {
}
/**
* Called to save this Controller's state in the event that its host Activity is destroyed.
*
* @param outState The Bundle into which data should be saved
*/
protected void onSaveInstanceState(@NonNull Bundle outState) { }
protected void onSaveInstanceState(@NonNull Bundle outState) {
}
/**
* Restores data that was saved in the {@link #onSaveInstanceState(Bundle)} method. This should be overridden
@@ -495,33 +515,28 @@ public abstract class Controller {
*
* @param savedInstanceState The bundle that has data to be restored
*/
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { }
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
}
/**
* Calls startActivity(Intent) from this Controller's host Activity.
*/
public final void startActivity(@NonNull final Intent intent) {
executeWithRouter(new RouterRequiringFunc() {
@Override public void execute() { router.startActivity(intent); }
});
executeWithRouter(() -> 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(new RouterRequiringFunc() {
@Override public void execute() { router.startActivityForResult(instanceId, intent, requestCode); }
});
executeWithRouter(() -> 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(new RouterRequiringFunc() {
@Override public void execute() { router.startActivityForResult(instanceId, intent, requestCode, options); }
});
executeWithRouter(() -> router.startActivityForResult(instanceId, intent, requestCode, options));
}
/**
@@ -539,9 +554,7 @@ public abstract class Controller {
* @param requestCode The request code being registered for.
*/
public final void registerForActivityResult(final int requestCode) {
executeWithRouter(new RouterRequiringFunc() {
@Override public void execute() { router.registerForActivityResult(instanceId, requestCode); }
});
executeWithRouter(() -> router.registerForActivityResult(instanceId, requestCode));
}
/**
@@ -552,7 +565,8 @@ public abstract class Controller {
* @param resultCode The resultCode that was returned to the host Activity's onActivityResult method
* @param data The data Intent that was returned to the host Activity's onActivityResult method
*/
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { }
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
}
/**
* Calls requestPermission(String[], int) from this Controller's host Activity. Results for this request,
@@ -563,9 +577,7 @@ public abstract class Controller {
public final void requestPermissions(@NonNull final String[] permissions, final int requestCode) {
requestedPermissions.addAll(Arrays.asList(permissions));
executeWithRouter(new RouterRequiringFunc() {
@Override public void execute() { router.requestPermissions(instanceId, permissions, requestCode); }
});
executeWithRouter(() -> router.requestPermissions(instanceId, permissions, requestCode));
}
/**
@@ -585,7 +597,8 @@ public abstract class Controller {
* @param permissions The array of permissions requested
* @param grantResults The results for each permission requested
*/
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { }
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
}
/**
* Should be overridden if this Controller needs to handle the back button being pressed.
@@ -599,15 +612,10 @@ public abstract class Controller {
childTransactions.addAll(childRouter.getBackstack());
}
Collections.sort(childTransactions, new Comparator<RouterTransaction>() {
@Override
public int compare(RouterTransaction o1, RouterTransaction o2) {
return o2.transactionIndex - o1.transactionIndex;
}
});
Collections.sort(childTransactions, (o1, o2) -> o2.getTransactionIndex() - o1.getTransactionIndex());
for (RouterTransaction transaction : childTransactions) {
Controller childController = transaction.controller;
Controller childController = transaction.controller();
if (childController.isAttached() && childController.getRouter().handleBack()) {
return true;
@@ -727,10 +735,11 @@ public abstract class Controller {
* Adds option items to the host Activity's standard options menu. This will only be called if
* {@link #setHasOptionsMenu(boolean)} has been called.
*
* @param menu The menu into which your options should be placed.
* @param menu The menu into which your options should be placed.
* @param inflater The inflater that can be used to inflate your menu items.
*/
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { }
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
}
/**
* Prepare the screen's options menu to be displayed. This is called directly before showing the
@@ -738,7 +747,8 @@ public abstract class Controller {
*
* @param menu The menu that will be displayed
*/
public void onPrepareOptionsMenu(@NonNull Menu menu) { }
public void onPrepareOptionsMenu(@NonNull Menu menu) {
}
/**
* Called when an option menu item has been selected by the user.
@@ -846,7 +856,7 @@ public abstract class Controller {
final void activityStopped(@NonNull Activity activity) {
final boolean attached = this.attached;
if (viewAttachHandler != null) {
viewAttachHandler.onActivityStopped();
}
@@ -883,7 +893,7 @@ public abstract class Controller {
void attach(@NonNull View view) {
attachedToUnownedParent = router == null || view.getParent() != router.container;
if (attachedToUnownedParent) {
if (attachedToUnownedParent || isBeingDestroyed) {
return;
}
@@ -917,10 +927,14 @@ public abstract class Controller {
for (ControllerHostedRouter childRouter : childRouters) {
for (RouterTransaction childTransaction : childRouter.backstack) {
if (childTransaction.controller.awaitingParentAttach) {
childTransaction.controller.attach(childTransaction.controller.view);
if (childTransaction.controller().awaitingParentAttach) {
childTransaction.controller().attach(childTransaction.controller().view);
}
}
if (childRouter.hasHost()) {
childRouter.rebindIfNeeded();
}
}
}
@@ -1009,7 +1023,8 @@ public abstract class Controller {
lifecycleListener.preCreateView(this);
}
view = onCreateView(LayoutInflater.from(parent.getContext()), parent);
Bundle savedViewState = viewState == null ? null : viewState.getBundle(KEY_VIEW_STATE_BUNDLE);
view = onCreateView(LayoutInflater.from(parent.getContext()), parent, savedViewState);
if (view == parent) {
throw new IllegalStateException("Controller's onCreateView method returned the parent ViewGroup. Perhaps you forgot to pass false for LayoutInflater.inflate's attachToRoot parameter?");
}
@@ -1060,7 +1075,7 @@ public abstract class Controller {
View containerView = view.findViewById(childRouter.getHostId());
if (containerView != null && containerView instanceof ViewGroup) {
childRouter.setHost(this, (ViewGroup)containerView);
childRouter.setHost(this, (ViewGroup) containerView);
childRouter.rebindIfNeeded();
}
}
@@ -1281,6 +1296,8 @@ public abstract class Controller {
}
destroyedView = null;
}
changeHandler.onEnd();
}
final void setDetachFrozen(boolean frozen) {
@@ -1292,7 +1309,11 @@ public abstract class Controller {
}
if (!frozen && view != null && viewWasDetached) {
View aView = view;
detach(view, false, false);
if (view == null && aView.getParent() == router.container) {
router.container.removeView(aView); // need to remove the view when this controller is a child controller
}
}
}
}
@@ -1344,46 +1365,84 @@ public abstract class Controller {
return null;
}
/** Modes that will influence when the Controller will allow its view to be destroyed */
/**
* Modes that will influence when the Controller will allow its view to be destroyed
*/
public enum RetainViewMode {
/** The Controller will release its reference to its view as soon as it is detached. */
/**
* The Controller will release its reference to its view as soon as it is detached.
*/
RELEASE_DETACH,
/** The Controller will retain its reference to its view when detached, but will still release the reference when a config change occurs. */
/**
* The Controller will retain its reference to its view when detached, but will still release the reference when a config change occurs.
*/
RETAIN_DETACH
}
/** Allows external classes to listen for lifecycle events in a Controller */
public static abstract class LifecycleListener {
/**
* Allows external classes to listen for lifecycle events in a Controller
*/
public interface LifecycleListener {
public void onChangeStart(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { }
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { }
default void onChangeStart(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
}
public void preCreateView(@NonNull Controller controller) { }
public void postCreateView(@NonNull Controller controller, @NonNull View view) { }
default void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
}
public void preAttach(@NonNull Controller controller, @NonNull View view) { }
public void postAttach(@NonNull Controller controller, @NonNull View view) { }
default void preCreateView(@NonNull Controller controller) {
}
public void preDetach(@NonNull Controller controller, @NonNull View view) { }
public void postDetach(@NonNull Controller controller, @NonNull View view) { }
default void postCreateView(@NonNull Controller controller, @NonNull View view) {
}
public void preDestroyView(@NonNull Controller controller, @NonNull View view) { }
public void postDestroyView(@NonNull Controller controller) { }
default void preAttach(@NonNull Controller controller, @NonNull View view) {
}
public void preDestroy(@NonNull Controller controller) { }
public void postDestroy(@NonNull Controller controller) { }
default void postAttach(@NonNull Controller controller, @NonNull View view) {
}
public void preContextAvailable(@NonNull Controller controller) { }
public void postContextAvailable(@NonNull Controller controller, @NonNull Context context) { }
default void preDetach(@NonNull Controller controller, @NonNull View view) {
}
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) { }
public void postContextUnavailable(@NonNull Controller controller) { }
default void postDetach(@NonNull Controller controller, @NonNull View view) {
}
public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) { }
public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) { }
default void preDestroyView(@NonNull Controller controller, @NonNull View view) {
}
public void onSaveViewState(@NonNull Controller controller, @NonNull Bundle outState) { }
public void onRestoreViewState(@NonNull Controller controller, @NonNull Bundle savedViewState) { }
default void postDestroyView(@NonNull Controller controller) {
}
default void preDestroy(@NonNull Controller controller) {
}
default void postDestroy(@NonNull Controller controller) {
}
default void preContextAvailable(@NonNull Controller controller) {
}
default void postContextAvailable(@NonNull Controller controller, @NonNull Context context) {
}
default void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
}
default void postContextUnavailable(@NonNull Controller controller) {
}
default void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
}
default void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
}
default void onSaveViewState(@NonNull Controller controller, @NonNull Bundle outState) {
}
default void onRestoreViewState(@NonNull Controller controller, @NonNull Bundle savedViewState) {
}
}
@@ -1,12 +1,13 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import com.bluelinelabs.conductor.internal.ClassUtils;
@@ -49,14 +50,16 @@ public abstract class ControllerChangeHandler {
*
* @param bundle The Bundle into which data should be stored.
*/
public void saveToBundle(@NonNull Bundle bundle) { }
public void saveToBundle(@NonNull Bundle bundle) {
}
/**
* Restores data that was saved in the {@link #saveToBundle(Bundle bundle)} method.
*
* @param bundle The bundle that has data to be restored
*/
public void restoreFromBundle(@NonNull Bundle bundle) { }
public void restoreFromBundle(@NonNull Bundle bundle) {
}
/**
* Will be called on change handlers that push a controller if the controller being pushed is
@@ -66,13 +69,15 @@ public abstract class ControllerChangeHandler {
* @param newTop The Controller that will now be at the top of the backstack or {@code null}
* if there will be no new Controller at the top
*/
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) { }
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
}
/**
* Will be called on change handlers that push a controller if the controller being pushed is
* needs to be attached immediately, without any animations or transitions.
*/
public void completeImmediately() { }
public void completeImmediately() {
}
/**
* Returns a copy of this ControllerChangeHandler. This method is internally used by the library, so
@@ -201,37 +206,37 @@ public abstract class ControllerChangeHandler {
fromView = null;
}
handler.performChange(container, fromView, toView, isPush, new ControllerChangeCompletedListener() {
@Override
public void onChangeCompleted() {
if (from != null) {
from.changeEnded(handler, fromChangeType);
}
handler.performChange(container, fromView, toView, isPush, () -> {
if (from != null) {
from.changeEnded(handler, fromChangeType);
}
if (to != null) {
inProgressChangeHandlers.remove(to.getInstanceId());
to.changeEnded(handler, toChangeType);
}
if (to != null) {
inProgressChangeHandlers.remove(to.getInstanceId());
to.changeEnded(handler, toChangeType);
}
for (ControllerChangeListener listener : listeners) {
listener.onChangeCompleted(to, from, isPush, container, handler);
}
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.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);
}
if (handler.removesFromViewOnPush() && from != null) {
from.setNeedsAttach(false);
}
});
}
}
protected void onEnd() {
}
public boolean removesFromViewOnPush() {
return true;
}
@@ -240,24 +245,6 @@ public abstract class ControllerChangeHandler {
forceRemoveViewOnPush = force;
}
static class ChangeTransaction {
@Nullable final Controller to;
@Nullable final Controller from;
final boolean isPush;
@Nullable final ViewGroup container;
@Nullable final ControllerChangeHandler changeHandler;
@NonNull final List<ControllerChangeListener> listeners;
public ChangeTransaction(@Nullable Controller to, @Nullable Controller from, boolean isPush, @Nullable ViewGroup container, @Nullable ControllerChangeHandler changeHandler, @NonNull List<ControllerChangeListener> listeners) {
this.to = to;
this.from = from;
this.isPush = isPush;
this.container = container;
this.changeHandler = changeHandler;
this.listeners = listeners;
}
}
/**
* A listener interface useful for allowing external classes to be notified of change events.
*/
@@ -271,7 +258,8 @@ public abstract class ControllerChangeHandler {
* @param container The containing ViewGroup
* @param handler The change handler being used.
*/
void onChangeStarted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler);
default 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
@@ -282,7 +270,31 @@ public abstract class ControllerChangeHandler {
* @param container The containing ViewGroup
* @param handler The change handler that was used.
*/
void onChangeCompleted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler);
default void onChangeCompleted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) {
}
}
static class ChangeTransaction {
@Nullable
final Controller to;
@Nullable
final Controller from;
final boolean isPush;
@Nullable
final ViewGroup container;
@Nullable
final ControllerChangeHandler changeHandler;
@NonNull
final List<ControllerChangeListener> listeners;
public ChangeTransaction(@Nullable Controller to, @Nullable Controller from, boolean isPush, @Nullable ViewGroup container, @Nullable ControllerChangeHandler changeHandler, @NonNull List<ControllerChangeListener> listeners) {
this.to = to;
this.from = from;
this.isPush = isPush;
this.container = container;
this.changeHandler = changeHandler;
this.listeners = listeners;
}
}
/**
@@ -1,26 +0,0 @@
package com.bluelinelabs.conductor;
/**
* All possible types of {@link Controller} changes to be used in {@link ControllerChangeHandler}s
*/
public enum ControllerChangeType {
/** The Controller is being pushed to the host container */
PUSH_ENTER(true, true),
/** The Controller is being pushed to the backstack as another Controller is pushed to the host container */
PUSH_EXIT(true, false),
/** The Controller is being popped from the backstack and placed in the host container as another Controller is popped */
POP_ENTER(false, true),
/** The Controller is being popped from the host container */
POP_EXIT(false, false);
public boolean isPush;
public boolean isEnter;
ControllerChangeType(boolean isPush, boolean isEnter) {
this.isPush = isPush;
this.isEnter = isEnter;
}
}
@@ -0,0 +1,18 @@
package com.bluelinelabs.conductor
/**
* All possible types of [Controller] changes to be used in [ControllerChangeHandler]s
*/
enum class ControllerChangeType(@JvmField val isPush: Boolean, @JvmField val isEnter: Boolean) {
/** The Controller is being pushed to the host container */
PUSH_ENTER(true, true),
/** The Controller is being pushed to the backstack as another Controller is pushed to the host container */
PUSH_EXIT(true, false),
/** The Controller is being popped from the backstack and placed in the host container as another Controller is popped */
POP_ENTER(false, true),
/** The Controller is being popped from the host container */
POP_EXIT(false, false);
}
@@ -5,11 +5,12 @@ import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.ViewGroup;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
@@ -47,7 +48,7 @@ class ControllerHostedRouter extends Router {
this.container = container;
for (RouterTransaction transaction : backstack) {
transaction.controller.setParentController(controller);
transaction.controller().setParentController(controller);
}
watchContainerAttach();
@@ -66,8 +67,8 @@ class ControllerHostedRouter extends Router {
}
}
for (RouterTransaction transaction : backstack) {
if (transaction.controller.getView() != null) {
transaction.controller.detach(transaction.controller.getView(), true, false);
if (transaction.controller().getView() != null) {
transaction.controller().detach(transaction.controller().getView(), true, false);
}
}
@@ -79,7 +80,7 @@ class ControllerHostedRouter extends Router {
final void setDetachFrozen(boolean frozen) {
isDetachFrozen = frozen;
for (RouterTransaction transaction : backstack) {
transaction.controller.setDetachFrozen(frozen);
transaction.controller().setDetachFrozen(frozen);
}
}
@@ -92,7 +93,7 @@ class ControllerHostedRouter extends Router {
@Override
protected void pushToBackstack(@NonNull RouterTransaction entry) {
if (isDetachFrozen) {
entry.controller.setDetachFrozen(true);
entry.controller().setDetachFrozen(true);
}
super.pushToBackstack(entry);
}
@@ -101,7 +102,7 @@ class ControllerHostedRouter extends Router {
public void setBackstack(@NonNull List<RouterTransaction> newBackstack, @Nullable ControllerChangeHandler changeHandler) {
if (isDetachFrozen) {
for (RouterTransaction transaction : newBackstack) {
transaction.controller.setDetachFrozen(true);
transaction.controller().setDetachFrozen(true);
}
}
super.setBackstack(newBackstack, changeHandler);
@@ -1,53 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
/**
* A simple controller subclass that changes the onCreateView signature to include a saved view state parameter.
* This is necessary for some third party libraries like Google Maps, which require passing in a saved state
* bundle at the time of creation.
*/
abstract public class RestoreViewOnCreateController extends Controller {
/**
* Convenience constructor for use when no arguments are needed.
*/
protected RestoreViewOnCreateController() {
super(null);
}
/**
* Constructor that takes arguments that need to be retained across restarts.
*
* @param args Any arguments that need to be retained.
*/
protected RestoreViewOnCreateController(@Nullable Bundle args) {
super(args);
}
@Override @NonNull
protected final View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return onCreateView(inflater, container, viewState == null ? null : viewState.getBundle(KEY_VIEW_STATE_BUNDLE));
}
/**
* Called when the controller is ready to display its view. A valid view must be returned. The standard body
* for this method will be {@code return inflater.inflate(R.layout.my_layout, container, false);}, plus
* any binding and state restoration code.
*
* @param inflater The LayoutInflater that should be used to inflate views
* @param container The parent view that this Controller's view will eventually be attached to.
* This Controller's view should NOT be added in this method. It is simply passed in
* so that valid LayoutParams can be used during inflation.
* @param savedViewState A bundle for the view's state, which would have been created in {@link #onSaveViewState(View, Bundle)},
* or {@code null} if no saved state exists.
*/
@NonNull
protected abstract View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState);
}
@@ -4,16 +4,16 @@ import android.app.Activity;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import com.bluelinelabs.conductor.ControllerChangeHandler.ChangeTransaction;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
@@ -92,7 +92,7 @@ public abstract class Router {
if (!backstack.isEmpty()) {
//noinspection ConstantConditions
if (backstack.peek().controller.handleBack()) {
if (backstack.peek().controller().handleBack()) {
return true;
} else if (popCurrentController()) {
return true;
@@ -116,7 +116,7 @@ public abstract class Router {
if (transaction == null) {
throw new IllegalStateException("Trying to pop the current controller when there are none on the backstack.");
}
return popController(transaction.controller);
return popController(transaction.controller());
}
/**
@@ -130,7 +130,7 @@ public abstract class Router {
ThreadUtils.ensureMainThread();
RouterTransaction topTransaction = backstack.peek();
boolean poppingTopController = topTransaction != null && topTransaction.controller == controller;
boolean poppingTopController = topTransaction != null && topTransaction.controller() == controller;
if (poppingTopController) {
trackDestroyingController(backstack.pop());
@@ -139,16 +139,17 @@ public abstract class Router {
RouterTransaction removedTransaction = null;
RouterTransaction nextTransaction = null;
Iterator<RouterTransaction> iterator = backstack.iterator();
ControllerChangeHandler topPushHandler = topTransaction != null ? topTransaction.pushChangeHandler() : null;
final boolean needsNextTransactionAttach = topPushHandler != null ? !topPushHandler.removesFromViewOnPush() : false;
while (iterator.hasNext()) {
RouterTransaction transaction = iterator.next();
if (transaction.controller == controller) {
if (controller.isAttached()) {
trackDestroyingController(transaction);
}
if (transaction.controller() == controller) {
trackDestroyingController(transaction);
iterator.remove();
removedTransaction = transaction;
} else if (removedTransaction != null) {
if (!transaction.controller.isAttached()) {
if (needsNextTransactionAttach && !transaction.controller().isAttached()) {
nextTransaction = transaction;
}
break;
@@ -225,7 +226,7 @@ public abstract class Router {
if (popViews && poppedControllers.size() > 0) {
RouterTransaction topTransaction = poppedControllers.get(0);
topTransaction.controller().addLifecycleListener(new LifecycleListener() {
topTransaction.controller().addLifecycleListener(new Controller.LifecycleListener() {
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
if (changeType == ControllerChangeType.POP_EXIT) {
@@ -345,7 +346,7 @@ public abstract class Router {
@Nullable
public Controller getControllerWithInstanceId(@NonNull String instanceId) {
for (RouterTransaction transaction : backstack) {
Controller controllerWithId = transaction.controller.findController(instanceId);
Controller controllerWithId = transaction.controller().findController(instanceId);
if (controllerWithId != null) {
return controllerWithId;
}
@@ -363,7 +364,7 @@ public abstract class Router {
public Controller getControllerWithTag(@NonNull String tag) {
for (RouterTransaction transaction : backstack) {
if (tag.equals(transaction.tag())) {
return transaction.controller;
return transaction.controller();
}
}
return null;
@@ -415,7 +416,7 @@ public abstract class Router {
for (RouterTransaction oldTransaction : oldTransactions) {
boolean contains = false;
for (RouterTransaction newTransaction : newBackstack) {
if (oldTransaction.controller == newTransaction.controller) {
if (oldTransaction.controller() == newTransaction.controller()) {
contains = true;
break;
}
@@ -423,7 +424,7 @@ public abstract class Router {
if (!contains) {
// Inform the controller that it will be destroyed soon
oldTransaction.controller.isBeingDestroyed = true;
oldTransaction.controller().isBeingDestroyed = true;
transactionsToBeRemoved.add(oldTransaction);
}
}
@@ -433,7 +434,7 @@ public abstract class Router {
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
transaction.onAttachedToRouter();
setControllerRouter(transaction.controller);
setControllerRouter(transaction.controller());
}
if (newBackstack.size() > 0) {
@@ -448,10 +449,10 @@ public abstract class Router {
RouterTransaction newRootTransaction = newVisibleTransactions.get(0);
// Replace the old root with the new one
if (oldRootTransaction == null || oldRootTransaction.controller != newRootTransaction.controller) {
if (oldRootTransaction == null || oldRootTransaction.controller() != newRootTransaction.controller()) {
// Ensure the existing root controller is fully pushed to the view hierarchy
if (oldRootTransaction != null) {
ControllerChangeHandler.completeHandlerImmediately(oldRootTransaction.controller.getInstanceId());
ControllerChangeHandler.completeHandlerImmediately(oldRootTransaction.controller().getInstanceId());
}
performControllerChange(newRootTransaction, oldRootTransaction, newRootRequiresPush, changeHandler);
}
@@ -462,7 +463,7 @@ public abstract class Router {
if (!newVisibleTransactions.contains(transaction)) {
ControllerChangeHandler localHandler = changeHandler != null ? changeHandler.copy() : new SimpleSwapChangeHandler();
localHandler.setForceRemoveViewOnPush(true);
ControllerChangeHandler.completeHandlerImmediately(transaction.controller.getInstanceId());
ControllerChangeHandler.completeHandlerImmediately(transaction.controller().getInstanceId());
performControllerChange(null, transaction, newRootRequiresPush, localHandler);
}
}
@@ -481,7 +482,7 @@ public abstract class Router {
for (int i = oldVisibleTransactions.size() - 1; i >= 0; i--) {
RouterTransaction transaction = oldVisibleTransactions.get(i);
ControllerChangeHandler localHandler = changeHandler != null ? changeHandler.copy() : new SimpleSwapChangeHandler();
ControllerChangeHandler.completeHandlerImmediately(transaction.controller.getInstanceId());
ControllerChangeHandler.completeHandlerImmediately(transaction.controller().getInstanceId());
performControllerChange(null, transaction, false, localHandler);
}
}
@@ -490,7 +491,18 @@ public abstract class Router {
// set the backstack to prevent the possibility that they'll be destroyed before the controller
// change handler runs.
for (RouterTransaction removedTransaction : transactionsToBeRemoved) {
removedTransaction.controller.destroy();
// Still need to ensure the controller isn't queued up to be removed later on.
boolean willBeRemoved = false;
for (ChangeTransaction pendingTransaction : pendingControllerChanges) {
if (pendingTransaction.from == removedTransaction.controller()) {
willBeRemoved = true;
}
}
if (!willBeRemoved) {
removedTransaction.controller().destroy();
}
}
}
@@ -534,8 +546,10 @@ public abstract class Router {
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
if (transaction.controller.getNeedsAttach()) {
if (transaction.controller().getNeedsAttach()) {
performControllerChange(transaction, null, true, new SimpleSwapChangeHandler(false));
} else {
setControllerRouter(transaction.controller());
}
}
}
@@ -551,9 +565,9 @@ public abstract class Router {
isActivityStopped = false;
for (RouterTransaction transaction : backstack) {
transaction.controller.activityStarted(activity);
transaction.controller().activityStarted(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
for (Router childRouter : transaction.controller().getChildRouters()) {
childRouter.onActivityStarted(activity);
}
}
@@ -561,9 +575,9 @@ public abstract class Router {
public final void onActivityResumed(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityResumed(activity);
transaction.controller().activityResumed(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
for (Router childRouter : transaction.controller().getChildRouters()) {
childRouter.onActivityResumed(activity);
}
}
@@ -571,9 +585,9 @@ public abstract class Router {
public final void onActivityPaused(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityPaused(activity);
transaction.controller().activityPaused(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
for (Router childRouter : transaction.controller().getChildRouters()) {
childRouter.onActivityPaused(activity);
}
}
@@ -581,9 +595,9 @@ public abstract class Router {
public final void onActivityStopped(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityStopped(activity);
transaction.controller().activityStopped(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
for (Router childRouter : transaction.controller().getChildRouters()) {
childRouter.onActivityStopped(activity);
}
}
@@ -596,9 +610,9 @@ public abstract class Router {
changeListeners.clear();
for (RouterTransaction transaction : backstack) {
transaction.controller.activityDestroyed(activity);
transaction.controller().activityDestroyed(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
for (Router childRouter : transaction.controller().getChildRouters()) {
childRouter.onActivityDestroyed(activity);
}
}
@@ -616,11 +630,14 @@ public abstract class Router {
}
public void prepareForHostDetach() {
pendingControllerChanges.clear(); // rely on backstack based restoration in rebindIfNeeded
for (RouterTransaction transaction : backstack) {
if (ControllerChangeHandler.completeHandlerImmediately(transaction.controller.getInstanceId())) {
transaction.controller.setNeedsAttach(true);
if (ControllerChangeHandler.completeHandlerImmediately(transaction.controller().getInstanceId())) {
transaction.controller().setNeedsAttach(true);
}
transaction.controller.prepareForHostDetach();
transaction.controller().prepareForHostDetach();
}
}
@@ -640,15 +657,15 @@ public abstract class Router {
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
setControllerRouter(backstackIterator.next().controller);
setControllerRouter(backstackIterator.next().controller());
}
}
public final void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
for (RouterTransaction transaction : backstack) {
transaction.controller.createOptionsMenu(menu, inflater);
transaction.controller().createOptionsMenu(menu, inflater);
for (Router childRouter : transaction.controller.getChildRouters()) {
for (Router childRouter : transaction.controller().getChildRouters()) {
childRouter.onCreateOptionsMenu(menu, inflater);
}
}
@@ -656,9 +673,9 @@ public abstract class Router {
public final void onPrepareOptionsMenu(@NonNull Menu menu) {
for (RouterTransaction transaction : backstack) {
transaction.controller.prepareOptionsMenu(menu);
transaction.controller().prepareOptionsMenu(menu);
for (Router childRouter : transaction.controller.getChildRouters()) {
for (Router childRouter : transaction.controller().getChildRouters()) {
childRouter.onPrepareOptionsMenu(menu);
}
}
@@ -666,11 +683,11 @@ public abstract class Router {
public final boolean onOptionsItemSelected(@NonNull MenuItem item) {
for (RouterTransaction transaction : backstack) {
if (transaction.controller.optionsItemSelected(item)) {
if (transaction.controller().optionsItemSelected(item)) {
return true;
}
for (Router childRouter : transaction.controller.getChildRouters()) {
for (Router childRouter : transaction.controller().getChildRouters()) {
if (childRouter.onOptionsItemSelected(item)) {
return true;
}
@@ -703,12 +720,7 @@ public abstract class Router {
}
void watchContainerAttach() {
container.post(new Runnable() {
@Override
public void run() {
containerFullyAttached = true;
}
});
container.post(() -> containerFullyAttached = true);
}
void prepareForContainerRemoval() {
@@ -721,7 +733,7 @@ public abstract class Router {
void onContextAvailable() {
for (RouterTransaction transaction : backstack) {
transaction.controller.onContextAvailable();
transaction.controller().onContextAvailable();
}
}
@@ -731,7 +743,7 @@ public abstract class Router {
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
controllers.add(backstackIterator.next().controller);
controllers.add(backstackIterator.next().controller());
}
return controllers;
@@ -740,8 +752,8 @@ public abstract class Router {
@Nullable
public final Boolean handleRequestedPermission(@NonNull String permission) {
for (RouterTransaction transaction : backstack) {
if (transaction.controller.didRequestPermission(permission)) {
return transaction.controller.shouldShowRequestPermissionRationale(permission);
if (transaction.controller().didRequestPermission(permission)) {
return transaction.controller().shouldShowRequestPermissionRationale(permission);
}
}
return null;
@@ -765,9 +777,9 @@ public abstract class Router {
performControllerChange(to, from, isPush, changeHandler);
}
void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush, @Nullable ControllerChangeHandler changeHandler) {
Controller toController = to != null ? to.controller : null;
Controller fromController = from != null ? from.controller : null;
private void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush, @Nullable ControllerChangeHandler changeHandler) {
Controller toController = to != null ? to.controller() : null;
Controller fromController = from != null ? from.controller() : null;
boolean forceDetachDestroy = false;
if (to != null) {
@@ -778,12 +790,20 @@ public abstract class Router {
// Activity or controller should be handling this by finishing or at least hiding this view.
changeHandler = new NoOpControllerChangeHandler();
forceDetachDestroy = true;
} else if (!isPush && fromController != null && !fromController.isAttached()) {
// We're popping fromController from the middle of the backstack,
// need to do it immediately and destroy the controller
forceDetachDestroy = true;
}
performControllerChange(toController, fromController, isPush, changeHandler);
if (forceDetachDestroy && fromController != null && fromController.getView() != null) {
fromController.detach(fromController.getView(), true, false);
if (forceDetachDestroy && fromController != null) {
if (fromController.getView() != null) {
fromController.detach(fromController.getView(), true, false);
} else {
from.controller().destroy();
}
}
}
@@ -797,18 +817,19 @@ public abstract class Router {
if (pendingControllerChanges.size() > 0) {
// If we already have changes queued up (awaiting full container attach), queue this one up as well so they don't happen
// out of order.
if (to != null) {
to.setNeedsAttach(true);
}
pendingControllerChanges.add(transaction);
} else if (from != null && (changeHandler == null || changeHandler.removesFromViewOnPush()) && !containerFullyAttached) {
// If the change handler will remove the from view, we have to make sure the container is fully attached first so we avoid NPEs
// within ViewGroup (details on issue #287). Post this to the container to ensure the attach is complete before we try to remove
// anything.
if (to != null) {
to.setNeedsAttach(true);
}
pendingControllerChanges.add(transaction);
container.post(new Runnable() {
@Override
public void run() {
performPendingControllerChanges();
}
});
container.post(this::performPendingControllerChanges);
} else {
ControllerChangeHandler.executeChange(transaction);
}
@@ -824,17 +845,17 @@ public abstract class Router {
}
protected void pushToBackstack(@NonNull RouterTransaction entry) {
if (backstack.contains(entry.controller)) {
if (backstack.contains(entry.controller())) {
throw new IllegalStateException("Trying to push a controller that already exists on the backstack.");
}
backstack.push(entry);
}
private void trackDestroyingController(@NonNull RouterTransaction transaction) {
if (!transaction.controller.isDestroyed()) {
destroyingControllers.add(transaction.controller);
if (!transaction.controller().isDestroyed()) {
destroyingControllers.add(transaction.controller());
transaction.controller.addLifecycleListener(new LifecycleListener() {
transaction.controller().addLifecycleListener(new Controller.LifecycleListener() {
@Override
public void postDestroy(@NonNull Controller controller) {
destroyingControllers.remove(controller);
@@ -853,8 +874,8 @@ public abstract class Router {
List<View> views = new ArrayList<>();
for (RouterTransaction transaction : getVisibleTransactions(backstack.iterator())) {
if (transaction.controller.getView() != null) {
views.add(transaction.controller.getView());
if (transaction.controller().getView() != null) {
views.add(transaction.controller().getView());
}
}
@@ -879,21 +900,21 @@ public abstract class Router {
List<Integer> indices = new ArrayList<>(backstack.size());
for (RouterTransaction transaction : backstack) {
transaction.ensureValidIndex(getTransactionIndexer());
indices.add(transaction.transactionIndex);
indices.add(transaction.getTransactionIndex());
}
Collections.sort(indices);
for (int i = 0; i < backstack.size(); i++) {
backstack.get(i).transactionIndex = indices.get(i);
backstack.get(i).setTransactionIndex(indices.get(i));
}
}
private void ensureNoDuplicateControllers(List<RouterTransaction> backstack) {
for (int i = 0; i < backstack.size(); i++) {
Controller controller = backstack.get(i).controller;
Controller controller = backstack.get(i).controller();
for (int j = i + 1; j < backstack.size(); j++) {
if (backstack.get(j).controller == controller) {
if (backstack.get(j).controller() == controller) {
throw new IllegalStateException("Trying to push the same controller to the backstack more than once.");
}
}
@@ -1,140 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
/**
* Metadata used for adding {@link Controller}s to a {@link Router}.
*/
public class RouterTransaction {
private static int INVALID_INDEX = -1;
private static final String KEY_VIEW_CONTROLLER_BUNDLE = "RouterTransaction.controller.bundle";
private static final String KEY_PUSH_TRANSITION = "RouterTransaction.pushControllerChangeHandler";
private static final String KEY_POP_TRANSITION = "RouterTransaction.popControllerChangeHandler";
private static final String KEY_TAG = "RouterTransaction.tag";
private static final String KEY_INDEX = "RouterTransaction.transactionIndex";
private static final String KEY_ATTACHED_TO_ROUTER = "RouterTransaction.attachedToRouter";
@NonNull final Controller controller;
private String tag;
private ControllerChangeHandler pushControllerChangeHandler;
private ControllerChangeHandler popControllerChangeHandler;
private boolean attachedToRouter;
int transactionIndex = INVALID_INDEX;
@NonNull
public static RouterTransaction with(@NonNull Controller controller) {
return new RouterTransaction(controller);
}
private RouterTransaction(@NonNull Controller controller) {
this.controller = controller;
}
RouterTransaction(@NonNull Bundle bundle) {
controller = Controller.newInstance(bundle.getBundle(KEY_VIEW_CONTROLLER_BUNDLE));
pushControllerChangeHandler = ControllerChangeHandler.fromBundle(bundle.getBundle(KEY_PUSH_TRANSITION));
popControllerChangeHandler = ControllerChangeHandler.fromBundle(bundle.getBundle(KEY_POP_TRANSITION));
tag = bundle.getString(KEY_TAG);
transactionIndex = bundle.getInt(KEY_INDEX);
attachedToRouter = bundle.getBoolean(KEY_ATTACHED_TO_ROUTER);
}
void onAttachedToRouter() {
attachedToRouter = true;
}
@NonNull
public Controller controller() {
return controller;
}
@Nullable
public String tag() {
return tag;
}
@NonNull
public RouterTransaction tag(@Nullable String tag) {
if (!attachedToRouter) {
this.tag = tag;
return this;
} else {
throw new RuntimeException(getClass().getSimpleName() + "s can not be modified after being added to a Router.");
}
}
@Nullable
public ControllerChangeHandler pushChangeHandler() {
ControllerChangeHandler handler = controller.getOverriddenPushHandler();
if (handler == null) {
handler = pushControllerChangeHandler;
}
return handler;
}
@NonNull
public RouterTransaction pushChangeHandler(@Nullable ControllerChangeHandler handler) {
if (!attachedToRouter) {
pushControllerChangeHandler = handler;
return this;
} else {
throw new RuntimeException(getClass().getSimpleName() + "s can not be modified after being added to a Router.");
}
}
@Nullable
public ControllerChangeHandler popChangeHandler() {
ControllerChangeHandler handler = controller.getOverriddenPopHandler();
if (handler == null) {
handler = popControllerChangeHandler;
}
return handler;
}
@NonNull
public RouterTransaction popChangeHandler(@Nullable ControllerChangeHandler handler) {
if (!attachedToRouter) {
popControllerChangeHandler = handler;
return this;
} else {
throw new RuntimeException(getClass().getSimpleName() + "s can not be modified after being added to a Router.");
}
}
void ensureValidIndex(@NonNull TransactionIndexer indexer) {
if (transactionIndex == INVALID_INDEX) {
transactionIndex = indexer.nextIndex();
}
}
/**
* Used to serialize this transaction into a Bundle
*/
@NonNull
public Bundle saveInstanceState() {
Bundle bundle = new Bundle();
bundle.putBundle(KEY_VIEW_CONTROLLER_BUNDLE, controller.saveInstanceState());
if (pushControllerChangeHandler != null) {
bundle.putBundle(KEY_PUSH_TRANSITION, pushControllerChangeHandler.toBundle());
}
if (popControllerChangeHandler != null) {
bundle.putBundle(KEY_POP_TRANSITION, popControllerChangeHandler.toBundle());
}
bundle.putString(KEY_TAG, tag);
bundle.putInt(KEY_INDEX, transactionIndex);
bundle.putBoolean(KEY_ATTACHED_TO_ROUTER, attachedToRouter);
return bundle;
}
}
@@ -0,0 +1,123 @@
package com.bluelinelabs.conductor
import android.os.Bundle
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY
import com.bluelinelabs.conductor.internal.TransactionIndexer
private const val INVALID_INDEX = -1
private const val KEY_VIEW_CONTROLLER_BUNDLE = "RouterTransaction.controller.bundle"
private const val KEY_PUSH_TRANSITION = "RouterTransaction.pushControllerChangeHandler"
private const val KEY_POP_TRANSITION = "RouterTransaction.popControllerChangeHandler"
private const val KEY_TAG = "RouterTransaction.tag"
private const val KEY_INDEX = "RouterTransaction.transactionIndex"
private const val KEY_ATTACHED_TO_ROUTER = "RouterTransaction.attachedToRouter"
/**
* Metadata used for adding [Controller]s to a [Router].
*/
class RouterTransaction
private constructor(
@get:JvmName("controller")
val controller: Controller,
private var tag: String? = null,
private var pushControllerChangeHandler: ControllerChangeHandler? = null,
private var popControllerChangeHandler: ControllerChangeHandler? = null,
private var attachedToRouter: Boolean = false,
@RestrictTo(LIBRARY)
var transactionIndex: Int = INVALID_INDEX
) {
@RestrictTo(LIBRARY)
internal constructor(bundle: Bundle) : this(
controller = Controller.newInstance(bundle.getBundle(KEY_VIEW_CONTROLLER_BUNDLE)!!),
pushControllerChangeHandler = ControllerChangeHandler.fromBundle(
bundle.getBundle(
KEY_PUSH_TRANSITION
)
),
popControllerChangeHandler = ControllerChangeHandler.fromBundle(
bundle.getBundle(
KEY_POP_TRANSITION
)
),
tag = bundle.getString(KEY_TAG),
transactionIndex = bundle.getInt(KEY_INDEX),
attachedToRouter = bundle.getBoolean(KEY_ATTACHED_TO_ROUTER)
)
fun onAttachedToRouter() {
attachedToRouter = true
}
fun tag(): String? = tag
fun tag(tag: String?): RouterTransaction {
return if (!attachedToRouter) {
this.tag = tag
this
} else {
throw RuntimeException(javaClass.simpleName + "s can not be modified after being added to a Router.")
}
}
fun pushChangeHandler(): ControllerChangeHandler? {
return controller.overriddenPushHandler ?: pushControllerChangeHandler
}
fun pushChangeHandler(handler: ControllerChangeHandler?): RouterTransaction {
return if (!attachedToRouter) {
pushControllerChangeHandler = handler
this
} else {
throw RuntimeException("${javaClass.simpleName}s can not be modified after being added to a Router.")
}
}
fun popChangeHandler(): ControllerChangeHandler? {
return controller.overriddenPopHandler ?: popControllerChangeHandler
}
fun popChangeHandler(handler: ControllerChangeHandler?): RouterTransaction {
return if (!attachedToRouter) {
popControllerChangeHandler = handler
this
} else {
throw RuntimeException("${javaClass.simpleName}s can not be modified after being added to a Router.")
}
}
fun ensureValidIndex(indexer: TransactionIndexer) {
if (transactionIndex == INVALID_INDEX) {
transactionIndex = indexer.nextIndex()
}
}
/**
* Used to serialize this transaction into a Bundle
*/
fun saveInstanceState(): Bundle = Bundle().apply {
putBundle(KEY_VIEW_CONTROLLER_BUNDLE, controller.saveInstanceState())
pushControllerChangeHandler?.let { putBundle(KEY_PUSH_TRANSITION, it.toBundle()) }
popControllerChangeHandler?.let { putBundle(KEY_POP_TRANSITION, it.toBundle()) }
putString(KEY_TAG, tag)
putInt(KEY_INDEX, transactionIndex)
putBoolean(KEY_ATTACHED_TO_ROUTER, attachedToRouter)
}
companion object {
@JvmStatic
fun with(controller: Controller): RouterTransaction = RouterTransaction(controller)
}
}
fun Controller.asTransaction(
popChangeHandler: ControllerChangeHandler? = null,
pushChangeHandler: ControllerChangeHandler? = null
): RouterTransaction {
return RouterTransaction.with(this)
.pushChangeHandler(pushChangeHandler)
.popChangeHandler(popChangeHandler)
}
@@ -4,8 +4,8 @@ import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
@@ -1,33 +0,0 @@
package com.bluelinelabs.conductor.changehandler;
import android.annotation.TargetApi;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.AutoTransition;
import android.transition.Transition;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* @deprecated It's very rare that a simple AutoTransition is what you want when changing controllers. This class
* is deprecated simply because it was often a red herring for people trying to make nice transitions.
*
* A change handler that will use an AutoTransition.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class AutoTransitionChangeHandler extends TransitionChangeHandler {
@Override @NonNull
protected Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
return new AutoTransition();
}
@Override @NonNull
public ControllerChangeHandler copy() {
return new AutoTransitionChangeHandler();
}
}
@@ -3,8 +3,8 @@ package com.bluelinelabs.conductor.changehandler;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
@@ -3,8 +3,8 @@ package com.bluelinelabs.conductor.changehandler;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
@@ -1,8 +1,8 @@
package com.bluelinelabs.conductor.changehandler;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
@@ -1,97 +0,0 @@
package com.bluelinelabs.conductor.changehandler;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.internal.ClassUtils;
/**
* A base {@link ControllerChangeHandler} that facilitates using {@link android.transition.Transition}s to replace Controller Views.
* If the target device is running on a version of Android that doesn't support transitions, a fallback {@link ControllerChangeHandler} will be used.
*/
public class TransitionChangeHandlerCompat extends ControllerChangeHandler {
private static final String KEY_CHANGE_HANDLER_CLASS = "TransitionChangeHandlerCompat.changeHandler.class";
private static final String KEY_HANDLER_STATE = "TransitionChangeHandlerCompat.changeHandler.state";
private ControllerChangeHandler changeHandler;
public TransitionChangeHandlerCompat() { }
/**
* Constructor that takes a {@link TransitionChangeHandler} for use with compatible devices, as well as a fallback
* {@link ControllerChangeHandler} for use with older devices.
*
* @param transitionChangeHandler The change handler that will be used on API 21 and above
* @param fallbackChangeHandler The change handler that will be used on APIs below 21
*/
public TransitionChangeHandlerCompat(@NonNull TransitionChangeHandler transitionChangeHandler, @NonNull ControllerChangeHandler fallbackChangeHandler) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
changeHandler = transitionChangeHandler;
} else {
changeHandler = fallbackChangeHandler;
}
}
@Override
public void performChange(@NonNull final ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
changeHandler.performChange(container, from, to, isPush, changeListener);
}
@Override
public void saveToBundle(@NonNull Bundle bundle) {
super.saveToBundle(bundle);
bundle.putString(KEY_CHANGE_HANDLER_CLASS, changeHandler.getClass().getName());
Bundle stateBundle = new Bundle();
changeHandler.saveToBundle(stateBundle);
bundle.putBundle(KEY_HANDLER_STATE, stateBundle);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
super.restoreFromBundle(bundle);
String className = bundle.getString(KEY_CHANGE_HANDLER_CLASS);
changeHandler = ClassUtils.newInstance(className);
//noinspection ConstantConditions
changeHandler.restoreFromBundle(bundle.getBundle(KEY_HANDLER_STATE));
}
@Override
public boolean removesFromViewOnPush() {
return changeHandler.removesFromViewOnPush();
}
@Override @NonNull
public ControllerChangeHandler copy() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return new TransitionChangeHandlerCompat((TransitionChangeHandler)changeHandler.copy(), null);
} else {
return new TransitionChangeHandlerCompat(null, changeHandler.copy());
}
}
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
changeHandler.onAbortPush(newHandler, newTop);
}
@Override
public void completeImmediately() {
changeHandler.completeImmediately();
}
@Override
public void setForceRemoveViewOnPush(boolean force) {
changeHandler.setForceRemoveViewOnPush(force);
}
}
@@ -3,8 +3,8 @@ package com.bluelinelabs.conductor.changehandler;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
@@ -1,7 +1,7 @@
package com.bluelinelabs.conductor.internal;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
public class ClassUtils {
@@ -11,14 +11,15 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.ActivityHostedRouter;
import com.bluelinelabs.conductor.Router;
@@ -100,7 +101,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
@NonNull
public List<Router> getRouters() {
return new ArrayList<Router>(routerMap.values());
return new ArrayList<>(routerMap.values());
}
@Nullable
@@ -132,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<String>();
permissionRequestMap = permissionParcel != null ? permissionParcel.getStringSparseArray() : new SparseArray<>();
StringSparseArrayParceler activityParcel = savedInstanceState.getParcelable(KEY_ACTIVITY_REQUEST_CODES);
activityRequestMap = activityParcel != null ? activityParcel.getStringSparseArray() : new SparseArray<String>();
activityRequestMap = activityParcel != null ? activityParcel.getStringSparseArray() : new SparseArray<>();
ArrayList<PendingPermissionRequest> pendingRequests = savedInstanceState.getParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS);
pendingPermissionRequests = pendingRequests != null ? pendingRequests : new ArrayList<PendingPermissionRequest>();
pendingPermissionRequests = pendingRequests != null ? pendingRequests : new ArrayList<>();
}
}
@@ -163,7 +164,6 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
}
}
@SuppressWarnings("deprecation")
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
@@ -1,27 +0,0 @@
package com.bluelinelabs.conductor.internal;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
public class NoOpControllerChangeHandler extends ControllerChangeHandler {
@Override
public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
changeListener.onChangeCompleted();
}
@NonNull
@Override
public ControllerChangeHandler copy() {
return new NoOpControllerChangeHandler();
}
@Override
public boolean isReusable() {
return true;
}
}
@@ -0,0 +1,22 @@
package com.bluelinelabs.conductor.internal
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler
class NoOpControllerChangeHandler : ControllerChangeHandler() {
override fun performChange(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
changeListener: ControllerChangeCompletedListener
) {
changeListener.onChangeCompleted()
}
override fun copy(): ControllerChangeHandler = NoOpControllerChangeHandler()
override fun isReusable() = true
}
@@ -1,5 +0,0 @@
package com.bluelinelabs.conductor.internal;
public interface RouterRequiringFunc {
void execute();
}
@@ -0,0 +1,9 @@
package com.bluelinelabs.conductor.internal
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
@RestrictTo(LIBRARY_GROUP)
interface RouterRequiringFunc {
fun execute()
}
@@ -2,7 +2,7 @@ package com.bluelinelabs.conductor.internal;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import androidx.annotation.NonNull;
import android.util.SparseArray;
public class StringSparseArrayParceler implements Parcelable {
@@ -1,20 +0,0 @@
package com.bluelinelabs.conductor.internal;
import android.os.Looper;
import android.util.AndroidRuntimeException;
public class ThreadUtils {
public static void ensureMainThread() {
if (Looper.getMainLooper().getThread() != Thread.currentThread()) {
throw new CalledFromWrongThreadException("Methods that affect the view hierarchy can can only be called from the main thread.");
}
}
private static final class CalledFromWrongThreadException extends AndroidRuntimeException {
CalledFromWrongThreadException(String msg) {
super(msg);
}
}
}
@@ -0,0 +1,17 @@
@file:JvmName("ThreadUtils")
package com.bluelinelabs.conductor.internal
import android.os.Looper
import android.util.AndroidRuntimeException
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
@RestrictTo(LIBRARY_GROUP)
internal fun ensureMainThread() {
if (Looper.getMainLooper().thread !== Thread.currentThread()) {
throw CalledFromWrongThreadException("Methods that affect the view hierarchy can can only be called from the main thread.")
}
}
private class CalledFromWrongThreadException(msg: String?) : AndroidRuntimeException(msg)
@@ -1,24 +0,0 @@
package com.bluelinelabs.conductor.internal;
import android.os.Bundle;
import android.support.annotation.NonNull;
public class TransactionIndexer {
private static final String KEY_INDEX = "TransactionIndexer.currentIndex";
private int currentIndex;
public int nextIndex() {
return ++currentIndex;
}
public void saveInstanceState(@NonNull Bundle outState) {
outState.putInt(KEY_INDEX, currentIndex);
}
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
currentIndex = savedInstanceState.getInt(KEY_INDEX);
}
}
@@ -0,0 +1,25 @@
package com.bluelinelabs.conductor.internal
import android.os.Bundle
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
@RestrictTo(LIBRARY_GROUP)
class TransactionIndexer {
private var currentIndex = 0
fun nextIndex(): Int {
return ++currentIndex
}
fun saveInstanceState(outState: Bundle) {
outState.putInt(KEY_INDEX, currentIndex)
}
fun restoreInstanceState(savedInstanceState: Bundle) {
currentIndex = savedInstanceState.getInt(KEY_INDEX)
}
}
private const val KEY_INDEX = "TransactionIndexer.currentIndex"
@@ -40,13 +40,10 @@ public class ViewAttachHandler implements OnAttachStateChangeListener {
}
rootAttached = true;
listenForDeepestChildAttach(v, new ChildAttachListener() {
@Override
public void onAttached() {
childrenAttached = true;
reportAttached();
listenForDeepestChildAttach(v, () -> {
childrenAttached = true;
reportAttached();
}
});
}
@@ -5,9 +5,12 @@ import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import static org.junit.Assert.assertEquals;
@RunWith(RobolectricTestRunner.class)
public class ControllerChangeHandlerTests {
@Test
@@ -26,8 +29,8 @@ public class ControllerChangeHandlerTests {
assertEquals(horizontalChangeHandler.getClass(), restoredHorizontal.getClass());
assertEquals(fadeChangeHandler.getClass(), restoredFade.getClass());
HorizontalChangeHandler restoredHorizontalCast = (HorizontalChangeHandler)restoredHorizontal;
FadeChangeHandler restoredFadeCast = (FadeChangeHandler)restoredFade;
HorizontalChangeHandler restoredHorizontalCast = (HorizontalChangeHandler) restoredHorizontal;
FadeChangeHandler restoredFadeCast = (FadeChangeHandler) restoredFade;
assertEquals(horizontalChangeHandler.getAnimationDuration(), restoredHorizontalCast.getAnimationDuration());
assertEquals(horizontalChangeHandler.removesFromViewOnPush(), restoredHorizontalCast.removesFromViewOnPush());
@@ -1,9 +1,9 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.MockChangeHandler;
@@ -85,7 +85,7 @@ public class ControllerLifecycleActivityReferenceTests {
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
Router childRouter = parent.getChildRouter(parent.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
@@ -132,7 +132,7 @@ public class ControllerLifecycleActivityReferenceTests {
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
Router childRouter = parent.getChildRouter(parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
@@ -161,7 +161,7 @@ public class ControllerLifecycleActivityReferenceTests {
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
Router childRouter = parent.getChildRouter(parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
@@ -211,7 +211,7 @@ public class ControllerLifecycleActivityReferenceTests {
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
Router childRouter = parent.getChildRouter(parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
@@ -227,7 +227,7 @@ public class ControllerLifecycleActivityReferenceTests {
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
static class ActivityReferencingLifecycleListener extends Controller.LifecycleListener {
static class ActivityReferencingLifecycleListener implements Controller.LifecycleListener {
final List<Boolean> changeEndReferences = new ArrayList<>();
final List<Boolean> postCreateViewReferences = new ArrayList<>();
final List<Boolean> postAttachReferences = new ArrayList<>();
@@ -2,7 +2,7 @@ package com.bluelinelabs.conductor;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import androidx.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
@@ -445,6 +445,38 @@ public class ControllerLifecycleCallbacksTests {
assertEquals(2, callState.destroyCalls);
}
@Test
public void testLifecycleWhenPopNonCurrentController() {
String controller1Tag = "controller1";
String controller2Tag = "controller2";
String controller3Tag = "controller3";
TestController controller1 = new TestController();
TestController controller2 = new TestController();
TestController controller3 = new TestController();
router.pushController(RouterTransaction.with(controller1)
.tag(controller1Tag));
router.pushController(RouterTransaction.with(controller2)
.tag(controller2Tag));
router.pushController(RouterTransaction.with(controller3)
.tag(controller3Tag));
router.popController(controller2);
assertEquals(1, controller2.currentCallState.attachCalls);
assertEquals(1, controller2.currentCallState.createViewCalls);
assertEquals(1, controller2.currentCallState.detachCalls);
assertEquals(1, controller2.currentCallState.destroyViewCalls);
assertEquals(1, controller2.currentCallState.destroyCalls);
assertEquals(1, controller2.currentCallState.contextAvailableCalls);
assertEquals(1, controller2.currentCallState.contextUnavailableCalls);
assertEquals(1, controller2.currentCallState.saveViewStateCalls);
assertEquals(0, controller2.currentCallState.restoreViewStateCalls);
}
@Test
public void testChildLifecycle() {
Controller parent = new TestController();
@@ -535,6 +567,29 @@ public class ControllerLifecycleCallbacksTests {
assertTrue(child.isAttached());
}
@Test
public void testChildLifecycleAfterPushAndPop() {
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));
router.popCurrentController();
assertTrue(parent.isAttached());
assertTrue(child.isAttached());
}
private MockChangeHandler getPushHandler(final CallState expectedCallState, final TestController controller) {
return MockChangeHandler.listeningChangeHandler(new ChangeHandlerListener() {
@Override
@@ -392,10 +392,10 @@ public class ControllerTests {
RouterTransaction restoredChildTransaction1 = childRouter.getBackstack().get(0);
RouterTransaction restoredChildTransaction2 = childRouter.getBackstack().get(1);
assertEquals(childTransaction1.transactionIndex, restoredChildTransaction1.transactionIndex);
assertEquals(childTransaction1.controller.getInstanceId(), restoredChildTransaction1.controller.getInstanceId());
assertEquals(childTransaction2.transactionIndex, restoredChildTransaction2.transactionIndex);
assertEquals(childTransaction2.controller.getInstanceId(), restoredChildTransaction2.controller.getInstanceId());
assertEquals(childTransaction1.getTransactionIndex(), restoredChildTransaction1.getTransactionIndex());
assertEquals(childTransaction1.controller().getInstanceId(), restoredChildTransaction1.controller().getInstanceId());
assertEquals(childTransaction2.getTransactionIndex(), restoredChildTransaction2.getTransactionIndex());
assertEquals(childTransaction2.controller().getInstanceId(), restoredChildTransaction2.controller().getInstanceId());
assertTrue(parent.handleBack());
assertEquals(1, childRouter.getBackstackSize());
@@ -7,9 +7,12 @@ import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import static org.junit.Assert.assertEquals;
@RunWith(RobolectricTestRunner.class)
public class ControllerTransactionTests {
@Test
@@ -23,10 +26,9 @@ public class ControllerTransactionTests {
RouterTransaction restoredTransaction = new RouterTransaction(bundle);
assertEquals(transaction.controller.getClass(), restoredTransaction.controller.getClass());
assertEquals(transaction.controller().getClass(), restoredTransaction.controller().getClass());
assertEquals(transaction.pushChangeHandler().getClass(), restoredTransaction.pushChangeHandler().getClass());
assertEquals(transaction.popChangeHandler().getClass(), restoredTransaction.popChangeHandler().getClass());
assertEquals(transaction.tag(), restoredTransaction.tag());
}
}
@@ -3,9 +3,12 @@ package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.internal.LifecycleHandler;
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;
@@ -257,6 +260,96 @@ public class ReattachCaseTests {
assertTrue(controller2.isAttached());
}
@Test
public void testPopMiddleControllerAttaches() {
Controller controller1 = new TestController();
Controller controller2 = new TestController();
Controller controller3 = new TestController();
router.setRoot(RouterTransaction.with(controller1));
router.pushController(RouterTransaction.with(controller2));
router.pushController(RouterTransaction.with(controller3));
router.popController(controller2);
assertFalse(controller1.isAttached());
assertFalse(controller2.isAttached());
assertTrue(controller3.isAttached());
controller1 = new TestController();
controller2 = new TestController();
controller3 = new TestController();
router.setRoot(RouterTransaction.with(controller1));
router.pushController(RouterTransaction.with(controller2));
router.pushController(RouterTransaction.with(controller3).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler()));
router.popController(controller2);
assertTrue(controller1.isAttached());
assertFalse(controller2.isAttached());
assertTrue(controller3.isAttached());
}
@Test
public void testPendingChanges() {
Controller controller1 = new TestController();
Controller controller2 = new TestController();
ActivityProxy activityProxy = new ActivityProxy().create(null);
AttachFakingFrameLayout container = new AttachFakingFrameLayout(activityProxy.getActivity());
container.setNeedDelayPost(true); // to simulate calling posts after resume
activityProxy.setView(container);
Router router = Conductor.attachRouter(activityProxy.getActivity(), container, null);
router.setRoot(RouterTransaction.with(controller1));
router.pushController(RouterTransaction.with(controller2));
activityProxy.start().resume();
container.setNeedDelayPost(false);
assertTrue(controller2.isAttached());
}
@Test
public void testPendingChangesAfterRotation() {
Controller controller1 = new TestController();
Controller controller2 = new TestController();
// first activity
ActivityProxy activityProxy = new ActivityProxy().create(null);
AttachFakingFrameLayout container1 = new AttachFakingFrameLayout(activityProxy.getActivity());
container1.setNeedDelayPost(true); // delay forever as view will be removed
activityProxy.setView(container1);
// first attachRouter: Conductor.attachRouter(activityProxy.getActivity(), container1, null)
LifecycleHandler lifecycleHandler = LifecycleHandler.install(activityProxy.getActivity());
Router router = lifecycleHandler.getRouter(container1, null);
router.setRoot(RouterTransaction.with(controller1));
// setup controllers
router.pushController(RouterTransaction.with(controller2));
// simulate setRequestedOrientation in activity onCreate
activityProxy.start().resume();
Bundle savedState = new Bundle();
activityProxy.saveInstanceState(savedState).pause().stop(true);
// recreate activity and view
activityProxy = new ActivityProxy().create(savedState);
AttachFakingFrameLayout container2 = new AttachFakingFrameLayout(activityProxy.getActivity());
activityProxy.setView(container2);
// second attach router with the same lifecycleHandler (do manually as Roboelectric recreates retained fragments)
// Conductor.attachRouter(activityProxy.getActivity(), container2, savedState);
router = lifecycleHandler.getRouter(container2, savedState);
router.rebindIfNeeded();
activityProxy.start().resume();
assertTrue(controller2.isAttached());
}
private void sleepWakeDevice() {
activityProxy.saveInstanceState(new Bundle()).pause();
activityProxy.resume();
@@ -1,9 +1,10 @@
package com.bluelinelabs.conductor;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
@@ -250,9 +251,9 @@ public class RouterTests {
assertEquals(middleTransaction, fetchedBackstack.get(1));
assertEquals(topTransaction, fetchedBackstack.get(2));
assertEquals(router, rootTransaction.controller.getRouter());
assertEquals(router, middleTransaction.controller.getRouter());
assertEquals(router, topTransaction.controller.getRouter());
assertEquals(router, rootTransaction.controller().getRouter());
assertEquals(router, middleTransaction.controller().getRouter());
assertEquals(router, topTransaction.controller().getRouter());
}
@Test
@@ -264,8 +265,8 @@ public class RouterTests {
router.pushController(oldTopTransaction);
assertEquals(2, router.getBackstackSize());
assertTrue(oldRootTransaction.controller.isAttached());
assertTrue(oldTopTransaction.controller.isAttached());
assertTrue(oldRootTransaction.controller().isAttached());
assertTrue(oldTopTransaction.controller().isAttached());
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction middleTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
@@ -281,11 +282,11 @@ public class RouterTests {
assertEquals(middleTransaction, fetchedBackstack.get(1));
assertEquals(topTransaction, fetchedBackstack.get(2));
assertFalse(oldRootTransaction.controller.isAttached());
assertFalse(oldTopTransaction.controller.isAttached());
assertTrue(rootTransaction.controller.isAttached());
assertTrue(middleTransaction.controller.isAttached());
assertTrue(topTransaction.controller.isAttached());
assertFalse(oldRootTransaction.controller().isAttached());
assertFalse(oldTopTransaction.controller().isAttached());
assertTrue(rootTransaction.controller().isAttached());
assertTrue(middleTransaction.controller().isAttached());
assertTrue(topTransaction.controller().isAttached());
}
@Test
@@ -304,9 +305,9 @@ public class RouterTests {
assertEquals(1, router.getBackstackSize());
assertEquals(rootTransaction, router.getBackstack().get(0));
assertTrue(rootTransaction.controller.isAttached());
assertFalse(transaction1.controller.isAttached());
assertFalse(transaction2.controller.isAttached());
assertTrue(rootTransaction.controller().isAttached());
assertFalse(transaction1.controller().isAttached());
assertFalse(transaction2.controller().isAttached());
}
@Test
@@ -325,9 +326,9 @@ public class RouterTests {
assertEquals(1, router.getBackstackSize());
assertEquals(rootTransaction, router.getBackstack().get(0));
assertTrue(rootTransaction.controller.isAttached());
assertFalse(transaction1.controller.isAttached());
assertFalse(transaction2.controller.isAttached());
assertTrue(rootTransaction.controller().isAttached());
assertFalse(transaction1.controller().isAttached());
assertFalse(transaction2.controller().isAttached());
}
@Test
@@ -364,8 +365,8 @@ public class RouterTests {
assertEquals(2, router.getBackstackSize());
assertTrue(rootTransaction.controller.isAttached());
assertTrue(topTransaction.controller.isAttached());
assertTrue(rootTransaction.controller().isAttached());
assertTrue(topTransaction.controller().isAttached());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
assertEquals(rootTransaction, fetchedBackstack.get(0));
@@ -381,9 +382,9 @@ public class RouterTests {
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(newTopTransaction, fetchedBackstack.get(1));
assertTrue(rootTransaction.controller.isAttached());
assertFalse(topTransaction.controller.isAttached());
assertTrue(newTopTransaction.controller.isAttached());
assertTrue(rootTransaction.controller().isAttached());
assertFalse(topTransaction.controller().isAttached());
assertTrue(newTopTransaction.controller().isAttached());
}
@Test
@@ -394,14 +395,14 @@ public class RouterTests {
List<RouterTransaction> backstack = Arrays.asList(transaction1, transaction2);
router.setBackstack(backstack, null);
assertEquals(1, transaction1.transactionIndex);
assertEquals(2, transaction2.transactionIndex);
assertEquals(1, transaction1.getTransactionIndex());
assertEquals(2, transaction2.getTransactionIndex());
backstack = Arrays.asList(transaction2, transaction1);
router.setBackstack(backstack, null);
assertEquals(1, transaction2.transactionIndex);
assertEquals(2, transaction1.transactionIndex);
assertEquals(1, transaction2.getTransactionIndex());
assertEquals(2, transaction1.getTransactionIndex());
router.handleBack();
@@ -425,14 +426,14 @@ public class RouterTests {
List<RouterTransaction> backstack = Arrays.asList(transaction1, transaction2);
childRouter.setBackstack(backstack, null);
assertEquals(2, transaction1.transactionIndex);
assertEquals(3, transaction2.transactionIndex);
assertEquals(2, transaction1.getTransactionIndex());
assertEquals(3, transaction2.getTransactionIndex());
backstack = Arrays.asList(transaction2, transaction1);
childRouter.setBackstack(backstack, null);
assertEquals(2, transaction2.transactionIndex);
assertEquals(3, transaction1.transactionIndex);
assertEquals(2, transaction2.getTransactionIndex());
assertEquals(3, transaction1.getTransactionIndex());
childRouter.handleBack();
@@ -1,8 +1,8 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;

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