Compare commits

..

37 Commits

Author SHA1 Message Date
EricKuck f5c8f8dbe6 Update readme, dependencies 2023-03-22 12:19:23 -04:00
EricKuck f1ae2edfdf Change handler -> Kotlin
Also update dependencies
2023-03-22 12:18:00 -04:00
EricKuck 6604dbdfe9 Don't destroy LifecycleHandler for configuration changes 2023-03-22 12:18:00 -04:00
EricKuck 83bcac2bd8 Handle AndroidX fragments destroying after their activities 2023-03-22 12:18:00 -04:00
EricKuck 505dbdf519 WIP transition on OnBackPressedDispatcher 2023-03-22 12:18:00 -04:00
EricKuck c1328d4f46 Dynamically switch between androidx and platform backing 2023-03-22 12:18:00 -04:00
EricKuck 5f866bcb3e Bump compile sdk 2023-03-22 12:18:00 -04:00
EricKuck ade7b0a92c Update LifecycleHandler backing to androidx 2023-03-22 12:18:00 -04:00
EricKuck c3f7d128f5 Version bump 2023-01-30 11:56:22 -05:00
EricKuck 5e1f072672 Update maven publishing 2023-01-27 10:38:57 -05:00
EricKuck b0d15d9f9e Update maven publishing 2023-01-27 10:26:15 -05:00
Eric Kuck 8ac2e04c62 Make change start/end callbacks when parent controller is popped. Fixes #683 (#684) 2023-01-26 10:11:33 -06:00
Eric Kuck cdbdee5c42 Don't pop the final controller in pager adapters. Fixes #681 (#682) 2023-01-26 09:57:39 -06:00
EricKuck 8488242a26 Version bump 2022-11-29 15:04:22 -05:00
Eric Kuck 1fe0187439 Update ancestor change listeners to prevent memory leak (#680) 2022-11-29 13:51:25 -06:00
EricKuck f78726b916 Version bump 2022-11-07 10:38:21 -05:00
EricKuck 1f918f10c5 Fix ControllerLifecycleOwner crash when onContextAvailable was never called 2022-11-03 15:29:15 -04:00
EricKuck bd584727be Fix edge case ConcurrentModificationException 2022-09-19 16:10:39 -04:00
EricKuck 91db7fe65f Capture view reference in inflate call 2022-07-28 11:29:02 -05:00
EricKuck 2abe2b33f9 Version bump 2022-07-28 09:41:02 -05:00
Mario Noll ac4e09cf67 Fix NPE when removing view reference (#678)
Great catch, thanks!
2022-07-28 08:59:29 -05:00
EricKuck 055532bb21 Fix github actions badge 2022-07-25 17:17:08 -05:00
EricKuck 15037c2217 Version bump 2022-07-25 16:48:26 -05:00
EricKuck 728f1fb4e9 Ensure onContextUnavailable called on child routers for edge case 2022-07-22 11:55:15 -05:00
EricKuck 55c8d64d8a Fix CI badge 2022-07-05 14:46:14 -05:00
EricKuck 88e0eb882b Fix crash when a parent is transitioned out before a child can create its view 2022-07-05 10:43:52 -05:00
EricKuck 63a92db540 Pass along View's context on destroy if available 2022-07-05 10:43:06 -05:00
py - Pierre Yves Ricau ba98e3b165 Add leak detection for destroyed controller views (#676) 2022-06-23 12:00:39 -05:00
Eric Kuck 966bc1645d Convert dependencies to version catalogs (#675)
Also bumps gradle, agp, and kotlin versions
2022-06-16 16:40:05 -05:00
EricKuck c8ac58ad6a Version bump 2022-06-14 13:44:57 -05:00
Eric Kuck 5f04d9de89 Fix edge case NPE when the user hits the back button very rapidly during state restoration (#674) 2022-06-14 10:12:44 -05:00
Eric Kuck d32fc813d0 Set awaitingParentAttach to false on detach, even if not currently attached (#673) 2022-06-13 18:09:55 -05:00
Eric Kuck c2bc72c5ce Fix issue where child controllers may not get their onPause event soon enough (#672) 2022-06-13 18:09:42 -05:00
EricKuck 924e4bebfa Version bump 2022-02-18 14:28:27 -06:00
EricKuck 4ea4aa5c56 Fix issue with detach callbacks happening while not yet attached 2022-02-18 14:15:05 -06:00
Eric Kuck 3b275d31c2 Add PopRootControllerMode to address Android 12 back button behavior (#663) 2022-01-27 16:41:20 -06:00
EricKuck 0e21c8c9c1 Version bump 2021-11-30 11:39:30 -06:00
69 changed files with 2289 additions and 2073 deletions
+25
View File
@@ -0,0 +1,25 @@
root = true
[*.{java,kt,kts,xml,gradle}]
charset=utf-8
end_of_line=lf
max_line_length=120
insert_final_newline=true
trim_trailing_whitespace=true
indent_style=space
[*.{java,kt,kts,gradle}]
spaces_around_operators=true
indent_brace_style=K&R
[*.{java,gradle}]
indent_size=4
[*.{kt,kts}]
indent_size=2
continuation_indent_size=2
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true
[*.xml]
indent_size=2
+4 -4
View File
@@ -31,8 +31,8 @@ jobs:
java-version: '11'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Test with Gradle
run: ./gradlew clean uploadArchives
- name: Release with Gradle
run: ./gradlew clean publishAllPublicationsToMavenCentral
env:
ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
ORG_GRADLE_PROJECT_SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
+2 -1
View File
@@ -8,7 +8,8 @@
*.iml
*.ipr
*.iws
*.idea/dictionaries
/.idea/*
!/.idea/scopes/
classes
gen-external-apklibs
-137
View File
@@ -1,137 +0,0 @@
<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="CALL_PARAMETERS_WRAP" value="5" />
<option name="CALL_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="CALL_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_WRAP" value="5" />
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<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
@@ -1,6 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default (1)" />
</state>
</component>
+42 -38
View File
@@ -1,4 +1,4 @@
[![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)
[![GitHub Actions Workflow](https://github.com/bluelinelabs/conductor/actions/workflows/main.yml/badge.svg)](https://github.com/bluelinelabs/conductor/actions/workflows/main.yml) [![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
@@ -19,28 +19,36 @@ Conductor is architecture-agnostic and does not try to force any design decision
## Installation
Conductor 4.0 is coming soon. It is already being used in production with many, many millions of users. It is, however, not guaranteed to be API stable. As such, it is being released as a preview rather than a standard release. Preview in this context is _not_ a commentary on stability. It is considered to be up to the same quality standards as the current 3.x stable release.
Changes in Conductor 4 are available in the [GitHub releases](https://github.com/bluelinelabs/Conductor/releases/). In preparation for the release of the next version, there are currently 3 installation options:
### Latest Stable 3.x
```gradle
implementation 'com.bluelinelabs:conductor:3.1.1'
def conductorVersion = '3.2.0'
implementation "com.bluelinelabs:conductor:$conductorVersion"
// AndroidX Transition change handlers:
implementation 'com.bluelinelabs:conductor-androidx-transition:3.1.1'
implementation "com.bluelinelabs:conductor-androidx-transition:$conductorVersion"
// ViewPager PagerAdapter:
implementation 'com.bluelinelabs:conductor-viewpager:3.1.1'
implementation "com.bluelinelabs:conductor-viewpager:$conductorVersion"
// ViewPager2 Adapter:
implementation 'com.bluelinelabs:conductor-viewpager2:3.1.1'
implementation "com.bluelinelabs:conductor-viewpager2:$conductorVersion"
// RxJava2 Autodispose support:
implementation 'com.bluelinelabs:conductor-autodispose:3.1.1'
implementation "com.bluelinelabs:conductor-autodispose:$conductorVersion"
// Lifecycle-aware Controllers (architecture components):
implementation 'com.bluelinelabs:conductor-archlifecycle:3.1.1'
implementation "com.bluelinelabs:conductor-archlifecycle:$conductorVersion"
```
**SNAPSHOT**
### 4.0 Preview
Use `4.0.0-preview-1` as your version number in any of the dependencies above.
Just use `3.1.2-SNAPSHOT` as your version number in any of the dependencies above and add the url to the snapshot repository:
### SNAPSHOT
Use `4.0.0-SNAPSHOT` as your version number in any of the dependencies above and add the url to the snapshot repository:
```gradle
allprojects {
@@ -63,47 +71,43 @@ __RouterTransaction__ | Transactions are used to define data about adding Contro
### Minimal Activity implementation
```java
public class MainActivity extends Activity {
```kotlin
class MainActivity : AppCompatActivity() {
private Router router;
private lateinit var router: Router
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main);
setContentView(R.layout.activity_main)
ViewGroup container = (ViewGroup) findViewById(R.id.controller_container);
val container = findViewById<ViewGroup>(R.id.controller_container)
router = Conductor.attachRouter(this, container, savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new HomeController()));
}
router = Conductor.attachRouter(this, binding.controllerContainer, savedInstanceState)
.setPopRootControllerMode(PopRootControllerMode.NEVER)
.setOnBackPressedDispatcherEnabled(true)
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(HomeController()))
}
}
@Override
public void onBackPressed() {
if (!router.handleBack()) {
super.onBackPressed();
}
}
}
```
### Minimal Controller implementation
```java
public class HomeController extends Controller {
@Override
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;
}
```kotlin
class HomeController : Controller() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup,
savedViewState: Bundle?
): View {
val view = inflater.inflate(R.layout.controller_home, container, false)
view.findViewById<TextView>(R.id.tv_title).text = "Hello World"
return view
}
}
```
+17 -11
View File
@@ -1,23 +1,29 @@
buildscript {
apply from: rootProject.file('dependencies.gradle')
repositories {
mavenCentral()
google()
}
dependencies {
classpath "com.android.tools.build:gradle:$agpVersion"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "com.vanniktech:gradle-maven-publish-plugin:$mvnPublishVersion"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokkaVersion"
classpath libs.agp
classpath libs.kotlin.plugin
classpath libs.dokka
}
}
allprojects {
repositories {
mavenCentral()
google()
if (project.hasProperty('maven_publish_url')) {
pluginManager.withPlugin("com.vanniktech.maven.publish") {
publishing {
repositories {
maven {
url = maven_publish_url
credentials {
username = maven_publish_username
password = maven_publish_password
}
}
}
}
}
}
}
apply from: rootProject.file('dependencies.gradle')
+7 -9
View File
@@ -1,17 +1,17 @@
apply plugin: 'kotlin'
configurations {
lintChecks
libs.lint.checks
}
dependencies {
compileOnly rootProject.ext.lintapi
compileOnly rootProject.ext.lintchecks
compileOnly rootProject.ext.kotlinStd
compileOnly libs.lint.api
compileOnly libs.lint.checks
compileOnly libs.kotlin.stdlib
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.lint
testImplementation rootProject.ext.lintTests
testImplementation libs.junit
testImplementation libs.lint
testImplementation libs.lint.tests
}
jar {
@@ -19,5 +19,3 @@ jar {
attributes('Lint-Registry-v2': 'com.bluelinelabs.conductor.lint.IssueRegistry')
}
}
apply from: rootProject.file('dependencies.gradle')
@@ -1,24 +1,24 @@
apply plugin: 'com.android.library'
plugins {
id("com.android.library")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
implementation rootProject.ext.androidxAppCompat
implementation rootProject.ext.androidxCollection
api rootProject.ext.androidxTransition
implementation libs.androidx.appcompat
implementation libs.androidx.collection
api libs.androidx.transition
implementation project(':conductor')
}
ext.artifactId = 'conductor-androidx-transition'
apply from: rootProject.file('dependencies.gradle')
apply plugin: "com.vanniktech.maven.publish"
@@ -113,7 +113,7 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
}
@Override
public boolean removesFromViewOnPush() {
public boolean getRemovesFromViewOnPush() {
return true;
}
@@ -142,7 +142,7 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
* @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) {
if (from != null && (getRemovesFromViewOnPush() || !isPush) && from.getParent() == container) {
container.removeView(from);
}
if (to != null && to.getParent() == null) {
@@ -1,23 +1,23 @@
apply plugin: 'com.android.library'
plugins {
id("com.android.library")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
api rootProject.ext.archComponentsLifecycle
implementation project(':conductor')
api libs.androidx.lifecycle.runtime
}
ext.artifactId = 'conductor-arch-components-lifecycle'
apply from: rootProject.file('dependencies.gradle')
apply plugin: "com.vanniktech.maven.publish"
@@ -51,7 +51,10 @@ public class ControllerLifecycleOwner implements LifecycleOwner {
@Override
public void preDestroy(@NonNull Controller controller) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_DESTROY); // --> State.DESTROYED;
// Only act on Controllers that have had at least the onContextAvailable call made on them.
if (lifecycleRegistry.getCurrentState() != Lifecycle.State.INITIALIZED) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_DESTROY); // --> State.DESTROYED;
}
}
});
+10 -10
View File
@@ -1,23 +1,23 @@
apply from: rootProject.file('dependencies.gradle')
apply plugin: 'com.android.library'
apply plugin: "com.vanniktech.maven.publish"
plugins {
id("com.android.library")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
api rootProject.ext.rxJava2
api rootProject.ext.autodispose
api rootProject.ext.autodisposeLifecycle
api libs.rxjava2
api libs.autodispose
api libs.autodispose.lifecycle
implementation project(':conductor')
}
+11 -11
View File
@@ -1,26 +1,26 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
plugins {
id("com.android.library")
id("kotlin-android")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.robolectric
testImplementation libs.junit
testImplementation libs.robolectric
implementation rootProject.ext.androidxAppCompat
implementation libs.androidx.appcompat
implementation project(':conductor')
}
apply from: rootProject.file('dependencies.gradle')
ext.artifactId = 'conductor-viewpager'
apply plugin: "com.vanniktech.maven.publish"
@@ -77,7 +77,8 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
savedPages.remove(position);
}
Router router = host.getChildRouter(container, name);
Router router = host.getChildRouter(container, name)
.setPopRootControllerMode(Router.PopRootControllerMode.NEVER);
if (!router.hasRootController()) {
Bundle routerSavedState = savedPages.get(position);
@@ -1,12 +1,12 @@
package com.bluelinelabs.conductor.viewpager.util
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.ViewCompat
import androidx.fragment.app.FragmentActivity
import androidx.viewpager.widget.ViewPager
import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Controller
@@ -15,7 +15,7 @@ import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.asTransaction
import com.bluelinelabs.conductor.viewpager.RouterPagerAdapter
class TestActivity : Activity() {
class TestActivity : FragmentActivity() {
private lateinit var router: Router
+13 -13
View File
@@ -1,13 +1,16 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-parcelize")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
@@ -20,15 +23,12 @@ android {
}
dependencies {
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.robolectric
testImplementation libs.junit
testImplementation libs.robolectric
implementation rootProject.ext.androidxAppCompat
implementation rootProject.ext.androidxViewPager2
implementation libs.androidx.appcompat
implementation libs.androidx.viewpager2
implementation project(':conductor')
}
apply from: rootProject.file('dependencies.gradle')
ext.artifactId = 'conductor-viewpager2'
apply plugin: "com.vanniktech.maven.publish"
@@ -155,6 +155,7 @@ abstract class RouterStateAdapter(private val host: Controller) :
private fun attachRouter(holder: RouterViewHolder, position: Int) {
val itemId = getItemId(position)
val router = host.getChildRouter(holder.container, "$itemId", true, false)!!
.setPopRootControllerMode(Router.PopRootControllerMode.NEVER)
// This should have already been handled by onViewRecycled, but it seems like this wasn't
// always reliably called
@@ -1,9 +1,9 @@
package com.bluelinelabs.conductor.viewpager2
import android.app.Activity
import android.os.Looper.getMainLooper
import android.widget.FrameLayout
import androidx.core.view.ViewCompat
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.widget.ViewPager2
import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Router
@@ -26,7 +26,7 @@ class StateSaveTests {
private val destroyedItems = mutableListOf<Int>()
init {
val activityController = Robolectric.buildActivity(Activity::class.java).setup()
val activityController = Robolectric.buildActivity(FragmentActivity::class.java).setup()
val layout = FrameLayout(activityController.get())
activityController.get().setContentView(layout)
val router = Conductor.attachRouter(activityController.get(), FrameLayout(activityController.get()), null)
+18 -15
View File
@@ -1,12 +1,16 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-parcelize")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
consumerProguardFiles 'proguard-rules.txt'
@@ -14,20 +18,19 @@ android {
}
dependencies {
implementation savedState
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.robolectric
testImplementation kotestAssertions
api libs.androidx.activity
api libs.androidx.appcompat
api libs.androidx.savedstate.ktx
testImplementation libs.junit
testImplementation libs.robolectric
testImplementation libs.kotest
implementation archComponentsLifecycle
api libs.androidx.lifecycle.runtime
api rootProject.ext.androidxAnnotations
api kotlinStd
api libs.androidx.annotation
api libs.kotlin.stdlib
lintPublish project(':conductor-lint')
}
apply from: rootProject.file('dependencies.gradle')
ext.artifactId = 'conductor'
apply plugin: "com.vanniktech.maven.publish"
@@ -68,8 +68,8 @@ public class ActivityHostedRouter extends Router {
@Override
public final void invalidateOptionsMenu() {
if (lifecycleHandler != null && lifecycleHandler.getFragmentManager() != null) {
lifecycleHandler.getFragmentManager().invalidateOptionsMenu();
if (lifecycleHandler != null && getActivity() != null) {
getActivity().invalidateOptionsMenu();
}
}
@@ -85,7 +85,7 @@ public class ActivityHostedRouter extends Router {
@Override
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) {
lifecycleHandler.startActivityForResult(instanceId, intent, requestCode);
lifecycleHandler.startActivityForResult(instanceId, intent, requestCode, null);
}
@Override
@@ -1,7 +1,8 @@
package com.bluelinelabs.conductor
import android.os.Bundle
import java.util.*
import java.util.ArrayDeque
import java.util.Deque
internal class Backstack : Iterable<RouterTransaction> {
@@ -11,6 +12,8 @@ internal class Backstack : Iterable<RouterTransaction> {
val size: Int get() = backstack.size
var onBackstackUpdatedListener: OnBackstackUpdatedListener? = null
fun root(): RouterTransaction? = backstack.lastOrNull()
override fun iterator(): MutableIterator<RouterTransaction> {
@@ -34,6 +37,7 @@ internal class Backstack : Iterable<RouterTransaction> {
fun pop(): RouterTransaction {
return backstack.pop().also {
onBackstackUpdatedListener?.onBackstackUpdated()
it.controller.destroy()
}
}
@@ -42,6 +46,7 @@ internal class Backstack : Iterable<RouterTransaction> {
fun push(transaction: RouterTransaction) {
backstack.push(transaction)
onBackstackUpdatedListener?.onBackstackUpdated()
}
fun popAll(): List<RouterTransaction> {
@@ -57,6 +62,8 @@ internal class Backstack : Iterable<RouterTransaction> {
backstack.forEach { transaction ->
this.backstack.push(transaction)
}
onBackstackUpdatedListener?.onBackstackUpdated()
}
operator fun contains(controller: Controller): Boolean {
@@ -81,6 +88,12 @@ internal class Backstack : Iterable<RouterTransaction> {
backstack.push(RouterTransaction(transactionBundle!!))
}
}
onBackstackUpdatedListener?.onBackstackUpdated()
}
fun interface OnBackstackUpdatedListener {
fun onBackstackUpdated()
}
companion object {
@@ -7,25 +7,33 @@ 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 {
/**
* 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.
* @param allowExperimentalAndroidXBacking Use AndroidX backing if true and if the activity parameter is a
* FragmentActivity.
* @return A fully configured [Router] instance for use with this Activity/ViewGroup pair.
*/
@UiThread
@JvmStatic
fun attachRouter(activity: Activity, container: ViewGroup, savedInstanceState: Bundle?): Router {
@JvmOverloads
fun attachRouter(
activity: Activity,
container: ViewGroup,
savedInstanceState: Bundle?,
allowExperimentalAndroidXBacking: Boolean = true,
): Router {
ensureMainThread()
return LifecycleHandler.install(activity)
return LifecycleHandler.install(activity, allowAndroidXBacking = allowExperimentalAndroidXBacking)
.getRouter(container, savedInstanceState)
.also { it.rebindIfNeeded() }
}
@@ -1,6 +1,5 @@
package com.bluelinelabs.conductor;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
@@ -17,6 +16,9 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.activity.ComponentActivity;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.OnBackPressedDispatcher;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -32,7 +34,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;
@@ -80,6 +81,7 @@ public abstract class Controller {
private boolean awaitingParentAttach;
private boolean hasSavedViewState;
boolean isDetachFrozen;
boolean onBackPressedDispatcherEnabled;
private ControllerChangeHandler overriddenPushHandler;
private ControllerChangeHandler overriddenPopHandler;
private RetainViewMode retainViewMode = RetainViewMode.RELEASE_DETACH;
@@ -92,6 +94,23 @@ public abstract class Controller {
private boolean isPerformingExitTransition;
private boolean isContextAvailable;
final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
// Root-level routers should have PopRootControllerMode.NEVER, and so should never return false here.
// This is meant to handle higher-level pops only, where the predictive back gesture doesn't come into play.
if (!router.getRootRouter().handleBackDispatch()) {
// Disable to ensure we don't have an infinite call loop.
setEnabled(false);
getOnBackPressedDispatcher().onBackPressed();
if (!isBeingDestroyed) {
setEnabled(true);
}
}
}
};
@NonNull
static Controller newInstance(@NonNull Bundle bundle) {
final String className = bundle.getString(KEY_CLASS_NAME);
@@ -313,6 +332,17 @@ public abstract class Controller {
return router != null ? router.getActivity() : null;
}
/**
* Returns the OnBackPressedDispatcher for this Controller's {@link Router} or {@code null} if:
* - This Router has not yet been attached to an Activity
* - The attached Activity does not extend ComponentActivity
* - The Activity has been destroyed
*/
@Nullable
public final OnBackPressedDispatcher getOnBackPressedDispatcher() {
return router != null ? router.getOnBackPressedDispatcher() : null;
}
/**
* Returns the Resources from the host Activity or {@code null} if this Controller has not
* yet been attached to an Activity or if the Activity has been destroyed.
@@ -541,36 +571,21 @@ public abstract class Controller {
* 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));
}
/**
@@ -588,12 +603,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));
}
/**
@@ -612,16 +622,10 @@ public abstract class Controller {
* including {@link #shouldShowRequestPermissionRationale(String)} and
* {@link #onRequestPermissionsResult(int, String[], int[])} will be forwarded back to this Controller by the system.
*/
@TargetApi(Build.VERSION_CODES.M)
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));
}
/**
@@ -647,8 +651,11 @@ public abstract class Controller {
/**
* Should be overridden if this Controller needs to handle the back button being pressed.
*
* Note: This method has been deprecated and should be replaced with registering an OnBackPressedCallback.
*
* @return True if this Controller has consumed the back button press, otherwise false
*/
@Deprecated
public boolean handleBack() {
List<RouterTransaction> childTransactions = new ArrayList<>();
@@ -656,12 +663,7 @@ public abstract class Controller {
childTransactions.addAll(childRouter.getBackstack());
}
Collections.sort(childTransactions, new Comparator<RouterTransaction>() {
@Override
public int compare(RouterTransaction o1, RouterTransaction o2) {
return o2.getTransactionIndex() - o1.getTransactionIndex();
}
});
Collections.sort(childTransactions, (t1, t2) -> t2.getTransactionIndex() - t1.getTransactionIndex());
for (RouterTransaction transaction : childTransactions) {
Controller childController = transaction.controller();
@@ -709,7 +711,7 @@ public abstract class Controller {
public void setRetainViewMode(@NonNull RetainViewMode retainViewMode) {
this.retainViewMode = retainViewMode != null ? retainViewMode : RetainViewMode.RELEASE_DETACH;
if (this.retainViewMode == RetainViewMode.RELEASE_DETACH && !attached) {
removeViewReference();
removeViewReference(null);
}
}
@@ -858,6 +860,14 @@ public abstract class Controller {
lifecycleListener.preContextAvailable(this);
}
onBackPressedDispatcherEnabled = router.onBackPressedDispatcherEnabled;
if (onBackPressedDispatcherEnabled) {
if (!(context instanceof ComponentActivity)) {
throw new IllegalStateException("Host activities must extend ComponentActivity when enabling OnBackPressedDispatcher support.");
}
getOnBackPressedDispatcher().addCallback(onBackPressedCallback);
}
isContextAvailable = true;
onContextAvailable(context);
@@ -873,11 +883,11 @@ public abstract class Controller {
}
final void onContextUnavailable(@NonNull Context context) {
if (isContextAvailable) {
for (Router childRouter : childRouters) {
childRouter.onContextUnavailable(context);
}
for (Router childRouter : childRouters) {
childRouter.onContextUnavailable(context);
}
if (isContextAvailable) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preContextUnavailable(this, context);
@@ -886,6 +896,10 @@ public abstract class Controller {
isContextAvailable = false;
onContextUnavailable();
if (onBackPressedDispatcherEnabled) {
onBackPressedCallback.remove();
}
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postContextUnavailable(this);
@@ -995,7 +1009,7 @@ public abstract class Controller {
}
}
void detach(@NonNull View view, boolean forceViewRefRemoval, boolean blockViewRefRemoval) {
void detach(View view, boolean forceViewRefRemoval, boolean blockViewRefRemoval) {
if (!attachedToUnownedParent) {
for (ControllerHostedRouter router : childRouters) {
router.prepareForHostDetach();
@@ -1005,34 +1019,41 @@ public abstract class Controller {
final boolean removeViewRef = !blockViewRefRemoval && (forceViewRefRemoval || retainViewMode == RetainViewMode.RELEASE_DETACH || isBeingDestroyed);
if (attached) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preDetach(this, view);
}
attached = false;
if (!awaitingParentAttach) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preDetach(this, view);
}
attached = false;
onDetach(view);
}
if (hasOptionsMenu && !optionsMenuHidden) {
router.invalidateOptionsMenu();
}
if (hasOptionsMenu && !optionsMenuHidden) {
router.invalidateOptionsMenu();
}
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postDetach(this, view);
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postDetach(this, view);
}
} else {
attached = false;
}
}
awaitingParentAttach = false;
if (removeViewRef) {
removeViewReference();
removeViewReference(view != null ? view.getContext() : null);
}
}
private void removeViewReference() {
private void removeViewReference(@Nullable Context context) {
if (view != null) {
if (context == null) {
context = view.getContext();
}
if (!isBeingDestroyed && !hasSavedViewState) {
saveViewState(view);
}
@@ -1068,14 +1089,15 @@ public abstract class Controller {
}
if (isBeingDestroyed) {
performDestroy();
performDestroy(context);
}
}
final View inflate(@NonNull ViewGroup parent) {
if (view != null && view.getParent() != null && view.getParent() != parent) {
View viewRef = view;
detach(view, true, false);
removeViewReference();
removeViewReference(viewRef.getContext());
}
if (view == null) {
@@ -1145,9 +1167,13 @@ public abstract class Controller {
}
}
private void performDestroy() {
private void performDestroy(@Nullable Context context) {
if (context == null) {
context = getActivity();
}
if (isContextAvailable) {
onContextUnavailable(getActivity());
onContextUnavailable(context);
}
if (!destroyed) {
@@ -1185,7 +1211,7 @@ public abstract class Controller {
}
if (!attached) {
removeViewReference();
removeViewReference(null);
} else if (removeViews) {
detach(view, true, false);
}
@@ -1,322 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
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;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* ControllerChangeHandlers are responsible for swapping the View for one Controller to the View
* of another. They can be useful for performing animations and transitions between Controllers. Several
* default ControllerChangeHandlers are included.
*/
public abstract class ControllerChangeHandler {
private static final String KEY_CLASS_NAME = "ControllerChangeHandler.className";
private static final String KEY_SAVED_STATE = "ControllerChangeHandler.savedState";
static final Map<String, ChangeHandlerData> inProgressChangeHandlers = new HashMap<>();
boolean forceRemoveViewOnPush;
private boolean hasBeenUsed;
/**
* Responsible for swapping Views from one Controller to another.
*
* @param container The container these Views are hosted in.
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop.
* @param changeListener This listener must be called when any transitions or animations are completed.
*/
public abstract void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener);
public ControllerChangeHandler() {
ensureDefaultConstructor();
}
/**
* Saves any data about this handler to a Bundle in case the application is killed.
*
* @param bundle The Bundle into which data should be stored.
*/
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) {
}
/**
* Will be called on change handlers that push a controller if the controller being pushed is
* popped before it has completed.
*
* @param newHandler The change handler that has caused this push to be aborted
* @param newTop The Controller that will now be at the top of the backstack or {@code null}
* if there will be no new Controller at the top
*/
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
}
/**
* Will be called on change handlers that push a controller if the controller being pushed is
* needs to be attached immediately, without any animations or transitions.
*/
public void completeImmediately() {
}
/**
* Returns a copy of this ControllerChangeHandler. This method is internally used by the library, so
* ensure it will return an exact copy of your handler if overriding. If not overriding, the handler
* will be saved and restored from the Bundle format.
*/
@NonNull
public ControllerChangeHandler copy() {
return fromBundle(toBundle());
}
/**
* Returns whether or not this is a reusable ControllerChangeHandler. Defaults to false and should
* ONLY be overridden if there are absolutely no side effects to using this handler more than once.
* In the case that a handler is not reusable, it will be copied using the {@link #copy()} method
* prior to use.
*/
public boolean isReusable() {
return false;
}
@NonNull
final Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putString(KEY_CLASS_NAME, getClass().getName());
Bundle savedState = new Bundle();
saveToBundle(savedState);
bundle.putBundle(KEY_SAVED_STATE, savedState);
return bundle;
}
private void ensureDefaultConstructor() {
try {
getClass().getConstructor();
} catch (Exception e) {
throw new RuntimeException(getClass() + " does not have a default constructor.");
}
}
@Nullable
public static ControllerChangeHandler fromBundle(@Nullable Bundle bundle) {
if (bundle != null) {
String className = bundle.getString(KEY_CLASS_NAME);
ControllerChangeHandler changeHandler = ClassUtils.newInstance(className);
//noinspection ConstantConditions
changeHandler.restoreFromBundle(bundle.getBundle(KEY_SAVED_STATE));
return changeHandler;
} else {
return null;
}
}
static boolean completeHandlerImmediately(@NonNull String controllerInstanceId) {
ChangeHandlerData changeHandlerData = inProgressChangeHandlers.get(controllerInstanceId);
if (changeHandlerData != null) {
changeHandlerData.changeHandler.completeImmediately();
inProgressChangeHandlers.remove(controllerInstanceId);
return true;
}
return false;
}
static void abortOrComplete(@NonNull Controller toAbort, @Nullable Controller newController, @NonNull ControllerChangeHandler newChangeHandler) {
ChangeHandlerData changeHandlerData = inProgressChangeHandlers.get(toAbort.getInstanceId());
if (changeHandlerData != null) {
if (changeHandlerData.isPush) {
changeHandlerData.changeHandler.onAbortPush(newChangeHandler, newController);
} else {
changeHandlerData.changeHandler.completeImmediately();
}
inProgressChangeHandlers.remove(toAbort.getInstanceId());
}
}
static void executeChange(@NonNull final ChangeTransaction transaction) {
executeChange(transaction.to, transaction.from, transaction.isPush, transaction.container, transaction.changeHandler, transaction.listeners);
}
private static void executeChange(@Nullable final Controller to, @Nullable final Controller from, final boolean isPush, @Nullable final ViewGroup container, @Nullable final ControllerChangeHandler inHandler, @NonNull final List<ControllerChangeListener> listeners) {
if (container != null) {
final ControllerChangeHandler handler;
if (inHandler == null) {
handler = new SimpleSwapChangeHandler();
} else if (inHandler.hasBeenUsed && !inHandler.isReusable()) {
handler = inHandler.copy();
} else {
handler = inHandler;
}
handler.hasBeenUsed = true;
if (from != null) {
if (isPush) {
completeHandlerImmediately(from.getInstanceId());
} else {
abortOrComplete(from, to, handler);
}
}
if (to != null) {
inProgressChangeHandlers.put(to.getInstanceId(), new ChangeHandlerData(handler, isPush));
}
for (ControllerChangeListener listener : listeners) {
listener.onChangeStarted(to, from, isPush, container, handler);
}
final ControllerChangeType toChangeType = isPush ? ControllerChangeType.PUSH_ENTER : ControllerChangeType.POP_ENTER;
final ControllerChangeType fromChangeType = isPush ? ControllerChangeType.PUSH_EXIT : ControllerChangeType.POP_EXIT;
final View toView;
if (to != null) {
toView = to.inflate(container);
to.changeStarted(handler, toChangeType);
} else {
toView = null;
}
final View fromView;
if (from != null) {
fromView = from.getView();
from.changeStarted(handler, fromChangeType);
} else {
fromView = null;
}
handler.performChange(container, fromView, toView, isPush, new ControllerChangeCompletedListener() {
@Override
public void onChangeCompleted() {
if (from != null) {
from.changeEnded(handler, fromChangeType);
}
if (to != null) {
inProgressChangeHandlers.remove(to.getInstanceId());
to.changeEnded(handler, toChangeType);
}
for (ControllerChangeListener listener : listeners) {
listener.onChangeCompleted(to, from, isPush, container, handler);
}
if (handler.forceRemoveViewOnPush && fromView != null) {
ViewParent fromParent = fromView.getParent();
if (fromParent != null && fromParent instanceof ViewGroup) {
((ViewGroup) fromParent).removeView(fromView);
}
}
if (handler.removesFromViewOnPush() && from != null) {
from.setNeedsAttach(false);
}
}
});
}
}
protected void onEnd() {
}
public boolean removesFromViewOnPush() {
return true;
}
public void setForceRemoveViewOnPush(boolean force) {
forceRemoveViewOnPush = force;
}
/**
* A listener interface useful for allowing external classes to be notified of change events.
*/
public interface ControllerChangeListener {
/**
* Called when a {@link ControllerChangeHandler} has started changing {@link Controller}s
*
* @param to The new Controller or {@code null} if no Controller is being transitioned to
* @param from The old Controller or {@code null} if there was no Controller before this transition
* @param isPush True if this is a push operation, or false if it's a pop.
* @param container The containing ViewGroup
* @param handler The change handler being used.
*/
void onChangeStarted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler);
/**
* Called when a {@link ControllerChangeHandler} has completed changing {@link Controller}s
*
* @param to The new Controller or {@code null} if no Controller is being transitioned to
* @param from The old Controller or {@code null} if there was no Controller before this transition
* @param isPush True if this was a push operation, or false if it's a pop
* @param container The containing ViewGroup
* @param handler The change handler that was used.
*/
void onChangeCompleted(@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;
}
}
/**
* A simplified listener for being notified when the change is complete. This MUST be called by any custom
* ControllerChangeHandlers in order to ensure that {@link Controller}s will be notified of this change.
*/
public interface ControllerChangeCompletedListener {
/**
* Called when the change is complete.
*/
void onChangeCompleted();
}
private static class ChangeHandlerData {
public final ControllerChangeHandler changeHandler;
public final boolean isPush;
public ChangeHandlerData(ControllerChangeHandler changeHandler, boolean isPush) {
this.changeHandler = changeHandler;
this.isPush = isPush;
}
}
}
@@ -0,0 +1,306 @@
package com.bluelinelabs.conductor
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.annotation.RestrictTo
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler
import com.bluelinelabs.conductor.internal.ClassUtils
/**
* 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.
*/
abstract class ControllerChangeHandler {
private var forceRemoveViewOnPush = false
/**
* Returns whether or not this is a reusable ControllerChangeHandler. Defaults to false and should
* ONLY be overridden if there are absolutely no side effects to using this handler more than once.
* In the case that a handler is not reusable, it will be copied using the [.copy] method
* prior to use.
*/
open val isReusable: Boolean = false
open val removesFromViewOnPush: Boolean = true
private var hasBeenUsed = false
init {
try {
javaClass.getConstructor()
} catch (e: Throwable) {
throw RuntimeException("$javaClass does not have a default constructor.")
}
}
/**
* Responsible for swapping Views from one Controller to another.
*
* @param container The container these Views are hosted in.
* @param from The previous View in the container or `null` if there was no Controller before this transition
* @param to The next View that should be put in the container or `null` if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop.
* @param changeListener This listener must be called when any transitions or animations are completed.
*/
abstract fun performChange(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
changeListener: ControllerChangeCompletedListener,
)
/**
* Saves any data about this handler to a Bundle in case the application is killed.
*
* @param bundle The Bundle into which data should be stored.
*/
open fun saveToBundle(bundle: Bundle) {}
/**
* Restores data that was saved in the [.saveToBundle] method.
*
* @param bundle The bundle that has data to be restored
*/
open fun restoreFromBundle(bundle: Bundle) {}
/**
* Will be called on change handlers that push a controller if the controller being pushed is
* popped before it has completed.
*
* @param newHandler The change handler that has caused this push to be aborted
* @param newTop The Controller that will now be at the top of the backstack or `null`
* if there will be no new Controller at the top
*/
open fun onAbortPush(newHandler: ControllerChangeHandler, newTop: Controller?) {}
/**
* 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.
*/
open fun completeImmediately() {}
/**
* Returns a copy of this ControllerChangeHandler. This method is internally used by the library, so
* ensure it will return an exact copy of your handler if overriding. If not overriding, the handler
* will be saved and restored from the Bundle format.
*/
open fun copy(): ControllerChangeHandler = fromBundle(toBundle())!!
open fun handleOnBackStarted(container: ViewGroup, to: View?, from: View, swipeEdge: Int) {}
open fun handleOnBackProgressed(container: ViewGroup, to: View?, from: View, progress: Float, swipeEdge: Int) {}
open fun handleOnBackCancelled(container: ViewGroup, to: View?, from: View) {}
protected open fun onEnd() {}
fun toBundle(): Bundle {
val bundle = Bundle()
bundle.putString(KEY_CLASS_NAME, javaClass.name)
val savedState = Bundle()
saveToBundle(savedState)
bundle.putBundle(KEY_SAVED_STATE, savedState)
return bundle
}
// Internal modifier plays weirdly with Java, which is what Router is still written in.
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun setForceRemoveViewOnPush(forceRemoveViewOnPush: Boolean) {
this.forceRemoveViewOnPush = forceRemoveViewOnPush
}
/**
* A listener interface useful for allowing external classes to be notified of change events.
*/
interface ControllerChangeListener {
/**
* Called when a [ControllerChangeHandler] has started changing [Controller]s
*
* @param to The new Controller or `null` if no Controller is being transitioned to
* @param from The old Controller or `null` if there was no Controller before this transition
* @param isPush True if this is a push operation, or false if it's a pop.
* @param container The containing ViewGroup
* @param handler The change handler being used.
*/
fun onChangeStarted(
to: Controller?,
from: Controller?,
isPush: Boolean,
container: ViewGroup,
handler: ControllerChangeHandler,
)
/**
* Called when a [ControllerChangeHandler] has completed changing [Controller]s
*
* @param to The new Controller or `null` if no Controller is being transitioned to
* @param from The old Controller or `null` if there was no Controller before this transition
* @param isPush True if this was a push operation, or false if it's a pop
* @param container The containing ViewGroup
* @param handler The change handler that was used.
*/
fun onChangeCompleted(
to: Controller?,
from: Controller?,
isPush: Boolean,
container: ViewGroup,
handler: ControllerChangeHandler,
)
}
class ChangeTransaction(
@JvmField val to: Controller?,
@JvmField val from: Controller?,
@JvmField val isPush: Boolean,
@JvmField val container: ViewGroup?,
@JvmField val changeHandler: ControllerChangeHandler?,
@JvmField val listeners: List<ControllerChangeListener>,
)
/**
* A simplified listener for being notified when the change is complete. This MUST be called by any custom
* ControllerChangeHandlers in order to ensure that [Controller]s will be notified of this change.
*/
interface ControllerChangeCompletedListener {
/**
* Called when the change is complete.
*/
fun onChangeCompleted()
}
class ChangeHandlerData(val changeHandler: ControllerChangeHandler, val isPush: Boolean)
companion object {
private const val KEY_CLASS_NAME = "ControllerChangeHandler.className"
private const val KEY_SAVED_STATE = "ControllerChangeHandler.savedState"
val inProgressChangeHandlers: MutableMap<String, ChangeHandlerData> = HashMap()
@JvmStatic
fun fromBundle(bundle: Bundle?): ControllerChangeHandler? {
val bundle = bundle ?: return null
val className = bundle.getString(KEY_CLASS_NAME) ?: return null
val savedState = bundle.getBundle(KEY_SAVED_STATE) ?: return null
return ClassUtils.newInstance<ControllerChangeHandler>(className)?.also {
it.restoreFromBundle(savedState)
}
}
@JvmStatic
fun completeHandlerImmediately(controllerInstanceId: String): Boolean {
inProgressChangeHandlers[controllerInstanceId]?.let { changeHandlerData ->
changeHandlerData.changeHandler.completeImmediately()
inProgressChangeHandlers.remove(controllerInstanceId)
return true
}
return false
}
fun abortOrComplete(toAbort: Controller, newController: Controller?, newChangeHandler: ControllerChangeHandler) {
inProgressChangeHandlers[toAbort.getInstanceId()]?.let { changeHandlerData ->
if (changeHandlerData.isPush) {
changeHandlerData.changeHandler.onAbortPush(newChangeHandler, newController)
} else {
changeHandlerData.changeHandler.completeImmediately()
}
inProgressChangeHandlers.remove(toAbort.getInstanceId())
}
}
@JvmStatic
fun executeChange(transaction: ChangeTransaction) {
executeChange(
to = transaction.to,
from = transaction.from,
isPush = transaction.isPush,
container = transaction.container,
inHandler = transaction.changeHandler,
listeners = transaction.listeners,
)
}
private fun executeChange(
to: Controller?,
from: Controller?,
isPush: Boolean,
container: ViewGroup?,
inHandler: ControllerChangeHandler?,
listeners: List<ControllerChangeListener>,
) {
container ?: return
val handler: ControllerChangeHandler = if (inHandler == null) {
SimpleSwapChangeHandler()
} else if (inHandler.hasBeenUsed && !inHandler.isReusable) {
inHandler.copy()
} else {
inHandler
}
handler.hasBeenUsed = true
if (from != null) {
if (isPush) {
completeHandlerImmediately(from.getInstanceId())
} else {
abortOrComplete(from, to, handler)
}
}
if (to != null) {
inProgressChangeHandlers[to.getInstanceId()] = ChangeHandlerData(handler, isPush)
}
listeners.forEach { it.onChangeStarted(to, from, isPush, container, handler) }
val toChangeType = if (isPush) ControllerChangeType.PUSH_ENTER else ControllerChangeType.POP_ENTER
val fromChangeType = if (isPush) ControllerChangeType.PUSH_EXIT else ControllerChangeType.POP_EXIT
val toView = to?.let {
it.inflate(container).also {
to.changeStarted(handler, toChangeType)
}
}
val fromView = from?.let {
from.getView().also {
from.changeStarted(handler, fromChangeType)
}
}
handler.performChange(
container = container,
from = fromView,
to = toView,
isPush = isPush,
changeListener = object : ControllerChangeCompletedListener {
override fun onChangeCompleted() {
from?.changeEnded(handler, fromChangeType)
to?.let {
inProgressChangeHandlers.remove(it.getInstanceId())
it.changeEnded(handler, toChangeType)
}
listeners.forEach { it.onChangeCompleted(to, from, isPush, container, handler) }
if (handler.forceRemoveViewOnPush) {
(fromView?.parent as? ViewGroup)?.let {
it.removeView(fromView)
}
}
if (handler.removesFromViewOnPush) {
from?.needsAttach = false
}
}
},
)
}
}
}
@@ -34,10 +34,6 @@ class ControllerHostedRouter extends Router {
ControllerHostedRouter() { }
ControllerHostedRouter(int hostId, @Nullable String tag) {
this(hostId, tag, false);
}
ControllerHostedRouter(int hostId, @Nullable String tag, boolean boundToContainer) {
if (!boundToContainer && tag == null) {
throw new IllegalStateException("ControllerHostedRouter can't be created without a tag if not bounded to its container");
@@ -50,6 +46,7 @@ class ControllerHostedRouter extends Router {
final void setHostController(@NonNull Controller controller) {
if (hostController == null) {
hostController = controller;
setOnBackPressedDispatcherEnabled(controller.onBackPressedDispatcherEnabled);
}
}
@@ -63,6 +60,7 @@ class ControllerHostedRouter extends Router {
hostController = controller;
this.container = container;
setOnBackPressedDispatcherEnabled(controller.onBackPressedDispatcherEnabled);
for (RouterTransaction transaction : backstack) {
transaction.controller().setParentController(controller);
@@ -131,7 +129,7 @@ class ControllerHostedRouter extends Router {
// If we're pushing a transaction that will detach controllers to an unattached child
// router, we need mark all other controllers as NOT needing to be reattached.
if (to != null && !hostController.isAttached()) {
if (to.pushChangeHandler() == null || to.pushChangeHandler().removesFromViewOnPush()) {
if (to.pushChangeHandler() == null || to.pushChangeHandler().getRemovesFromViewOnPush()) {
for (RouterTransaction transaction : backstack) {
transaction.controller().setNeedsAttach(false);
}
@@ -5,12 +5,15 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.activity.ComponentActivity;
import androidx.activity.OnBackPressedDispatcher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
@@ -34,20 +37,39 @@ import java.util.List;
*/
public abstract class Router {
private static final String TAG = "Conductor";
private static final String KEY_BACKSTACK = "Router.backstack";
private static final String KEY_POPS_LAST_VIEW = "Router.popsLastView";
private static final String KEY_POP_ROOT_CONTROLLER_MODE = "Router.popRootControllerMode";
private static final String KEY_ON_BACK_PRESSED_DISPATCHER_ENABLED = "Router.onBackPressedDispatcherEnabled";
final Backstack backstack = new Backstack();
private final List<ControllerChangeListener> changeListeners = new ArrayList<>();
private final List<ChangeTransaction> pendingControllerChanges = new ArrayList<>();
final List<Controller> destroyingControllers = new ArrayList<>();
private boolean popsLastView = false;
PopRootControllerMode popRootControllerMode = PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW;
boolean onBackPressedDispatcherEnabled;
boolean containerFullyAttached = false;
boolean isActivityStopped = false;
ViewGroup container;
Router() {
backstack.setOnBackstackUpdatedListener(() -> {
if (!onBackPressedDispatcherEnabled) {
return;
}
Iterator<RouterTransaction> iterator = getBackstack().iterator();
int index = 0;
while (iterator.hasNext()) {
iterator.next().controller().onBackPressedCallback.setEnabled(
index++ > 0 || popRootControllerMode != PopRootControllerMode.NEVER
);
}
});
}
/**
* Returns this Router's host Activity or {@code null} if it has either not yet been attached to
* an Activity or if the Activity has been destroyed.
@@ -65,6 +87,22 @@ public abstract class Router {
*/
public abstract void onActivityResult(int requestCode, int resultCode, @Nullable Intent data);
/**
* Returns the OnBackPressedDispatcher for this Router's host Activity or {@code null} if:
* - This Router has not yet been attached to an Activity
* - The attached Activity does not extend ComponentActivity
* - The Activity has been destroyed
*/
@Nullable
public OnBackPressedDispatcher getOnBackPressedDispatcher() {
Activity activity = getActivity();
if (activity instanceof ComponentActivity) {
return ((ComponentActivity) activity).getOnBackPressedDispatcher();
}
return null;
}
/**
* This should be called by the host Activity when its onRequestPermissionsResult method is called. The call will be forwarded
* to the {@link Controller} with the instanceId passed in.
@@ -85,17 +123,25 @@ public abstract class Router {
* This should be called by the host Activity when its onBackPressed method is called. The call will be forwarded
* to its top {@link Controller}. If that controller doesn't handle it, then it will be popped.
*
* Note: This method has been deprecated and should be replaced with registering OnBackPressedCallbacks with
* Controller instances.
*
* @return Whether or not a back action was handled by the Router
*/
@UiThread
@Deprecated
public boolean handleBack() {
ThreadUtils.ensureMainThread();
return handleBackDispatch();
}
protected boolean handleBackDispatch() {
if (!backstack.isEmpty()) {
//noinspection ConstantConditions
if (backstack.peek().controller().handleBack()) {
return true;
} else if (popCurrentController()) {
} else if ((backstack.getSize() > 1 || popRootControllerMode != PopRootControllerMode.NEVER) && popCurrentController()) {
return true;
}
}
@@ -141,7 +187,7 @@ public abstract class Router {
RouterTransaction nextTransaction = null;
Iterator<RouterTransaction> iterator = backstack.iterator();
ControllerChangeHandler topPushHandler = topTransaction != null ? topTransaction.pushChangeHandler() : null;
final boolean needsNextTransactionAttach = topPushHandler != null ? !topPushHandler.removesFromViewOnPush() : false;
final boolean needsNextTransactionAttach = topPushHandler != null ? !topPushHandler.getRemovesFromViewOnPush() : false;
while (iterator.hasNext()) {
RouterTransaction transaction = iterator.next();
@@ -162,7 +208,7 @@ public abstract class Router {
}
}
if (popsLastView) {
if (popRootControllerMode == PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW) {
return topTransaction != null;
} else {
return !backstack.isEmpty();
@@ -203,8 +249,8 @@ public abstract class Router {
final ControllerChangeHandler handler = transaction.pushChangeHandler();
if (topTransaction != null) {
//noinspection ConstantConditions
final boolean oldHandlerRemovedViews = topTransaction.pushChangeHandler() == null || topTransaction.pushChangeHandler().removesFromViewOnPush();
final boolean newHandlerRemovesViews = handler == null || handler.removesFromViewOnPush();
final boolean oldHandlerRemovedViews = topTransaction.pushChangeHandler() == null || topTransaction.pushChangeHandler().getRemovesFromViewOnPush();
final boolean newHandlerRemovesViews = handler == null || handler.getRemovesFromViewOnPush();
if (!oldHandlerRemovedViews && newHandlerRemovesViews) {
for (RouterTransaction visibleTransaction : getVisibleTransactions(backstack.iterator(), true)) {
performControllerChange(null, visibleTransaction, true, handler);
@@ -221,12 +267,13 @@ public abstract class Router {
}
void destroy(boolean popViews) {
popsLastView = true;
popRootControllerMode = PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW;
final List<RouterTransaction> poppedControllers = backstack.popAll();
trackDestroyingControllers(poppedControllers);
RouterTransaction topTransaction = null;
if (popViews && poppedControllers.size() > 0) {
RouterTransaction topTransaction = poppedControllers.get(0);
topTransaction = poppedControllers.get(0);
topTransaction.controller().addLifecycleListener(new Controller.LifecycleListener() {
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
@@ -241,6 +288,16 @@ public abstract class Router {
performControllerChange(null, topTransaction, false, topTransaction.popChangeHandler());
}
if (poppedControllers.size() > 0) {
NoOpControllerChangeHandler changeHandler = new NoOpControllerChangeHandler();
for (RouterTransaction routerTransaction : poppedControllers) {
if (routerTransaction != topTransaction) {
routerTransaction.controller().changeStarted(changeHandler, ControllerChangeType.POP_EXIT);
routerTransaction.controller().changeEnded(changeHandler, ControllerChangeType.POP_EXIT);
}
}
}
}
public int getContainerId() {
@@ -251,10 +308,33 @@ public abstract class Router {
* If set to true, this router will handle back presses by performing a change handler on the last controller and view
* in the stack. This defaults to false so that the developer can either finish its containing Activity or otherwise
* hide its parent view without any strange artifacting.
*
* Note: This method has been deprecated and should be replaced with setPopRootControllerMode.
*/
@NonNull
@Deprecated
public Router setPopsLastView(boolean popsLastView) {
this.popsLastView = popsLastView;
this.popRootControllerMode = popsLastView ? PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW : PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW;
return this;
}
/**
* Sets the method this router will use to handle back presses when there is only one controller left in the backstack.
* Defaults to POP_ROOT_CONTROLLER_LEAVING_VIEW so that the developer can either finish its containing Activity or
* otherwise hide its parent view without any strange artifacting.
*/
@NonNull
public Router setPopRootControllerMode(@NonNull PopRootControllerMode popRootControllerMode) {
this.popRootControllerMode = popRootControllerMode;
return this;
}
@NonNull
public Router setOnBackPressedDispatcherEnabled(boolean enabled) {
if (backstack.getSize() > 0 && enabled != onBackPressedDispatcherEnabled) {
Log.e(TAG, "setOnBackPressedDispatcherEnabled call ignored, as controllers with a different setting have already been pushed.");
}
onBackPressedDispatcherEnabled = enabled;
return this;
}
@@ -546,10 +626,9 @@ public abstract class Router {
public void rebindIfNeeded() {
ThreadUtils.ensureMainThread();
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
// Not directly using the iterator in order to prevent ConcurrentModificationExceptions if controllers pop
// themselves on re-attach.
for (RouterTransaction transaction : getTransactions()) {
if (transaction.controller().getNeedsAttach()) {
performControllerChange(transaction, null, true, new SimpleSwapChangeHandler(false));
} else {
@@ -649,15 +728,17 @@ public abstract class Router {
Bundle backstackState = new Bundle();
backstack.saveInstanceState(backstackState);
outState.putInt(KEY_POP_ROOT_CONTROLLER_MODE, popRootControllerMode.ordinal());
outState.putBoolean(KEY_ON_BACK_PRESSED_DISPATCHER_ENABLED, onBackPressedDispatcherEnabled);
outState.putParcelable(KEY_BACKSTACK, backstackState);
outState.putBoolean(KEY_POPS_LAST_VIEW, popsLastView);
}
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
Bundle backstackBundle = savedInstanceState.getParcelable(KEY_BACKSTACK);
//noinspection ConstantConditions
popRootControllerMode = PopRootControllerMode.values()[savedInstanceState.getInt(KEY_POP_ROOT_CONTROLLER_MODE)];
onBackPressedDispatcherEnabled = savedInstanceState.getBoolean(KEY_ON_BACK_PRESSED_DISPATCHER_ENABLED);
backstack.restoreInstanceState(backstackBundle);
popsLastView = savedInstanceState.getBoolean(KEY_POPS_LAST_VIEW);
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
@@ -728,6 +809,7 @@ public abstract class Router {
@Override
public void run() {
containerFullyAttached = true;
performPendingControllerChanges();
}
});
}
@@ -767,6 +849,18 @@ public abstract class Router {
return controllers;
}
@NonNull
final List<RouterTransaction> getTransactions() {
List<RouterTransaction> transactions = new ArrayList<>(backstack.getSize());
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
transactions.add(backstackIterator.next());
}
return transactions;
}
@Nullable
public final Boolean handleRequestedPermission(@NonNull String permission) {
for (RouterTransaction transaction : backstack) {
@@ -803,7 +897,7 @@ public abstract class Router {
if (to != null) {
to.ensureValidIndex(getTransactionIndexer());
setRouterOnController(toController);
} else if (backstack.getSize() == 0 && !popsLastView) {
} else if (backstack.getSize() == 0 && popRootControllerMode == PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW) {
// We're emptying out the backstack. Views get weird if you transition them out, so just no-op it. The host
// Activity or controller should be handling this by finishing or at least hiding this view.
changeHandler = new NoOpControllerChangeHandler();
@@ -839,7 +933,7 @@ public abstract class Router {
to.setNeedsAttach(true);
}
pendingControllerChanges.add(transaction);
} else if (from != null && (changeHandler == null || changeHandler.removesFromViewOnPush()) && !containerFullyAttached) {
} else if (from != null && (changeHandler == null || changeHandler.getRemovesFromViewOnPush()) && !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.
@@ -847,12 +941,9 @@ public abstract class Router {
to.setNeedsAttach(true);
}
pendingControllerChanges.add(transaction);
container.post(new Runnable() {
@Override
public void run() {
performPendingControllerChanges();
}
});
if (container != null) {
container.post(this::performPendingControllerChanges);
}
} else {
ControllerChangeHandler.executeChange(transaction);
}
@@ -967,7 +1058,7 @@ public abstract class Router {
transactions.add(transaction);
}
visible = transaction.pushChangeHandler() != null && !transaction.pushChangeHandler().removesFromViewOnPush();
visible = transaction.pushChangeHandler() != null && !transaction.pushChangeHandler().getRemovesFromViewOnPush();
if (onlyTop && !visible) {
break;
@@ -1011,4 +1102,25 @@ public abstract class Router {
@NonNull abstract Router getRootRouter();
@NonNull abstract TransactionIndexer getTransactionIndexer();
/**
* Defines the way a Router will handle back button or pop events when there is only one controller
* left in the backstack.
*/
public enum PopRootControllerMode {
/**
* The Router will not pop the final controller left on the backstack when the back button is pressed
* or when pop events are called. This mode should generally be used for Activity-hosted routers.
*/
NEVER,
/**
* The Router will pop the final controller, but will leave its view in the hierarchy. This is useful
* when the developer wishes to allow its containing Activity to finish or otherwise hide its parent
* view without any strange artifacting.
*/
POP_ROOT_CONTROLLER_BUT_NOT_VIEW,
/**
* The Router will pop both the final controller as well as its view.
*/
POP_ROOT_CONTROLLER_AND_VIEW
}
}
@@ -16,8 +16,7 @@ private const val KEY_ATTACHED_TO_ROUTER = "RouterTransaction.attachedToRouter"
/**
* Metadata used for adding [Controller]s to a [Router].
*/
class RouterTransaction
private constructor(
class RouterTransaction private constructor(
@get:JvmName("controller")
val controller: Controller,
private var tag: String? = null,
@@ -29,7 +28,6 @@ private constructor(
var transactionIndex: Int = INVALID_INDEX
) {
@RestrictTo(LIBRARY)
internal constructor(bundle: Bundle) : this(
controller = Controller.newInstance(bundle.getBundle(KEY_VIEW_CONTROLLER_BUNDLE)!!),
@@ -1,257 +0,0 @@
package com.bluelinelabs.conductor.changehandler;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* A base {@link ControllerChangeHandler} that facilitates using {@link android.animation.Animator}s to replace Controller Views
*/
public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
private static final String KEY_DURATION = "AnimatorChangeHandler.duration";
private static final String KEY_REMOVES_FROM_ON_PUSH = "AnimatorChangeHandler.removesFromViewOnPush";
@SuppressWarnings("WeakerAccess")
public static final long DEFAULT_ANIMATION_DURATION = -1;
private long animationDuration;
boolean removesFromViewOnPush;
boolean canceled;
boolean needsImmediateCompletion;
private boolean completed;
Animator animator;
private OnAnimationReadyOrAbortedListener onAnimationReadyOrAbortedListener;
@SuppressWarnings("WeakerAccess")
public AnimatorChangeHandler() {
this(DEFAULT_ANIMATION_DURATION, true);
}
@SuppressWarnings("WeakerAccess")
public AnimatorChangeHandler(boolean removesFromViewOnPush) {
this(DEFAULT_ANIMATION_DURATION, removesFromViewOnPush);
}
@SuppressWarnings("WeakerAccess")
public AnimatorChangeHandler(long duration) {
this(duration, true);
}
public AnimatorChangeHandler(long duration, boolean removesFromViewOnPush) {
animationDuration = duration;
this.removesFromViewOnPush = removesFromViewOnPush;
}
@Override
public void saveToBundle(@NonNull Bundle bundle) {
super.saveToBundle(bundle);
bundle.putLong(KEY_DURATION, animationDuration);
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, removesFromViewOnPush);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
super.restoreFromBundle(bundle);
animationDuration = bundle.getLong(KEY_DURATION);
removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH);
}
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
canceled = true;
if (animator != null) {
animator.cancel();
} else if (onAnimationReadyOrAbortedListener != null) {
onAnimationReadyOrAbortedListener.onReadyOrAborted();
}
}
@Override
public void completeImmediately() {
super.completeImmediately();
needsImmediateCompletion = true;
if (animator != null) {
animator.end();
} else if (onAnimationReadyOrAbortedListener != null) {
onAnimationReadyOrAbortedListener.onReadyOrAborted();
}
}
public long getAnimationDuration() {
return animationDuration;
}
@Override
public boolean removesFromViewOnPush() {
return removesFromViewOnPush;
}
/**
* Should be overridden to return the Animator to use while replacing Views.
*
* @param container The container these Views are hosted in.
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop.
* @param toAddedToContainer True if the "to" view was added to the container as a part of this ChangeHandler. False if it was already in the hierarchy.
*/
@NonNull
protected abstract Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer);
/**
* Will be called after the animation is complete to reset the View that was removed to its pre-animation state.
*/
protected abstract void resetFromView(@NonNull View from);
@Override
public final void performChange(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
boolean readyToAnimate = true;
final boolean addingToView = to != null && to.getParent() == null;
if (addingToView) {
if (isPush || from == null) {
container.addView(to);
} else if (to.getParent() == null) {
container.addView(to, container.indexOfChild(from));
}
if (to.getWidth() <= 0 && to.getHeight() <= 0) {
readyToAnimate = false;
onAnimationReadyOrAbortedListener = new OnAnimationReadyOrAbortedListener(container, from, to, isPush, true, changeListener);
to.getViewTreeObserver().addOnPreDrawListener(onAnimationReadyOrAbortedListener);
}
}
if (readyToAnimate) {
performAnimation(container, from, to, isPush, addingToView, changeListener);
}
}
void complete(@NonNull ControllerChangeCompletedListener changeListener, @Nullable AnimatorListener animatorListener) {
if (!completed) {
completed = true;
changeListener.onChangeCompleted();
}
if (animator != null) {
if (animatorListener != null) {
animator.removeListener(animatorListener);
}
animator.cancel();
animator = null;
}
onAnimationReadyOrAbortedListener = null;
}
void performAnimation(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, final boolean toAddedToContainer, @NonNull final ControllerChangeCompletedListener changeListener) {
if (canceled) {
complete(changeListener, null);
return;
}
if (needsImmediateCompletion) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from);
}
complete(changeListener, null);
if (isPush && from != null) {
resetFromView(from);
}
return;
}
animator = getAnimator(container, from, to, isPush, toAddedToContainer);
if (animationDuration > 0) {
animator.setDuration(animationDuration);
}
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(Animator animation) {
if (from != null) {
resetFromView(from);
}
if (to != null && to.getParent() == container) {
container.removeView(to);
}
complete(changeListener, this);
}
@Override
public void onAnimationEnd(Animator animation) {
if (!canceled && animator != null) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from);
}
complete(changeListener, this);
if (isPush && from != null) {
resetFromView(from);
}
}
}
});
animator.start();
}
private class OnAnimationReadyOrAbortedListener implements ViewTreeObserver.OnPreDrawListener {
@NonNull final ViewGroup container;
@Nullable final View from;
@Nullable final View to;
final boolean isPush;
final boolean addingToView;
@NonNull final ControllerChangeCompletedListener changeListener;
private boolean hasRun;
OnAnimationReadyOrAbortedListener(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean addingToView, @NonNull ControllerChangeCompletedListener changeListener) {
this.container = container;
this.from = from;
this.to = to;
this.isPush = isPush;
this.addingToView = addingToView;
this.changeListener = changeListener;
}
@Override
public boolean onPreDraw() {
onReadyOrAborted();
return true;
}
void onReadyOrAborted() {
if (!hasRun) {
hasRun = true;
if (to != null) {
final ViewTreeObserver observer = to.getViewTreeObserver();
if (observer.isAlive()) {
observer.removeOnPreDrawListener(this);
}
}
performAnimation(container, from, to, isPush, addingToView, changeListener);
}
}
}
}
@@ -0,0 +1,228 @@
package com.bluelinelabs.conductor.changehandler
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
/**
* A base [ControllerChangeHandler] that facilitates using [android.animation.Animator]s to replace Controller Views
*/
abstract class AnimatorChangeHandler @JvmOverloads constructor(
animationDuration: Long = DEFAULT_ANIMATION_DURATION,
removesFromViewOnPush: Boolean = true,
) : ControllerChangeHandler() {
var animationDuration: Long = animationDuration
private set
private var canceled = false
private var needsImmediateCompletion = false
private var completed = false
private var animator: Animator? = null
private var onAnimationReadyOrAbortedListener: OnAnimationReadyOrAbortedListener? = null
private var _removesFromViewOnPush = removesFromViewOnPush
override val removesFromViewOnPush: Boolean
get() = _removesFromViewOnPush
constructor(removesFromViewOnPush: Boolean = true) : this(
animationDuration = DEFAULT_ANIMATION_DURATION,
removesFromViewOnPush = removesFromViewOnPush,
)
override fun saveToBundle(bundle: Bundle) {
super.saveToBundle(bundle)
bundle.putLong(KEY_DURATION, animationDuration)
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, removesFromViewOnPush)
}
override fun restoreFromBundle(bundle: Bundle) {
super.restoreFromBundle(bundle)
animationDuration = bundle.getLong(KEY_DURATION)
_removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH)
}
override fun onAbortPush(newHandler: ControllerChangeHandler, newTop: Controller?) {
super.onAbortPush(newHandler, newTop)
canceled = true
if (animator != null) {
animator!!.cancel()
} else if (onAnimationReadyOrAbortedListener != null) {
onAnimationReadyOrAbortedListener!!.onReadyOrAborted()
}
}
override fun completeImmediately() {
super.completeImmediately()
needsImmediateCompletion = true
if (animator != null) {
animator!!.end()
} else if (onAnimationReadyOrAbortedListener != null) {
onAnimationReadyOrAbortedListener!!.onReadyOrAborted()
}
}
/**
* Should be overridden to return the Animator to use while replacing Views.
*
* @param container The container these Views are hosted in.
* @param from The previous View in the container or `null` if there was no Controller before this transition
* @param to The next View that should be put in the container or `null` if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop.
* @param toAddedToContainer True if the "to" view was added to the container as a part of this ChangeHandler. False if it was already in the hierarchy.
*/
protected abstract fun getAnimator(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
toAddedToContainer: Boolean,
): Animator
/**
* Will be called after the animation is complete to reset the View that was removed to its pre-animation state.
*/
protected abstract fun resetFromView(from: View)
override fun performChange(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
changeListener: ControllerChangeCompletedListener,
) {
var readyToAnimate = true
val addingToView = to != null && to.parent == null
if (addingToView) {
if (isPush || from == null) {
container.addView(to)
} else if (to!!.parent == null) {
container.addView(to, container.indexOfChild(from))
}
if (to!!.width <= 0 && to.height <= 0) {
readyToAnimate = false
onAnimationReadyOrAbortedListener = OnAnimationReadyOrAbortedListener(container, from, to, isPush, true, changeListener)
to.viewTreeObserver.addOnPreDrawListener(onAnimationReadyOrAbortedListener)
}
}
if (readyToAnimate) {
performAnimation(container, from, to, isPush, addingToView, changeListener)
}
}
fun complete(changeListener: ControllerChangeCompletedListener, animatorListener: Animator.AnimatorListener?) {
if (!completed) {
completed = true
changeListener.onChangeCompleted()
}
if (animator != null) {
if (animatorListener != null) {
animator!!.removeListener(animatorListener)
}
animator!!.cancel()
animator = null
}
onAnimationReadyOrAbortedListener = null
}
fun performAnimation(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
toAddedToContainer: Boolean,
changeListener: ControllerChangeCompletedListener,
) {
if (canceled) {
complete(changeListener, null)
return
}
if (needsImmediateCompletion) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from)
}
complete(changeListener, null)
if (isPush && from != null) {
resetFromView(from)
}
return
}
animator = getAnimator(container, from, to, isPush, toAddedToContainer)
if (animationDuration > 0) {
animator!!.duration = animationDuration
}
animator!!.addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationCancel(animation: Animator) {
from?.let { resetFromView(it) }
if (to != null && to.parent === container) {
container.removeView(to)
}
complete(changeListener, this)
}
override fun onAnimationEnd(animation: Animator) {
if (!canceled && animator != null) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from)
}
complete(changeListener, this)
if (isPush && from != null) {
resetFromView(from)
}
}
}
},
)
animator!!.start()
}
private inner class OnAnimationReadyOrAbortedListener constructor(
val container: ViewGroup,
val from: View?,
val to: View?,
val isPush: Boolean,
val addingToView: Boolean,
val changeListener: ControllerChangeCompletedListener,
) : ViewTreeObserver.OnPreDrawListener {
private var hasRun = false
override fun onPreDraw(): Boolean {
onReadyOrAborted()
return true
}
fun onReadyOrAborted() {
if (!hasRun) {
hasRun = true
if (to != null) {
val observer = to.viewTreeObserver
if (observer.isAlive) {
observer.removeOnPreDrawListener(this)
}
}
performAnimation(container, from, to, isPush, addingToView, changeListener)
}
}
}
companion object {
private const val KEY_DURATION = "AnimatorChangeHandler.duration"
private const val KEY_REMOVES_FROM_ON_PUSH = "AnimatorChangeHandler.removesFromViewOnPush"
const val DEFAULT_ANIMATION_DURATION: Long = -1
}
}
@@ -1,57 +0,0 @@
package com.bluelinelabs.conductor.changehandler;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* An {@link AnimatorChangeHandler} that will cross fade two views
*/
public class FadeChangeHandler extends AnimatorChangeHandler {
public FadeChangeHandler() { }
public FadeChangeHandler(boolean removesFromViewOnPush) {
super(removesFromViewOnPush);
}
public FadeChangeHandler(long duration) {
super(duration);
}
public FadeChangeHandler(long duration, boolean removesFromViewOnPush) {
super(duration, removesFromViewOnPush);
}
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animator = new AnimatorSet();
if (to != null) {
float start = toAddedToContainer ? 0 : to.getAlpha();
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1));
}
if (from != null && (!isPush || removesFromViewOnPush())) {
animator.play(ObjectAnimator.ofFloat(from, View.ALPHA, 0));
}
return animator;
}
@Override
protected void resetFromView(@NonNull View from) {
from.setAlpha(1);
}
@Override @NonNull
public ControllerChangeHandler copy() {
return new FadeChangeHandler(getAnimationDuration(), removesFromViewOnPush());
}
}
@@ -0,0 +1,42 @@
package com.bluelinelabs.conductor.changehandler
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler
/**
* An [AnimatorChangeHandler] that will cross fade two views
*/
class FadeChangeHandler : AnimatorChangeHandler {
constructor() : super()
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
constructor(duration: Long) : super(duration)
constructor(duration: Long, removesFromViewOnPush: Boolean) : super(duration, removesFromViewOnPush)
override fun getAnimator(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
toAddedToContainer: Boolean,
): Animator {
val animator = AnimatorSet()
if (to != null) {
val start = if (toAddedToContainer) 0F else to.alpha
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1f))
}
if (from != null && (!isPush || removesFromViewOnPush)) {
animator.play(ObjectAnimator.ofFloat(from, View.ALPHA, 0f))
}
return animator
}
override fun resetFromView(from: View) {
from.alpha = 1f
}
override fun copy(): ControllerChangeHandler = FadeChangeHandler(animationDuration, removesFromViewOnPush)
}
@@ -1,67 +0,0 @@
package com.bluelinelabs.conductor.changehandler;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* An {@link AnimatorChangeHandler} that will slide the views left or right, depending on if it's a push or pop.
*/
public class HorizontalChangeHandler extends AnimatorChangeHandler {
public HorizontalChangeHandler() { }
public HorizontalChangeHandler(boolean removesFromViewOnPush) {
super(removesFromViewOnPush);
}
public HorizontalChangeHandler(long duration) {
super(duration);
}
public HorizontalChangeHandler(long duration, boolean removesFromViewOnPush) {
super(duration, removesFromViewOnPush);
}
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animatorSet = new AnimatorSet();
if (isPush) {
if (from != null) {
animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, -from.getWidth()));
}
if (to != null) {
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, to.getWidth(), 0));
}
} else {
if (from != null) {
animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, from.getWidth()));
}
if (to != null) {
// Allow this to have a nice transition when coming off an aborted push animation
float fromLeft = from != null ? from.getTranslationX() : 0;
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, fromLeft - to.getWidth(), 0));
}
}
return animatorSet;
}
@Override
protected void resetFromView(@NonNull View from) {
from.setTranslationX(0);
}
@Override @NonNull
public ControllerChangeHandler copy() {
return new HorizontalChangeHandler(getAnimationDuration(), removesFromViewOnPush());
}
}
@@ -0,0 +1,52 @@
package com.bluelinelabs.conductor.changehandler
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler
/**
* An [AnimatorChangeHandler] that will slide the views left or right, depending on if it's a push or pop.
*/
class HorizontalChangeHandler : AnimatorChangeHandler {
constructor() : super()
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
constructor(duration: Long) : super(duration)
constructor(duration: Long, removesFromViewOnPush: Boolean) : super(duration, removesFromViewOnPush)
override fun getAnimator(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
toAddedToContainer: Boolean,
): Animator {
val animatorSet = AnimatorSet()
if (isPush) {
if (from != null) {
animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, -from.width.toFloat()))
}
if (to != null) {
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, to.width.toFloat(), 0f))
}
} else {
if (from != null) {
animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, from.width.toFloat()))
}
if (to != null) {
// Allow this to have a nice transition when coming off an aborted push animation
val fromLeft = from?.translationX ?: 0F
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, fromLeft - to.width, 0f))
}
}
return animatorSet
}
override fun resetFromView(from: View) {
from.translationX = 0f
}
override fun copy(): ControllerChangeHandler = HorizontalChangeHandler(animationDuration, removesFromViewOnPush)
}
@@ -1,115 +0,0 @@
package com.bluelinelabs.conductor.changehandler;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* A {@link ControllerChangeHandler} that will instantly swap Views with no animations or transitions.
*/
public class SimpleSwapChangeHandler extends ControllerChangeHandler implements OnAttachStateChangeListener {
private static final String KEY_REMOVES_FROM_ON_PUSH = "SimpleSwapChangeHandler.removesFromViewOnPush";
private boolean removesFromViewOnPush;
private boolean canceled;
private ViewGroup container;
private ControllerChangeCompletedListener changeListener;
public SimpleSwapChangeHandler() {
this(true);
}
public SimpleSwapChangeHandler(boolean removesFromViewOnPush) {
this.removesFromViewOnPush = removesFromViewOnPush;
}
@Override
public void saveToBundle(@NonNull Bundle bundle) {
super.saveToBundle(bundle);
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, removesFromViewOnPush);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
super.restoreFromBundle(bundle);
removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH);
}
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
canceled = true;
}
@Override
public void completeImmediately() {
if (changeListener != null) {
changeListener.onChangeCompleted();
changeListener = null;
container.removeOnAttachStateChangeListener(this);
container = null;
}
}
@Override
public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
if (!canceled) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from);
}
if (to != null && to.getParent() == null) {
container.addView(to);
}
}
if (container.getWindowToken() != null) {
changeListener.onChangeCompleted();
} else {
this.changeListener = changeListener;
this.container = container;
container.addOnAttachStateChangeListener(this);
}
}
@Override
public boolean removesFromViewOnPush() {
return removesFromViewOnPush;
}
@Override
public void onViewAttachedToWindow(@NonNull View v) {
v.removeOnAttachStateChangeListener(this);
if (changeListener != null) {
changeListener.onChangeCompleted();
changeListener = null;
container = null;
}
}
@Override
public void onViewDetachedFromWindow(@NonNull View v) { }
@Override @NonNull
public ControllerChangeHandler copy() {
return new SimpleSwapChangeHandler(removesFromViewOnPush());
}
@Override
public boolean isReusable() {
return true;
}
}
@@ -0,0 +1,88 @@
package com.bluelinelabs.conductor.changehandler
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
/**
* A [ControllerChangeHandler] that will instantly swap Views with no animations or transitions.
*/
class SimpleSwapChangeHandler @JvmOverloads constructor(
removesFromViewOnPush: Boolean = true,
) : ControllerChangeHandler(), View.OnAttachStateChangeListener {
private var _removesFromViewOnPush = removesFromViewOnPush
override val removesFromViewOnPush: Boolean
get() = _removesFromViewOnPush
override val isReusable = true
private var canceled = false
private var container: ViewGroup? = null
private var changeListener: ControllerChangeCompletedListener? = null
override fun saveToBundle(bundle: Bundle) {
super.saveToBundle(bundle)
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, removesFromViewOnPush)
}
override fun restoreFromBundle(bundle: Bundle) {
super.restoreFromBundle(bundle)
_removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH)
}
override fun onAbortPush(newHandler: ControllerChangeHandler, newTop: Controller?) {
super.onAbortPush(newHandler, newTop)
canceled = true
}
override fun completeImmediately() {
changeListener?.onChangeCompleted()
changeListener = null
container?.removeOnAttachStateChangeListener(this)
container = null
}
override fun performChange(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
changeListener: ControllerChangeCompletedListener,
) {
if (canceled) return
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from)
}
if (to != null && to.parent == null) {
container.addView(to)
}
if (container.windowToken != null) {
changeListener.onChangeCompleted()
} else {
this.changeListener = changeListener
this.container = container
container.addOnAttachStateChangeListener(this)
}
}
override fun onViewAttachedToWindow(v: View) {
v.removeOnAttachStateChangeListener(this)
changeListener?.onChangeCompleted()
changeListener = null
container?.removeOnAttachStateChangeListener(this)
container = null
}
override fun onViewDetachedFromWindow(v: View) = Unit
override fun copy(): ControllerChangeHandler = SimpleSwapChangeHandler(removesFromViewOnPush)
}
private const val KEY_REMOVES_FROM_ON_PUSH = "SimpleSwapChangeHandler.removesFromViewOnPush"
@@ -1,59 +0,0 @@
package com.bluelinelabs.conductor.changehandler;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import java.util.ArrayList;
import java.util.List;
/**
* An {@link AnimatorChangeHandler} that will slide either slide a new View up or slide an old View down,
* depending on whether a push or pop change is happening.
*/
public class VerticalChangeHandler extends AnimatorChangeHandler {
public VerticalChangeHandler() { }
public VerticalChangeHandler(boolean removesFromViewOnPush) {
super(removesFromViewOnPush);
}
public VerticalChangeHandler(long duration) {
super(duration);
}
public VerticalChangeHandler(long duration, boolean removesFromViewOnPush) {
super(duration, removesFromViewOnPush);
}
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animator = new AnimatorSet();
List<Animator> viewAnimators = new ArrayList<>();
if (isPush && to != null) {
viewAnimators.add(ObjectAnimator.ofFloat(to, View.TRANSLATION_Y, to.getHeight(), 0));
} else if (!isPush && from != null) {
viewAnimators.add(ObjectAnimator.ofFloat(from, View.TRANSLATION_Y, from.getHeight()));
}
animator.playTogether(viewAnimators);
return animator;
}
@Override
protected void resetFromView(@NonNull View from) { }
@Override @NonNull
public ControllerChangeHandler copy() {
return new VerticalChangeHandler(getAnimationDuration(), removesFromViewOnPush());
}
}
@@ -0,0 +1,43 @@
package com.bluelinelabs.conductor.changehandler
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler
/**
* An [AnimatorChangeHandler] that will slide either slide a new View up or slide an old View down,
* depending on whether a push or pop change is happening.
*/
class VerticalChangeHandler : AnimatorChangeHandler {
constructor() : super()
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
constructor(duration: Long) : super(duration)
constructor(duration: Long, removesFromViewOnPush: Boolean) : super(duration, removesFromViewOnPush)
override fun getAnimator(
container: ViewGroup,
from: View?,
to: View?,
isPush: Boolean,
toAddedToContainer: Boolean,
): Animator {
val animator = AnimatorSet()
val viewAnimators: MutableList<Animator> = ArrayList()
if (isPush && to != null) {
viewAnimators.add(ObjectAnimator.ofFloat(to, View.TRANSLATION_Y, to.height.toFloat(), 0f))
} else if (!isPush && from != null) {
viewAnimators.add(ObjectAnimator.ofFloat(from, View.TRANSLATION_Y, from.height.toFloat()))
}
animator.playTogether(viewAnimators)
return animator
}
override fun resetFromView(from: View) = Unit
override fun copy(): ControllerChangeHandler = VerticalChangeHandler(animationDuration, removesFromViewOnPush)
}
@@ -1,451 +0,0 @@
package com.bluelinelabs.conductor.internal;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Application.ActivityLifecycleCallbacks;
import android.app.Fragment;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
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;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class LifecycleHandler extends Fragment implements ActivityLifecycleCallbacks {
private static final String FRAGMENT_TAG = "LifecycleHandler";
private static final String KEY_PENDING_PERMISSION_REQUESTS = "LifecycleHandler.pendingPermissionRequests";
private static final String KEY_PERMISSION_REQUEST_CODES = "LifecycleHandler.permissionRequests";
private static final String KEY_ACTIVITY_REQUEST_CODES = "LifecycleHandler.activityRequests";
private static final String KEY_ROUTER_STATE_PREFIX = "LifecycleHandler.routerState";
private Activity activity;
private boolean hasRegisteredCallbacks;
private boolean destroyed;
private boolean attached;
private boolean hasPreparedForHostDetach;
private static final Map<Activity, LifecycleHandler> activeLifecycleHandlers = new HashMap<>();
private SparseArray<String> permissionRequestMap = new SparseArray<>();
private SparseArray<String> activityRequestMap = new SparseArray<>();
private ArrayList<PendingPermissionRequest> pendingPermissionRequests = new ArrayList<>();
private final Map<Integer, ActivityHostedRouter> routerMap = new HashMap<>();
public LifecycleHandler() {
setRetainInstance(true);
setHasOptionsMenu(true);
}
@Nullable
private static LifecycleHandler findInActivity(@NonNull Activity activity) {
LifecycleHandler lifecycleHandler = activeLifecycleHandlers.get(activity);
if (lifecycleHandler == null) {
lifecycleHandler = (LifecycleHandler)activity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
}
if (lifecycleHandler != null) {
lifecycleHandler.registerActivityListener(activity);
}
return lifecycleHandler;
}
@NonNull
public static LifecycleHandler install(@NonNull Activity activity) {
LifecycleHandler lifecycleHandler = findInActivity(activity);
if (lifecycleHandler == null) {
lifecycleHandler = new LifecycleHandler();
activity.getFragmentManager().beginTransaction().add(lifecycleHandler, FRAGMENT_TAG).commit();
}
lifecycleHandler.registerActivityListener(activity);
return lifecycleHandler;
}
@NonNull
public Router getRouter(@NonNull ViewGroup container, @Nullable Bundle savedInstanceState) {
ActivityHostedRouter router = routerMap.get(getRouterHashKey(container));
if (router == null) {
router = new ActivityHostedRouter();
router.setHost(this, container);
if (savedInstanceState != null) {
Bundle routerSavedState = savedInstanceState.getBundle(KEY_ROUTER_STATE_PREFIX + router.getContainerId());
if (routerSavedState != null) {
router.restoreInstanceState(routerSavedState);
}
}
routerMap.put(getRouterHashKey(container), router);
} else {
router.setHost(this, container);
}
return router;
}
@NonNull
public List<Router> getRouters() {
return new ArrayList<Router>(routerMap.values());
}
@Nullable
public Activity getLifecycleActivity() {
return activity;
}
private static int getRouterHashKey(@NonNull ViewGroup viewGroup) {
return viewGroup.getId();
}
private void registerActivityListener(@NonNull Activity activity) {
this.activity = activity;
if (!hasRegisteredCallbacks) {
hasRegisteredCallbacks = true;
activity.getApplication().registerActivityLifecycleCallbacks(this);
// Since Fragment transactions are async, we have to keep an <Activity, LifecycleHandler> map in addition
// to trying to find the LifecycleHandler fragment in the Activity to handle the case of the developer
// trying to immediately get > 1 router in the same Activity. See issue #299.
activeLifecycleHandlers.put(activity, this);
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
StringSparseArrayParceler permissionParcel = savedInstanceState.getParcelable(KEY_PERMISSION_REQUEST_CODES);
permissionRequestMap = permissionParcel != null ? permissionParcel.getStringSparseArray() : new SparseArray<String>();
StringSparseArrayParceler activityParcel = savedInstanceState.getParcelable(KEY_ACTIVITY_REQUEST_CODES);
activityRequestMap = activityParcel != null ? activityParcel.getStringSparseArray() : new SparseArray<String>();
ArrayList<PendingPermissionRequest> pendingRequests = savedInstanceState.getParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS);
pendingPermissionRequests = pendingRequests != null ? pendingRequests : new ArrayList<PendingPermissionRequest>();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(KEY_PERMISSION_REQUEST_CODES, new StringSparseArrayParceler(permissionRequestMap));
outState.putParcelable(KEY_ACTIVITY_REQUEST_CODES, new StringSparseArrayParceler(activityRequestMap));
outState.putParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS, pendingPermissionRequests);
}
@Override
public void onDestroy() {
super.onDestroy();
if (activity != null) {
activity.getApplication().unregisterActivityLifecycleCallbacks(this);
activeLifecycleHandlers.remove(activity);
destroyRouters(false);
activity = null;
}
routerMap.clear();
}
@Override
public void onAttach(Activity activity) {
this.activity = activity;
super.onAttach(activity);
destroyed = false;
setAttached();
}
@Override
public void onAttach(Context context) {
if (context instanceof Activity) {
this.activity = (Activity) context;
}
super.onAttach(context);
destroyed = false;
setAttached();
}
@Override
public void onDetach() {
super.onDetach();
attached = false;
if (activity != null) {
destroyRouters(activity.isChangingConfigurations());
}
}
private void setAttached() {
if (!attached) {
attached = true;
for (int i = pendingPermissionRequests.size() - 1; i >= 0; i--) {
PendingPermissionRequest request = pendingPermissionRequests.remove(i);
requestPermissions(request.instanceId, request.permissions, request.requestCode);
}
for (ActivityHostedRouter router : new ArrayList<>(routerMap.values())) {
router.onContextAvailable();
}
}
}
private void destroyRouters(boolean configurationChange) {
if (!destroyed) {
destroyed = true;
if (activity != null) {
for (Router router : getRouters()) {
router.onActivityDestroyed(activity, configurationChange);
}
}
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
String instanceId = activityRequestMap.get(requestCode);
if (instanceId != null) {
for (Router router : getRouters()) {
router.onActivityResult(instanceId, requestCode, resultCode, data);
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
String instanceId = permissionRequestMap.get(requestCode);
if (instanceId != null) {
for (Router router : getRouters()) {
router.onRequestPermissionsResult(instanceId, requestCode, permissions, grantResults);
}
}
}
@Override
public boolean shouldShowRequestPermissionRationale(@NonNull String permission) {
for (Router router : getRouters()) {
Boolean handled = router.handleRequestedPermission(permission);
if (handled != null) {
return handled;
}
}
return super.shouldShowRequestPermissionRationale(permission);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
for (Router router : getRouters()) {
router.onCreateOptionsMenu(menu, inflater);
}
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
for (Router router : getRouters()) {
router.onPrepareOptionsMenu(menu);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
for (Router router : getRouters()) {
if (router.onOptionsItemSelected(item)) {
return true;
}
}
return super.onOptionsItemSelected(item);
}
public void registerForActivityResult(@NonNull String instanceId, int requestCode) {
activityRequestMap.put(requestCode, instanceId);
}
public void unregisterForActivityResults(@NonNull String instanceId) {
for (int i = activityRequestMap.size() - 1; i >= 0; i--) {
if (instanceId.equals(activityRequestMap.get(activityRequestMap.keyAt(i)))) {
activityRequestMap.removeAt(i);
}
}
}
public void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) {
registerForActivityResult(instanceId, requestCode);
startActivityForResult(intent, requestCode);
}
public void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options) {
registerForActivityResult(instanceId, requestCode);
startActivityForResult(intent, requestCode, options);
}
@TargetApi(Build.VERSION_CODES.N)
public void startIntentSenderForResult(@NonNull String instanceId, @NonNull IntentSender intent, int requestCode,
@Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,
@Nullable Bundle options) throws IntentSender.SendIntentException {
registerForActivityResult(instanceId, requestCode);
startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options);
}
@TargetApi(Build.VERSION_CODES.M)
public void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
if (attached) {
permissionRequestMap.put(requestCode, instanceId);
requestPermissions(permissions, requestCode);
} else {
pendingPermissionRequests.add(new PendingPermissionRequest(instanceId, permissions, requestCode));
}
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (findInActivity(activity) == LifecycleHandler.this) {
this.activity = activity;
for (ActivityHostedRouter router : new ArrayList<>(routerMap.values())) {
router.onContextAvailable();
}
}
}
@Override
public void onActivityStarted(Activity activity) {
if (this.activity == activity) {
hasPreparedForHostDetach = false;
for (Router router : getRouters()) {
router.onActivityStarted(activity);
}
}
}
@Override
public void onActivityResumed(Activity activity) {
if (this.activity == activity) {
for (Router router : getRouters()) {
router.onActivityResumed(activity);
}
}
}
@Override
public void onActivityPaused(Activity activity) {
if (this.activity == activity) {
for (Router router : getRouters()) {
router.onActivityPaused(activity);
}
}
}
@Override
public void onActivityStopped(Activity activity) {
if (this.activity == activity) {
prepareForHostDetachIfNeeded();
for (Router router : getRouters()) {
router.onActivityStopped(activity);
}
}
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
if (this.activity == activity) {
prepareForHostDetachIfNeeded();
for (Router router : getRouters()) {
Bundle bundle = new Bundle();
router.saveInstanceState(bundle);
outState.putBundle(KEY_ROUTER_STATE_PREFIX + router.getContainerId(), bundle);
}
}
}
@Override
public void onActivityDestroyed(Activity activity) {
activeLifecycleHandlers.remove(activity);
}
private void prepareForHostDetachIfNeeded() {
if (!hasPreparedForHostDetach) {
hasPreparedForHostDetach = true;
for (Router router : getRouters()) {
router.prepareForHostDetach();
}
}
}
private static class PendingPermissionRequest implements Parcelable {
final String instanceId;
final String[] permissions;
final int requestCode;
PendingPermissionRequest(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
this.instanceId = instanceId;
this.permissions = permissions;
this.requestCode = requestCode;
}
PendingPermissionRequest(Parcel in) {
instanceId = in.readString();
permissions = in.createStringArray();
requestCode = in.readInt();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeString(instanceId);
out.writeStringArray(permissions);
out.writeInt(requestCode);
}
public static final Parcelable.Creator<PendingPermissionRequest> CREATOR = new Parcelable.Creator<PendingPermissionRequest>() {
@Override
public PendingPermissionRequest createFromParcel(Parcel in) {
return new PendingPermissionRequest(in);
}
@Override
public PendingPermissionRequest[] newArray(int size) {
return new PendingPermissionRequest[size];
}
};
}
}
@@ -0,0 +1,605 @@
package com.bluelinelabs.conductor.internal
import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.util.SparseArray
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.ViewGroup
import androidx.annotation.RequiresApi
import androidx.fragment.app.FragmentActivity
import com.bluelinelabs.conductor.ActivityHostedRouter
import com.bluelinelabs.conductor.Router
import kotlinx.parcelize.Parcelize
internal interface LifecycleHandler {
val routers: List<Router>
val lifecycleActivity: Activity?
fun getRouter(container: ViewGroup, savedInstanceState: Bundle?): Router
fun registerActivityListener(activity: Activity)
fun registerForActivityResult(instanceId: String, requestCode: Int)
fun unregisterForActivityResults(instanceId: String)
fun startActivity(intent: Intent?)
fun startActivityForResult(instanceId: String, intent: Intent, requestCode: Int, options: Bundle? = null)
@Throws(IntentSender.SendIntentException::class)
fun startIntentSenderForResult(
instanceId: String,
intent: IntentSender,
requestCode: Int,
fillInIntent: Intent?,
flagsMask: Int,
flagsValues: Int,
extraFlags: Int,
options: Bundle?,
)
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
fun requestPermissions(instanceId: String, permissions: Array<String>, requestCode: Int)
companion object {
fun install(activity: Activity, allowAndroidXBacking: Boolean = true): LifecycleHandler {
var lifecycleHandler = findInActivity(activity, allowAndroidXBacking)
if (lifecycleHandler == null) {
if (allowAndroidXBacking && activity is FragmentActivity) {
lifecycleHandler = AndroidXLifecycleHandlerImpl()
activity.supportFragmentManager.beginTransaction().add(lifecycleHandler, FRAGMENT_TAG).commit()
} else {
lifecycleHandler = PlatformLifecycleHandlerImpl()
@Suppress("DEPRECATION")
activity.fragmentManager.beginTransaction().add(lifecycleHandler, FRAGMENT_TAG).commit()
}
}
lifecycleHandler.registerActivityListener(activity)
return lifecycleHandler
}
}
}
internal class AndroidXLifecycleHandlerImpl : androidx.fragment.app.Fragment(), LifecycleHandler, LifecycleHandlerDelegate {
override val data: LifecycleHandlerData = LifecycleHandlerData(isAndroidXLifecycleHandler = true)
override val routers: List<Router>
get() = data.routerMap.values.toList()
override val lifecycleActivity: Activity?
get() = data.activity
init {
@Suppress("DEPRECATION")
retainInstance = true
setHasOptionsMenu(true)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleOnCreate(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
handleOnSaveInstanceState(outState)
}
override fun onAttach(context: Context) {
super.onAttach(context)
handleOnAttach(context)
}
@Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
handleOnActivityResult(requestCode, resultCode, data)
}
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
handleOnRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
return handleShouldShowRequestPermissionRationale(permission) {
super.shouldShowRequestPermissionRationale(permission)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
handleOnCreateOptionsMenu(menu, inflater)
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
handleOnPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return handleOnOptionsItemSelected(item) {
super.onOptionsItemSelected(item)
}
}
override fun getRouter(container: ViewGroup, savedInstanceState: Bundle?): Router {
return getRouter(container, savedInstanceState, this)
}
override fun registerActivityListener(activity: Activity) {
handleRegisterActivityListener(activity, this)
}
override fun registerForActivityResult(instanceId: String, requestCode: Int) {
handleRegisterForActivityResult(instanceId, requestCode)
}
override fun unregisterForActivityResults(instanceId: String) {
handleUnregisterForActivityResults(instanceId)
}
override fun startActivityForResult(instanceId: String, intent: Intent, requestCode: Int, options: Bundle?) {
handleStartActivityForResult(instanceId, intent, requestCode, options)
}
@Suppress("DEPRECATION")
override fun startIntentSenderForResult(
instanceId: String,
intent: IntentSender,
requestCode: Int,
fillInIntent: Intent?,
flagsMask: Int,
flagsValues: Int,
extraFlags: Int,
options: Bundle?,
) {
handleStartIntentSenderForResult(
instanceId = instanceId,
intent = intent,
requestCode = requestCode,
fillInIntent = fillInIntent,
flagsMask = flagsMask,
flagsValues = flagsValues,
extraFlags = extraFlags,
options = options,
) {
startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options)
}
}
override fun requestPermissions(instanceId: String, permissions: Array<String>, requestCode: Int) {
handleRequestPermissions(instanceId, permissions, requestCode)
}
override fun onDetach() {
super.onDetach()
handleOnDetach()
}
override fun onDestroy() {
super.onDestroy()
handleOnDestroy()
}
}
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
internal class PlatformLifecycleHandlerImpl : android.app.Fragment(), LifecycleHandler, LifecycleHandlerDelegate {
override val data: LifecycleHandlerData = LifecycleHandlerData(isAndroidXLifecycleHandler = false)
override val routers: List<Router>
get() = data.routerMap.values.toList()
override val lifecycleActivity: Activity?
get() = data.activity
init {
retainInstance = true
setHasOptionsMenu(true)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleOnCreate(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
handleOnSaveInstanceState(outState)
}
override fun onAttach(activity: Activity) {
super.onAttach(activity)
handleOnAttach(activity)
}
override fun onAttach(context: Context) {
super.onAttach(context)
handleOnAttach(context)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
handleOnActivityResult(requestCode, resultCode, data)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
handleOnRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
return handleShouldShowRequestPermissionRationale(permission) {
super.shouldShowRequestPermissionRationale(permission)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
handleOnCreateOptionsMenu(menu, inflater)
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
handleOnPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return handleOnOptionsItemSelected(item) {
super.onOptionsItemSelected(item)
}
}
override fun getRouter(container: ViewGroup, savedInstanceState: Bundle?): Router {
return getRouter(container, savedInstanceState, this)
}
override fun registerActivityListener(activity: Activity) {
handleRegisterActivityListener(activity, this)
}
override fun registerForActivityResult(instanceId: String, requestCode: Int) {
handleRegisterForActivityResult(instanceId, requestCode)
}
override fun unregisterForActivityResults(instanceId: String) {
handleUnregisterForActivityResults(instanceId)
}
override fun startActivityForResult(instanceId: String, intent: Intent, requestCode: Int, options: Bundle?) {
handleStartActivityForResult(instanceId, intent, requestCode, options)
}
@RequiresApi(Build.VERSION_CODES.N)
override fun startIntentSenderForResult(
instanceId: String,
intent: IntentSender,
requestCode: Int,
fillInIntent: Intent?,
flagsMask: Int,
flagsValues: Int,
extraFlags: Int,
options: Bundle?,
) {
handleStartIntentSenderForResult(
instanceId = instanceId,
intent = intent,
requestCode = requestCode,
fillInIntent = fillInIntent,
flagsMask = flagsMask,
flagsValues = flagsValues,
extraFlags = extraFlags,
options = options,
) {
startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options)
}
}
override fun requestPermissions(instanceId: String, permissions: Array<String>, requestCode: Int) {
handleRequestPermissions(instanceId, permissions, requestCode)
}
override fun onDetach() {
super.onDetach()
handleOnDetach()
}
override fun onDestroy() {
super.onDestroy()
handleOnDestroy()
}
}
private interface LifecycleHandlerDelegate : ActivityLifecycleCallbacks {
val data: LifecycleHandlerData
private val routers: List<ActivityHostedRouter>
get() = data.routerMap.values.toList()
fun handleOnCreate(savedInstanceState: Bundle?) {
savedInstanceState ?: return
data.permissionRequestMap = savedInstanceState.getParcelable<StringSparseArrayParceler>(KEY_PERMISSION_REQUEST_CODES)
?.stringSparseArray
?: SparseArray()
data.activityRequestMap = savedInstanceState.getParcelable<StringSparseArrayParceler>(KEY_ACTIVITY_REQUEST_CODES)
?.stringSparseArray
?: SparseArray()
data.pendingPermissionRequests = savedInstanceState.getParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS)
?: ArrayList()
}
fun handleOnSaveInstanceState(outState: Bundle) {
outState.putParcelable(KEY_PERMISSION_REQUEST_CODES, StringSparseArrayParceler(data.permissionRequestMap))
outState.putParcelable(KEY_ACTIVITY_REQUEST_CODES, StringSparseArrayParceler(data.activityRequestMap))
outState.putParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS, data.pendingPermissionRequests)
}
fun handleOnDestroy() {
data.activity?.let { activity ->
activity.application.unregisterActivityLifecycleCallbacks(this)
activeLifecycleHandlers.remove(activity)
destroyRouters(false)
data.activity = null
}
data.routerMap.clear()
}
fun getRouter(container: ViewGroup, savedInstanceState: Bundle?, handler: LifecycleHandler): Router {
data.routerMap[routerHashKey(container)]?.let {
it.setHost(handler, container)
return it
}
val router = ActivityHostedRouter()
router.setHost(handler, container)
savedInstanceState?.getBundle("$KEY_ROUTER_STATE_PREFIX${router.containerId}")?.let {
router.restoreInstanceState(it)
}
data.routerMap[routerHashKey(container)] = router
return router
}
fun handleRegisterActivityListener(activity: Activity, handler: LifecycleHandler) {
data.activity = activity
if (!data.hasRegisteredCallbacks) {
data.hasRegisteredCallbacks = true
activity.application.registerActivityLifecycleCallbacks(this)
// Since Fragment transactions are async, we have to keep an <Activity, LifecycleHandler> map in addition
// to trying to find the LifecycleHandler fragment in the Activity to handle the case of the developer
// trying to immediately get > 1 router in the same Activity. See issue #299.
activeLifecycleHandlers[activity] = handler
}
}
fun requestPermissions(permissions: Array<String>, requestCode: Int)
fun handleOnAttach(context: Context) {
if (context is Activity) {
data.activity = context
}
data.destroyed = false
if (!data.attached) {
data.attached = true
for (i in data.pendingPermissionRequests.indices.reversed()) {
val request = data.pendingPermissionRequests.removeAt(i)
handleRequestPermissions(request.instanceId, request.permissions, request.requestCode)
}
routers.forEach { it.onContextAvailable() }
}
}
fun handleOnDetach() {
data.attached = false
data.activity?.let { destroyRouters(it.isChangingConfigurations) }
}
private fun destroyRouters(configurationChange: Boolean) {
if (!data.destroyed) {
data.destroyed = true
data.activity?.let { activity ->
routers.forEach { it.onActivityDestroyed(activity, configurationChange) }
}
}
}
fun handleOnActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
this.data.activityRequestMap[requestCode]?.let { instanceId ->
routers.forEach { it.onActivityResult(instanceId, requestCode, resultCode, data) }
}
}
fun handleOnRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
data.permissionRequestMap[requestCode]?.let { instanceId ->
routers.forEach { it.onRequestPermissionsResult(instanceId, requestCode, permissions, grantResults) }
}
}
fun handleShouldShowRequestPermissionRationale(permission: String, callSuper: () -> Boolean): Boolean {
for (router in routers) {
val handled = router.handleRequestedPermission(permission)
if (handled != null) {
return handled
}
}
return callSuper()
}
fun handleOnCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
routers.forEach { it.onCreateOptionsMenu(menu, inflater) }
}
fun handleOnPrepareOptionsMenu(menu: Menu) {
routers.forEach { it.onPrepareOptionsMenu(menu) }
}
fun handleOnOptionsItemSelected(item: MenuItem, callSuper: () -> Boolean): Boolean {
return routers.any { it.onOptionsItemSelected(item) } || callSuper()
}
fun handleRegisterForActivityResult(instanceId: String, requestCode: Int) {
data.activityRequestMap.put(requestCode, instanceId)
}
fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?)
fun handleStartActivityForResult(instanceId: String, intent: Intent, requestCode: Int, options: Bundle?) {
handleRegisterForActivityResult(instanceId, requestCode)
startActivityForResult(intent, requestCode, options)
}
fun handleStartIntentSenderForResult(
instanceId: String,
intent: IntentSender,
requestCode: Int,
fillInIntent: Intent?,
flagsMask: Int,
flagsValues: Int,
extraFlags: Int,
options: Bundle?,
startIntentSender: () -> Unit,
) {
handleRegisterForActivityResult(instanceId, requestCode)
startIntentSender()
}
fun handleUnregisterForActivityResults(instanceId: String) {
for (i in data.activityRequestMap.size() - 1 downTo 0) {
if (instanceId == data.activityRequestMap[data.activityRequestMap.keyAt(i)]) {
data.activityRequestMap.removeAt(i)
}
}
}
fun handleRequestPermissions(
instanceId: String,
permissions: Array<String>,
requestCode: Int,
) {
if (data.attached) {
data.permissionRequestMap.put(requestCode, instanceId)
requestPermissions(permissions, requestCode)
} else {
data.pendingPermissionRequests.add(PendingPermissionRequest(instanceId, permissions, requestCode))
}
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (findInActivity(activity, data.isAndroidXLifecycleHandler) === this) {
data.activity = activity
data.routerMap.values.toList().forEach { it.onContextAvailable() }
}
}
override fun onActivityStarted(activity: Activity) {
if (data.activity === activity) {
data.hasPreparedForHostDetach = false
routers.forEach { it.onActivityStarted(activity) }
}
}
override fun onActivityResumed(activity: Activity) {
if (data.activity === activity) {
routers.forEach { it.onActivityResumed(activity) }
}
}
override fun onActivityPaused(activity: Activity) {
if (data.activity === activity) {
routers.forEach { it.onActivityPaused(activity) }
}
}
override fun onActivityStopped(activity: Activity) {
if (data.activity === activity) {
prepareForHostDetachIfNeeded()
routers.forEach { it.onActivityStopped(activity) }
}
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
if (data.activity === activity) {
prepareForHostDetachIfNeeded()
routers.forEach {
val bundle = Bundle()
it.saveInstanceState(bundle)
outState.putBundle("$KEY_ROUTER_STATE_PREFIX${it.containerId}", bundle)
}
}
}
override fun onActivityPreDestroyed(activity: Activity) {
if (data.activity === activity && !activity.isChangingConfigurations) {
handleOnDestroy()
}
}
override fun onActivityDestroyed(activity: Activity) {
activeLifecycleHandlers.remove(activity)
}
private fun prepareForHostDetachIfNeeded() {
if (!data.hasPreparedForHostDetach) {
data.hasPreparedForHostDetach = true
routers.forEach { it.prepareForHostDetach() }
}
}
}
@Parcelize
internal class PendingPermissionRequest(
val instanceId: String,
val permissions: Array<String>,
val requestCode: Int,
) : Parcelable
internal class LifecycleHandlerData(
val isAndroidXLifecycleHandler: Boolean,
var activity: Activity? = null,
var hasRegisteredCallbacks: Boolean = false,
var destroyed: Boolean = false,
var attached: Boolean = false,
var hasPreparedForHostDetach: Boolean = false,
var permissionRequestMap: SparseArray<String> = SparseArray(),
var activityRequestMap: SparseArray<String> = SparseArray(),
var pendingPermissionRequests: ArrayList<PendingPermissionRequest> = arrayListOf(),
val routerMap: MutableMap<Int, ActivityHostedRouter> = mutableMapOf(),
)
private fun findInActivity(activity: Activity, allowAndroidXBacking: Boolean): LifecycleHandler? {
var lifecycleHandler = activeLifecycleHandlers[activity]
if (lifecycleHandler == null) {
lifecycleHandler = if (allowAndroidXBacking && activity is FragmentActivity) {
activity.supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? LifecycleHandler
} else {
@Suppress("DEPRECATION")
activity.fragmentManager.findFragmentByTag(FRAGMENT_TAG) as? LifecycleHandler
}
}
lifecycleHandler?.registerActivityListener(activity)
return lifecycleHandler
}
private fun routerHashKey(viewGroup: ViewGroup) = viewGroup.id
private val activeLifecycleHandlers = mutableMapOf<Activity, LifecycleHandler>()
private const val FRAGMENT_TAG = "LifecycleHandler"
private const val KEY_PENDING_PERMISSION_REQUESTS = "LifecycleHandler.pendingPermissionRequests"
private const val KEY_PERMISSION_REQUEST_CODES = "LifecycleHandler.permissionRequests"
private const val KEY_ACTIVITY_REQUEST_CODES = "LifecycleHandler.activityRequests"
private const val KEY_ROUTER_STATE_PREFIX = "LifecycleHandler.routerState"
@@ -6,6 +6,8 @@ import com.bluelinelabs.conductor.ControllerChangeHandler
class NoOpControllerChangeHandler : ControllerChangeHandler() {
override val isReusable = true
override fun performChange(
container: ViewGroup,
from: View?,
@@ -17,6 +19,4 @@ class NoOpControllerChangeHandler : ControllerChangeHandler() {
}
override fun copy(): ControllerChangeHandler = NoOpControllerChangeHandler()
override fun isReusable() = true
}
@@ -1,14 +1,16 @@
package com.bluelinelabs.conductor.internal
import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.ViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.ViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
@@ -28,6 +30,10 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
private var hasSavedState = false
private var savedRegistryState = Bundle.EMPTY
override val lifecycle: LifecycleRegistry
get() = lifecycleRegistry
override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry
init {
controller.addLifecycleListener(object : Controller.LifecycleListener() {
@@ -53,11 +59,8 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
view.getTag(R.id.view_tree_lifecycle_owner) == null &&
view.getTag(R.id.view_tree_saved_state_registry_owner) == null
) {
ViewTreeLifecycleOwner.set(view, this@OwnViewTreeLifecycleAndRegistry)
ViewTreeSavedStateRegistryOwner.set(
view,
this@OwnViewTreeLifecycleAndRegistry
)
view.setViewTreeLifecycleOwner(this@OwnViewTreeLifecycleAndRegistry)
view.setViewTreeSavedStateRegistryOwner(this@OwnViewTreeLifecycleAndRegistry)
}
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
@@ -76,7 +79,7 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
if (
controller === changeController &&
changeType.isEnter &&
changeHandler.removesFromViewOnPush() &&
changeHandler.removesFromViewOnPush &&
changeController.view?.windowToken != null &&
lifecycleRegistry.currentState == Lifecycle.State.STARTED
) {
@@ -84,29 +87,19 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
}
}
// AbstractComposeView adds its own OnAttachStateChangeListener by default. Since it
// does this on init, its detach callbacks get called before ours, which prevents us
// from saving state in onDetach. The if statement in here should detect upcoming
// detachment.
override fun onChangeStart(
changeController: Controller,
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
changeType: ControllerChangeType,
) {
if (
controller === changeController &&
!changeType.isEnter &&
changeHandler.removesFromViewOnPush() &&
changeController.view != null &&
lifecycleRegistry.currentState == Lifecycle.State.RESUMED
) {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
pauseOnChangeStart(
targetController = controller,
changeController = changeController,
changeHandler = changeHandler,
changeType = changeType,
)
savedRegistryState = Bundle()
savedStateRegistryController.performSave(savedRegistryState)
hasSavedState = true
}
GlobalChangeStartListener.onChangeStart(changeController, changeHandler, changeType)
}
override fun preDetach(controller: Controller, view: View) {
@@ -135,10 +128,9 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
override fun preDestroyView(controller: Controller, view: View) {
if (controller.isBeingDestroyed && controller.router.backstackSize == 0) {
val parent = view.parent as? View
parent?.addOnAttachStateChangeListener(object :
View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) = Unit
override fun onViewDetachedFromWindow(v: View?) {
parent?.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) = Unit
override fun onViewDetachedFromWindow(v: View) {
parent.removeOnAttachStateChangeListener(this)
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
@@ -147,18 +139,111 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
}
override fun postContextAvailable(controller: Controller, context: Context) {
listenForAncestorChangeStart(controller)
}
override fun preContextUnavailable(controller: Controller, context: Context) {
stopListeningForAncestorChangeStart(controller)
}
})
}
override fun getLifecycle() = lifecycleRegistry
private fun listenForAncestorChangeStart(controller: Controller) {
GlobalChangeStartListener.subscribe(controller, controller.ancestors()) { ancestor, changeHandler, changeType ->
// No-op on the case where we (the child controller) hasn't yet created a View as our parent is being
// changed out.
if (::lifecycleRegistry.isInitialized) {
pauseOnChangeStart(
targetController = ancestor,
changeController = ancestor,
changeHandler = changeHandler,
changeType = changeType,
)
}
}
}
override fun getSavedStateRegistry() = savedStateRegistryController.savedStateRegistry
private fun stopListeningForAncestorChangeStart(controller: Controller) {
GlobalChangeStartListener.unsubscribe(controller)
}
// AbstractComposeView adds its own OnAttachStateChangeListener by default. Since it
// does this on init, its detach callbacks get called before ours, which prevents us
// from saving state in onDetach. The if statement in here should detect upcoming
// detachment.
private fun pauseOnChangeStart(
targetController: Controller,
changeController: Controller,
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType,
) {
if (
targetController === changeController &&
!changeType.isEnter &&
changeHandler.removesFromViewOnPush &&
changeController.view != null &&
lifecycleRegistry.currentState == Lifecycle.State.RESUMED
) {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
savedRegistryState = Bundle()
savedStateRegistryController.performSave(savedRegistryState)
hasSavedState = true
}
}
private fun Controller.ancestors(): Collection<String> {
return buildList {
var ancestor = parentController
while (ancestor != null) {
add(ancestor.instanceId)
ancestor = ancestor.parentController
}
}
}
companion object {
private const val KEY_SAVED_STATE = "Registry.savedState"
fun own(target: Controller) {
OwnViewTreeLifecycleAndRegistry(target)
fun own(target: Controller): OwnViewTreeLifecycleAndRegistry {
return OwnViewTreeLifecycleAndRegistry(target)
}
}
}
// In order to prevent child controllers from having strong references to all of their ancestors, some of which may
// break their connection before the child is made aware, this shared listener is used to call all interested parties
// when a controller begins transitioning.
private object GlobalChangeStartListener {
private val listeners = mutableMapOf<String, Listener>()
fun subscribe(
controller: Controller,
targetControllers: Collection<String>,
listener: (Controller, ControllerChangeHandler, ControllerChangeType) -> Unit,
) {
listeners[controller.instanceId] = Listener(targetControllers, listener)
}
fun unsubscribe(controller: Controller) {
listeners.remove(controller.instanceId)
}
fun onChangeStart(controller: Controller, changeHandler: ControllerChangeHandler, changeType: ControllerChangeType) {
listeners.values.forEach { it.call(controller, changeHandler, changeType) }
}
private class Listener(
private val targetControllers: Collection<String>,
private val listener: (Controller, ControllerChangeHandler, ControllerChangeType) -> Unit,
) {
fun call(controller: Controller, changeHandler: ControllerChangeHandler, changeType: ControllerChangeType) {
if (targetControllers.contains(controller.instanceId)) {
listener(controller, changeHandler, changeType)
}
}
}
}
@@ -1,5 +1,7 @@
package com.bluelinelabs.conductor;
import static org.junit.Assert.assertEquals;
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
@@ -7,8 +9,6 @@ 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 {
@@ -32,10 +32,10 @@ public class ControllerChangeHandlerTests {
FadeChangeHandler restoredFadeCast = (FadeChangeHandler) restoredFade;
assertEquals(horizontalChangeHandler.getAnimationDuration(), restoredHorizontalCast.getAnimationDuration());
assertEquals(horizontalChangeHandler.removesFromViewOnPush(), restoredHorizontalCast.removesFromViewOnPush());
assertEquals(horizontalChangeHandler.getRemovesFromViewOnPush(), restoredHorizontalCast.getRemovesFromViewOnPush());
assertEquals(fadeChangeHandler.getAnimationDuration(), restoredFadeCast.getAnimationDuration());
assertEquals(fadeChangeHandler.removesFromViewOnPush(), restoredFadeCast.removesFromViewOnPush());
assertEquals(fadeChangeHandler.getRemovesFromViewOnPush(), restoredFadeCast.getRemovesFromViewOnPush());
}
}
@@ -112,7 +112,7 @@ class ControllerLifecycleActivityReferenceTests {
child.addLifecycleListener(listener)
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
childRouter.setPopsLastView(true)
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.pushController(
child.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
@@ -146,7 +146,7 @@ class ControllerLifecycleActivityReferenceTests {
child.addLifecycleListener(listener)
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
childRouter.setPopsLastView(true)
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.pushController(
child.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
@@ -155,7 +155,7 @@ class ControllerLifecycleActivityReferenceTests {
)
activity.router.popCurrentController()
Assert.assertEquals(listOf(true), listener.changeEndReferences)
Assert.assertEquals(listOf(true, true), listener.changeEndReferences)
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
Assert.assertEquals(listOf(true), listener.postAttachReferences)
Assert.assertEquals(listOf(true), listener.postDetachReferences)
@@ -199,7 +199,7 @@ class ControllerLifecycleActivityReferenceTests {
val listener = ActivityReferencingLifecycleListener()
child.addLifecycleListener(listener)
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
childRouter.setPopsLastView(true)
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.pushController(
child.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
@@ -208,7 +208,7 @@ class ControllerLifecycleActivityReferenceTests {
)
activityController.pause().stop().destroy()
Assert.assertEquals(listOf(true), listener.changeEndReferences)
Assert.assertEquals(listOf(true, true), listener.changeEndReferences)
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
Assert.assertEquals(listOf(true), listener.postAttachReferences)
Assert.assertEquals(listOf(true), listener.postDetachReferences)
@@ -465,6 +465,8 @@ class ControllerLifecycleCallbacksTests {
assertCalls(expectedCallState, child)
activityController.get().router.popCurrentController()
expectedCallState.changeStartCalls++
expectedCallState.changeEndCalls++
expectedCallState.detachCalls++
expectedCallState.destroyViewCalls++
expectedCallState.contextUnavailableCalls++
@@ -129,16 +129,8 @@ class ControllerTests {
assertCalls(expectedCallState, controller)
// Ensure requesting the permission gets us the result back
try {
controller.requestPermissions(requestedPermissions, 1)
} catch (ignored: NoSuchMethodError) { }
controller.requestPermissions(requestedPermissions, 1)
router.onRequestPermissionsResult(
controller.instanceId,
1,
requestedPermissions,
intArrayOf(1)
)
expectedCallState.onRequestPermissionsResultCalls++
assertCalls(expectedCallState, controller)
}
@@ -162,11 +154,8 @@ class ControllerTests {
assertCalls(parentExpectedCallState, parent)
// Ensure requesting the permission gets us the result back
try {
child.requestPermissions(requestedPermissions, 1)
} catch (ignored: NoSuchMethodError) { }
child.requestPermissions(requestedPermissions, 1)
router.onRequestPermissionsResult(child.instanceId, 1, requestedPermissions, intArrayOf(1))
childExpectedCallState.onRequestPermissionsResultCalls++
assertCalls(childExpectedCallState, child)
assertCalls(parentExpectedCallState, parent)
@@ -276,7 +265,7 @@ class ControllerTests {
Assert.assertNull(child2.parentController)
var childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
childRouter.setPopsLastView(true)
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.setRoot(child1.asTransaction())
Assert.assertEquals(1, parent.childRouters.size)
Assert.assertEquals(childRouter, parent.childRouters[0])
@@ -362,7 +351,7 @@ class ControllerTests {
val childTransaction1 = TestController().asTransaction()
val childTransaction2 = TestController().asTransaction()
var childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.CHILD_VIEW_ID_1))
childRouter.setPopsLastView(true)
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.setRoot(childTransaction1)
childRouter.pushController(childTransaction2)
val savedState = Bundle()
@@ -124,7 +124,7 @@ class ReattachCaseTests {
val childRouter = controllerB.getChildRouter(
controllerB.view!!.findViewById(TestController.VIEW_ID)
)
childRouter.setPopsLastView(true)
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.pushController(
childController.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
@@ -179,7 +179,7 @@ class ReattachCaseTests {
val childRouter = controllerB.getChildRouter(
controllerB.view!!.findViewById(TestController.VIEW_ID)
)
childRouter.setPopsLastView(true)
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.pushController(
childController.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
@@ -0,0 +1,190 @@
package com.bluelinelabs.conductor.internal
import android.content.Context
import android.os.Looper
import android.view.View
import androidx.lifecycle.Lifecycle
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.TestController
import com.bluelinelabs.conductor.asTransaction
import com.bluelinelabs.conductor.util.TestActivity
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class OwnViewTreeLifecycleAndRegistryTest {
private val router = Robolectric.buildActivity(TestActivity::class.java)
.setup()
.get()
.router
@Test
fun `onCreate lifecycle event before create view`() {
assertControllerState(
preCreateViewAssertedState = Lifecycle.State.CREATED,
setup = { router.setRoot(it.asTransaction()) }
)
}
@Test
fun `onStart lifecycle event after create view`() {
assertControllerState(
postCreateViewAssertedState = Lifecycle.State.STARTED,
setup = { router.setRoot(it.asTransaction()) }
)
}
@Test
fun `onResume lifecycle event on attach`() {
assertControllerState(
postAttachAssertedState = Lifecycle.State.RESUMED,
setup = { router.setRoot(it.asTransaction()) }
)
}
@Test
fun `onPause lifecycle event on exit change start`() {
assertControllerState(
onChangeStartAssertedState = Lifecycle.State.STARTED,
setup = {
router.setRoot(it.asTransaction())
router.pushController(TestController().asTransaction())
}
)
}
@Test
fun `onPause lifecycle event on parent exit change start`() {
val parent = TestController()
val controller = TestController()
var hasAsserted = false
val ownViewTreeLifecycleAndRegistry = OwnViewTreeLifecycleAndRegistry.own(controller)
// Ensure our listener gets added after OwnViewTreeLifecycleAndRegistry's by waiting until
// postContextAvailable to add the lifecycle listener on the parent controller
controller.addLifecycleListener(object : Controller.LifecycleListener() {
override fun postContextAvailable(controller: Controller, context: Context) {
parent.addLifecycleListener(object : Controller.LifecycleListener() {
override fun onChangeStart(
controller: Controller,
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
Assert.assertEquals(Lifecycle.State.STARTED, ownViewTreeLifecycleAndRegistry.lifecycle.currentState)
hasAsserted = true
}
})
}
})
router.setRoot(parent.asTransaction())
parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID)).setRoot(controller.asTransaction())
router.pushController(TestController().asTransaction())
Shadows.shadowOf(Looper.getMainLooper()).idle()
Assert.assertTrue(hasAsserted)
}
@Test
fun `onStop lifecycle event on detach`() {
assertControllerState(
preDetachAssertedState = Lifecycle.State.CREATED,
setup = {
router.setRoot(it.asTransaction())
router.pushController(TestController().asTransaction())
}
)
}
@Test
fun `onDestroy lifecycle event on destroy view`() {
assertControllerState(
preDestroyViewAssertedState = Lifecycle.State.DESTROYED,
setup = {
router.setRoot(it.asTransaction())
router.pushController(TestController().asTransaction())
}
)
}
private fun assertControllerState(
preCreateViewAssertedState: Lifecycle.State? = null,
postCreateViewAssertedState: Lifecycle.State? = null,
postAttachAssertedState: Lifecycle.State? = null,
preDetachAssertedState: Lifecycle.State? = null,
preDestroyViewAssertedState: Lifecycle.State? = null,
onChangeStartAssertedState: Lifecycle.State? = null,
setup: (Controller) -> Unit = { },
) {
val controller = TestController()
val ownViewTreeLifecycleAndRegistry = OwnViewTreeLifecycleAndRegistry.own(controller)
var hasAsserted = false
val assertState: (Lifecycle.State) -> Unit = {
Assert.assertEquals(it, ownViewTreeLifecycleAndRegistry.lifecycle.currentState)
}
controller.addLifecycleListener(object : Controller.LifecycleListener() {
override fun preCreateView(controller: Controller) {
preCreateViewAssertedState?.let {
assertState(it)
hasAsserted = true
}
}
override fun postCreateView(controller: Controller, view: View) {
postCreateViewAssertedState?.let {
assertState(it)
hasAsserted = true
}
}
override fun postAttach(controller: Controller, view: View) {
postAttachAssertedState?.let {
assertState(it)
hasAsserted = true
}
}
override fun preDetach(controller: Controller, view: View) {
preDetachAssertedState?.let {
assertState(it)
hasAsserted = true
}
}
override fun preDestroyView(controller: Controller, view: View) {
preDestroyViewAssertedState?.let {
assertState(it)
hasAsserted = true
}
}
override fun onChangeStart(
controller: Controller,
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
if (!changeType.isEnter) {
onChangeStartAssertedState?.let {
assertState(it)
hasAsserted = true
}
}
}
})
setup(controller)
Shadows.shadowOf(Looper.getMainLooper()).idle()
Assert.assertTrue(hasAsserted)
}
}
@@ -1,11 +1,12 @@
package com.bluelinelabs.conductor.util;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.ControllerChangeHandler;
public class MockChangeHandler extends ControllerChangeHandler {
@@ -92,7 +93,7 @@ public class MockChangeHandler extends ControllerChangeHandler {
}
@Override
public boolean removesFromViewOnPush() {
public boolean getRemovesFromViewOnPush() {
return removesFromViewOnPush;
}
@@ -1,11 +1,11 @@
package com.bluelinelabs.conductor.util
import android.app.Activity
import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Router
class TestActivity : Activity() {
class TestActivity : FragmentActivity() {
lateinit var router: Router
+15 -20
View File
@@ -5,7 +5,7 @@ android {
defaultConfig {
applicationId "com.bluelinelabs.conductor.demo"
minSdkVersion 21
targetSdkVersion rootProject.ext.targetSdkVersion
targetSdkVersion libs.versions.targetsdk.get()
versionCode 1
versionName "1.0.0"
vectorDrawables.useSupportLibrary true
@@ -27,7 +27,7 @@ android {
compose = true
}
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion 33
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -39,21 +39,19 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion composeVersion
kotlinCompilerExtensionVersion libs.versions.compose.compiler.get()
}
}
dependencies {
implementation rootProject.ext.androidxAppCompat
implementation rootProject.ext.androidxViewPager2
implementation rootProject.ext.material
implementation rootProject.ext.androidxCoreKtx
implementation libs.androidx.appcompat
implementation libs.androidx.viewpager2
implementation libs.material
implementation libs.androidx.core.ktx
implementation rootProject.ext.archComponentsLiveDataCore // Fix duplicate classes
implementation libs.picasso
implementation rootProject.ext.picasso
implementation rootProject.ext.autodisposeKtx
implementation libs.autodispose.ktx
implementation project(':conductor')
implementation project(':conductor-modules:viewpager')
@@ -62,14 +60,11 @@ dependencies {
implementation project(':conductor-modules:arch-components-lifecycle')
implementation project(':conductor-modules:androidx-transition')
implementation "androidx.compose.ui:ui:$composeVersion"
implementation "androidx.compose.ui:ui-tooling:$composeVersion"
implementation "androidx.compose.foundation:foundation:$composeVersion"
implementation "androidx.compose.material:material:$composeVersion"
implementation "androidx.compose.material:material-icons-core:$composeVersion"
implementation "androidx.compose.material:material-icons-extended:$composeVersion"
implementation "androidx.activity:activity-compose:1.3.0-beta02"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07"
implementation libs.compose.ui
implementation libs.compose.ui.tooling
implementation libs.compose.foundation
implementation libs.compose.material
implementation libs.activity.compose
implementation rootProject.ext.leakCanary
implementation libs.leakCanary
}
@@ -3,9 +3,10 @@ package com.bluelinelabs.conductor.demo
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import com.bluelinelabs.conductor.Conductor.attachRouter
import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
import com.bluelinelabs.conductor.Router.PopRootControllerMode
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.demo.controllers.HomeController
import com.bluelinelabs.conductor.demo.databinding.ActivityMainBinding
@@ -22,15 +23,14 @@ class MainActivity : AppCompatActivity(), ToolbarProvider {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
router = attachRouter(this, binding.controllerContainer, savedInstanceState)
router = Conductor.attachRouter(this, binding.controllerContainer, savedInstanceState)
.setPopRootControllerMode(PopRootControllerMode.NEVER)
.setOnBackPressedDispatcherEnabled(true)
if (!router.hasRootController()) {
router.setRoot(with(HomeController()))
router.setRoot(RouterTransaction.with(HomeController()))
}
}
override fun onBackPressed() {
if (!router.handleBack()) {
super.onBackPressed()
}
}
}
}
@@ -1,20 +1,16 @@
package com.bluelinelabs.conductor.demo.changehandler;
import android.animation.Animator;
import android.annotation.TargetApi;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler;
/**
* An {@link AnimatorChangeHandler} that will perform a circular reveal
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class CircularRevealChangeHandler extends AnimatorChangeHandler {
private static final String KEY_CX = "CircularRevealChangeHandler.cx";
@@ -1,7 +1,6 @@
package com.bluelinelabs.conductor.demo.controllers
import android.content.Intent
import android.graphics.PorterDuff
import android.net.Uri
import android.os.Bundle
import android.text.SpannableString
@@ -14,6 +13,8 @@ import android.view.ViewGroup
import androidx.annotation.ColorRes
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.text.buildSpannedString
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -112,7 +113,7 @@ class HomeController : BaseController(R.layout.controller_home) {
}
DemoModel.CHILD_CONTROLLERS -> {
router.pushController(
RouterTransaction.with(ParentController())
RouterTransaction.with(MultipleChildRouterController())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
)
@@ -152,13 +153,6 @@ class HomeController : BaseController(R.layout.controller_home) {
.popChangeHandler(HorizontalChangeHandler())
)
}
DemoModel.MULTIPLE_CHILD_ROUTERS -> {
router.pushController(
RouterTransaction.with(MultipleChildRouterController())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
)
}
DemoModel.MASTER_DETAIL -> {
router.pushController(
RouterTransaction.with(MasterDetailListController())
@@ -219,7 +213,6 @@ private enum class DemoModel(val title: String, @ColorRes val color: Int) {
VIEW_PAGER("ViewPager", R.color.green_300),
VIEW_PAGER_2("ViewPager2", R.color.pink_300),
TARGET_CONTROLLER("Target Controller", R.color.deep_orange_300),
MULTIPLE_CHILD_ROUTERS("Multiple Child Routers", R.color.grey_300),
MASTER_DETAIL("Master Detail", R.color.lime_300),
DRAG_DISMISS("Drag Dismiss", R.color.teal_300),
EXTERNAL_MODULES("Bonus Modules", R.color.deep_purple_300)
@@ -251,9 +244,9 @@ private class HomeAdapter(
fun bind(position: Int, item: DemoModel) {
binding.title.text = item.title
binding.dot.drawable.setColorFilter(
binding.dot.drawable.colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
ContextCompat.getColor(binding.root.context, item.color),
PorterDuff.Mode.SRC_ATOP
BlendModeCompat.SRC_ATOP,
)
binding.root.setOnClickListener { modelClickListener(item, position) }
@@ -263,4 +256,4 @@ private class HomeAdapter(
binding.root.resources.getString(R.string.transition_tag_dot_indexed, position)
}
}
}
}
@@ -1,6 +1,7 @@
package com.bluelinelabs.conductor.demo.controllers
import android.view.View
import com.bluelinelabs.conductor.Router.PopRootControllerMode
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
@@ -18,10 +19,11 @@ class MultipleChildRouterController : BaseController(R.layout.controller_multipl
val childContainers = listOf(binding.container0, binding.container1, binding.container2)
childContainers.forEach { container ->
val childRouter = getChildRouter(container).setPopsLastView(false)
val childRouter = getChildRouter(container).setPopRootControllerMode(PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW)
.setPopRootControllerMode(PopRootControllerMode.NEVER)
if (!childRouter.hasRootController()) {
childRouter.setRoot(RouterTransaction.with(NavigationDemoController(0, NavigationDemoController.DisplayUpMode.HIDE)))
}
}
}
}
}
@@ -1,102 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
import com.bluelinelabs.conductor.demo.databinding.ControllerParentBinding
import com.bluelinelabs.conductor.demo.util.getMaterialColor
import com.bluelinelabs.conductor.demo.util.viewBinding
class ParentController : BaseController(R.layout.controller_parent) {
private val binding: ControllerParentBinding by viewBinding(ControllerParentBinding::bind)
override val title = "Parent/Child Demo"
private var hasShownAll = false
private var animatingOut = false
override fun onChangeEnded(
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
if (changeType == ControllerChangeType.PUSH_ENTER) {
addChild(0)
}
}
override fun handleBack(): Boolean {
val childControllers = childRouters.filter { it.hasRootController() }.size
return if (childControllers != NUMBER_OF_CHILDREN || animatingOut) {
true
} else {
animatingOut = true
super.handleBack()
}
}
private fun addChild(index: Int) {
val container = when (index) {
0 -> binding.childContent1
1 -> binding.childContent2
2 -> binding.childContent3
3 -> binding.childContent4
4 -> binding.childContent5
else -> throw IllegalStateException("Invalid child index $index")
}
val childRouter = getChildRouter(container).setPopsLastView(true)
if (!childRouter.hasRootController()) {
val child = ChildController(
title = "Child Controller #$index",
backgroundColor = resources!!.getMaterialColor(index),
colorIsResId = false
)
child.addLifecycleListener(object : LifecycleListener() {
override fun onChangeEnd(
controller: Controller,
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
if (!isBeingDestroyed) {
if (changeType == ControllerChangeType.PUSH_ENTER && !hasShownAll) {
if (index < NUMBER_OF_CHILDREN - 1) {
addChild(index + 1)
} else {
hasShownAll = true
}
} else if (changeType == ControllerChangeType.POP_EXIT) {
if (index > 0) {
removeChild(index - 1)
} else {
router.popController(this@ParentController)
}
}
}
}
})
childRouter.setRoot(
RouterTransaction.with(child)
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
)
}
}
private fun removeChild(index: Int) {
val childRouters = childRouters
if (index < childRouters.size) {
removeChildRouter(childRouters[index])
}
}
companion object {
private const val NUMBER_OF_CHILDREN = 5
}
}
@@ -9,12 +9,13 @@ import androidx.appcompat.widget.Toolbar
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.archlifecycle.LifecycleController
import com.bluelinelabs.conductor.demo.ToolbarProvider
abstract class BaseController(
@LayoutRes private val layoutRes: Int,
args: Bundle? = null
) : Controller(args) {
) : LifecycleController(args) {
init {
watchForLeaks()
@@ -1,5 +1,6 @@
package com.bluelinelabs.conductor.demo.controllers.base
import android.view.View
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
@@ -15,6 +16,13 @@ private class RefWatchingControllerLifecycleListener : Controller.LifecycleListe
}
}
override fun preDestroyView(controller: Controller, view: View) {
AppWatcher.objectWatcher.expectWeaklyReachable(
view,
"A destroyed controller view should have only weak references."
)
}
override fun onChangeEnd(
controller: Controller,
changeHandler: ControllerChangeHandler,
@@ -18,12 +18,9 @@ package com.bluelinelabs.conductor.demo.util;
import android.animation.Animator;
import android.animation.TimeInterpolator;
import android.annotation.TargetApi;
import android.os.Build;
import android.util.ArrayMap;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import androidx.annotation.NonNull;
import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
@@ -82,7 +79,6 @@ public class AnimUtils {
* Interrupting Activity transitions can yield an OperationNotSupportedException when the
* transition tries to pause the animator. Yikes! We can fix this by wrapping the Animator:
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
public static class NoPauseAnimator extends Animator {
private final Animator mAnimator;
private final ArrayMap<AnimatorListener, AnimatorListener> mListeners = new ArrayMap<>();
@@ -16,14 +16,13 @@
package com.bluelinelabs.conductor.demo.widget;
import android.annotation.TargetApi;
import android.content.Context;
import androidx.core.view.NestedScrollingParent;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.widget.FrameLayout;
import androidx.core.view.NestedScrollingParent;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import java.util.ArrayList;
import java.util.List;
@@ -33,7 +32,6 @@ import java.util.List;
* Applies an elasticity factor to reduce movement as you approach the given dismiss distance.
* Optionally also scales down content during drag.
*/
@TargetApi(21)
public class ElasticDragDismissFrameLayout extends FrameLayout implements NestedScrollingParent {
public static abstract class ElasticDragDismissCallback {
@@ -1,56 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/orange_300"
android:paddingLeft="24dp"
android:paddingRight="24dp" >
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="28sp"
android:padding="16dp"
android:text="@string/parent_controller"
/>
<FrameLayout
android:id="@+id/child_content_1"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
/>
<FrameLayout
android:id="@+id/child_content_2"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
/>
<FrameLayout
android:id="@+id/child_content_3"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
/>
<FrameLayout
android:id="@+id/child_content_4"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
/>
<FrameLayout
android:id="@+id/child_content_5"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
/>
</LinearLayout>
@@ -14,7 +14,6 @@
android:elevation="6dp"
android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
tools:targetApi="lollipop"
/>
<androidx.viewpager.widget.ViewPager
@@ -14,7 +14,6 @@
android:elevation="6dp"
android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
tools:targetApi="lollipop"
/>
<androidx.viewpager2.widget.ViewPager2
-55
View File
@@ -1,55 +0,0 @@
ext {
minSdkVersion = 16
compileSdkVersion = 28
targetSdkVersion = 28
picassoVersion = '2.5.2'
rxJava2Version = '2.1.14'
autodisposeVersion = '1.0.0'
archComponentsVersion = '2.3.1'
junitVersion = '4.12'
mvnPublishVersion = '0.13.0'
dokkaVersion = '1.4.32'
composeVersion = "1.0.0-beta09"
agpVersion = '7.0.3'
lintVersion = agpVersion.replaceFirst(~/\d*/) { version ->
// the major version of lint is always 23 version higher than the major version of agp
version.toInteger() + 23
}
kotlinVersion = '1.5.10'
kotlinStd = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
material = "com.google.android.material:material:1.1.0"
androidxAnnotations = "androidx.annotation:annotation:1.1.0"
androidxAppCompat = "androidx.appcompat:appcompat:1.3.0"
androidxTransition = "androidx.transition:transition:1.3.1"
androidxCollection = "androidx.collection:collection:1.1.0"
androidxViewPager2 = "androidx.viewpager2:viewpager2:1.0.0"
androidxCoreKtx = "androidx.core:core-ktx:1.3.2"
picasso = "com.squareup.picasso:picasso:$picassoVersion"
leakCanary = "com.squareup.leakcanary:leakcanary-android:2.7"
rxJava2 = "io.reactivex.rxjava2:rxjava:$rxJava2Version"
autodispose = "com.uber.autodispose:autodispose:$autodisposeVersion"
autodisposeLifecycle = "com.uber.autodispose:autodispose-lifecycle:$autodisposeVersion"
autodisposeKtx = "com.uber.autodispose:autodispose-ktx:$autodisposeVersion"
archComponentsLifecycle = "androidx.lifecycle:lifecycle-runtime:$archComponentsVersion"
archComponentsLiveDataCore = "androidx.lifecycle:lifecycle-livedata-core:$archComponentsVersion"
savedState = "androidx.savedstate:savedstate-ktx:1.1.0"
junit = "junit:junit:$junitVersion"
robolectric = "org.robolectric:robolectric:4.5.1"
kotestAssertions = "io.kotest:kotest-assertions-core:4.6.0"
lintapi = "com.android.tools.lint:lint-api:$lintVersion"
lintchecks = "com.android.tools.lint:lint-checks:$lintVersion"
lint = "com.android.tools.lint:lint:$lintVersion"
lintTests = "com.android.tools.lint:lint-tests:$lintVersion"
}
+4 -2
View File
@@ -1,5 +1,5 @@
VERSION_CODE=3
VERSION_NAME=3.1.2-SNAPSHOT
VERSION_CODE=4
VERSION_NAME=4.0.0-SNAPSHOT
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536m
@@ -14,3 +14,5 @@ POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
POM_LICENCE_DIST=repo
POM_DEVELOPER_ID=erickuck
POM_DEVELOPER_NAME=Eric Kuck
SONATYPE_HOST=DEFAULT
RELEASE_SIGNING_ENABLED=true
+68
View File
@@ -0,0 +1,68 @@
[versions]
minsdk = "16"
compilesdk = "33"
targetsdk = "28"
agp = "7.4.2" # Note: update lint version whenever this is updated
androidx-activity = "1.6.1"
androidx-annotation = "1.1.0"
androidx-appcompat = "1.4.2"
androidx-collection = "1.1.0"
androidx-core = "1.3.2"
androidx-lifecycle = "2.6.0"
androidx-savedstate = "1.2.0"
androidx-transition = "1.3.1"
androidx-viewpager2 = "1.0.0"
autodispose = "1.0.0"
compose = "1.3.1"
compose-compiler = "1.4.3"
dokka = "1.8.10"
junit = "4.13"
kotest = "4.6.0"
kotlin = "1.8.10"
leakCanary = "2.7"
lint = "30.4.2" # Should always be agp + 23
material = "1.2.1"
mvnpublish = "0.23.2"
picasso = "2.5.2"
robolectric = "4.5.1"
rxjava2 = "2.1.14"
[libraries]
activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
agp = { module = "com.android.tools.build:gradle", version.ref = "agp" }
androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" }
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
androidx-collection = { module = "androidx.collection:collection", version.ref = "androidx-collection" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "androidx-lifecycle" }
androidx-lifecycle-livedata-core = { module = "androidx.lifecycle:lifecycle-livedata-core", version.ref = "androidx-lifecycle" }
androidx-savedstate-ktx = { module = "androidx.savedstate:savedstate-ktx", version.ref = "androidx-savedstate" }
androidx-transition = { module = "androidx.transition:transition", version.ref = "androidx-transition" }
androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "androidx-viewpager2" }
autodispose = { module = "com.uber.autodispose:autodispose", version.ref = "autodispose" }
autodispose-lifecycle = { module = "com.uber.autodispose:autodispose-lifecycle", version.ref = "autodispose" }
autodispose-ktx = { module = "com.uber.autodispose:autodispose-ktx", version.ref = "autodispose" }
compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" }
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
compose-material = { module = "androidx.compose.material:material", version.ref = "compose" }
dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
junit = { module = "junit:junit", version.ref = "junit" }
kotest = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
leakCanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakCanary" }
lint = { module = "com.android.tools.lint:lint", version.ref = "lint" }
lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "lint" }
lint-checks = { module = "com.android.tools.lint:lint-checks", version.ref = "lint" }
lint-tests = { module = "com.android.tools.lint:lint-tests", version.ref = "lint" }
material = { module = "com.google.android.material:material", version.ref = "material" }
picasso = { module = "com.squareup.picasso:picasso", version.ref = "picasso" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
rxjava2 = { module = "io.reactivex.rxjava2:rxjava", version.ref = "rxjava2" }
[plugins]
mvnpublish = { id = "com.vanniktech.maven.publish", version.ref = "mvnpublish" }
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+15
View File
@@ -1,3 +1,18 @@
pluginManagement {
repositories {
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
google()
}
}
include ':conductor'
include ':conductor-lint'
include ':conductor-modules:viewpager'