Compare commits

...

133 Commits

Author SHA1 Message Date
Jameson Williams 56bff9a5cf Refine highlighting and error reporting in Lint rules (#693) 2024-04-10 17:00:41 -04:00
Steven Schoen 5a1746b2d3 Fix doc mistake in ControllerChangeHandler (#691) 2024-01-11 13:28:13 -05:00
Steven Schoen 2ffafaee79 Add kdoc to ControllerChangeHandler::removesFromViewOnPush (#688) 2023-09-29 23:19:21 -04:00
EricKuck eabfc005d0 Preview version bump 2023-08-15 14:50:42 -04:00
EricKuck a90ca5180e Fix issue with child controllers frozen at detach time not re-attaching 2023-08-15 14:30:19 -04:00
EricKuck 4d4d8bfbd1 Fix ConcurrentModificationException when manipulating backstack during instantiation 2023-05-09 11:09:58 -04:00
EricKuck f69fab6062 Merge LifecycleOwner into Controller, add back handling demo 2023-03-24 11:50:37 -04:00
Eric Kuck 590debf975 Conductor 4.0-preview-1 (#686) 2023-03-22 11:26:52 -05: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
EricKuck 8297e0273d RouterStateAdapter now properly restores routers after process death 2021-11-28 11:27:47 -06:00
EricKuck 46519c2c2c Ensure preContextUnavailable can't use a null context in the parameter
Fixes #377 and #516
2021-11-23 15:50:52 -06:00
EricKuck 211da8b2ea Version bump for 3.1.1 release 2021-07-19 17:42:43 -05:00
EricKuck 26db962168 Revert maven-publish plugin migration 2021-07-19 17:41:17 -05:00
EricKuck f4c1c6ccf5 Provide a bit more safety around pushing controllers outside of normal lifecycle events 2021-07-19 17:20:32 -05:00
Mario c89caa87e0 Apply the kotlin plugin to lint module (#653)
Fixes missing IssueRegistry from lint.jar
2021-07-19 13:08:34 -05:00
Paul Woitaschek 2748566437 Make the StringSparseArrayParceler internal. 2021-07-03 22:09:39 +02:00
Paul Woitaschek 506c99ed41 Convert the StringSparseArrayParceler to kotlin. 2021-07-03 22:08:18 +02:00
Paul Woitaschek 4fe0ec5f51 Rename .java to .kt 2021-07-03 22:08:18 +02:00
Paul Woitaschek afa93f2cc1 Unit-test the StringSparseArrayParceler. 2021-07-03 22:02:08 +02:00
Paul Woitaschek 836f92b615 Update the publish task name. 2021-07-03 21:41:21 +02:00
Paul Woitaschek 94c817bbd9 Remove support for rxLifecycle2. 2021-07-03 21:40:43 +02:00
Paul Woitaschek fc1fee3e17 Kotlin backstack (#652)
Migrate the Backstack to kotlin
2021-07-03 21:32:11 +02:00
Paul Woitaschek 76b7572a01 Migrate to the maven-publish plugin. (#651) 2021-07-03 21:31:24 +02:00
Paul Woitaschek 3fc63b7f5f Remove the deprecated kotlin-android-extensions plugin and use the kotlin-parcelize plugin replacement instead. 2021-07-03 14:37:40 +02:00
Paul Woitaschek 0ef52211a2 Update LeakCanary to 2.7. (#650)
Also simplify the usage by extracting the leak watching logic into a lifecycle listener.
2021-07-03 14:31:04 +02:00
Paul Woitaschek 7574131940 Remove the unused rxJavaVersion declaration. 2021-07-02 22:40:01 +02:00
Paul Woitaschek 1ab9a4c4f6 Remove the unused rxLifecycleVersion declaration. 2021-07-02 22:35:52 +02:00
Paul Woitaschek 3bc23bd5cd Update to agp 7.0.0-beta05 2021-07-02 22:31:53 +02:00
EricKuck 5f138e5d43 Version bump for 3.1.0 release 2021-07-02 11:18:50 -05:00
Paul Woitaschek 03701d05a9 Add basic support for compose (#644)
Includes required lifecycle and saved state owners
2021-07-02 10:23:00 -05:00
Paul Woitaschek a19968e0c9 Update dokka 2021-06-18 08:19:31 +02:00
Paul Woitaschek 5501ab2ac8 Remove the deprecated jcenter repo. 2021-06-17 21:48:52 +02:00
Paul Woitaschek 804fdb615e Update Gradle to 7.1 2021-06-16 12:29:18 +02:00
EricKuck c01b2a74d6 Version bump 2021-02-05 15:33:01 -06:00
EricKuck 8a8622c261 Fixed issue with VP2 routers potentially restoring incorrectly 2021-02-05 13:04:37 -06:00
Eric Kuck 6820aa7d6a Convert demo app to Kotlin w/ ViewBinding (#635) 2020-12-22 13:09:57 -06:00
EricKuck 9ce27e4dee Added proguard rules to keep empty constructors 2020-12-22 11:49:40 -06:00
Eric Kuck 3c8ad0a833 Switch to GitHub Actions for CI 2020-12-21 14:53:11 -06:00
EricKuck a720ac57e8 Move env vars out of travis config 2020-12-21 11:28:31 -06:00
EricKuck 7d6901389b Always attempt to restore child controllers, even if the views might not exist anymore
Likely fixes #631, #632
2020-11-30 22:43:19 -06:00
EricKuck e54e88bf0d Ensure VP2 adapter saves visible controller instance states
Fixes #634
2020-11-30 22:37:00 -06:00
EricKuck 010117603c Ensure all views are removed when VP2 pages are detached 2020-11-06 10:25:49 -06:00
EricKuck cd11ac9d6b Version bump for 3.0.0 release 2020-11-05 14:40:15 -06:00
EricKuck e78347709b Revert to Java 7 - we'll be kotlinizing soon anyway 2020-11-05 14:12:34 -06:00
EricKuck 341debc5b9 Bump back kotlin version 2020-11-04 16:59:35 -06:00
EricKuck 2346e48154 Fix errant onAttach calls for children whose parents were popped on attach
Fixes #481
Fixes #426
Fixes #407
2020-11-04 16:58:04 -06:00
EricKuck 4174e12958 Fix bug where old views could remain in the hierarchy after being popped
Fixes #608
2020-11-04 16:06:55 -06:00
EricKuck c0abed0813 Fix popping a controller during view creation
Fixes #348
2020-11-04 12:58:50 -06:00
Eric Kuck 6fdb1d6ed3 Adds ViewPager2 Adapter (#594) 2020-11-03 16:17:30 -06:00
Eric Kuck 3334b8e21f Don't remove host references on configuration changes (#629)
Should fix #567
2020-11-03 16:13:17 -06:00
Paul Woitaschek 240424dc63 Lint changehandler subclass fix (#627)
* Updated gradle to 6.7

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

* Suppress the UnstableApiUsage on the lint tests.

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

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

* Added a controller stub for the controleller detector tests.

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

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

* Accept licenses before installing for travis.

* Attempt to not add the android components.

* Removed the licenses command.

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

* Migrated the ThreadUtils to kotlin.

* Migrated the TransactionIndexer to kotlin.

* Migrated the ChangeHandlerFrameLayout to kotlin.

* Migrated Conductor to kotlin.

* Added RestrictTo annotations.

* Migrated the RouterRequiringFunc to kotlin.

* Migrated the RouterTransaction to kotlin

* Make use of diamonds.

* Make use of lambdas and method references.

* Use an interface with default implementations for the LifecycleListener.

* Simplify the attachRouter function.

* Updated the code style and checkin the idea folder.

* Use a Controller extension function for the router transaction.

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

* Added default implementations of ControllerChangeListener

* Migrated the NoOpControllerChangeHandler to kotlin.

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

* Add JvmField annotations to the ControllerChangeType fields.

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

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

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

* Remove fully qualified reference

Signed-off-by: shaishavgandhi05 <shaishgandhi@gmail.com>
2018-08-22 09:26:50 -05:00
Eric Kuck 7a24f77b59 No longer re-attaches controllers that shouldn't be attached when popController(controller) is called. Fixes #463. 2018-08-09 14:40:15 -05:00
Paul Woitaschek 562ed171c3 Specify the current api for the IssueRegistry. (#460) 2018-08-02 09:18:56 +02:00
252 changed files with 9198 additions and 10466 deletions
-26
View File
@@ -1,26 +0,0 @@
#!/bin/bash
#
# Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo.
#
# Adapted from https://coderwall.com/p/9b_lfq and
# http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/
SLUG="bluelinelabs/Conductor"
JDK="oraclejdk8"
BRANCH="develop"
set -e
if [ "$TRAVIS_REPO_SLUG" != "$SLUG" ]; then
echo "Skipping snapshot deployment: wrong repository. Expected '$SLUG' but was '$TRAVIS_REPO_SLUG'."
elif [ "$TRAVIS_JDK_VERSION" != "$JDK" ]; then
echo "Skipping snapshot deployment: wrong JDK. Expected '$JDK' but was '$TRAVIS_JDK_VERSION'."
elif [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
echo "Skipping snapshot deployment: was pull request."
elif [ "$TRAVIS_BRANCH" != "$BRANCH" ]; then
echo "Skipping snapshot deployment: wrong branch. Expected '$BRANCH' but was '$TRAVIS_BRANCH'."
else
echo "Deploying snapshot..."
./gradlew clean uploadArchives
echo "Snapshot deployed!"
fi
+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
+38
View File
@@ -0,0 +1,38 @@
name: Test & Publish
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: set up JDK 11
uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '11'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Test with Gradle
run: ./gradlew test
publish:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
needs: test
steps:
- uses: actions/checkout@v2
- name: set up JDK 11
uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '11'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Release with Gradle
run: ./gradlew clean publishAllPublicationsToMavenCentral
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
+3 -4
View File
@@ -5,10 +5,11 @@
.checkstyle
# IntelliJ IDEA
.idea
*.iml
*.ipr
*.iws
/.idea/*
!/.idea/scopes/
classes
gen-external-apklibs
@@ -35,6 +36,4 @@ pom.xml.*
local.properties
*.prefs
# The keystore file
app/spothero-release.keystore
.DS_Store
+9
View File
@@ -0,0 +1,9 @@
/libraries
/runConfigurations.xml
/misc.xml
/vcs.xml
/workspace.xml
/caches
/gradle.xml
/modules.xml
/compiler.xml
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>
-35
View File
@@ -1,35 +0,0 @@
language: android
android:
components:
- tools
- build-tools-27.0.3
- android-27
- extra-android-m2repository
licenses:
- '.+'
script:
- ./gradlew test
jdk:
- oraclejdk8
branches:
only:
- develop
- master
after_success:
- .buildscript/deploy_snapshot.sh
cache:
directories:
- $HOME/.gradle
sudo: false
env:
global:
- secure: Px0uj7aMtKUVZVBnoEjHrEHh4cwO2qKJrHHPvwBiDqhwLBaWQIVXYOy0njaYc5o8p96Fv7bMu7NZx/72vMu1+nmTKxgzrMIxvMW6kczBvJUpv6xd1NuC34x+5Xq5gBNOFvb8JarWStcKgIvnFqvBsRUeI1Hsz7Olb8HF+fEo1kShuP18ezSsBkXruw8JuGiU9x0kq4YhZ7vRvFnc3sJX2FL6heuvQsnUWrolUOsKRadNkCibo+Euuls7ExvbbAXN4LEO3rs0G2eBUBbi2wXvTMG9symtItEHTMPO7K+aQfNQnHsY91TYveH/IJM1u5p6OldsUSOUigzpDmpVYW94aLuJaYqc6Ibq3eUws+tv2didOHXZW5zOCFjldDFBIQFPA3fih/wK/JP0taQ0uIu1+2eifvuERarMkGsYlOFe5tJd10ipi+kK5vNxoRwS9kGv5WwP5fVPX2m5XbD2y1LnugCCcAumfNX7NyNBIRqTy7BP34O3EMLZpMxjwSLnUBnYd4V/0LEvoVmbYmrLhWwpojBJmdwe2QknrPuvRErxNujRA1uEVupbU5A6RW1BmrtzSahJYoROI+ayG7UTOSbFN8+DorER1SUXsrOFlawak8yWsoi6OIynTKucrFM3YcBdJ3Su5AIhfBAOASZa6CUa6sn6Zo8mHmDVGKeckvXnLCU=
- secure: 3Dj6roVTO2a9Z8lwlTGzJJ+QGeyIYSuQ/Z6YsYnW3wB9Mw36uWqw9rOmMNIGjjyAlER8bKgalHr90Pus87oaNaIlbEyvq+L+I0FVAwognViFjo/a2apCcq81THoKjT1l0sgGRzvLNDynQe1q2L0tOu21wtBhMLb7FKQYB9+oD3H+rcU63xD2tv3ToJz+j+dccZ9nrtgk0MQ1xAeMtEb4tdq3flKATKhIuDkp9chaDxx/ZGwyhdE0UP29UUyP5Np1QvpFAlAJIZloZPvde2e05fwxTh4rwUCetkfJknDK6WrGiq97WGRXJpfORNuwGn7jxDCtgxcAm9nGF8qmI/v78BhjJ857CfJBTLGv4QI0RszlhXyezJqqRYjCn9S4yx8UAOixVJfJfFNHLqD4MFn41b7j8J3HDJPxNt0t/qYhUMrgrZVosNOUqhwCyQTKDqtrpvmSUbhHpk2+fxZF1GEL5N540rA0OjLmFUUEDSvRQVaa/waeqXrRefOhsXIx20dHs93C6XDffjnxVMKlqtPab2MlHV/23QCuSa6eW+lKyOZ6ZkWLwwDsLbnuD0WJfJpiL1xaTdPJNIDb6kB/Bno4V1O4xrYncEOaA4Vr4amO8le9Q33/QeNrUZSXs/eye5t0f02wCzubuiEDVEeoR/qj5+5CNWv3AWr/f8AgXp80Pzw=
+53 -48
View File
@@ -1,4 +1,4 @@
[![Travis Build](https://travis-ci.org/bluelinelabs/Conductor.svg)](https://travis-ci.org/bluelinelabs/Conductor) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Conductor-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/3361) [![Javadocs](http://javadoc.io/badge/com.bluelinelabs/conductor.svg)](http://javadoc.io/doc/com.bluelinelabs/conductor)
[![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
@@ -13,35 +13,36 @@ A small, yet full-featured framework that allows building View-based Android app
:twisted_rightwards_arrows: | Beautiful transitions between views
:floppy_disk: | State persistence
:phone: | Callbacks for onActivityResult, onRequestPermissionsResult, etc
:european_post_office: | MVP / MVVM / VIPER / MVC ready
:european_post_office: | MVP / MVVM / MVI / VIPER / MVC ready
Conductor is architecture-agnostic and does not try to force any design decisions on the developer. We here at BlueLine Labs tend to use either MVP or MVVM, but it would work equally well with standard MVC or whatever else you want to throw at it.
## Installation
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:2.1.5'
def conductorVersion = '3.2.0'
// If you want the components that go along with
// Android's support libraries (currently just a PagerAdapter):
implementation 'com.bluelinelabs:conductor-support:2.1.5'
implementation "com.bluelinelabs:conductor:$conductorVersion"
// If you want RxJava lifecycle support:
implementation 'com.bluelinelabs:conductor-rxlifecycle:2.1.5'
// AndroidX Transition change handlers:
implementation "com.bluelinelabs:conductor-androidx-transition:$conductorVersion"
// If you want RxJava2 lifecycle support:
implementation 'com.bluelinelabs:conductor-rxlifecycle2:2.1.5'
// ViewPager PagerAdapter:
implementation "com.bluelinelabs:conductor-viewpager:$conductorVersion"
// If you want RxJava2 Autodispose support:
implementation 'com.bluelinelabs:conductor-autodispose:2.1.5'
// If you want Controllers that are Lifecycle-aware (architecture components):
implementation 'com.bluelinelabs:conductor-archlifecycle:2.1.5'
// ViewPager2 Adapter:
implementation "com.bluelinelabs:conductor-viewpager2:$conductorVersion"
```
**SNAPSHOT**
### 4.0 Preview
Use `4.0.0-preview-4` as your version number in any of the dependencies above.
Just use `2.1.6-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 {
@@ -58,53 +59,49 @@ allprojects {
__Controller__ | The Controller is the View wrapper that will give you all of your lifecycle management features. Think of it as a lighter-weight and more predictable Fragment alternative with an easier to manage lifecycle.
__Router__ | A Router implements navigation and backstack handling for Controllers. Router objects are attached to Activity/containing ViewGroup pairs. Routers do not directly render or push Views to the container ViewGroup, but instead defer this responsibility to the ControllerChangeHandler specified in a given transaction.
__ControllerChangeHandler__ | ControllerChangeHandlers are responsible for swapping the View for one Controller to the View of another. They can be useful for performing animations and transitions between Controllers. Several default ControllerChangeHandlers are included.
__ControllerTransaction__ | Transactions are used to define data about adding Controllers. RouterControllerTransactions are used to push a Controller to a Router with specified ControllerChangeHandlers, while ChildControllerTransactions are used to add child Controllers.
__RouterTransaction__ | Transactions are used to define data about adding Controllers. RouterTransactions are used to push a Controller to a Router with specified ControllerChangeHandlers, while ChildControllerTransactions are used to add child Controllers.
## Getting Started
### 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) {
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
}
}
```
@@ -132,9 +129,17 @@ The lifecycle of a Controller is significantly simpler to understand than that o
### RxJava Lifecycle
If the AutoDispose dependency has been added, there is a `ControllerScopeProvider` available that can be used along with the standard [AutoDispose library](https://github.com/uber/AutoDispose).
## Community Projects
The community has provided several helpful modules to make developing apps with Conductor even easier. Here's a collection of helpful libraries:
* [ConductorGlide](https://github.com/MkhytarMkhoian/ConductorGlide) - Adds Glide lifecycle support to Controllers
* [ConductorDialog](https://github.com/MkhytarMkhoian/ConductorDialog) - Adds a helpful DialogController (a Conductor version of DialogFragment)
* [Mosby-Conductor](https://github.com/sockeqwe/mosby-conductor) - A plugin to integrate Mosby, an MVP/MVI library
## License
```
Copyright 2016 BlueLine Labs, Inc.
Copyright 2020 BlueLine Labs, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+18 -8
View File
@@ -1,19 +1,29 @@
buildscript {
repositories {
jcenter()
mavenCentral()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.3'
classpath libs.agp
classpath libs.kotlin.plugin
classpath libs.dokka
}
}
allprojects {
repositories {
mavenCentral()
maven { url 'https://maven.google.com' }
jcenter()
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')
+9 -14
View File
@@ -1,26 +1,21 @@
apply plugin: 'java'
targetCompatibility = JavaVersion.VERSION_1_7
sourceCompatibility = JavaVersion.VERSION_1_7
apply plugin: 'kotlin'
configurations {
lintChecks
libs.lint.checks
}
dependencies {
implementation rootProject.ext.lintapi
implementation rootProject.ext.lintchecks
compileOnly libs.lint.api
compileOnly libs.lint.checks
compileOnly libs.kotlin.stdlib
testImplementation rootProject.ext.lint
testImplementation rootProject.ext.lintTests
lintChecks files(jar)
testImplementation libs.junit
testImplementation libs.lint
testImplementation libs.lint.tests
}
jar {
manifest {
attributes('Lint-Registry': 'com.bluelinelabs.conductor.lint.IssueRegistry')
attributes('Lint-Registry-v2': 'com.bluelinelabs.conductor.lint.IssueRegistry')
}
}
apply from: rootProject.file('dependencies.gradle')
@@ -10,17 +10,17 @@ import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UTypeReferenceExpression;
import java.util.Collections;
import java.util.List;
public final class ControllerChangeHandlerIssueDetector extends Detector implements Detector.UastScanner {
public static final Issue ISSUE =
static final Issue ISSUE =
Issue.create("ValidControllerChangeHandler", "ControllerChangeHandler not instantiatable",
"Non-abstract ControllerChangeHandler instances must have a default constructor for the"
+ " system to re-create them in the case of the process being killed.",
@@ -31,7 +31,7 @@ public final class ControllerChangeHandlerIssueDetector extends Detector impleme
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.<Class<? extends UElement>>singletonList(UClass.class);
return Collections.singletonList(UClass.class);
}
@Override
@@ -41,53 +41,46 @@ public final class ControllerChangeHandlerIssueDetector extends Detector impleme
return new UElementHandler() {
@Override
public void visitClass(UClass node) {
public void visitClass(@NotNull UClass node) {
if (evaluator.isAbstract(node)) {
return;
}
boolean hasSuperType = false;
for (UTypeReferenceExpression superType : node.getUastSuperTypes()) {
if (CLASS_NAME.equals(superType.asRenderString())) {
hasSuperType = true;
break;
}
}
final boolean hasSuperType = evaluator.extendsClass(node.getJavaPsi(), CLASS_NAME, true);
if (!hasSuperType) {
return;
}
if (!evaluator.isPublic(node)) {
String message = String.format("This ControllerChangeHandler class should be public (%1$s)", node.getQualifiedName());
context.report(ISSUE, node, context.getLocation((UElement) node), message);
context.report(ISSUE, node, context.getLocation(Identify.byName(node)), message);
return;
}
if (node.getContainingClass() != null && !evaluator.isStatic(node)) {
String message = String.format("This ControllerChangeHandler inner class should be static (%1$s)", node.getQualifiedName());
context.report(ISSUE, node, context.getLocation((UElement) node), message);
context.report(ISSUE, node, context.getLocation(Identify.byName(node)), message);
return;
}
boolean hasConstructor = false;
UMethod constructor = null;
boolean hasDefaultConstructor = false;
for (UMethod method : node.getMethods()) {
if (method.isConstructor()) {
hasConstructor = true;
if (evaluator.isPublic(method) && method.getUastParameters().size() == 0) {
constructor = method;
if (evaluator.isPublic(method) && method.getUastParameters().isEmpty()) {
hasDefaultConstructor = true;
break;
}
}
}
if (hasConstructor && !hasDefaultConstructor) {
if (constructor != null && !hasDefaultConstructor) {
String message = String.format(
"This ControllerChangeHandler needs to have a public default constructor (`%1$s`)", node.getQualifiedName());
context.report(ISSUE, node, context.getLocation((UElement) node), message);
context.report(ISSUE, node, context.getLocation(Identify.byName(constructor)), message);
}
}
};
}
}
@@ -10,19 +10,20 @@ import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.intellij.psi.PsiType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UParameter;
import org.jetbrains.uast.UTypeReferenceExpression;
import java.util.Collections;
import java.util.List;
public final class ControllerIssueDetector extends Detector implements Detector.UastScanner {
public static final Issue ISSUE =
static final Issue ISSUE =
Issue.create("ValidController", "Controller not instantiatable",
"Non-abstract Controller instances must have a default or single-argument constructor"
+ " that takes a Bundle in order for the system to re-create them in the"
@@ -33,7 +34,7 @@ public final class ControllerIssueDetector extends Detector implements Detector.
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.<Class<? extends UElement>>singletonList(UClass.class);
return Collections.singletonList(UClass.class);
}
@Override
@@ -42,63 +43,58 @@ public final class ControllerIssueDetector extends Detector implements Detector.
return new UElementHandler() {
@Override
public void visitClass(UClass node) {
public void visitClass(@NotNull UClass node) {
if (evaluator.isAbstract(node)) {
return;
}
boolean hasSuperType = false;
for (UTypeReferenceExpression superType : node.getUastSuperTypes()) {
if (CLASS_NAME.equals(superType.asRenderString())) {
hasSuperType = true;
break;
}
}
final boolean hasSuperType = evaluator.extendsClass(node.getJavaPsi(), CLASS_NAME, true);
if (!hasSuperType) {
return;
}
if (!evaluator.isPublic(node)) {
String message = String.format("This Controller class should be public (%1$s)", node.getQualifiedName());
context.report(ISSUE, node, context.getLocation((UElement) node), message);
context.report(ISSUE, node, context.getLocation(Identify.byName(node)), message);
return;
}
if (node.getContainingClass() != null && !evaluator.isStatic(node)) {
String message = String.format("This Controller inner class should be static (%1$s)", node.getQualifiedName());
context.report(ISSUE, node, context.getLocation((UElement) node), message);
context.report(ISSUE, node, context.getLocation(Identify.byName(node)), message);
return;
}
boolean hasConstructor = false;
UMethod constructor = null;
boolean hasDefaultConstructor = false;
boolean hasBundleConstructor = false;
for (UMethod method : node.getMethods()) {
if (method.isConstructor()) {
hasConstructor = true;
constructor = method;
if (evaluator.isPublic(method)) {
List<UParameter> parameters = method.getUastParameters();
if (parameters.size() == 0) {
if (parameters.isEmpty()) {
hasDefaultConstructor = true;
break;
} else if (parameters.size() == 1 &&
(parameters.get(0).getType().equalsToText(SdkConstants.CLASS_BUNDLE)) ||
parameters.get(0).getType().equalsToText("Bundle")) {
hasBundleConstructor = true;
} else if (parameters.size() == 1) {
PsiType type = parameters.get(0).getType();
if (type.equalsToText(SdkConstants.CLASS_BUNDLE) || type.equalsToText("Bundle")) {
hasBundleConstructor = true;
break;
}
}
}
}
}
if (hasConstructor && !hasDefaultConstructor && !hasBundleConstructor) {
if (constructor != null && !hasDefaultConstructor && !hasBundleConstructor) {
String message = String.format(
"This Controller needs to have either a public default constructor or a" +
" public single-argument constructor that takes a Bundle. (`%1$s`)",
node.getQualifiedName());
context.report(ISSUE, node, context.getLocation((UElement) node), message);
context.report(ISSUE, node, context.getLocation(Identify.byName(constructor)), message);
}
}
};
}
}
@@ -0,0 +1,15 @@
package com.bluelinelabs.conductor.lint;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiNameIdentifierOwner;
final class Identify {
private Identify() {}
static PsiElement byName(PsiNameIdentifierOwner psiNameIdentifierOwner) {
if (psiNameIdentifierOwner.getNameIdentifier() != null) {
return psiNameIdentifierOwner.getNameIdentifier();
}
return psiNameIdentifierOwner;
}
}
@@ -1,14 +0,0 @@
package com.bluelinelabs.conductor.lint;
import com.android.tools.lint.detector.api.Issue;
import java.util.Arrays;
import java.util.List;
public final class IssueRegistry extends com.android.tools.lint.client.api.IssueRegistry {
@Override public List<Issue> getIssues() {
return Arrays.asList(
ControllerIssueDetector.ISSUE,
ControllerChangeHandlerIssueDetector.ISSUE);
}
}
@@ -0,0 +1,24 @@
package com.bluelinelabs.conductor.lint
import com.android.tools.lint.client.api.Vendor
import com.android.tools.lint.detector.api.CURRENT_API
import com.android.tools.lint.client.api.IssueRegistry as LintIssueRegistry
@Suppress("UnstableApiUsage", "unused")
class IssueRegistry : LintIssueRegistry() {
override val issues = listOf(
ControllerIssueDetector.ISSUE,
ControllerChangeHandlerIssueDetector.ISSUE
)
override val api: Int = CURRENT_API
private val githubIssueLink = "https://github.com/bluelinelabs/Conductor/issues/new"
override val vendor = Vendor(
vendorName = "Conductor",
feedbackUrl = githubIssueLink,
contact = githubIssueLink
)
}
@@ -1,23 +1,19 @@
package com.bluelinelabs.conductor.lint;
import org.intellij.lang.annotations.Language;
import org.junit.Test;
import static com.android.tools.lint.checks.infrastructure.TestFiles.java;
import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint;
import com.android.tools.lint.checks.infrastructure.TestFile;
import org.intellij.lang.annotations.Language;
import org.junit.Test;
public class ControllerChangeHandlerDetectorTest {
private static final String CONSTRUCTOR =
"src/test/SampleHandler.java:2: Error: This ControllerChangeHandler needs to have a public default constructor (test.SampleHandler) [ValidControllerChangeHandler]\n"
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ "^\n"
+ "1 errors, 0 warnings\n";
private static final String PRIVATE_CLASS_ERROR =
"src/test/SampleHandler.java:2: Error: This ControllerChangeHandler class should be public (test.SampleHandler) [ValidControllerChangeHandler]\n"
+ "private class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ "^\n"
+ "1 errors, 0 warnings\n";
private final TestFile controllerChangeHandlerStub = java(
"package com.bluelinelabs.conductor;\n"
+ "abstract class ControllerChangeHandler {}"
);
@Test
public void testWithNoConstructor() {
@@ -27,7 +23,7 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
@@ -42,7 +38,7 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
@@ -57,10 +53,15 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(CONSTRUCTOR);
.expect(""
+ "src/test/SampleHandler.java:3: Error: This ControllerChangeHandler needs to have a public default constructor (test.SampleHandler) [ValidControllerChangeHandler]\n"
+ " public SampleHandler(int number) { }\n"
+ " ~~~~~~~~~~~~~\n"
+ "1 errors, 0 warnings\n"
);
}
@Test
@@ -73,7 +74,7 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
@@ -88,10 +89,15 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(CONSTRUCTOR);
.expect(""
+ "src/test/SampleHandler.java:3: Error: This ControllerChangeHandler needs to have a public default constructor (test.SampleHandler) [ValidControllerChangeHandler]\n"
+ " private SampleHandler() { }\n"
+ " ~~~~~~~~~~~~~\n"
+ "1 errors, 0 warnings\n"
);
}
@Test
@@ -103,10 +109,32 @@ public class ControllerChangeHandlerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerChangeHandlerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(PRIVATE_CLASS_ERROR);
.expect("src/test/SampleHandler.java:2: Error: This ControllerChangeHandler class should be public (test.SampleHandler) [ValidControllerChangeHandler]\n"
+ "private class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ " ~~~~~~~~~~~~~\n"
+ "1 errors, 0 warnings\n");
}
@Test
public void testWithPrivateClassOfBaseClass() {
@Language("JAVA") String baseClass = ""
+ "package test;\n"
+ "abstract class BaseChangeHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {}";
@Language("JAVA") String source = ""
+ "package test;\n"
+ "private class SampleHandler extends test.BaseChangeHandler {}";
lint()
.files(controllerChangeHandlerStub, java(baseClass), java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect("src/test/SampleHandler.java:2: Error: This ControllerChangeHandler class should be public (test.SampleHandler) [ValidControllerChangeHandler]\n" +
"private class SampleHandler extends test.BaseChangeHandler {}\n" +
" ~~~~~~~~~~~~~\n" +
"1 errors, 0 warnings");
}
}
@@ -1,24 +1,27 @@
package com.bluelinelabs.conductor.lint;
import org.intellij.lang.annotations.Language;
import org.junit.Test;
import static com.android.tools.lint.checks.infrastructure.TestFiles.java;
import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint;
import com.android.tools.lint.checks.infrastructure.TestFile;
import org.intellij.lang.annotations.Language;
import org.junit.Test;
public class ControllerDetectorTest {
private static final String CONSTRUCTOR_ERROR =
"src/test/SampleController.java:2: Error: This Controller needs to have either a public default constructor or a public single-argument constructor that takes a Bundle. (test.SampleController) [ValidController]\n"
+ "public class SampleController extends com.bluelinelabs.conductor.Controller {\n"
+ "^\n"
+ "1 errors, 0 warnings\n";
private static final String CLASS_ERROR =
"src/test/SampleController.java:2: Error: This Controller class should be public (test.SampleController) [ValidController]\n"
+ "private class SampleController extends com.bluelinelabs.conductor.Controller {\n"
+ "^\n"
+ " ~~~~~~~~~~~~~~~~\n"
+ "1 errors, 0 warnings\n";
private final TestFile controllerStub = java(
"package com.bluelinelabs.conductor;\n"
+ "abstract class Controller {}"
);
@Test
public void testWithNoConstructor() {
@Language("JAVA") String source = ""
@@ -27,7 +30,7 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
@@ -42,7 +45,7 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
@@ -57,10 +60,15 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(CONSTRUCTOR_ERROR);
.expect(""
+ "src/test/SampleController.java:3: Error: This Controller needs to have either a public default constructor or a public single-argument constructor that takes a Bundle. (test.SampleController) [ValidController]\n"
+ " public SampleController(int number) { }\n"
+ " ~~~~~~~~~~~~~~~~\n"
+ "1 errors, 0 warnings\n"
);
}
@Test
@@ -73,12 +81,38 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
}
@Test
public void testWithBaseClassAndPrivateConstructor() {
@Language("JAVA")
String baseClass = ""
+ "package test;\n"
+ "public class BaseController extends com.bluelinelabs.conductor.Controller {}";
@Language("JAVA")
String source = ""
+ "package test;\n"
+ "public class SampleController extends BaseController {\n"
+ " private SampleController() { }\n"
+ "}";
lint()
.files(controllerStub, java(baseClass), java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(""
+ "src/test/SampleController.java:3: Error: This Controller needs to have either a public default constructor or a public single-argument constructor that takes a Bundle. (test.SampleController) [ValidController]\n"
+ " private SampleController() { }\n"
+ " ~~~~~~~~~~~~~~~~\n"
+ "1 errors, 0 warnings"
);
}
@Test
public void testWithPrivateConstructor() {
@Language("JAVA") String source = ""
@@ -88,10 +122,15 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(CONSTRUCTOR_ERROR);
.expect(""
+ "src/test/SampleController.java:3: Error: This Controller needs to have either a public default constructor or a public single-argument constructor that takes a Bundle. (test.SampleController) [ValidController]\n"
+ " private SampleController() { }\n"
+ " ~~~~~~~~~~~~~~~~\n"
+ "1 errors, 0 warnings\n"
);
}
@Test
@@ -103,7 +142,7 @@ public class ControllerDetectorTest {
+ "}";
lint()
.files(java(source))
.files(controllerStub, java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(CLASS_ERROR);
@@ -0,0 +1,24 @@
plugins {
id("com.android.library")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
implementation libs.androidx.appcompat
implementation libs.androidx.collection
api libs.androidx.transition
implementation project(':conductor')
}
ext.artifactId = 'conductor-androidx-transition'
@@ -0,0 +1,3 @@
POM_NAME=Conductor AndroidX Transition Extensions
POM_ARTIFACT_ID=conductor-androidx-transition
POM_PACKAGING=aar
@@ -0,0 +1,3 @@
<manifest package="com.bluelinelabs.conductor.androidxtransition">
<application />
</manifest>
@@ -1,20 +1,20 @@
package com.bluelinelabs.conductor.changehandler;
package com.bluelinelabs.conductor.changehandler.androidxtransition;
import android.annotation.TargetApi;
import android.app.SharedElementCallback;
import android.graphics.Rect;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.Transition;
import android.transition.Transition.TransitionListener;
import android.transition.TransitionSet;
import android.util.ArrayMap;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnPreDrawListener;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.ArrayMap;
import androidx.core.app.SharedElementCallback;
import androidx.core.view.ViewCompat;
import androidx.core.view.ViewGroupCompat;
import androidx.transition.Transition;
import androidx.transition.TransitionSet;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.internal.TransitionUtils;
@@ -25,9 +25,11 @@ import java.util.List;
/**
* A TransitionChangeHandler that facilitates using different Transitions for the entering view, the exiting view,
* and shared elements between the two.
* <p/>
* Note that this class uses the <b>androidx</b> {@link Transition}. If you're using Android's platform transitions,
* consider using the {@code SharedElementTransitionChangeHandler} provided by the {@code android-transitions} Conductor module.
*/
// Much of this class is based on FragmentTransition.java and FragmentTransitionCompat21.java from the Android support library
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class SharedElementTransitionChangeHandler extends TransitionChangeHandler {
// A map of from -> to names. Generally these will be the same.
@@ -160,7 +162,7 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
OneShotPreDrawListener.add(true, view, new Runnable() {
@Override
public void run() {
waitForTransitionNames.remove(view.getTransitionName());
waitForTransitionNames.remove(ViewCompat.getTransitionName(view));
removedViews.add(new ViewParentPair(view, (ViewGroup)view.getParent()));
((ViewGroup)view.getParent()).removeView(view);
@@ -319,7 +321,7 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
final ArrayMap<String, View> toSharedElements = new ArrayMap<>();
TransitionUtils.findNamedViews(toSharedElements, to);
for (ViewParentPair removedView : removedViews) {
toSharedElements.put(removedView.view.getTransitionName(), removedView.view);
toSharedElements.put(ViewCompat.getTransitionName(removedView.view), removedView.view);
}
final List<String> names = new ArrayList<>(sharedElementNames.values());
@@ -335,10 +337,10 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
if (key != null) {
sharedElementNames.remove(key);
}
} else if (!name.equals(view.getTransitionName())) {
} else if (!name.equals(ViewCompat.getTransitionName(view))) {
String key = findKeyForValue(sharedElementNames, name);
if (key != null) {
sharedElementNames.put(key, view.getTransitionName());
sharedElementNames.put(key, ViewCompat.getTransitionName(view));
}
}
}
@@ -383,9 +385,9 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
View view = fromSharedElements.get(name);
if (view == null) {
sharedElementNames.remove(name);
} else if (!name.equals(view.getTransitionName())) {
} else if (!name.equals(ViewCompat.getTransitionName(view))) {
String targetValue = sharedElementNames.remove(name);
sharedElementNames.put(view.getTransitionName(), targetValue);
sharedElementNames.put(ViewCompat.getTransitionName(view), targetValue);
}
}
} else {
@@ -415,7 +417,7 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
if (view.getVisibility() == View.VISIBLE) {
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
if (viewGroup.isTransitionGroup()) {
if (ViewGroupCompat.isTransitionGroup(viewGroup)) {
transitioningViews.add(viewGroup);
} else {
int count = viewGroup.getChildCount();
@@ -434,7 +436,7 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
@Nullable final Transition enterTransition, @Nullable final List<View> enteringViews,
@Nullable final Transition exitTransition, @Nullable final List<View> exitingViews,
@Nullable final Transition sharedElementTransition, @Nullable final List<View> toSharedElements) {
overallTransition.addListener(new TransitionListener() {
overallTransition.addListener(new Transition.TransitionListener() {
@Override
public void onTransitionStart(Transition transition) {
if (enterTransition != null && enteringViews != null) {
@@ -469,10 +471,10 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
final int numSharedElements = toSharedElements.size();
for (int i = 0; i < numSharedElements; i++) {
View view = toSharedElements.get(i);
String name = view.getTransitionName();
String name = ViewCompat.getTransitionName(view);
if (name != null) {
String inName = findKeyForValue(sharedElementNames, name);
view.setTransitionName(inName);
ViewCompat.setTransitionName(view, inName);
}
}
}
@@ -486,9 +488,9 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
final int numSharedElements = toSharedElements.size();
for (int i = 0; i < numSharedElements; i++) {
final View view = toSharedElements.get(i);
final String name = view.getTransitionName();
final String name = ViewCompat.getTransitionName(view);
final String inName = sharedElementNames.get(name);
view.setTransitionName(inName);
ViewCompat.setTransitionName(view, inName);
}
}
});
@@ -572,7 +574,7 @@ public abstract class SharedElementTransitionChangeHandler extends TransitionCha
* @param toName The transition name used in the "to" view
*/
protected final void addSharedElement(@NonNull View sharedElement, @NonNull String toName) {
String transitionName = sharedElement.getTransitionName();
String transitionName = ViewCompat.getTransitionName(sharedElement);
if (transitionName == null) {
throw new IllegalArgumentException("Unique transitionNames are required for all sharedElements");
}
@@ -1,22 +1,22 @@
package com.bluelinelabs.conductor.changehandler;
package com.bluelinelabs.conductor.changehandler.androidxtransition;
import android.annotation.TargetApi;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.Transition;
import android.transition.Transition.TransitionListener;
import android.transition.TransitionManager;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.transition.Transition;
import androidx.transition.TransitionManager;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* A base {@link ControllerChangeHandler} that facilitates using {@link android.transition.Transition}s to replace Controller Views.
* A base {@link ControllerChangeHandler} that facilitates using {@link Transition}s to replace Controller Views.
* <p/>
* Note that this class uses the <b>androidx</b> {@link Transition}. If you're using Android's platform transitions,
* consider using the {@code TransitionChangeHandler} provided by the {@code android-transitions} Conductor module.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class TransitionChangeHandler extends ControllerChangeHandler {
public interface OnTransitionPreparedListener {
@@ -51,8 +51,12 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
needsImmediateCompletion = true;
}
@Nullable
private ControllerChangeCompletedListener listener;
@Override
public void performChange(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
listener = changeListener;
if (canceled) {
changeListener.onChangeCompleted();
return;
@@ -63,19 +67,30 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
return;
}
final Transition transition = getTransition(container, from, to, isPush);
transition.addListener(new TransitionListener() {
final Runnable onTransitionNotStarted = new Runnable() {
@Override
public void onTransitionStart(Transition transition) { }
public void run() {
changeListener.onChangeCompleted();
}
};
final Transition transition = getTransition(container, from, to, isPush);
transition.addListener(new Transition.TransitionListener() {
@Override
public void onTransitionStart(Transition transition) {
container.removeCallbacks(onTransitionNotStarted);
}
@Override
public void onTransitionEnd(Transition transition) {
changeListener.onChangeCompleted();
listener.onChangeCompleted();
listener = null;
}
@Override
public void onTransitionCancel(Transition transition) {
changeListener.onChangeCompleted();
listener.onChangeCompleted();
listener = null;
}
@Override
@@ -91,13 +106,14 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
if (!canceled) {
TransitionManager.beginDelayedTransition(container, transition);
executePropertyChanges(container, from, to, transition, isPush);
container.post(onTransitionNotStarted);
}
}
});
}
@Override
public boolean removesFromViewOnPush() {
public boolean getRemovesFromViewOnPush() {
return true;
}
@@ -126,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,24 +1,23 @@
package com.bluelinelabs.conductor.internal;
import android.annotation.TargetApi;
import android.graphics.Rect;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.Transition;
import android.transition.TransitionSet;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.transition.Transition;
import androidx.transition.TransitionSet;
import java.util.List;
import java.util.Map;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class TransitionUtils {
public static void findNamedViews(@NonNull Map<String, View> namedViews, View view) {
if (view.getVisibility() == View.VISIBLE) {
String transitionName = view.getTransitionName();
String transitionName = ViewCompat.getTransitionName(view);
if (transitionName != null) {
namedViews.put(transitionName, view);
}
@@ -36,7 +35,7 @@ public class TransitionUtils {
@Nullable
public static View findNamedView(@NonNull View view, @NonNull String transitionName) {
if (transitionName.equals(view.getTransitionName())) {
if (transitionName.equals(ViewCompat.getTransitionName(view))) {
return view;
}
@@ -1,28 +0,0 @@
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
api rootProject.ext.archComponentsLifecycle
implementation project(':conductor')
}
ext.artifactId = 'conductor-arch-components-lifecycle'
@@ -1,3 +0,0 @@
POM_NAME=Conductor Architecture Components Lifecycle Extensions
POM_ARTIFACT_ID=conductor-archlifecycle
POM_PACKAGING=aar
@@ -1,3 +0,0 @@
<manifest package="com.bluelinelabs.conductor.archlifecycle">
<application />
</manifest>
@@ -1,65 +0,0 @@
package com.bluelinelabs.conductor.archlifecycle;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.Lifecycle.Event;
import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.LifecycleRegistry;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
public class ControllerLifecycleOwner implements LifecycleOwner {
private final LifecycleRegistry lifecycleRegistry;
public <T extends Controller & LifecycleOwner> ControllerLifecycleOwner(@NonNull T lifecycleController) {
lifecycleRegistry = new LifecycleRegistry(lifecycleController); // --> State.INITIALIZED
lifecycleController.addLifecycleListener(new LifecycleListener() {
@Override
public void postContextAvailable(@NonNull Controller controller, @NonNull Context context) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_CREATE); // --> State.CREATED;
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_START); // --> State.STARTED;
}
@Override
public void postAttach(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_RESUME); // --> State.RESUMED;
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_PAUSE); // --> State.STARTED;
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_STOP); // --> State.CREATED;
}
@Override
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
// do nothing
}
@Override
public void preDestroy(@NonNull Controller controller) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_DESTROY); // --> State.DESTROYED;
}
});
}
@Override @NonNull
public Lifecycle getLifecycle() {
return lifecycleRegistry;
}
}
@@ -1,28 +0,0 @@
package com.bluelinelabs.conductor.archlifecycle;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleOwner;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
public abstract class LifecycleController extends Controller implements LifecycleOwner {
private final ControllerLifecycleOwner lifecycleOwner = new ControllerLifecycleOwner(this);
public LifecycleController() {
super();
}
public LifecycleController(@Nullable Bundle args) {
super(args);
}
@Override @NonNull
public Lifecycle getLifecycle() {
return lifecycleOwner.getLifecycle();
}
}
@@ -1,28 +0,0 @@
package com.bluelinelabs.conductor.archlifecycle;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleOwner;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bluelinelabs.conductor.RestoreViewOnCreateController;
public abstract class LifecycleRestoreViewOnCreateController extends RestoreViewOnCreateController implements LifecycleOwner {
private final ControllerLifecycleOwner mLifecycleOwner = new ControllerLifecycleOwner(this);
public LifecycleRestoreViewOnCreateController() {
super();
}
public LifecycleRestoreViewOnCreateController(@Nullable Bundle args) {
super(args);
}
@Override @NonNull
public Lifecycle getLifecycle() {
return mLifecycleOwner.getLifecycle();
}
}
@@ -1,29 +0,0 @@
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
api rootProject.ext.rxJava2
api rootProject.ext.autodispose
implementation project(':conductor')
}
ext.artifactId = 'conductor-autodispose'
@@ -1,3 +0,0 @@
POM_NAME=Conductor AutoDispose Extensions
POM_ARTIFACT_ID=conductor-autodispose
POM_PACKAGING=aar
@@ -1,3 +0,0 @@
<manifest package="com.bluelinelabs.conductor.autodispose">
<application />
</manifest>
@@ -1,14 +0,0 @@
package com.bluelinelabs.conductor.autodispose;
public enum ControllerEvent {
CREATE,
CONTEXT_AVAILABLE,
CREATE_VIEW,
ATTACH,
DETACH,
DESTROY_VIEW,
CONTEXT_UNAVAILABLE,
DESTROY
}
@@ -1,71 +0,0 @@
package com.bluelinelabs.conductor.autodispose;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.uber.autodispose.OutsideLifecycleException;
import io.reactivex.subjects.BehaviorSubject;
public class ControllerLifecycleSubjectHelper {
private ControllerLifecycleSubjectHelper() { }
@NonNull
public static BehaviorSubject<ControllerEvent> create(@NonNull Controller controller) {
ControllerEvent initialState;
if (controller.isBeingDestroyed() || controller.isDestroyed()) {
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
} else if (controller.isAttached()) {
initialState = ControllerEvent.ATTACH;
} else if (controller.getView() != null) {
initialState = ControllerEvent.CREATE_VIEW;
} else if (controller.getActivity() != null) {
initialState = ControllerEvent.CONTEXT_AVAILABLE;
} else {
initialState = ControllerEvent.CREATE;
}
final BehaviorSubject<ControllerEvent> subject = BehaviorSubject.createDefault(initialState);
controller.addLifecycleListener(new Controller.LifecycleListener() {
@Override
public void preContextAvailable(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CONTEXT_AVAILABLE);
}
@Override
public void preCreateView(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CREATE_VIEW);
}
@Override
public void preAttach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.ATTACH);
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DETACH);
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DESTROY_VIEW);
}
@Override
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
subject.onNext(ControllerEvent.CONTEXT_UNAVAILABLE);
}
@Override
public void preDestroy(@NonNull Controller controller) {
subject.onNext(ControllerEvent.DESTROY);
}
});
return subject;
}
}
@@ -1,74 +0,0 @@
package com.bluelinelabs.conductor.autodispose;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.Controller;
import com.uber.autodispose.LifecycleScopeProvider;
import com.uber.autodispose.OutsideLifecycleException;
import io.reactivex.Observable;
import io.reactivex.functions.Function;
import io.reactivex.subjects.BehaviorSubject;
public class ControllerScopeProvider implements LifecycleScopeProvider<ControllerEvent> {
private static final CorrespondingEventsFunction CORRESPONDING_EVENTS =
new CorrespondingEventsFunction() {
@Override
public ControllerEvent apply(ControllerEvent lastEvent) throws OutsideLifecycleException {
switch (lastEvent) {
case CREATE:
return ControllerEvent.DESTROY;
case CONTEXT_AVAILABLE:
return ControllerEvent.CONTEXT_UNAVAILABLE;
case CREATE_VIEW:
return ControllerEvent.DESTROY_VIEW;
case ATTACH:
return ControllerEvent.DETACH;
case DETACH:
return ControllerEvent.DESTROY;
default:
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
}
}
};
@NonNull private final BehaviorSubject<ControllerEvent> lifecycleSubject;
@NonNull private final Function<ControllerEvent, ControllerEvent> correspondingEventsFunction;
public static ControllerScopeProvider from(@NonNull Controller controller) {
return new ControllerScopeProvider(controller, CORRESPONDING_EVENTS);
}
public static ControllerScopeProvider from(@NonNull Controller controller, @NonNull final ControllerEvent untilEvent) {
return new ControllerScopeProvider(controller, new CorrespondingEventsFunction() {
@Override
public ControllerEvent apply(ControllerEvent controllerEvent) {
return untilEvent;
}
});
}
public static ControllerScopeProvider from(@NonNull Controller controller, @NonNull final CorrespondingEventsFunction correspondingEventsFunction) {
return new ControllerScopeProvider(controller, correspondingEventsFunction);
}
private ControllerScopeProvider(@NonNull Controller controller, @NonNull CorrespondingEventsFunction correspondingEventsFunction) {
lifecycleSubject = ControllerLifecycleSubjectHelper.create(controller);
this.correspondingEventsFunction = correspondingEventsFunction;
}
@Override
public Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
public Function<ControllerEvent, ControllerEvent> correspondingEvents() {
return correspondingEventsFunction;
}
@Override
public ControllerEvent peekLifecycle() {
return lifecycleSubject.getValue();
}
}
@@ -1,24 +0,0 @@
package com.bluelinelabs.conductor.autodispose;
import com.uber.autodispose.OutsideLifecycleException;
import io.reactivex.functions.Function;
/**
* Based on https://github.com/uber/AutoDispose/blob/master/lifecycle/autodispose-lifecycle/src/main/java/com/uber/autodispose/lifecycle/CorrespondingEventsFunction.java
*
* A corresponding events function that acts as a normal {@link Function} but ensures ControllerEvent event
* types are used in the generic and tightens the possible exception thrown to {@link OutsideLifecycleException}.
*/
public interface CorrespondingEventsFunction extends Function<ControllerEvent, ControllerEvent> {
/**
* Given an event {@code event}, returns the next corresponding event that this lifecycle should
* dispose on.
*
* @param event the source or start event.
* @return the target event that should signal disposal.
* @throws OutsideLifecycleException if the lifecycle exceeds its scope boundaries.
*/
@Override ControllerEvent apply(ControllerEvent event) throws OutsideLifecycleException;
}
@@ -1,30 +0,0 @@
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
api rootProject.ext.rxJava
api rootProject.ext.rxLifecycle
api rootProject.ext.rxLifecycleAndroid
implementation project(':conductor')
}
ext.artifactId = 'conductor-rxlifecycle'
@@ -1,3 +0,0 @@
POM_NAME=Conductor RxLifecycle Extensions
POM_ARTIFACT_ID=conductor-rxlifecycle
POM_PACKAGING=aar
@@ -1,3 +0,0 @@
<manifest package="com.bluelinelabs.conductor.rxlifecycle">
<application />
</manifest>
@@ -1,14 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle;
public enum ControllerEvent {
CREATE,
CONTEXT_AVAILABLE,
CREATE_VIEW,
ATTACH,
DETACH,
DESTROY_VIEW,
CONTEXT_UNAVAILABLE,
DESTROY
}
@@ -1,77 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.trello.rxlifecycle.OutsideLifecycleException;
import rx.subjects.BehaviorSubject;
/**
* A simple utility class that will create a {@link BehaviorSubject} that calls onNext when events
* occur in your {@link Controller}
*/
public class ControllerLifecycleSubjectHelper {
private ControllerLifecycleSubjectHelper() { }
public static BehaviorSubject<ControllerEvent> create(Controller controller) {
ControllerEvent initialState;
if (controller.isBeingDestroyed() || controller.isDestroyed()) {
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
} else if (controller.isAttached()) {
initialState = ControllerEvent.ATTACH;
} else if (controller.getView() != null) {
initialState = ControllerEvent.CREATE_VIEW;
} else if (controller.getActivity() != null) {
initialState = ControllerEvent.CONTEXT_AVAILABLE;
} else {
initialState = ControllerEvent.CREATE;
}
final BehaviorSubject<ControllerEvent> subject = BehaviorSubject.create(initialState);
controller.addLifecycleListener(new LifecycleListener() {
@Override
public void preContextAvailable(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CONTEXT_AVAILABLE);
}
@Override
public void preCreateView(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CREATE_VIEW);
}
@Override
public void preAttach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.ATTACH);
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DETACH);
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DESTROY_VIEW);
}
@Override
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
subject.onNext(ControllerEvent.CONTEXT_UNAVAILABLE);
}
@Override
public void preDestroy(@NonNull Controller controller) {
subject.onNext(ControllerEvent.DESTROY);
}
});
return subject;
}
}
@@ -1,52 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.Controller;
import com.trello.rxlifecycle.LifecycleProvider;
import com.trello.rxlifecycle.LifecycleTransformer;
import com.trello.rxlifecycle.RxLifecycle;
import rx.Observable;
import rx.subjects.BehaviorSubject;
/**
* A base {@link Controller} that can be used to expose lifecycle events using RxJava
*/
public abstract class RxController extends Controller implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxController() {
this(null);
}
public RxController(Bundle args) {
super(args);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.asObservable();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
@@ -1,48 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import com.trello.rxlifecycle.LifecycleTransformer;
import com.trello.rxlifecycle.OutsideLifecycleException;
import com.trello.rxlifecycle.RxLifecycle;
import rx.Observable;
import rx.functions.Func1;
public class RxControllerLifecycle {
/**
* Binds the given source to a Controller lifecycle. This is the Controller version of
* {@link com.trello.rxlifecycle.android.RxLifecycleAndroid#bindFragment(Observable)}.
*
* @param lifecycle the lifecycle sequence of a Controller
* @return a reusable {@link rx.Observable.Transformer} that unsubscribes the source during the Controller lifecycle
*/
@NonNull
@CheckResult
public static <T> LifecycleTransformer<T> bindController(@NonNull final Observable<ControllerEvent> lifecycle) {
return RxLifecycle.bind(lifecycle, CONTROLLER_LIFECYCLE);
}
private static final Func1<ControllerEvent, ControllerEvent> CONTROLLER_LIFECYCLE =
new Func1<ControllerEvent, ControllerEvent>() {
@Override
public ControllerEvent call(ControllerEvent lastEvent) {
switch (lastEvent) {
case CREATE:
return ControllerEvent.DESTROY;
case CONTEXT_AVAILABLE:
return ControllerEvent.CONTEXT_UNAVAILABLE;
case ATTACH:
return ControllerEvent.DETACH;
case CREATE_VIEW:
return ControllerEvent.DESTROY_VIEW;
case DETACH:
return ControllerEvent.DESTROY;
default:
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
}
}
};
}
@@ -1,52 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.RestoreViewOnCreateController;
import com.trello.rxlifecycle.LifecycleProvider;
import com.trello.rxlifecycle.LifecycleTransformer;
import com.trello.rxlifecycle.RxLifecycle;
import rx.Observable;
import rx.subjects.BehaviorSubject;
/**
* A base {@link RestoreViewOnCreateController} that can be used to expose lifecycle events using RxJava
*/
public abstract class RxRestoreViewOnCreateController extends RestoreViewOnCreateController implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxRestoreViewOnCreateController() {
this(null);
}
public RxRestoreViewOnCreateController(Bundle args) {
super(args);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.asObservable();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
@@ -1,30 +0,0 @@
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
api rootProject.ext.rxJava2
api rootProject.ext.rxLifecycle2
api rootProject.ext.rxLifecycleAndroid2
implementation project(':conductor')
}
ext.artifactId = 'conductor-rxlifecycle2'
@@ -1,3 +0,0 @@
POM_NAME=Conductor RxLifecycle2 Extensions
POM_ARTIFACT_ID=conductor-rxlifecycle2
POM_PACKAGING=aar
@@ -1,3 +0,0 @@
<manifest package="com.bluelinelabs.conductor.rxlifecycle2">
<application />
</manifest>
@@ -1,14 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle2;
public enum ControllerEvent {
CREATE,
CONTEXT_AVAILABLE,
CREATE_VIEW,
ATTACH,
DETACH,
DESTROY_VIEW,
CONTEXT_UNAVAILABLE,
DESTROY
}
@@ -1,71 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.trello.rxlifecycle2.OutsideLifecycleException;
import io.reactivex.subjects.BehaviorSubject;
public class ControllerLifecycleSubjectHelper {
private ControllerLifecycleSubjectHelper() {
}
public static BehaviorSubject<ControllerEvent> create(Controller controller) {
ControllerEvent initialState;
if (controller.isBeingDestroyed() || controller.isDestroyed()) {
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
} else if (controller.isAttached()) {
initialState = ControllerEvent.ATTACH;
} else if (controller.getView() != null) {
initialState = ControllerEvent.CREATE_VIEW;
} else if (controller.getActivity() != null) {
initialState = ControllerEvent.CONTEXT_AVAILABLE;
} else {
initialState = ControllerEvent.CREATE;
}
final BehaviorSubject<ControllerEvent> subject = BehaviorSubject.createDefault(initialState);
controller.addLifecycleListener(new Controller.LifecycleListener() {
@Override
public void preContextAvailable(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CONTEXT_AVAILABLE);
}
@Override
public void preCreateView(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CREATE_VIEW);
}
@Override
public void preAttach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.ATTACH);
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DETACH);
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DESTROY_VIEW);
}
@Override
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
subject.onNext(ControllerEvent.CONTEXT_UNAVAILABLE);
}
@Override
public void preDestroy(@NonNull Controller controller) {
subject.onNext(ControllerEvent.DESTROY);
}
});
return subject;
}
}
@@ -1,49 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
import com.trello.rxlifecycle2.LifecycleProvider;
import com.trello.rxlifecycle2.LifecycleTransformer;
import com.trello.rxlifecycle2.RxLifecycle;
import io.reactivex.Observable;
import io.reactivex.subjects.BehaviorSubject;
/**
* A base {@link Controller} that can be used to expose lifecycle events using RxJava
*/
public abstract class RxController extends Controller implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxController(){
this(null);
}
public RxController(@Nullable Bundle args) {
super(args);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
@@ -1,43 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.support.annotation.NonNull;
import com.trello.rxlifecycle2.LifecycleTransformer;
import com.trello.rxlifecycle2.OutsideLifecycleException;
import com.trello.rxlifecycle2.RxLifecycle;
import io.reactivex.Observable;
import io.reactivex.functions.Function;
public class RxControllerLifecycle {
/**
* Binds the given source to a Controller lifecycle. This is the Controller version of
* {@link com.trello.rxlifecycle2.android.RxLifecycleAndroid#bindFragment(Observable)}.
*
* @param lifecycle the lifecycle sequence of a Controller
* @return a reusable {@link io.reactivex.ObservableTransformer} that unsubscribes the source during the Controller lifecycle
*/
public static <T> LifecycleTransformer<T> bindController(@NonNull final Observable<ControllerEvent> lifecycle) {
return RxLifecycle.bind(lifecycle, CONTROLLER_LIFECYCLE);
}
private static final Function<ControllerEvent, ControllerEvent> CONTROLLER_LIFECYCLE =
new Function<ControllerEvent, ControllerEvent>() {
@Override
public ControllerEvent apply(ControllerEvent lastEvent) {
switch (lastEvent) {
case CREATE:
return ControllerEvent.DESTROY;
case CONTEXT_AVAILABLE:
return ControllerEvent.CONTEXT_UNAVAILABLE;
case ATTACH:
return ControllerEvent.DETACH;
case CREATE_VIEW:
return ControllerEvent.DESTROY_VIEW;
case DETACH:
return ControllerEvent.DESTROY;
default:
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
}
}
};
}
@@ -1,45 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.RestoreViewOnCreateController;
import com.trello.rxlifecycle2.LifecycleProvider;
import com.trello.rxlifecycle2.LifecycleTransformer;
import com.trello.rxlifecycle2.RxLifecycle;
import io.reactivex.Observable;
import io.reactivex.subjects.BehaviorSubject;
public abstract class RxRestoreViewOnCreateController extends RestoreViewOnCreateController implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxRestoreViewOnCreateController() {
this(null);
}
public RxRestoreViewOnCreateController(Bundle args) {
super(args);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
-40
View File
@@ -1,40 +0,0 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'de.mobilej.unmock:UnMockPlugin:0.6.5'
}
}
apply plugin: 'com.android.library'
apply plugin: 'de.mobilej.unmock'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.roboelectric
implementation rootProject.ext.supportV4
implementation project(':conductor')
}
ext.artifactId = 'conductor-support'
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
@@ -1,3 +0,0 @@
POM_NAME=Conductor Support Extensions
POM_ARTIFACT_ID=conductor-support
POM_PACKAGING=aar
@@ -1,3 +0,0 @@
<manifest package="com.bluelinelabs.conductor.support">
<application />
</manifest>
@@ -1,154 +0,0 @@
package com.bluelinelabs.conductor.support;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
/**
* @deprecated Use RouterPagerAdapter instead! This implementation was too limited and had too many
* gotchas associated with it.
*
* An adapter for ViewPagers that will handle adding and removing Controllers
*/
@Deprecated
public abstract class ControllerPagerAdapter extends PagerAdapter {
private static final String KEY_SAVED_PAGES = "ControllerPagerAdapter.savedStates";
private static final String KEY_SAVES_STATE = "ControllerPagerAdapter.savesState";
private static final String KEY_VISIBLE_PAGE_IDS_KEYS = "ControllerPagerAdapter.visiblePageIds.keys";
private static final String KEY_VISIBLE_PAGE_IDS_VALUES = "ControllerPagerAdapter.visiblePageIds.values";
private final Controller host;
private boolean savesState;
private SparseArray<Bundle> savedPages = new SparseArray<>();
private SparseArray<String> visiblePageIds = new SparseArray<>();
/**
* Creates a new ControllerPagerAdapter using the passed host.
*/
public ControllerPagerAdapter(@NonNull Controller host, boolean saveControllerState) {
this.host = host;
savesState = saveControllerState;
}
/**
* Return the Controller associated with a specified position.
*/
@NonNull
public abstract Controller getItem(int position);
@Override
public Object instantiateItem(ViewGroup container, int position) {
final String name = makeControllerName(container.getId(), getItemId(position));
Router router = host.getChildRouter(container, name);
if (savesState && !router.hasRootController()) {
Bundle routerSavedState = savedPages.get(position);
if (routerSavedState != null) {
router.restoreInstanceState(routerSavedState);
}
}
final Controller controller;
if (!router.hasRootController()) {
controller = getItem(position);
router.setRoot(RouterTransaction.with(controller).tag(name));
} else {
router.rebindIfNeeded();
controller = router.getControllerWithTag(name);
}
if (controller != null) {
visiblePageIds.put(position, controller.getInstanceId());
}
return router.getControllerWithTag(name);
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Router router = ((Controller)object).getRouter();
if (savesState) {
Bundle savedState = new Bundle();
router.saveInstanceState(savedState);
savedPages.put(position, savedState);
}
visiblePageIds.remove(position);
host.removeChildRouter(router);
}
@Override
public boolean isViewFromObject(View view, Object object) {
return ((Controller)object).getView() == view;
}
@Override
public Parcelable saveState() {
Bundle bundle = new Bundle();
bundle.putBoolean(KEY_SAVES_STATE, savesState);
bundle.putSparseParcelableArray(KEY_SAVED_PAGES, savedPages);
int[] visiblePageIdsKeys = new int[visiblePageIds.size()];
String[] visiblePageIdsValues = new String[visiblePageIds.size()];
for (int i = 0; i < visiblePageIds.size(); i++) {
visiblePageIdsKeys[i] = visiblePageIds.keyAt(i);
visiblePageIdsValues[i] = visiblePageIds.valueAt(i);
}
bundle.putIntArray(KEY_VISIBLE_PAGE_IDS_KEYS, visiblePageIdsKeys);
bundle.putStringArray(KEY_VISIBLE_PAGE_IDS_VALUES, visiblePageIdsValues);
return bundle;
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
Bundle bundle = (Bundle)state;
if (state != null) {
savesState = bundle.getBoolean(KEY_SAVES_STATE, false);
savedPages = bundle.getSparseParcelableArray(KEY_SAVED_PAGES);
int[] visiblePageIdsKeys = bundle.getIntArray(KEY_VISIBLE_PAGE_IDS_KEYS);
String[] visiblePageIdsValues = bundle.getStringArray(KEY_VISIBLE_PAGE_IDS_VALUES);
visiblePageIds = new SparseArray<>(visiblePageIdsKeys.length);
for (int i = 0; i < visiblePageIdsKeys.length; i++) {
visiblePageIds.put(visiblePageIdsKeys[i], visiblePageIdsValues[i]);
}
}
}
/**
* Returns the already instantiated Controller in the specified position or {@code null} if
* this position does not yet have a controller.
*/
@Nullable
public Controller getController(int position) {
String instanceId = visiblePageIds.get(position);
if (instanceId != null) {
return host.getRouter().getControllerWithInstanceId(instanceId);
} else {
return null;
}
}
public long getItemId(int position) {
return position;
}
private static String makeControllerName(int viewId, long id) {
return viewId + ":" + id;
}
}
@@ -1,113 +0,0 @@
package com.bluelinelabs.conductor.support;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.util.SparseArray;
import android.widget.FrameLayout;
import com.bluelinelabs.conductor.Conductor;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.support.util.FakePager;
import com.bluelinelabs.conductor.support.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class StateSaveTests {
private FakePager pager;
private RouterPagerAdapter pagerAdapter;
public void createActivityController(Bundle savedInstanceState) {
ActivityController<Activity> activityController = Robolectric.buildActivity(Activity.class).create().start().resume();
Router router = Conductor.attachRouter(activityController.get(), new FrameLayout(activityController.get()), savedInstanceState);
TestController controller = new TestController();
router.setRoot(RouterTransaction.with(controller));
pager = new FakePager(new FrameLayout(activityController.get()));
pager.setOffscreenPageLimit(1);
pagerAdapter = new RouterPagerAdapter(controller) {
@Override
public void configureRouter(@NonNull Router router, int position) {
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Override
public int getCount() {
return 20;
}
};
pager.setAdapter(pagerAdapter);
}
@Before
public void setup() {
createActivityController(null);
}
@Test
public void testNoMaxSaves() {
// Load all pages
for (int i = 0; i < pagerAdapter.getCount(); i++) {
pager.pageTo(i);
}
pager.pageTo(pagerAdapter.getCount() / 2);
// Ensure all non-visible pages are saved
assertEquals(pagerAdapter.getCount() - 1 - pager.getOffscreenPageLimit() * 2, pagerAdapter.getSavedPages().size());
}
@Test
public void testMaxSavedSet() {
final int maxPages = 3;
pagerAdapter.setMaxPagesToStateSave(maxPages);
// Load all pages
for (int i = 0; i < pagerAdapter.getCount(); i++) {
pager.pageTo(i);
}
final int firstSelectedItem = pagerAdapter.getCount() / 2;
pager.pageTo(firstSelectedItem);
SparseArray<Bundle> savedPages = pagerAdapter.getSavedPages();
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size());
// Ensure correct pages are saved
assertEquals(pagerAdapter.getCount() - 3, savedPages.keyAt(0));
assertEquals(pagerAdapter.getCount() - 2, savedPages.keyAt(1));
assertEquals(pagerAdapter.getCount() - 1, savedPages.keyAt(2));
final int secondSelectedItem = 1;
pager.pageTo(secondSelectedItem);
savedPages = pagerAdapter.getSavedPages();
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size());
// Ensure correct pages are saved
assertEquals(firstSelectedItem - 1, savedPages.keyAt(0));
assertEquals(firstSelectedItem, savedPages.keyAt(1));
assertEquals(firstSelectedItem + 1, savedPages.keyAt(2));
}
}
@@ -1,61 +0,0 @@
package com.bluelinelabs.conductor.support.util;
import android.util.SparseArray;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.support.RouterPagerAdapter;
import java.util.ArrayList;
import java.util.List;
public class FakePager {
private ViewGroup container;
private int offscreenPageLimit;
private final SparseArray<Object> pages = new SparseArray<>();
private RouterPagerAdapter adapter;
public FakePager(ViewGroup container) {
this.container = container;
}
public void setAdapter(RouterPagerAdapter adapter) {
this.adapter = adapter;
}
public void pageTo(int page) {
int firstPage = Math.max(0, page - offscreenPageLimit);
int lastPage = Math.min(adapter.getCount() - 1, page + offscreenPageLimit);
List<Integer> pagesI = new ArrayList<>();
for (int i = 0; i < pages.size(); i++) {
pagesI.add(pages.keyAt(i));
}
for (int i = pages.size() - 1; i >= 0; i--) {
int key = pages.keyAt(i);
if (key < firstPage || key > lastPage) {
adapter.destroyItem(container, key, pages.get(key));
pages.remove(key);
}
}
for (int key = firstPage; key <= lastPage; key++) {
if (pages.get(key) == null) {
pages.put(key, adapter.instantiateItem(container, key));
}
}
adapter.setPrimaryItem(container, page, pages.get(page));
}
public int getOffscreenPageLimit() {
return offscreenPageLimit;
}
public void setOffscreenPageLimit(int offscreenPageLimit) {
this.offscreenPageLimit = offscreenPageLimit;
}
}
+26
View File
@@ -0,0 +1,26 @@
plugins {
id("com.android.library")
id("kotlin-android")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
testImplementation libs.junit
testImplementation libs.robolectric
implementation libs.androidx.appcompat
implementation project(':conductor')
}
ext.artifactId = 'conductor-viewpager'
@@ -0,0 +1,3 @@
POM_NAME=Conductor PagerAdapter
POM_ARTIFACT_ID=conductor-viewpager
POM_PACKAGING=aar
@@ -0,0 +1,3 @@
<manifest package="com.bluelinelabs.conductor.viewpager">
<application />
</manifest>
@@ -1,34 +1,40 @@
package com.bluelinelabs.conductor.support;
package com.bluelinelabs.conductor.viewpager;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager.widget.PagerAdapter;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* An adapter for ViewPagers that uses Routers as pages
* An ViewPager adapter that uses Routers as pages
*/
public abstract class RouterPagerAdapter extends PagerAdapter {
private static final String KEY_SAVED_PAGES = "RouterPagerAdapter.savedStates";
private static final String KEY_TAGS_KEYS = "RouterPagerAdapter.tags.keys";
private static final String KEY_TAGS_VALUES = "RouterPagerAdapter.tags.values";
private static final String KEY_MAX_PAGES_TO_STATE_SAVE = "RouterPagerAdapter.maxPagesToStateSave";
private static final String KEY_SAVE_PAGE_HISTORY = "RouterPagerAdapter.savedPageHistory";
private final Controller host;
private int maxPagesToStateSave = Integer.MAX_VALUE;
private final Map<Integer, String> tags = new HashMap<>();
private SparseArray<Bundle> savedPages = new SparseArray<>();
private SparseArray<Router> visibleRouters = new SparseArray<>();
private final SparseArray<Router> visibleRouters = new SparseArray<>();
private ArrayList<Integer> savedPageHistory = new ArrayList<>();
private Router currentPrimaryRouter;
@@ -65,7 +71,14 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
public Object instantiateItem(ViewGroup container, int position) {
final String name = makeRouterName(container.getId(), getItemId(position));
Router router = host.getChildRouter(container, name);
// Ensure we don't try to restore state for a router with a different ID just because
// the position was reused. Fixes https://github.com/bluelinelabs/Conductor/issues/582
if (tags.get(position) != null && !tags.get(position).equals(name)) {
savedPages.remove(position);
}
Router router = host.getChildRouter(container, name)
.setPopRootControllerMode(Router.PopRootControllerMode.NEVER);
if (!router.hasRootController()) {
Bundle routerSavedState = savedPages.get(position);
@@ -85,12 +98,13 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
}
}
tags.put(position, name);
visibleRouters.put(position, router);
return router;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
Router router = (Router)object;
Bundle savedState = new Bundle();
@@ -108,8 +122,8 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
Router router = (Router)object;
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
Router router = (Router) object;
if (router != currentPrimaryRouter) {
if (currentPrimaryRouter != null) {
for (RouterTransaction transaction : currentPrimaryRouter.getBackstack()) {
@@ -126,7 +140,7 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
}
@Override
public boolean isViewFromObject(View view, Object object) {
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
Router router = (Router)object;
final List<RouterTransaction> backstack = router.getBackstack();
for (RouterTransaction transaction : backstack) {
@@ -141,6 +155,8 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
public Parcelable saveState() {
Bundle bundle = new Bundle();
bundle.putSparseParcelableArray(KEY_SAVED_PAGES, savedPages);
bundle.putIntegerArrayList(KEY_TAGS_KEYS, new ArrayList<>(tags.keySet()));
bundle.putStringArrayList(KEY_TAGS_VALUES, new ArrayList<>(tags.values()));
bundle.putInt(KEY_MAX_PAGES_TO_STATE_SAVE, maxPagesToStateSave);
bundle.putIntegerArrayList(KEY_SAVE_PAGE_HISTORY, savedPageHistory);
return bundle;
@@ -153,6 +169,14 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
savedPages = bundle.getSparseParcelableArray(KEY_SAVED_PAGES);
maxPagesToStateSave = bundle.getInt(KEY_MAX_PAGES_TO_STATE_SAVE);
savedPageHistory = bundle.getIntegerArrayList(KEY_SAVE_PAGE_HISTORY);
List<Integer> tagsKeys = bundle.getIntegerArrayList(KEY_TAGS_KEYS);
List<String> tagsValues = bundle.getStringArrayList(KEY_TAGS_VALUES);
if (tagsKeys != null && tagsValues != null && tagsKeys.size() == tagsValues.size()) {
for (int i = 0; i < tagsKeys.size(); i++) {
tags.put(tagsKeys.get(i), tagsValues.get(i));
}
}
}
}
@@ -0,0 +1,78 @@
package com.bluelinelabs.conductor.viewpager
import com.bluelinelabs.conductor.viewpager.util.TestActivity
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class StateSaveTests {
private val testController = Robolectric.buildActivity(TestActivity::class.java)
.setup()
.get()
.testController()
private val pagerAdapter = testController.pagerAdapter
private val pager = testController.pager
private val destroyedItems = testController.destroyedItems
@Test
fun testNoMaxSaves() {
// Load all pages
for (i in 0 until pagerAdapter.count) {
pager.currentItem = i
}
// Ensure all non-visible pages are saved
assertEquals(
destroyedItems.size,
pagerAdapter.savedPages.size()
)
}
@Test
fun testMaxSavedSet() {
val maxPages = 3
pagerAdapter.setMaxPagesToStateSave(maxPages)
// Load all pages
for (i in 0 until pagerAdapter.count) {
pager.currentItem = i
}
val firstSelectedItem = pagerAdapter.count / 2
for (i in pagerAdapter.count downTo firstSelectedItem) {
pager.currentItem = i
}
var savedPages = pagerAdapter.savedPages
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size())
// Ensure correct pages are saved
assertEquals(destroyedItems[destroyedItems.lastIndex], savedPages.keyAt(0))
assertEquals(destroyedItems[destroyedItems.lastIndex - 1], savedPages.keyAt(1))
assertEquals(destroyedItems[destroyedItems.lastIndex - 2], savedPages.keyAt(2))
val secondSelectedItem = 1
for (i in firstSelectedItem downTo secondSelectedItem) {
pager.currentItem = i
}
savedPages = pagerAdapter.savedPages
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size())
// Ensure correct pages are saved
assertEquals(destroyedItems[destroyedItems.lastIndex], savedPages.keyAt(0))
assertEquals(destroyedItems[destroyedItems.lastIndex - 1], savedPages.keyAt(1))
assertEquals(destroyedItems[destroyedItems.lastIndex - 2], savedPages.keyAt(2))
}
}
@@ -0,0 +1,85 @@
package com.bluelinelabs.conductor.viewpager.util
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
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.asTransaction
import com.bluelinelabs.conductor.viewpager.RouterPagerAdapter
class TestActivity : FragmentActivity() {
private lateinit var router: Router
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
router = Conductor.attachRouter(
this,
findViewById(android.R.id.content),
savedInstanceState
)
if (!router.hasRootController()) {
router.setRoot(TestController().asTransaction())
}
}
fun testController(): TestController {
return router.backstack.single().controller as TestController
}
}
class TestController : Controller() {
val destroyedItems = mutableListOf<Int>()
lateinit var pagerAdapter: RouterPagerAdapter
lateinit var pager: ViewPager
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup,
savedViewState: Bundle?
): View {
pager = ViewPager(container.context).also {
it.id = ViewCompat.generateViewId()
}
pager.offscreenPageLimit = 1
pagerAdapter = object : RouterPagerAdapter(this) {
override fun configureRouter(router: Router, position: Int) {
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(PageController()))
}
}
override fun getCount(): Int {
return 20
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
super.destroyItem(container, position, `object`)
destroyedItems.add(position)
}
}
pager.adapter = pagerAdapter
return pager
}
}
class PageController : Controller() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup,
savedViewState: Bundle?
): View {
return FrameLayout(container.context)
}
}
+34
View File
@@ -0,0 +1,34 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-parcelize")
alias(libs.plugins.mvnpublish)
}
android {
compileSdkVersion libs.versions.compilesdk.get() as Integer
defaultConfig {
minSdkVersion libs.versions.minsdk.get()
targetSdkVersion libs.versions.targetsdk.get()
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
testImplementation libs.junit
testImplementation libs.robolectric
implementation libs.androidx.appcompat
implementation libs.androidx.viewpager2
implementation project(':conductor')
}
ext.artifactId = 'conductor-viewpager2'
@@ -0,0 +1,3 @@
POM_NAME=Conductor ViewPager2 Adapter
POM_ARTIFACT_ID=conductor-viewpager2
POM_PACKAGING=aar
@@ -0,0 +1,3 @@
<manifest package="com.bluelinelabs.conductor.viewpager2">
<application />
</manifest>
@@ -0,0 +1,256 @@
package com.bluelinelabs.conductor.viewpager2
import android.os.Bundle
import android.os.Parcelable
import android.util.LongSparseArray
import android.util.SparseArray
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.StatefulAdapter
import androidx.viewpager2.widget.ViewPager2
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router
import kotlinx.parcelize.Parcelize
/**
* An ViewPager2 adapter that uses Routers as pages
*/
abstract class RouterStateAdapter(private val host: Controller) :
RecyclerView.Adapter<RouterViewHolder>(), StatefulAdapter {
private var savedPages = LongSparseArray<Bundle>()
internal var savedPageHistory = mutableListOf<Long>()
private var maxPagesToStateSave = Int.MAX_VALUE
private val visibleRouters = SparseArray<Router>()
private var currentPrimaryRouterPosition = 0
private var primaryItemCallback: PrimaryItemCallback? = null
init {
super.setHasStableIds(true)
}
/**
* Called when a router is instantiated. Here the router's root should be set if needed.
*
* @param router The router used for the page
* @param position The page position to be instantiated.
*/
abstract fun configureRouter(router: Router, position: Int)
/**
* Sets the maximum number of pages that will have their states saved. When this number is exceeded,
* the page that was state saved least recently will have its state removed from the save data.
*/
open fun setMaxPagesToStateSave(maxPagesToStateSave: Int) {
require(maxPagesToStateSave >= 0) { "Only positive integers may be passed for maxPagesToStateSave." }
this.maxPagesToStateSave = maxPagesToStateSave
ensurePagesSaved()
}
private fun inferViewPager(recyclerView: RecyclerView): ViewPager2 {
return recyclerView.parent as? ViewPager2
?: error("Expected ViewPager2 instance. Got: ${recyclerView.parent}")
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
val viewPager = inferViewPager(recyclerView)
primaryItemCallback = PrimaryItemCallback().also {
viewPager.registerOnPageChangeCallback(it)
}
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
val viewPager = inferViewPager(recyclerView)
primaryItemCallback?.let {
viewPager.unregisterOnPageChangeCallback(it)
}
primaryItemCallback = null
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RouterViewHolder {
return RouterViewHolder(parent)
}
override fun onBindViewHolder(holder: RouterViewHolder, position: Int) {
holder.currentItemPosition = position
attachRouter(holder, position)
}
override fun onViewAttachedToWindow(holder: RouterViewHolder) {
super.onViewAttachedToWindow(holder)
if (!holder.attached) {
attachRouter(holder, holder.currentItemPosition)
}
}
override fun onViewDetachedFromWindow(holder: RouterViewHolder) {
super.onViewDetachedFromWindow(holder)
detachRouter(holder)
// Controller has fully detached and destroyed its view reference by now. Remove the leftover
// view from the container.
holder.container.removeAllViews()
}
override fun onViewRecycled(holder: RouterViewHolder) {
super.onViewRecycled(holder)
detachRouter(holder)
holder.currentRouter?.let { router ->
host.removeChildRouter(router)
holder.currentRouter = null
}
}
override fun onFailedToRecycleView(holder: RouterViewHolder): Boolean {
return true
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun setHasStableIds(hasStableIds: Boolean) {
throw UnsupportedOperationException("Stable Ids are required for the adapter to function properly")
}
override fun saveState(): Parcelable {
// Ensure all visible pages are saved, starting at the outermost pages and working our way in
val visiblePositions = (0 until visibleRouters.size())
.map { visibleRouters.keyAt(it) }.toMutableList()
while (visiblePositions.isNotEmpty()) {
val lastPosition = visiblePositions.removeAt(visiblePositions.lastIndex)
savePage(getItemId(lastPosition), visibleRouters[lastPosition])
if (visiblePositions.isNotEmpty()) {
val firstPosition = visiblePositions.removeAt(0)
savePage(getItemId(firstPosition), visibleRouters[firstPosition])
}
}
return SavedState(
savedPagesKeys = (0 until savedPages.size()).map { savedPages.keyAt(it) },
savedPagesValues = (0 until savedPages.size()).map { savedPages.valueAt(it) },
savedPageHistory = savedPageHistory,
maxPagesToStateSave = maxPagesToStateSave
)
}
override fun restoreState(state: Parcelable) {
if (state !is SavedState) return
savedPages = LongSparseArray()
state.savedPagesKeys.indices.forEach { index ->
savedPages.put(state.savedPagesKeys[index], state.savedPagesValues[index])
}
savedPageHistory = state.savedPageHistory.toMutableList()
maxPagesToStateSave = state.maxPagesToStateSave
}
private fun attachRouter(holder: RouterViewHolder, position: Int) {
val itemId = getItemId(position)
val router = host.getChildRouter(holder.container, "$itemId", true, false)!!
.setPopRootControllerMode(Router.PopRootControllerMode.NEVER)
// This should have already been handled by onViewRecycled, but it seems like this wasn't
// always reliably called
if (router != holder.currentRouter) {
holder.currentRouter?.let { host.removeChildRouter(it) }
}
holder.currentRouter = router
holder.currentItemId = itemId
if (!router.hasRootController()) {
val routerSavedState = savedPages[itemId]
if (routerSavedState != null) {
router.restoreInstanceState(routerSavedState)
savedPages.remove(itemId)
savedPageHistory.remove(itemId)
}
}
router.rebindIfNeeded()
configureRouter(router, position)
if (position != currentPrimaryRouterPosition) {
for (transaction in router.backstack) {
transaction.controller.setOptionsMenuHidden(true)
}
}
visibleRouters.put(position, router)
holder.attached = true
}
private fun detachRouter(holder: RouterViewHolder) {
if (!holder.attached) {
return
}
holder.currentRouter?.let { router ->
router.prepareForHostDetach()
savePage(holder.currentItemId, router)
if (visibleRouters[holder.currentItemPosition] == router) {
visibleRouters.remove(holder.currentItemPosition)
}
}
holder.attached = false
}
private fun savePage(itemId: Long, router: Router) {
val savedState = Bundle()
router.saveInstanceState(savedState)
savedPages.put(itemId, savedState)
savedPageHistory.remove(itemId)
savedPageHistory.add(itemId)
ensurePagesSaved()
}
private fun ensurePagesSaved() {
while (savedPages.size() > maxPagesToStateSave) {
val routerIdToRemove = savedPageHistory.removeAt(0)
savedPages.remove(routerIdToRemove)
}
}
/**
* Returns the already instantiated Router in the specified position or `null` if there
* is no router associated with this position.
*/
fun getRouter(position: Int): Router? {
return visibleRouters[position]
}
inner class PrimaryItemCallback : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
val router = visibleRouters[position]
if (position != currentPrimaryRouterPosition) {
val previousRouter = visibleRouters[currentPrimaryRouterPosition]
previousRouter?.backstack?.forEach { it.controller.setOptionsMenuHidden(true) }
router?.backstack?.forEach { it.controller.setOptionsMenuHidden(false) }
currentPrimaryRouterPosition = position
}
}
}
@Parcelize
private data class SavedState(
val savedPagesKeys: List<Long>,
val savedPagesValues: List<Bundle>,
val savedPageHistory: List<Long>,
val maxPagesToStateSave: Int
) : Parcelable
}
@@ -0,0 +1,27 @@
package com.bluelinelabs.conductor.viewpager2
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.bluelinelabs.conductor.ChangeHandlerFrameLayout
import com.bluelinelabs.conductor.Router
class RouterViewHolder private constructor(val container: ChangeHandlerFrameLayout) : ViewHolder(container) {
var currentRouter: Router? = null
var currentItemPosition = 0
var currentItemId = 0L
var attached = false
companion object {
operator fun invoke(parent: ViewGroup): RouterViewHolder {
val container = ChangeHandlerFrameLayout(parent.context)
container.id = ViewCompat.generateViewId()
container.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
container.isSaveEnabled = false
return RouterViewHolder(container)
}
}
}
@@ -0,0 +1,119 @@
package com.bluelinelabs.conductor.viewpager2
import android.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
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
import com.bluelinelabs.conductor.viewpager2.util.TestController
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class StateSaveTests {
private val pager: ViewPager2
private val adapter: RouterStateAdapter
private val destroyedItems = mutableListOf<Int>()
init {
val activityController = Robolectric.buildActivity(FragmentActivity::class.java).setup()
val layout = FrameLayout(activityController.get())
activityController.get().setContentView(layout)
val router = Conductor.attachRouter(activityController.get(), FrameLayout(activityController.get()), null)
val controller = TestController()
router.setRoot(with(controller))
pager = ViewPager2(activityController.get()).also {
it.id = ViewCompat.generateViewId()
}
layout.addView(pager)
pager.offscreenPageLimit = 1
adapter = object : RouterStateAdapter(controller) {
override fun configureRouter(router: Router, position: Int) {
if (!router.hasRootController()) {
router.setRoot(with(TestController()))
}
}
override fun getItemCount(): Int {
return 20
}
override fun onViewDetachedFromWindow(holder: RouterViewHolder) {
super.onViewDetachedFromWindow(holder)
destroyedItems.add(holder.currentItemPosition)
}
}
pager.adapter = adapter
shadowOf(getMainLooper()).idle()
}
@Test
fun testNoMaxSaves() {
// Load all pages
for (i in 0 until adapter.itemCount) {
pager.setCurrentItem(i, false)
shadowOf(getMainLooper()).idle()
}
// Ensure all non-visible pages are saved
assertEquals(
destroyedItems.size,
adapter.savedPageHistory.size
)
}
@Test
fun testMaxSavedSet() {
val maxPages = 3
adapter.setMaxPagesToStateSave(maxPages)
// Load all pages
for (i in 0 until adapter.itemCount) {
pager.setCurrentItem(i, false)
shadowOf(getMainLooper()).idle()
}
val firstSelectedItem = adapter.itemCount / 2
for (i in adapter.itemCount downTo firstSelectedItem) {
pager.setCurrentItem(i, false)
shadowOf(getMainLooper()).idle()
}
var savedPages = adapter.savedPageHistory
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size)
// Ensure correct pages are saved
assertEquals(destroyedItems[destroyedItems.lastIndex], savedPages[savedPages.lastIndex].toInt())
assertEquals(destroyedItems[destroyedItems.lastIndex - 1], savedPages[savedPages.lastIndex - 1].toInt())
assertEquals(destroyedItems[destroyedItems.lastIndex - 2], savedPages[savedPages.lastIndex - 2].toInt())
val secondSelectedItem = 1
for (i in adapter.itemCount downTo secondSelectedItem) {
pager.setCurrentItem(i, false)
shadowOf(getMainLooper()).idle()
}
savedPages = adapter.savedPageHistory
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size)
// Ensure correct pages are saved
assertEquals(destroyedItems[destroyedItems.lastIndex], savedPages[savedPages.lastIndex].toInt())
assertEquals(destroyedItems[destroyedItems.lastIndex - 1], savedPages[savedPages.lastIndex - 1].toInt())
assertEquals(destroyedItems[destroyedItems.lastIndex - 2], savedPages[savedPages.lastIndex - 2].toInt())
}
}
@@ -1,17 +1,20 @@
package com.bluelinelabs.conductor.support.util;
package com.bluelinelabs.conductor.viewpager2.util;
import android.support.annotation.NonNull;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
public class TestController extends Controller {
@Override @NonNull
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) {
return new FrameLayout(inflater.getContext());
}
+18 -40
View File
@@ -1,58 +1,36 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'de.mobilej.unmock:UnMockPlugin:0.6.5'
}
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-parcelize")
alias(libs.plugins.mvnpublish)
}
apply plugin: 'com.android.library'
apply plugin: 'de.mobilej.unmock'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
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'
}
}
configurations {
lintChecks
}
dependencies {
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.roboelectric
api libs.androidx.activity
api libs.androidx.appcompat
api libs.androidx.savedstate.ktx
testImplementation libs.junit
testImplementation libs.robolectric
testImplementation libs.kotest
api rootProject.ext.supportAnnotations
api libs.androidx.lifecycle.runtime
lintChecks project(path: ':conductor-lint', configuration: 'lintChecks')
}
api libs.androidx.annotation
api libs.kotlin.stdlib
task copyLintJar(type: Copy) {
from(configurations.lintChecks) {
rename { 'lint.jar' }
}
into 'build/intermediates/lint/'
}
project.afterEvaluate {
def compileLintTask = project.tasks.find { it.name == 'compileLint' }
compileLintTask.dependsOn(copyLintJar)
lintPublish project(':conductor-lint')
}
ext.artifactId = 'conductor'
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
+3
View File
@@ -3,3 +3,6 @@
public <init>();
public <init>(android.os.Bundle);
}
-keepclassmembers public class * extends com.bluelinelabs.conductor.ControllerChangeHandler {
public <init>();
}
@@ -5,10 +5,11 @@ import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.internal.LifecycleHandler;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
@@ -20,6 +21,10 @@ public class ActivityHostedRouter extends Router {
private LifecycleHandler lifecycleHandler;
private final TransactionIndexer transactionIndexer = new TransactionIndexer();
public ActivityHostedRouter() {
popRootControllerMode = PopRootControllerMode.NEVER;
}
public final void setHost(@NonNull LifecycleHandler lifecycleHandler, @NonNull ViewGroup container) {
if (this.lifecycleHandler != lifecycleHandler || this.container != container) {
if (this.container != null && this.container instanceof ControllerChangeListener) {
@@ -57,15 +62,18 @@ public class ActivityHostedRouter extends Router {
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
super.onActivityDestroyed(activity);
lifecycleHandler = null;
public void onActivityDestroyed(@NonNull Activity activity, boolean isConfigurationChange) {
super.onActivityDestroyed(activity, isConfigurationChange);
if (!isConfigurationChange) {
lifecycleHandler = null;
}
}
@Override
public final void invalidateOptionsMenu() {
if (lifecycleHandler != null && lifecycleHandler.getFragmentManager() != null) {
lifecycleHandler.getFragmentManager().invalidateOptionsMenu();
if (lifecycleHandler != null && getActivity() != null) {
getActivity().invalidateOptionsMenu();
}
}
@@ -81,7 +89,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,117 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
class Backstack implements Iterable<RouterTransaction> {
private static final String KEY_ENTRIES = "Backstack.entries";
private final Deque<RouterTransaction> backstack = new ArrayDeque<>();
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
boolean isEmpty() {
return backstack.isEmpty();
}
int size() {
return backstack.size();
}
@Nullable
RouterTransaction root() {
return backstack.size() > 0 ? backstack.getLast() : null;
}
@Override @NonNull
public Iterator<RouterTransaction> iterator() {
return backstack.iterator();
}
@NonNull
Iterator<RouterTransaction> reverseIterator() {
return backstack.descendingIterator();
}
@NonNull
List<RouterTransaction> popTo(@NonNull RouterTransaction transaction) {
List<RouterTransaction> popped = new ArrayList<>();
if (backstack.contains(transaction)) {
while (backstack.peek() != transaction) {
RouterTransaction poppedTransaction = pop();
popped.add(poppedTransaction);
}
} else {
throw new RuntimeException("Tried to pop to a transaction that was not on the back stack");
}
return popped;
}
@NonNull
RouterTransaction pop() {
RouterTransaction popped = backstack.pop();
popped.controller.destroy();
return popped;
}
@Nullable
RouterTransaction peek() {
return backstack.peek();
}
void push(@NonNull RouterTransaction transaction) {
backstack.push(transaction);
}
@NonNull
List<RouterTransaction> popAll() {
List<RouterTransaction> list = new ArrayList<>();
while (!isEmpty()) {
list.add(pop());
}
return list;
}
void setBackstack(@NonNull List<RouterTransaction> backstack) {
this.backstack.clear();
for (RouterTransaction transaction : backstack) {
this.backstack.push(transaction);
}
}
boolean contains(@NonNull Controller controller) {
for (RouterTransaction transaction : backstack) {
if (controller == transaction.controller) {
return true;
}
}
return false;
}
void saveInstanceState(@NonNull Bundle outState) {
ArrayList<Bundle> entryBundles = new ArrayList<>(backstack.size());
for (RouterTransaction entry : backstack) {
entryBundles.add(entry.saveInstanceState());
}
outState.putParcelableArrayList(KEY_ENTRIES, entryBundles);
}
void restoreInstanceState(@NonNull Bundle savedInstanceState) {
ArrayList<Bundle> entryBundles = savedInstanceState.getParcelableArrayList(KEY_ENTRIES);
if (entryBundles != null) {
Collections.reverse(entryBundles);
for (Bundle transactionBundle : entryBundles) {
backstack.push(new RouterTransaction(transactionBundle));
}
}
}
}
@@ -0,0 +1,102 @@
package com.bluelinelabs.conductor
import android.os.Bundle
import java.util.ArrayDeque
import java.util.Deque
internal class Backstack : Iterable<RouterTransaction> {
private val backstack: Deque<RouterTransaction> = ArrayDeque()
val isEmpty: Boolean get() = backstack.isEmpty()
val size: Int get() = backstack.size
var onBackstackUpdatedListener: OnBackstackUpdatedListener? = null
fun root(): RouterTransaction? = backstack.lastOrNull()
override fun iterator(): Iterator<RouterTransaction> = backstack.toTypedArray().iterator()
fun reverseIterator(): Iterator<RouterTransaction> = backstack.reversed().iterator()
fun remove(transaction: RouterTransaction) = backstack.remove(transaction)
fun popTo(transaction: RouterTransaction): List<RouterTransaction> {
if (transaction in backstack) {
val popped: MutableList<RouterTransaction> = ArrayList()
while (backstack.peek() != transaction) {
val poppedTransaction = pop()
popped.add(poppedTransaction)
}
return popped
} else {
throw RuntimeException("Tried to pop to a transaction that was not on the back stack")
}
}
fun pop(): RouterTransaction {
return backstack.pop().also {
onBackstackUpdatedListener?.onBackstackUpdated()
it.controller.destroy()
}
}
fun peek(): RouterTransaction? = backstack.peek()
fun push(transaction: RouterTransaction) {
backstack.push(transaction)
onBackstackUpdatedListener?.onBackstackUpdated()
}
fun popAll(): List<RouterTransaction> {
val list: MutableList<RouterTransaction> = ArrayList()
while (!isEmpty) {
list.add(pop())
}
return list
}
fun setBackstack(backstack: List<RouterTransaction>) {
this.backstack.clear()
backstack.forEach { transaction ->
this.backstack.push(transaction)
}
onBackstackUpdatedListener?.onBackstackUpdated()
}
operator fun contains(controller: Controller): Boolean {
return backstack.any {
it.controller == controller
}
}
fun saveInstanceState(outState: Bundle) {
val entryBundles = ArrayList<Bundle>(backstack.size)
backstack.mapTo(entryBundles) {
it.saveInstanceState()
}
outState.putParcelableArrayList(KEY_ENTRIES, entryBundles)
}
fun restoreInstanceState(savedInstanceState: Bundle) {
val entryBundles = savedInstanceState.getParcelableArrayList<Bundle?>(KEY_ENTRIES)
if (entryBundles != null) {
entryBundles.reverse()
for (transactionBundle in entryBundles) {
backstack.push(RouterTransaction(transactionBundle!!))
}
}
onBackstackUpdatedListener?.onBackstackUpdated()
}
fun interface OnBackstackUpdatedListener {
fun onBackstackUpdated()
}
companion object {
private const val KEY_ENTRIES = "Backstack.entries"
}
}
@@ -1,56 +0,0 @@
package com.bluelinelabs.conductor;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
/**
* A FrameLayout implementation that can be used to block user interactions while
* {@link ControllerChangeHandler}s are performing changes. It is not required to use this
* ViewGroup, but it can be helpful.
*/
public class ChangeHandlerFrameLayout extends FrameLayout implements ControllerChangeListener {
private int inProgressTransactionCount;
public ChangeHandlerFrameLayout(Context context) {
super(context);
}
public ChangeHandlerFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ChangeHandlerFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ChangeHandlerFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return (inProgressTransactionCount > 0) || super.onInterceptTouchEvent(ev);
}
@Override
public void onChangeStarted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) {
inProgressTransactionCount++;
}
@Override
public void onChangeCompleted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) {
inProgressTransactionCount--;
}
}
@@ -0,0 +1,60 @@
package com.bluelinelabs.conductor
import android.annotation.TargetApi
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.ViewGroup
import android.widget.FrameLayout
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener
/**
* A FrameLayout implementation that can be used to block user interactions while
* [ControllerChangeHandler]s are performing changes. It is not required to use this
* ViewGroup, but it can be helpful.
*/
open class ChangeHandlerFrameLayout : FrameLayout, ControllerChangeListener {
private var inProgressTransactionCount = 0
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(
context,
attrs,
defStyleAttr,
defStyleRes
)
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
return inProgressTransactionCount > 0 || super.onInterceptTouchEvent(ev)
}
override fun onChangeStarted(
to: Controller?,
from: Controller?,
isPush: Boolean,
container: ViewGroup,
handler: ControllerChangeHandler
) {
inProgressTransactionCount++
}
override fun onChangeCompleted(
to: Controller?,
from: Controller?,
isPush: Boolean,
container: ViewGroup,
handler: ControllerChangeHandler
) {
inProgressTransactionCount--
}
}
@@ -1,44 +0,0 @@
package com.bluelinelabs.conductor;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.internal.LifecycleHandler;
import com.bluelinelabs.conductor.internal.ThreadUtils;
/**
* Point of initial interaction with Conductor. Used to attach a {@link Router} to your Activity.
*/
public final class Conductor {
private Conductor() {}
/**
* Conductor will create a {@link Router} that has been initialized for your Activity and containing ViewGroup.
* If an existing {@link Router} is already associated with this Activity/ViewGroup pair, either in memory
* or in the savedInstanceState, that router will be used and rebound instead of creating a new one with
* an empty backstack.
*
* @param activity The Activity that will host the {@link Router} being attached.
* @param container The ViewGroup in which the {@link Router}'s {@link Controller} views will be hosted
* @param savedInstanceState The savedInstanceState passed into the hosting Activity's onCreate method. Used
* for restoring the Router's state if possible.
* @return A fully configured {@link Router} instance for use with this Activity/ViewGroup pair.
*/
@NonNull @UiThread
public static Router attachRouter(@NonNull Activity activity, @NonNull ViewGroup container, @Nullable Bundle savedInstanceState) {
ThreadUtils.ensureMainThread();
LifecycleHandler lifecycleHandler = LifecycleHandler.install(activity);
Router router = lifecycleHandler.getRouter(container, savedInstanceState);
router.rebindIfNeeded();
return router;
}
}
@@ -0,0 +1,41 @@
package com.bluelinelabs.conductor
import android.app.Activity
import android.os.Bundle
import android.view.ViewGroup
import androidx.annotation.UiThread
import com.bluelinelabs.conductor.internal.LifecycleHandler
import com.bluelinelabs.conductor.internal.ensureMainThread
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
@JvmOverloads
fun attachRouter(
activity: Activity,
container: ViewGroup,
savedInstanceState: Bundle?,
allowExperimentalAndroidXBacking: Boolean = true,
): Router {
ensureMainThread()
return LifecycleHandler.install(activity, allowAndroidXBacking = allowExperimentalAndroidXBacking)
.getRouter(container, savedInstanceState)
.also { it.rebindIfNeeded() }
.setPopRootControllerMode(Router.PopRootControllerMode.NEVER)
}
}
@@ -1,6 +1,5 @@
package com.bluelinelabs.conductor;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
@@ -9,10 +8,6 @@ import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -21,7 +16,17 @@ 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;
import androidx.lifecycle.LifecycleOwner;
import com.bluelinelabs.conductor.internal.ClassUtils;
import com.bluelinelabs.conductor.internal.ControllerLifecycleOwner;
import com.bluelinelabs.conductor.internal.OwnViewTreeLifecycleAndRegistry;
import com.bluelinelabs.conductor.internal.RouterRequiringFunc;
import com.bluelinelabs.conductor.internal.ViewAttachHandler;
import com.bluelinelabs.conductor.internal.ViewAttachHandler.ViewAttachListener;
@@ -31,7 +36,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;
@@ -79,6 +83,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;
@@ -91,6 +96,25 @@ 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);
}
}
}
};
public final LifecycleOwner lifecycleOwner = new ControllerLifecycleOwner(this);
@NonNull
static Controller newInstance(@NonNull Bundle bundle) {
final String className = bundle.getString(KEY_CLASS_NAME);
@@ -107,10 +131,10 @@ public abstract class Controller {
Controller controller;
try {
if (bundleConstructor != null) {
controller = (Controller)bundleConstructor.newInstance(args);
controller = (Controller) bundleConstructor.newInstance(args);
} else {
//noinspection ConstantConditions
controller = (Controller)getDefaultConstructor(constructors).newInstance();
controller = (Controller) getDefaultConstructor(constructors).newInstance();
// Restore the args that existed before the last process death
if (args != null) {
@@ -141,6 +165,7 @@ public abstract class Controller {
this.args = args != null ? args : new Bundle(getClass().getClassLoader());
instanceId = UUID.randomUUID().toString();
ensureRequiredConstructor();
OwnViewTreeLifecycleAndRegistry.Companion.own(this);
}
/**
@@ -148,13 +173,15 @@ public abstract class Controller {
* for this method will be {@code return inflater.inflate(R.layout.my_layout, container, false);}, plus
* any binding code.
*
* @param inflater The LayoutInflater that should be used to inflate views
* @param container The parent view that this Controller's view will eventually be attached to.
* This Controller's view should NOT be added in this method. It is simply passed in
* so that valid LayoutParams can be used during inflation.
* @param inflater The LayoutInflater that should be used to inflate views
* @param container The parent view that this Controller's view will eventually be attached to.
* This Controller's view should NOT be added in this method. It is simply passed in
* so that valid LayoutParams can be used during inflation.
* @param savedViewState A bundle for the view's state, which would have been created in {@link #onSaveViewState(View, Bundle)},
* or {@code null} if no saved state exists.
*/
@NonNull
protected abstract View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container);
protected abstract View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState);
/**
* Returns the {@link Router} object that can be used for pushing or popping other Controllers
@@ -189,7 +216,7 @@ public abstract class Controller {
* the same container unless you have a great reason to do so (ex: ViewPagers).
*
* @param container The ViewGroup that hosts the child Router
* @param tag The router's tag or {@code null} if none is needed
* @param tag The router's tag or {@code null} if none is needed
*/
@NonNull
public final Router getChildRouter(@NonNull ViewGroup container, @Nullable String tag) {
@@ -204,17 +231,37 @@ public abstract class Controller {
* The only time this method will return {@code null} is when the child router does not exist prior
* to calling this method and the createIfNeeded parameter is set to false.
*
* @param container The ViewGroup that hosts the child Router
* @param tag The router's tag or {@code null} if none is needed
* @param container The ViewGroup that hosts the child Router
* @param tag The router's tag or {@code null} if none is needed
* @param createIfNeeded If true, a router will be created if one does not yet exist. Else {@code null} will be returned in this case.
*/
@Nullable
public final Router getChildRouter(@NonNull ViewGroup container, @Nullable String tag, boolean createIfNeeded) {
return getChildRouter(container, tag, createIfNeeded, true);
}
/**
* Retrieves the child {@link Router} for the given container/tag combination. Note that multiple
* routers should not exist in the same container unless a lot of care is taken to maintain order
* between them. Avoid using the same container unless you have a great reason to do so (ex: ViewPagers).
* The only time this method will return {@code null} is when the child router does not exist prior
* to calling this method and the createIfNeeded parameter is set to false.
*
* @param container The ViewGroup that hosts the child Router
* @param tag The router's tag or {@code null} if none is needed
* @param createIfNeeded If true, a router will be created if one does not yet exist. Else {@code null} will be returned in this case.
* @param boundToHostContainerId If true, a router will only ever rebind with a container with the same view id on state restoration. Note that this must be set to true if the tag is null.
*/
@Nullable
public final Router getChildRouter(@NonNull ViewGroup container, @Nullable String tag, boolean createIfNeeded, boolean boundToHostContainerId) {
@IdRes final int containerId = container.getId();
if (containerId == View.NO_ID) {
throw new IllegalStateException("You must set an id on your container.");
}
ControllerHostedRouter childRouter = null;
for (ControllerHostedRouter router : childRouters) {
if (router.getHostId() == containerId && TextUtils.equals(tag, router.getTag())) {
if (router.matches(containerId, tag)) {
childRouter = router;
break;
}
@@ -222,8 +269,8 @@ public abstract class Controller {
if (childRouter == null) {
if (createIfNeeded) {
childRouter = new ControllerHostedRouter(container.getId(), tag);
childRouter.setHost(this, container);
childRouter = new ControllerHostedRouter(container.getId(), tag, boundToHostContainerId);
childRouter.setHostContainer(this, container);
childRouters.add(childRouter);
if (isPerformingExitTransition) {
@@ -231,7 +278,7 @@ public abstract class Controller {
}
}
} else if (!childRouter.hasHost()) {
childRouter.setHost(this, container);
childRouter.setHostContainer(this, container);
childRouter.rebindIfNeeded();
}
@@ -289,6 +336,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.
@@ -393,7 +451,8 @@ public abstract class Controller {
*
* @param view The View to which this Controller should be bound.
*/
protected void onDestroyView(@NonNull View view) { }
protected void onDestroyView(@NonNull View view) {
}
/**
* Called when this Controller begins the process of being swapped in or out of the host view.
@@ -401,7 +460,8 @@ public abstract class Controller {
* @param changeHandler The {@link ControllerChangeHandler} that's managing the swap
* @param changeType The type of change that's occurring
*/
protected void onChangeStarted(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { }
protected void onChangeStarted(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
}
/**
* Called when this Controller completes the process of being swapped in or out of the host view.
@@ -409,59 +469,69 @@ public abstract class Controller {
* @param changeHandler The {@link ControllerChangeHandler} that's managing the swap
* @param changeType The type of change that occurred
*/
protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { }
protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
}
/**
* Called when this Controller has a Context available to it. This will happen very early on in the lifecycle
* (before a view is created). If the host activity is re-created (ex: for orientation change), this will be
* called again when the new context is available.
*/
protected void onContextAvailable(@NonNull Context context) { }
protected void onContextAvailable(@NonNull Context context) {
}
/**
* Called when this Controller's Context is no longer available. This can happen when the Controller is
* destroyed or when the host Activity is destroyed.
*/
protected void onContextUnavailable() { }
protected void onContextUnavailable() {
}
/**
* Called when this Controller is attached to its host ViewGroup
*
* @param view The View for this Controller (passed for convenience)
*/
protected void onAttach(@NonNull View view) { }
protected void onAttach(@NonNull View view) {
}
/**
* Called when this Controller is detached from its host ViewGroup
*
* @param view The View for this Controller (passed for convenience)
*/
protected void onDetach(@NonNull View view) { }
protected void onDetach(@NonNull View view) {
}
/**
* Called when this Controller has been destroyed.
*/
protected void onDestroy() { }
protected void onDestroy() {
}
/**
* Called when this Controller's host Activity is started
*/
protected void onActivityStarted(@NonNull Activity activity) { }
protected void onActivityStarted(@NonNull Activity activity) {
}
/**
* Called when this Controller's host Activity is resumed
*/
protected void onActivityResumed(@NonNull Activity activity) { }
protected void onActivityResumed(@NonNull Activity activity) {
}
/**
* Called when this Controller's host Activity is paused
*/
protected void onActivityPaused(@NonNull Activity activity) { }
protected void onActivityPaused(@NonNull Activity activity) {
}
/**
* Called when this Controller's host Activity is stopped
*/
protected void onActivityStopped(@NonNull Activity activity) { }
protected void onActivityStopped(@NonNull Activity activity) {
}
/**
* Called to save this Controller's View state. As Views can be detached and destroyed as part of the
@@ -471,7 +541,8 @@ public abstract class Controller {
* @param view This Controller's View, passed for convenience
* @param outState The Bundle into which the View state should be saved
*/
protected void onSaveViewState(@NonNull View view, @NonNull Bundle outState) { }
protected void onSaveViewState(@NonNull View view, @NonNull Bundle outState) {
}
/**
* Restores data that was saved in the {@link #onSaveViewState(View, Bundle)} method. This should be overridden
@@ -480,14 +551,16 @@ public abstract class Controller {
* @param view This Controller's View, passed for convenience
* @param savedViewState The bundle that has data to be restored
*/
protected void onRestoreViewState(@NonNull View view, @NonNull Bundle savedViewState) { }
protected void onRestoreViewState(@NonNull View view, @NonNull Bundle savedViewState) {
}
/**
* Called to save this Controller's state in the event that its host Activity is destroyed.
*
* @param outState The Bundle into which data should be saved
*/
protected void onSaveInstanceState(@NonNull Bundle outState) { }
protected void onSaveInstanceState(@NonNull Bundle outState) {
}
/**
* Restores data that was saved in the {@link #onSaveInstanceState(Bundle)} method. This should be overridden
@@ -495,33 +568,28 @@ public abstract class Controller {
*
* @param savedInstanceState The bundle that has data to be restored
*/
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { }
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
}
/**
* Calls startActivity(Intent) from this Controller's host Activity.
*/
public final void startActivity(@NonNull final Intent intent) {
executeWithRouter(new RouterRequiringFunc() {
@Override public void execute() { router.startActivity(intent); }
});
executeWithRouter(() -> router.startActivity(intent));
}
/**
* Calls startActivityForResult(Intent, int) from this Controller's host Activity.
*/
public final void startActivityForResult(@NonNull final Intent intent, final int requestCode) {
executeWithRouter(new RouterRequiringFunc() {
@Override public void execute() { router.startActivityForResult(instanceId, intent, requestCode); }
});
executeWithRouter(() -> router.startActivityForResult(instanceId, intent, requestCode));
}
/**
* Calls startActivityForResult(Intent, int, Bundle) from this Controller's host Activity.
*/
public final void startActivityForResult(@NonNull final Intent intent, final int requestCode, @Nullable final Bundle options) {
executeWithRouter(new RouterRequiringFunc() {
@Override public void execute() { router.startActivityForResult(instanceId, intent, requestCode, options); }
});
executeWithRouter(() -> router.startActivityForResult(instanceId, intent, requestCode, options));
}
/**
@@ -539,9 +607,7 @@ public abstract class Controller {
* @param requestCode The request code being registered for.
*/
public final void registerForActivityResult(final int requestCode) {
executeWithRouter(new RouterRequiringFunc() {
@Override public void execute() { router.registerForActivityResult(instanceId, requestCode); }
});
executeWithRouter(() -> router.registerForActivityResult(instanceId, requestCode));
}
/**
@@ -552,20 +618,18 @@ public abstract class Controller {
* @param resultCode The resultCode that was returned to the host Activity's onActivityResult method
* @param data The data Intent that was returned to the host Activity's onActivityResult method
*/
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { }
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
}
/**
* Calls requestPermission(String[], int) from this Controller's host Activity. Results for this request,
* 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));
}
/**
@@ -585,13 +649,17 @@ public abstract class Controller {
* @param permissions The array of permissions requested
* @param grantResults The results for each permission requested
*/
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { }
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
}
/**
* Should be overridden if this Controller needs to handle the back button being pressed.
*
* 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<>();
@@ -599,15 +667,10 @@ public abstract class Controller {
childTransactions.addAll(childRouter.getBackstack());
}
Collections.sort(childTransactions, new Comparator<RouterTransaction>() {
@Override
public int compare(RouterTransaction o1, RouterTransaction o2) {
return o2.transactionIndex - o1.transactionIndex;
}
});
Collections.sort(childTransactions, (t1, t2) -> t2.getTransactionIndex() - t1.getTransactionIndex());
for (RouterTransaction transaction : childTransactions) {
Controller childController = transaction.controller;
Controller childController = transaction.controller();
if (childController.isAttached() && childController.getRouter().handleBack()) {
return true;
@@ -652,7 +715,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);
}
}
@@ -727,10 +790,11 @@ public abstract class Controller {
* Adds option items to the host Activity's standard options menu. This will only be called if
* {@link #setHasOptionsMenu(boolean)} has been called.
*
* @param menu The menu into which your options should be placed.
* @param menu The menu into which your options should be placed.
* @param inflater The inflater that can be used to inflate your menu items.
*/
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { }
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
}
/**
* Prepare the screen's options menu to be displayed. This is called directly before showing the
@@ -738,7 +802,8 @@ public abstract class Controller {
*
* @param menu The menu that will be displayed
*/
public void onPrepareOptionsMenu(@NonNull Menu menu) { }
public void onPrepareOptionsMenu(@NonNull Menu menu) {
}
/**
* Called when an option menu item has been selected by the user.
@@ -799,6 +864,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);
@@ -813,6 +886,31 @@ public abstract class Controller {
}
}
final void onContextUnavailable(@NonNull Context context) {
for (Router childRouter : childRouters) {
childRouter.onContextUnavailable(context);
}
if (isContextAvailable) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preContextUnavailable(this, context);
}
isContextAvailable = false;
onContextUnavailable();
if (onBackPressedDispatcherEnabled) {
onBackPressedCallback.remove();
}
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postContextUnavailable(this);
}
}
}
final void executeWithRouter(@NonNull RouterRequiringFunc listener) {
if (router != null) {
listener.execute();
@@ -846,7 +944,7 @@ public abstract class Controller {
final void activityStopped(@NonNull Activity activity) {
final boolean attached = this.attached;
if (viewAttachHandler != null) {
viewAttachHandler.onActivityStopped();
}
@@ -865,25 +963,12 @@ public abstract class Controller {
destroy(true);
}
if (isContextAvailable) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preContextUnavailable(this, activity);
}
isContextAvailable = false;
onContextUnavailable();
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postContextUnavailable(this);
}
}
onContextUnavailable(activity);
}
void attach(@NonNull View view) {
attachedToUnownedParent = router == null || view.getParent() != router.container;
if (attachedToUnownedParent) {
if (attachedToUnownedParent || isBeingDestroyed) {
return;
}
@@ -917,14 +1002,18 @@ public abstract class Controller {
for (ControllerHostedRouter childRouter : childRouters) {
for (RouterTransaction childTransaction : childRouter.backstack) {
if (childTransaction.controller.awaitingParentAttach) {
childTransaction.controller.attach(childTransaction.controller.view);
if (childTransaction.controller().awaitingParentAttach) {
childTransaction.controller().attach(childTransaction.controller().view);
}
}
if (childRouter.hasHost()) {
childRouter.rebindIfNeeded();
}
}
}
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();
@@ -934,34 +1023,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);
}
@@ -973,7 +1069,11 @@ public abstract class Controller {
onDestroyView(view);
viewAttachHandler.unregisterAttachListener(view);
// viewAttachHandler may be null iff the controller was popped before we got here
if (viewAttachHandler != null) {
viewAttachHandler.unregisterAttachListener(view);
}
viewAttachHandler = null;
viewIsAttached = false;
@@ -993,14 +1093,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) {
@@ -1009,7 +1110,8 @@ public abstract class Controller {
lifecycleListener.preCreateView(this);
}
view = onCreateView(LayoutInflater.from(parent.getContext()), parent);
Bundle savedViewState = viewState == null ? null : viewState.getBundle(KEY_VIEW_STATE_BUNDLE);
view = onCreateView(LayoutInflater.from(parent.getContext()), parent, savedViewState);
if (view == parent) {
throw new IllegalStateException("Controller's onCreateView method returned the parent ViewGroup. Perhaps you forgot to pass false for LayoutInflater.inflate's attachToRoot parameter?");
}
@@ -1021,33 +1123,35 @@ public abstract class Controller {
restoreViewState(view);
viewAttachHandler = new ViewAttachHandler(new ViewAttachListener() {
@Override
public void onAttached() {
viewIsAttached = true;
viewWasDetached = false;
attach(view);
}
@Override
public void onDetached(boolean fromActivityStop) {
viewIsAttached = false;
viewWasDetached = true;
if (!isDetachFrozen) {
detach(view, false, fromActivityStop);
if (!isBeingDestroyed) {
viewAttachHandler = new ViewAttachHandler(new ViewAttachListener() {
@Override
public void onAttached() {
viewIsAttached = true;
viewWasDetached = false;
attach(view);
}
}
@Override
public void onViewDetachAfterStop() {
if (!isDetachFrozen) {
detach(view, false, false);
@Override
public void onDetached(boolean fromActivityStop) {
viewIsAttached = false;
viewWasDetached = true;
if (!isDetachFrozen) {
detach(view, false, fromActivityStop);
}
}
}
});
viewAttachHandler.listenForAttach(view);
} else if (retainViewMode == RetainViewMode.RETAIN_DETACH) {
@Override
public void onViewDetachAfterStop() {
if (!isDetachFrozen) {
detach(view, false, false);
}
}
});
viewAttachHandler.listenForAttach(view);
}
} else {
restoreChildControllerHosts();
}
@@ -1059,28 +1163,21 @@ public abstract class Controller {
if (!childRouter.hasHost()) {
View containerView = view.findViewById(childRouter.getHostId());
if (containerView != null && containerView instanceof ViewGroup) {
childRouter.setHost(this, (ViewGroup)containerView);
if (containerView instanceof ViewGroup) {
childRouter.setHostContainer(this, (ViewGroup) containerView);
childRouter.rebindIfNeeded();
}
}
}
}
private void performDestroy() {
private void performDestroy(@Nullable Context context) {
if (context == null) {
context = getActivity();
}
if (isContextAvailable) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preContextUnavailable(this, getActivity());
}
isContextAvailable = false;
onContextUnavailable();
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postContextUnavailable(this);
}
onContextUnavailable(context);
}
if (!destroyed) {
@@ -1118,7 +1215,7 @@ public abstract class Controller {
}
if (!attached) {
removeViewReference();
removeViewReference(null);
} else if (removeViews) {
detach(view, true, false);
}
@@ -1219,6 +1316,7 @@ public abstract class Controller {
List<Bundle> childBundles = savedInstanceState.getParcelableArrayList(KEY_CHILD_ROUTERS);
for (Bundle childBundle : childBundles) {
ControllerHostedRouter childRouter = new ControllerHostedRouter();
childRouter.setHostController(this);
childRouter.restoreInstanceState(childBundle);
childRouters.add(childRouter);
}
@@ -1281,18 +1379,30 @@ public abstract class Controller {
}
destroyedView = null;
}
changeHandler.onEnd();
}
final void setDetachFrozen(boolean frozen) {
if (isDetachFrozen != frozen) {
isDetachFrozen = frozen;
boolean detach = !frozen && view != null && viewWasDetached;
for (ControllerHostedRouter router : childRouters) {
if (detach) {
router.prepareForHostDetach();
}
router.setDetachFrozen(frozen);
}
if (!frozen && view != null && viewWasDetached) {
if (detach) {
View aView = view;
detach(view, false, false);
if (view == null && aView.getParent() == router.container) {
router.container.removeView(aView); // need to remove the view when this controller is a child controller
}
}
}
}
@@ -1344,46 +1454,84 @@ public abstract class Controller {
return null;
}
/** Modes that will influence when the Controller will allow its view to be destroyed */
/**
* Modes that will influence when the Controller will allow its view to be destroyed
*/
public enum RetainViewMode {
/** The Controller will release its reference to its view as soon as it is detached. */
/**
* The Controller will release its reference to its view as soon as it is detached.
*/
RELEASE_DETACH,
/** The Controller will retain its reference to its view when detached, but will still release the reference when a config change occurs. */
/**
* The Controller will retain its reference to its view when detached, but will still release the reference when a config change occurs.
*/
RETAIN_DETACH
}
/** Allows external classes to listen for lifecycle events in a Controller */
/**
* Allows external classes to listen for lifecycle events in a Controller
*/
public static abstract class LifecycleListener {
public void onChangeStart(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { }
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { }
public void onChangeStart(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
}
public void preCreateView(@NonNull Controller controller) { }
public void postCreateView(@NonNull Controller controller, @NonNull View view) { }
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
}
public void preAttach(@NonNull Controller controller, @NonNull View view) { }
public void postAttach(@NonNull Controller controller, @NonNull View view) { }
public void preCreateView(@NonNull Controller controller) {
}
public void preDetach(@NonNull Controller controller, @NonNull View view) { }
public void postDetach(@NonNull Controller controller, @NonNull View view) { }
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
}
public void preDestroyView(@NonNull Controller controller, @NonNull View view) { }
public void postDestroyView(@NonNull Controller controller) { }
public void preAttach(@NonNull Controller controller, @NonNull View view) {
}
public void preDestroy(@NonNull Controller controller) { }
public void postDestroy(@NonNull Controller controller) { }
public void postAttach(@NonNull Controller controller, @NonNull View view) {
}
public void preContextAvailable(@NonNull Controller controller) { }
public void postContextAvailable(@NonNull Controller controller, @NonNull Context context) { }
public void preDetach(@NonNull Controller controller, @NonNull View view) {
}
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) { }
public void postContextUnavailable(@NonNull Controller controller) { }
public void postDetach(@NonNull Controller controller, @NonNull View view) {
}
public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) { }
public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) { }
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
}
public void onSaveViewState(@NonNull Controller controller, @NonNull Bundle outState) { }
public void onRestoreViewState(@NonNull Controller controller, @NonNull Bundle savedViewState) { }
public void postDestroyView(@NonNull Controller controller) {
}
public void preDestroy(@NonNull Controller controller) {
}
public void postDestroy(@NonNull Controller controller) {
}
public void preContextAvailable(@NonNull Controller controller) {
}
public void postContextAvailable(@NonNull Controller controller, @NonNull Context context) {
}
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
}
public void postContextUnavailable(@NonNull Controller controller) {
}
public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
}
public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
}
public void onSaveViewState(@NonNull Controller controller, @NonNull Bundle outState) {
}
public void onRestoreViewState(@NonNull Controller controller, @NonNull Bundle savedViewState) {
}
}
@@ -1,309 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import 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);
}
}
});
}
}
public boolean removesFromViewOnPush() {
return true;
}
public void setForceRemoveViewOnPush(boolean force) {
forceRemoveViewOnPush = force;
}
static class ChangeTransaction {
@Nullable final Controller to;
@Nullable final Controller from;
final boolean isPush;
@Nullable final ViewGroup container;
@Nullable final ControllerChangeHandler changeHandler;
@NonNull final List<ControllerChangeListener> listeners;
public ChangeTransaction(@Nullable Controller to, @Nullable Controller from, boolean isPush, @Nullable ViewGroup container, @Nullable ControllerChangeHandler changeHandler, @NonNull List<ControllerChangeListener> listeners) {
this.to = to;
this.from = from;
this.isPush = isPush;
this.container = container;
this.changeHandler = changeHandler;
this.listeners = listeners;
}
}
/**
* A listener interface useful for allowing external classes to be notified of change events.
*/
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);
}
/**
* 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,329 @@
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
/**
* Returns whether or not this handler removes the `from` view from the container when performing a push.
*
* If this is true:
* - This handler's implementation of [performChange] should remove `from` from `container`
* before calling `changeListener.onChangeCompleted()`
* - When a controller is pushed, the previous controller will be detached and its view will be destroyed
*
* If this is false:
* - This handler's implementation of [performChange] should only remove `from` from `container`
* when `isPush` is false
* - When a controller is pushed, the previous controller will stay attached and its view will remain created
* - When a view is recreated (e.g. after a configuration change), any controllers underneath a transaction
* using this handler will have their view recreated and attached, even though they're not the top-most
* controller
*
* If a controller pushed onto the backstack will completely cover the previous controller,
* using a change handler with [removesFromViewOnPush] true should result in no visual interruption
* to the user, while allowing the previous controller's view to be destroyed to reclaim resources.
* If instead, the previous controller should still be visible after the new controller is pushed,
* using a change handler with [removesFromViewOnPush] false will keep the previous controller's
* view in the view hierarchy, where it can still be seen (and even interacted with).
*/
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
}
}
},
)
}
}
}
@@ -1,26 +0,0 @@
package com.bluelinelabs.conductor;
/**
* All possible types of {@link Controller} changes to be used in {@link ControllerChangeHandler}s
*/
public enum ControllerChangeType {
/** The Controller is being pushed to the host container */
PUSH_ENTER(true, true),
/** The Controller is being pushed to the backstack as another Controller is pushed to the host container */
PUSH_EXIT(true, false),
/** The Controller is being popped from the backstack and placed in the host container as another Controller is popped */
POP_ENTER(false, true),
/** The Controller is being popped from the host container */
POP_EXIT(false, false);
public boolean isPush;
public boolean isEnter;
ControllerChangeType(boolean isPush, boolean isEnter) {
this.isPush = isPush;
this.isEnter = isEnter;
}
}
@@ -0,0 +1,18 @@
package com.bluelinelabs.conductor
/**
* All possible types of [Controller] changes to be used in [ControllerChangeHandler]s
*/
enum class ControllerChangeType(@JvmField val isPush: Boolean, @JvmField val isEnter: Boolean) {
/** The Controller is being pushed to the host container */
PUSH_ENTER(true, true),
/** The Controller is being pushed to the backstack as another Controller is pushed to the host container */
PUSH_EXIT(true, false),
/** The Controller is being popped from the backstack and placed in the host container as another Controller is popped */
POP_ENTER(false, true),
/** The Controller is being popped from the host container */
POP_EXIT(false, false);
}
@@ -5,11 +5,13 @@ import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.view.ViewGroup;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
@@ -21,33 +23,50 @@ class ControllerHostedRouter extends Router {
private final String KEY_HOST_ID = "ControllerHostedRouter.hostId";
private final String KEY_TAG = "ControllerHostedRouter.tag";
private final String KEY_BOUND_TO_CONTAINER = "ControllerHostedRouter.boundToContainer";
private Controller hostController;
@IdRes private int hostId;
private String tag;
private boolean isDetachFrozen;
private boolean boundToContainer;
ControllerHostedRouter() { }
ControllerHostedRouter(int hostId, @Nullable String tag) {
this.hostId = hostId;
this.tag = tag;
ControllerHostedRouter() {
popRootControllerMode = PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW;
}
final void setHost(@NonNull Controller controller, @NonNull ViewGroup container) {
ControllerHostedRouter(int hostId, @Nullable String tag, boolean boundToContainer) {
this();
if (!boundToContainer && tag == null) {
throw new IllegalStateException("ControllerHostedRouter can't be created without a tag if not bounded to its container");
}
this.hostId = hostId;
this.tag = tag;
this.boundToContainer = boundToContainer;
}
final void setHostController(@NonNull Controller controller) {
if (hostController == null) {
hostController = controller;
setOnBackPressedDispatcherEnabled(controller.onBackPressedDispatcherEnabled);
}
}
final void setHostContainer(@NonNull Controller controller, @NonNull ViewGroup container) {
if (hostController != controller || this.container != container) {
removeHost();
if (container instanceof ControllerChangeListener) {
addChangeListener((ControllerChangeListener)container);
addChangeListener((ControllerChangeListener) container);
}
hostController = controller;
this.container = container;
setOnBackPressedDispatcherEnabled(controller.onBackPressedDispatcherEnabled);
for (RouterTransaction transaction : backstack) {
transaction.controller.setParentController(controller);
transaction.controller().setParentController(controller);
}
watchContainerAttach();
@@ -56,7 +75,7 @@ class ControllerHostedRouter extends Router {
final void removeHost() {
if (container != null && container instanceof ControllerChangeListener) {
removeChangeListener((ControllerChangeListener)container);
removeChangeListener((ControllerChangeListener) container);
}
final List<Controller> controllersToDestroy = new ArrayList<>(destroyingControllers);
@@ -66,20 +85,19 @@ class ControllerHostedRouter extends Router {
}
}
for (RouterTransaction transaction : backstack) {
if (transaction.controller.getView() != null) {
transaction.controller.detach(transaction.controller.getView(), true, false);
if (transaction.controller().getView() != null) {
transaction.controller().detach(transaction.controller().getView(), true, false);
}
}
prepareForContainerRemoval();
hostController = null;
container = null;
}
final void setDetachFrozen(boolean frozen) {
isDetachFrozen = frozen;
for (RouterTransaction transaction : backstack) {
transaction.controller.setDetachFrozen(frozen);
transaction.controller().setDetachFrozen(frozen);
}
}
@@ -92,7 +110,7 @@ class ControllerHostedRouter extends Router {
@Override
protected void pushToBackstack(@NonNull RouterTransaction entry) {
if (isDetachFrozen) {
entry.controller.setDetachFrozen(true);
entry.controller().setDetachFrozen(true);
}
super.pushToBackstack(entry);
}
@@ -101,20 +119,35 @@ class ControllerHostedRouter extends Router {
public void setBackstack(@NonNull List<RouterTransaction> newBackstack, @Nullable ControllerChangeHandler changeHandler) {
if (isDetachFrozen) {
for (RouterTransaction transaction : newBackstack) {
transaction.controller.setDetachFrozen(true);
transaction.controller().setDetachFrozen(true);
}
}
super.setBackstack(newBackstack, changeHandler);
}
@Override
void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush) {
super.performControllerChange(to, from, isPush);
// If we're pushing a transaction that will detach controllers to an unattached child
// router, we need mark all other controllers as NOT needing to be reattached.
if (to != null && !hostController.isAttached()) {
if (to.pushChangeHandler() == null || to.pushChangeHandler().getRemovesFromViewOnPush()) {
for (RouterTransaction transaction : backstack) {
transaction.controller().setNeedsAttach(false);
}
}
}
}
@Override @Nullable
public Activity getActivity() {
return hostController != null ? hostController.getActivity() : null;
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
super.onActivityDestroyed(activity);
public void onActivityDestroyed(@NonNull Activity activity, boolean isConfigurationChange) {
super.onActivityDestroyed(activity, isConfigurationChange);
removeHost();
}
@@ -184,7 +217,7 @@ class ControllerHostedRouter extends Router {
@Override
boolean hasHost() {
return hostController != null;
return hostController != null && container != null;
}
@Override
@@ -192,6 +225,7 @@ class ControllerHostedRouter extends Router {
super.saveInstanceState(outState);
outState.putInt(KEY_HOST_ID, hostId);
outState.putBoolean(KEY_BOUND_TO_CONTAINER, boundToContainer);
outState.putString(KEY_TAG, tag);
}
@@ -200,22 +234,32 @@ class ControllerHostedRouter extends Router {
super.restoreInstanceState(savedInstanceState);
hostId = savedInstanceState.getInt(KEY_HOST_ID);
boundToContainer = savedInstanceState.getBoolean(KEY_BOUND_TO_CONTAINER);
tag = savedInstanceState.getString(KEY_TAG);
}
@Override
void setControllerRouter(@NonNull Controller controller) {
void setRouterOnController(@NonNull Controller controller) {
controller.setParentController(hostController);
super.setControllerRouter(controller);
super.setRouterOnController(controller);
}
int getHostId() {
return hostId;
}
@Nullable
String getTag() {
return tag;
boolean matches(int hostId, @Nullable String tag) {
if (!boundToContainer && container == null) {
if (this.tag == null) {
throw new IllegalStateException("Host ID can't be variable with a null tag");
}
if (this.tag.equals(tag)) {
this.hostId = hostId;
return true;
}
}
return this.hostId == hostId && TextUtils.equals(tag, this.tag);
}
@Override @NonNull
@@ -1,53 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
/**
* A simple controller subclass that changes the onCreateView signature to include a saved view state parameter.
* This is necessary for some third party libraries like Google Maps, which require passing in a saved state
* bundle at the time of creation.
*/
abstract public class RestoreViewOnCreateController extends Controller {
/**
* Convenience constructor for use when no arguments are needed.
*/
protected RestoreViewOnCreateController() {
super(null);
}
/**
* Constructor that takes arguments that need to be retained across restarts.
*
* @param args Any arguments that need to be retained.
*/
protected RestoreViewOnCreateController(@Nullable Bundle args) {
super(args);
}
@Override @NonNull
protected final View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return onCreateView(inflater, container, viewState == null ? null : viewState.getBundle(KEY_VIEW_STATE_BUNDLE));
}
/**
* Called when the controller is ready to display its view. A valid view must be returned. The standard body
* for this method will be {@code return inflater.inflate(R.layout.my_layout, container, false);}, plus
* any binding and state restoration code.
*
* @param inflater The LayoutInflater that should be used to inflate views
* @param container The parent view that this Controller's view will eventually be attached to.
* This Controller's view should NOT be added in this method. It is simply passed in
* so that valid LayoutParams can be used during inflation.
* @param savedViewState A bundle for the view's state, which would have been created in {@link #onSaveViewState(View, Bundle)},
* or {@code null} if no saved state exists.
*/
@NonNull
protected abstract View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState);
}
@@ -1,19 +1,23 @@
package com.bluelinelabs.conductor;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import androidx.activity.ComponentActivity;
import androidx.activity.OnBackPressedDispatcher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import com.bluelinelabs.conductor.ControllerChangeHandler.ChangeTransaction;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
@@ -33,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;
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.
@@ -64,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.
@@ -84,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()) {
if (backstack.peek().controller().handleBack()) {
return true;
} else if (popCurrentController()) {
} else if ((backstack.getSize() > 1 || popRootControllerMode != PopRootControllerMode.NEVER) && popCurrentController()) {
return true;
}
}
@@ -116,7 +163,7 @@ public abstract class Router {
if (transaction == null) {
throw new IllegalStateException("Trying to pop the current controller when there are none on the backstack.");
}
return popController(transaction.controller);
return popController(transaction.controller());
}
/**
@@ -130,7 +177,7 @@ public abstract class Router {
ThreadUtils.ensureMainThread();
RouterTransaction topTransaction = backstack.peek();
boolean poppingTopController = topTransaction != null && topTransaction.controller == controller;
boolean poppingTopController = topTransaction != null && topTransaction.controller() == controller;
if (poppingTopController) {
trackDestroyingController(backstack.pop());
@@ -139,16 +186,17 @@ public abstract class Router {
RouterTransaction removedTransaction = null;
RouterTransaction nextTransaction = null;
Iterator<RouterTransaction> iterator = backstack.iterator();
ControllerChangeHandler topPushHandler = topTransaction != null ? topTransaction.pushChangeHandler() : null;
final boolean needsNextTransactionAttach = topPushHandler != null ? !topPushHandler.getRemovesFromViewOnPush() : false;
while (iterator.hasNext()) {
RouterTransaction transaction = iterator.next();
if (transaction.controller == controller) {
if (controller.isAttached()) {
trackDestroyingController(transaction);
}
iterator.remove();
if (transaction.controller() == controller) {
trackDestroyingController(transaction);
backstack.remove(transaction);
removedTransaction = transaction;
} else if (removedTransaction != null) {
if (!transaction.controller.isAttached()) {
if (needsNextTransactionAttach && !transaction.controller().isAttached()) {
nextTransaction = transaction;
}
break;
@@ -160,7 +208,7 @@ public abstract class Router {
}
}
if (popsLastView) {
if (popRootControllerMode == PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW) {
return topTransaction != null;
} else {
return !backstack.isEmpty();
@@ -201,10 +249,10 @@ 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())) {
for (RouterTransaction visibleTransaction : getVisibleTransactions(backstack.iterator(), true)) {
performControllerChange(null, visibleTransaction, true, handler);
}
}
@@ -219,13 +267,14 @@ 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.controller().addLifecycleListener(new LifecycleListener() {
topTransaction = poppedControllers.get(0);
topTransaction.controller().addLifecycleListener(new Controller.LifecycleListener() {
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
if (changeType == ControllerChangeType.POP_EXIT) {
@@ -239,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() {
@@ -249,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_BUT_NOT_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;
}
@@ -279,7 +361,7 @@ public abstract class Router {
public boolean popToRoot(@Nullable ControllerChangeHandler changeHandler) {
ThreadUtils.ensureMainThread();
if (backstack.size() > 1) {
if (backstack.getSize() > 1) {
//noinspection ConstantConditions
popToTransaction(backstack.root(), changeHandler);
return true;
@@ -345,7 +427,7 @@ public abstract class Router {
@Nullable
public Controller getControllerWithInstanceId(@NonNull String instanceId) {
for (RouterTransaction transaction : backstack) {
Controller controllerWithId = transaction.controller.findController(instanceId);
Controller controllerWithId = transaction.controller().findController(instanceId);
if (controllerWithId != null) {
return controllerWithId;
}
@@ -363,7 +445,7 @@ public abstract class Router {
public Controller getControllerWithTag(@NonNull String tag) {
for (RouterTransaction transaction : backstack) {
if (tag.equals(transaction.tag())) {
return transaction.controller;
return transaction.controller();
}
}
return null;
@@ -374,7 +456,7 @@ public abstract class Router {
*/
@SuppressWarnings("WeakerAccess")
public int getBackstackSize() {
return backstack.size();
return backstack.getSize();
}
/**
@@ -382,7 +464,7 @@ public abstract class Router {
*/
@NonNull
public List<RouterTransaction> getBackstack() {
List<RouterTransaction> list = new ArrayList<>(backstack.size());
List<RouterTransaction> list = new ArrayList<>(backstack.getSize());
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
list.add(backstackIterator.next());
@@ -403,7 +485,7 @@ public abstract class Router {
ThreadUtils.ensureMainThread();
List<RouterTransaction> oldTransactions = getBackstack();
List<RouterTransaction> oldVisibleTransactions = getVisibleTransactions(backstack.iterator());
List<RouterTransaction> oldVisibleTransactions = getVisibleTransactions(backstack.iterator(), false);
removeAllExceptVisibleAndUnowned();
ensureOrderedTransactionIndices(newBackstack);
@@ -415,7 +497,7 @@ public abstract class Router {
for (RouterTransaction oldTransaction : oldTransactions) {
boolean contains = false;
for (RouterTransaction newTransaction : newBackstack) {
if (oldTransaction.controller == newTransaction.controller) {
if (oldTransaction.controller() == newTransaction.controller()) {
contains = true;
break;
}
@@ -423,7 +505,7 @@ public abstract class Router {
if (!contains) {
// Inform the controller that it will be destroyed soon
oldTransaction.controller.isBeingDestroyed = true;
oldTransaction.controller().isBeingDestroyed = true;
transactionsToBeRemoved.add(oldTransaction);
}
}
@@ -433,13 +515,13 @@ public abstract class Router {
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
transaction.onAttachedToRouter();
setControllerRouter(transaction.controller);
setRouterOnController(transaction.controller());
}
if (newBackstack.size() > 0) {
List<RouterTransaction> reverseNewBackstack = new ArrayList<>(newBackstack);
Collections.reverse(reverseNewBackstack);
List<RouterTransaction> newVisibleTransactions = getVisibleTransactions(reverseNewBackstack.iterator());
List<RouterTransaction> newVisibleTransactions = getVisibleTransactions(reverseNewBackstack.iterator(), false);
boolean newRootRequiresPush = !(newVisibleTransactions.size() > 0 && oldTransactions.contains(newVisibleTransactions.get(0)));
boolean visibleTransactionsChanged = !backstacksAreEqual(newVisibleTransactions, oldVisibleTransactions);
@@ -448,10 +530,10 @@ public abstract class Router {
RouterTransaction newRootTransaction = newVisibleTransactions.get(0);
// Replace the old root with the new one
if (oldRootTransaction == null || oldRootTransaction.controller != newRootTransaction.controller) {
if (oldRootTransaction == null || oldRootTransaction.controller() != newRootTransaction.controller()) {
// Ensure the existing root controller is fully pushed to the view hierarchy
if (oldRootTransaction != null) {
ControllerChangeHandler.completeHandlerImmediately(oldRootTransaction.controller.getInstanceId());
ControllerChangeHandler.completeHandlerImmediately(oldRootTransaction.controller().getInstanceId());
}
performControllerChange(newRootTransaction, oldRootTransaction, newRootRequiresPush, changeHandler);
}
@@ -462,8 +544,11 @@ public abstract class Router {
if (!newVisibleTransactions.contains(transaction)) {
ControllerChangeHandler localHandler = changeHandler != null ? changeHandler.copy() : new SimpleSwapChangeHandler();
localHandler.setForceRemoveViewOnPush(true);
ControllerChangeHandler.completeHandlerImmediately(transaction.controller.getInstanceId());
performControllerChange(null, transaction, newRootRequiresPush, localHandler);
ControllerChangeHandler.completeHandlerImmediately(transaction.controller().getInstanceId());
if (transaction.controller().view != null) {
performControllerChange(null, transaction, newRootRequiresPush, localHandler);
}
}
}
@@ -481,7 +566,7 @@ public abstract class Router {
for (int i = oldVisibleTransactions.size() - 1; i >= 0; i--) {
RouterTransaction transaction = oldVisibleTransactions.get(i);
ControllerChangeHandler localHandler = changeHandler != null ? changeHandler.copy() : new SimpleSwapChangeHandler();
ControllerChangeHandler.completeHandlerImmediately(transaction.controller.getInstanceId());
ControllerChangeHandler.completeHandlerImmediately(transaction.controller().getInstanceId());
performControllerChange(null, transaction, false, localHandler);
}
}
@@ -490,7 +575,18 @@ public abstract class Router {
// set the backstack to prevent the possibility that they'll be destroyed before the controller
// change handler runs.
for (RouterTransaction removedTransaction : transactionsToBeRemoved) {
removedTransaction.controller.destroy();
// Still need to ensure the controller isn't queued up to be removed later on.
boolean willBeRemoved = false;
for (ChangeTransaction pendingTransaction : pendingControllerChanges) {
if (pendingTransaction.from == removedTransaction.controller()) {
willBeRemoved = true;
}
}
if (!willBeRemoved) {
removedTransaction.controller().destroy();
}
}
}
@@ -530,12 +626,13 @@ public abstract class Router {
public void rebindIfNeeded() {
ThreadUtils.ensureMainThread();
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
if (transaction.controller.getNeedsAttach()) {
// 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 {
setRouterOnController(transaction.controller());
}
}
}
@@ -551,9 +648,9 @@ public abstract class Router {
isActivityStopped = false;
for (RouterTransaction transaction : backstack) {
transaction.controller.activityStarted(activity);
transaction.controller().activityStarted(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
for (Router childRouter : transaction.controller().getChildRouters()) {
childRouter.onActivityStarted(activity);
}
}
@@ -561,9 +658,9 @@ public abstract class Router {
public final void onActivityResumed(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityResumed(activity);
transaction.controller().activityResumed(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
for (Router childRouter : transaction.controller().getChildRouters()) {
childRouter.onActivityResumed(activity);
}
}
@@ -571,9 +668,9 @@ public abstract class Router {
public final void onActivityPaused(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityPaused(activity);
transaction.controller().activityPaused(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
for (Router childRouter : transaction.controller().getChildRouters()) {
childRouter.onActivityPaused(activity);
}
}
@@ -581,9 +678,9 @@ public abstract class Router {
public final void onActivityStopped(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityStopped(activity);
transaction.controller().activityStopped(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
for (Router childRouter : transaction.controller().getChildRouters()) {
childRouter.onActivityStopped(activity);
}
}
@@ -591,15 +688,15 @@ public abstract class Router {
isActivityStopped = true;
}
public void onActivityDestroyed(@NonNull Activity activity) {
public void onActivityDestroyed(@NonNull Activity activity, boolean isConfigurationChange) {
prepareForContainerRemoval();
changeListeners.clear();
for (RouterTransaction transaction : backstack) {
transaction.controller.activityDestroyed(activity);
transaction.controller().activityDestroyed(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
childRouter.onActivityDestroyed(activity);
for (Router childRouter : transaction.controller().getChildRouters()) {
childRouter.onActivityDestroyed(activity, isConfigurationChange);
}
}
@@ -608,7 +705,7 @@ public abstract class Router {
controller.activityDestroyed(activity);
for (Router childRouter : controller.getChildRouters()) {
childRouter.onActivityDestroyed(activity);
childRouter.onActivityDestroyed(activity, isConfigurationChange);
}
}
@@ -616,11 +713,14 @@ public abstract class Router {
}
public void prepareForHostDetach() {
pendingControllerChanges.clear(); // rely on backstack based restoration in rebindIfNeeded
for (RouterTransaction transaction : backstack) {
if (ControllerChangeHandler.completeHandlerImmediately(transaction.controller.getInstanceId())) {
transaction.controller.setNeedsAttach(true);
if (ControllerChangeHandler.completeHandlerImmediately(transaction.controller().getInstanceId())) {
transaction.controller().setNeedsAttach(true);
}
transaction.controller.prepareForHostDetach();
transaction.controller().prepareForHostDetach();
}
}
@@ -628,27 +728,29 @@ 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()) {
setControllerRouter(backstackIterator.next().controller);
setRouterOnController(backstackIterator.next().controller());
}
}
public final void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
for (RouterTransaction transaction : backstack) {
transaction.controller.createOptionsMenu(menu, inflater);
transaction.controller().createOptionsMenu(menu, inflater);
for (Router childRouter : transaction.controller.getChildRouters()) {
for (Router childRouter : transaction.controller().getChildRouters()) {
childRouter.onCreateOptionsMenu(menu, inflater);
}
}
@@ -656,9 +758,9 @@ public abstract class Router {
public final void onPrepareOptionsMenu(@NonNull Menu menu) {
for (RouterTransaction transaction : backstack) {
transaction.controller.prepareOptionsMenu(menu);
transaction.controller().prepareOptionsMenu(menu);
for (Router childRouter : transaction.controller.getChildRouters()) {
for (Router childRouter : transaction.controller().getChildRouters()) {
childRouter.onPrepareOptionsMenu(menu);
}
}
@@ -666,11 +768,11 @@ public abstract class Router {
public final boolean onOptionsItemSelected(@NonNull MenuItem item) {
for (RouterTransaction transaction : backstack) {
if (transaction.controller.optionsItemSelected(item)) {
if (transaction.controller().optionsItemSelected(item)) {
return true;
}
for (Router childRouter : transaction.controller.getChildRouters()) {
for (Router childRouter : transaction.controller().getChildRouters()) {
if (childRouter.onOptionsItemSelected(item)) {
return true;
}
@@ -680,7 +782,7 @@ public abstract class Router {
}
private void popToTransaction(@NonNull RouterTransaction transaction, @Nullable ControllerChangeHandler changeHandler) {
if (backstack.size() > 0) {
if (backstack.getSize() > 0) {
RouterTransaction topTransaction = backstack.peek();
List<RouterTransaction> updatedBackstack = new ArrayList<>();
@@ -707,6 +809,7 @@ public abstract class Router {
@Override
public void run() {
containerFullyAttached = true;
performPendingControllerChanges();
}
});
}
@@ -721,33 +824,54 @@ public abstract class Router {
void onContextAvailable() {
for (RouterTransaction transaction : backstack) {
transaction.controller.onContextAvailable();
transaction.controller().onContextAvailable();
}
}
void onContextUnavailable(@NonNull Context context) {
for (RouterTransaction transaction : backstack) {
transaction.controller().onContextUnavailable(context);
}
for (Controller controller : destroyingControllers) {
controller.onContextUnavailable(context);
}
}
@NonNull
final List<Controller> getControllers() {
List<Controller> controllers = new ArrayList<>(backstack.size());
List<Controller> controllers = new ArrayList<>(backstack.getSize());
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
controllers.add(backstackIterator.next().controller);
controllers.add(backstackIterator.next().controller());
}
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) {
if (transaction.controller.didRequestPermission(permission)) {
return transaction.controller.shouldShowRequestPermissionRationale(permission);
if (transaction.controller().didRequestPermission(permission)) {
return transaction.controller().shouldShowRequestPermissionRationale(permission);
}
}
return null;
}
private void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush) {
void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush) {
if (isPush && to != null) {
to.onAttachedToRouter();
}
@@ -765,25 +889,33 @@ public abstract class Router {
performControllerChange(to, from, isPush, changeHandler);
}
void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush, @Nullable ControllerChangeHandler changeHandler) {
Controller toController = to != null ? to.controller : null;
Controller fromController = from != null ? from.controller : null;
private void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush, @Nullable ControllerChangeHandler changeHandler) {
Controller toController = to != null ? to.controller() : null;
Controller fromController = from != null ? from.controller() : null;
boolean forceDetachDestroy = false;
if (to != null) {
to.ensureValidIndex(getTransactionIndexer());
setControllerRouter(toController);
} else if (backstack.size() == 0 && !popsLastView) {
setRouterOnController(toController);
} 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();
forceDetachDestroy = true;
} else if (!isPush && fromController != null && !fromController.isAttached()) {
// We're popping fromController from the middle of the backstack,
// need to do it immediately and destroy the controller
forceDetachDestroy = true;
}
performControllerChange(toController, fromController, isPush, changeHandler);
if (forceDetachDestroy && fromController != null && fromController.getView() != null) {
fromController.detach(fromController.getView(), true, false);
if (forceDetachDestroy && fromController != null) {
if (fromController.getView() != null) {
fromController.detach(fromController.getView(), true, false);
} else {
fromController.destroy();
}
}
}
@@ -797,18 +929,21 @@ public abstract class Router {
if (pendingControllerChanges.size() > 0) {
// If we already have changes queued up (awaiting full container attach), queue this one up as well so they don't happen
// out of order.
if (to != null) {
to.setNeedsAttach(true);
}
pendingControllerChanges.add(transaction);
} else if (from != null && (changeHandler == null || changeHandler.removesFromViewOnPush()) && !containerFullyAttached) {
} 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.
if (to != null) {
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);
}
@@ -824,17 +959,17 @@ public abstract class Router {
}
protected void pushToBackstack(@NonNull RouterTransaction entry) {
if (backstack.contains(entry.controller)) {
if (backstack.contains(entry.controller())) {
throw new IllegalStateException("Trying to push a controller that already exists on the backstack.");
}
backstack.push(entry);
}
private void trackDestroyingController(@NonNull RouterTransaction transaction) {
if (!transaction.controller.isDestroyed()) {
destroyingControllers.add(transaction.controller);
if (!transaction.controller().isDestroyed()) {
destroyingControllers.add(transaction.controller());
transaction.controller.addLifecycleListener(new LifecycleListener() {
transaction.controller().addLifecycleListener(new Controller.LifecycleListener() {
@Override
public void postDestroy(@NonNull Controller controller) {
destroyingControllers.remove(controller);
@@ -852,9 +987,9 @@ public abstract class Router {
private void removeAllExceptVisibleAndUnowned() {
List<View> views = new ArrayList<>();
for (RouterTransaction transaction : getVisibleTransactions(backstack.iterator())) {
if (transaction.controller.getView() != null) {
views.add(transaction.controller.getView());
for (RouterTransaction transaction : getVisibleTransactions(backstack.iterator(), false)) {
if (transaction.controller().getView() != null) {
views.add(transaction.controller().getView());
}
}
@@ -879,21 +1014,21 @@ public abstract class Router {
List<Integer> indices = new ArrayList<>(backstack.size());
for (RouterTransaction transaction : backstack) {
transaction.ensureValidIndex(getTransactionIndexer());
indices.add(transaction.transactionIndex);
indices.add(transaction.getTransactionIndex());
}
Collections.sort(indices);
for (int i = 0; i < backstack.size(); i++) {
backstack.get(i).transactionIndex = indices.get(i);
backstack.get(i).setTransactionIndex(indices.get(i));
}
}
private void ensureNoDuplicateControllers(List<RouterTransaction> backstack) {
for (int i = 0; i < backstack.size(); i++) {
Controller controller = backstack.get(i).controller;
Controller controller = backstack.get(i).controller();
for (int j = i + 1; j < backstack.size(); j++) {
if (backstack.get(j).controller == controller) {
if (backstack.get(j).controller() == controller) {
throw new IllegalStateException("Trying to push the same controller to the backstack more than once.");
}
}
@@ -912,14 +1047,20 @@ public abstract class Router {
}
}
private List<RouterTransaction> getVisibleTransactions(@NonNull Iterator<RouterTransaction> backstackIterator) {
private List<RouterTransaction> getVisibleTransactions(@NonNull Iterator<RouterTransaction> backstackIterator, boolean onlyTop) {
boolean visible = true;
List<RouterTransaction> transactions = new ArrayList<>();
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
transactions.add(transaction);
//noinspection ConstantConditions
if (transaction.pushChangeHandler() == null || transaction.pushChangeHandler().removesFromViewOnPush()) {
if (visible) {
transactions.add(transaction);
}
visible = transaction.pushChangeHandler() != null && !transaction.pushChangeHandler().getRemovesFromViewOnPush();
if (onlyTop && !visible) {
break;
}
}
@@ -942,7 +1083,7 @@ public abstract class Router {
return true;
}
void setControllerRouter(@NonNull Controller controller) {
void setRouterOnController(@NonNull Controller controller) {
controller.setRouter(this);
controller.onContextAvailable();
}
@@ -961,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 is the default 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
}
}
@@ -1,140 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
/**
* Metadata used for adding {@link Controller}s to a {@link Router}.
*/
public class RouterTransaction {
private static int INVALID_INDEX = -1;
private static final String KEY_VIEW_CONTROLLER_BUNDLE = "RouterTransaction.controller.bundle";
private static final String KEY_PUSH_TRANSITION = "RouterTransaction.pushControllerChangeHandler";
private static final String KEY_POP_TRANSITION = "RouterTransaction.popControllerChangeHandler";
private static final String KEY_TAG = "RouterTransaction.tag";
private static final String KEY_INDEX = "RouterTransaction.transactionIndex";
private static final String KEY_ATTACHED_TO_ROUTER = "RouterTransaction.attachedToRouter";
@NonNull final Controller controller;
private String tag;
private ControllerChangeHandler pushControllerChangeHandler;
private ControllerChangeHandler popControllerChangeHandler;
private boolean attachedToRouter;
int transactionIndex = INVALID_INDEX;
@NonNull
public static RouterTransaction with(@NonNull Controller controller) {
return new RouterTransaction(controller);
}
private RouterTransaction(@NonNull Controller controller) {
this.controller = controller;
}
RouterTransaction(@NonNull Bundle bundle) {
controller = Controller.newInstance(bundle.getBundle(KEY_VIEW_CONTROLLER_BUNDLE));
pushControllerChangeHandler = ControllerChangeHandler.fromBundle(bundle.getBundle(KEY_PUSH_TRANSITION));
popControllerChangeHandler = ControllerChangeHandler.fromBundle(bundle.getBundle(KEY_POP_TRANSITION));
tag = bundle.getString(KEY_TAG);
transactionIndex = bundle.getInt(KEY_INDEX);
attachedToRouter = bundle.getBoolean(KEY_ATTACHED_TO_ROUTER);
}
void onAttachedToRouter() {
attachedToRouter = true;
}
@NonNull
public Controller controller() {
return controller;
}
@Nullable
public String tag() {
return tag;
}
@NonNull
public RouterTransaction tag(@Nullable String tag) {
if (!attachedToRouter) {
this.tag = tag;
return this;
} else {
throw new RuntimeException(getClass().getSimpleName() + "s can not be modified after being added to a Router.");
}
}
@Nullable
public ControllerChangeHandler pushChangeHandler() {
ControllerChangeHandler handler = controller.getOverriddenPushHandler();
if (handler == null) {
handler = pushControllerChangeHandler;
}
return handler;
}
@NonNull
public RouterTransaction pushChangeHandler(@Nullable ControllerChangeHandler handler) {
if (!attachedToRouter) {
pushControllerChangeHandler = handler;
return this;
} else {
throw new RuntimeException(getClass().getSimpleName() + "s can not be modified after being added to a Router.");
}
}
@Nullable
public ControllerChangeHandler popChangeHandler() {
ControllerChangeHandler handler = controller.getOverriddenPopHandler();
if (handler == null) {
handler = popControllerChangeHandler;
}
return handler;
}
@NonNull
public RouterTransaction popChangeHandler(@Nullable ControllerChangeHandler handler) {
if (!attachedToRouter) {
popControllerChangeHandler = handler;
return this;
} else {
throw new RuntimeException(getClass().getSimpleName() + "s can not be modified after being added to a Router.");
}
}
void ensureValidIndex(@NonNull TransactionIndexer indexer) {
if (transactionIndex == INVALID_INDEX) {
transactionIndex = indexer.nextIndex();
}
}
/**
* Used to serialize this transaction into a Bundle
*/
@NonNull
public Bundle saveInstanceState() {
Bundle bundle = new Bundle();
bundle.putBundle(KEY_VIEW_CONTROLLER_BUNDLE, controller.saveInstanceState());
if (pushControllerChangeHandler != null) {
bundle.putBundle(KEY_PUSH_TRANSITION, pushControllerChangeHandler.toBundle());
}
if (popControllerChangeHandler != null) {
bundle.putBundle(KEY_POP_TRANSITION, popControllerChangeHandler.toBundle());
}
bundle.putString(KEY_TAG, tag);
bundle.putInt(KEY_INDEX, transactionIndex);
bundle.putBoolean(KEY_ATTACHED_TO_ROUTER, attachedToRouter);
return bundle;
}
}
@@ -0,0 +1,122 @@
package com.bluelinelabs.conductor
import android.os.Bundle
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY
import com.bluelinelabs.conductor.internal.TransactionIndexer
private const val INVALID_INDEX = -1
private const val KEY_VIEW_CONTROLLER_BUNDLE = "RouterTransaction.controller.bundle"
private const val KEY_PUSH_TRANSITION = "RouterTransaction.pushControllerChangeHandler"
private const val KEY_POP_TRANSITION = "RouterTransaction.popControllerChangeHandler"
private const val KEY_TAG = "RouterTransaction.tag"
private const val KEY_INDEX = "RouterTransaction.transactionIndex"
private const val KEY_ATTACHED_TO_ROUTER = "RouterTransaction.attachedToRouter"
/**
* Metadata used for adding [Controller]s to a [Router].
*/
class RouterTransaction private constructor(
@get:JvmName("controller")
val controller: Controller,
private var tag: String? = null,
private var pushControllerChangeHandler: ControllerChangeHandler? = null,
private var popControllerChangeHandler: ControllerChangeHandler? = null,
private var attachedToRouter: Boolean = false,
@get:RestrictTo(LIBRARY)
@set:RestrictTo(LIBRARY)
var transactionIndex: Int = INVALID_INDEX
) {
@RestrictTo(LIBRARY)
internal constructor(bundle: Bundle) : this(
controller = Controller.newInstance(bundle.getBundle(KEY_VIEW_CONTROLLER_BUNDLE)!!),
pushControllerChangeHandler = ControllerChangeHandler.fromBundle(
bundle.getBundle(
KEY_PUSH_TRANSITION
)
),
popControllerChangeHandler = ControllerChangeHandler.fromBundle(
bundle.getBundle(
KEY_POP_TRANSITION
)
),
tag = bundle.getString(KEY_TAG),
transactionIndex = bundle.getInt(KEY_INDEX),
attachedToRouter = bundle.getBoolean(KEY_ATTACHED_TO_ROUTER)
)
fun onAttachedToRouter() {
attachedToRouter = true
}
fun tag(): String? = tag
fun tag(tag: String?): RouterTransaction {
return if (!attachedToRouter) {
this.tag = tag
this
} else {
throw RuntimeException(javaClass.simpleName + "s can not be modified after being added to a Router.")
}
}
fun pushChangeHandler(): ControllerChangeHandler? {
return controller.overriddenPushHandler ?: pushControllerChangeHandler
}
fun pushChangeHandler(handler: ControllerChangeHandler?): RouterTransaction {
return if (!attachedToRouter) {
pushControllerChangeHandler = handler
this
} else {
throw RuntimeException("${javaClass.simpleName}s can not be modified after being added to a Router.")
}
}
fun popChangeHandler(): ControllerChangeHandler? {
return controller.overriddenPopHandler ?: popControllerChangeHandler
}
fun popChangeHandler(handler: ControllerChangeHandler?): RouterTransaction {
return if (!attachedToRouter) {
popControllerChangeHandler = handler
this
} else {
throw RuntimeException("${javaClass.simpleName}s can not be modified after being added to a Router.")
}
}
fun ensureValidIndex(indexer: TransactionIndexer) {
if (transactionIndex == INVALID_INDEX) {
transactionIndex = indexer.nextIndex()
}
}
/**
* Used to serialize this transaction into a Bundle
*/
fun saveInstanceState(): Bundle = Bundle().apply {
putBundle(KEY_VIEW_CONTROLLER_BUNDLE, controller.saveInstanceState())
pushControllerChangeHandler?.let { putBundle(KEY_PUSH_TRANSITION, it.toBundle()) }
popControllerChangeHandler?.let { putBundle(KEY_POP_TRANSITION, it.toBundle()) }
putString(KEY_TAG, tag)
putInt(KEY_INDEX, transactionIndex)
putBoolean(KEY_ATTACHED_TO_ROUTER, attachedToRouter)
}
companion object {
@JvmStatic
fun with(controller: Controller): RouterTransaction = RouterTransaction(controller)
}
}
fun Controller.asTransaction(
popChangeHandler: ControllerChangeHandler? = null,
pushChangeHandler: ControllerChangeHandler? = null
): RouterTransaction {
return RouterTransaction.with(this)
.pushChangeHandler(pushChangeHandler)
.popChangeHandler(popChangeHandler)
}
@@ -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 android.support.annotation.NonNull;
import android.support.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,33 +0,0 @@
package com.bluelinelabs.conductor.changehandler;
import android.annotation.TargetApi;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.AutoTransition;
import android.transition.Transition;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* @deprecated It's very rare that a simple AutoTransition is what you want when changing controllers. This class
* is deprecated simply because it was often a red herring for people trying to make nice transitions.
*
* A change handler that will use an AutoTransition.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class AutoTransitionChangeHandler extends TransitionChangeHandler {
@Override @NonNull
protected Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
return new AutoTransition();
}
@Override @NonNull
public ControllerChangeHandler copy() {
return new AutoTransitionChangeHandler();
}
}
@@ -1,57 +0,0 @@
package com.bluelinelabs.conductor.changehandler;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import 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 android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* An {@link AnimatorChangeHandler} that will slide the views left or right, depending on if it's a push or pop.
*/
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 android.support.annotation.NonNull;
import android.support.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,97 +0,0 @@
package com.bluelinelabs.conductor.changehandler;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.internal.ClassUtils;
/**
* A base {@link ControllerChangeHandler} that facilitates using {@link android.transition.Transition}s to replace Controller Views.
* If the target device is running on a version of Android that doesn't support transitions, a fallback {@link ControllerChangeHandler} will be used.
*/
public class TransitionChangeHandlerCompat extends ControllerChangeHandler {
private static final String KEY_CHANGE_HANDLER_CLASS = "TransitionChangeHandlerCompat.changeHandler.class";
private static final String KEY_HANDLER_STATE = "TransitionChangeHandlerCompat.changeHandler.state";
private ControllerChangeHandler changeHandler;
public TransitionChangeHandlerCompat() { }
/**
* Constructor that takes a {@link TransitionChangeHandler} for use with compatible devices, as well as a fallback
* {@link ControllerChangeHandler} for use with older devices.
*
* @param transitionChangeHandler The change handler that will be used on API 21 and above
* @param fallbackChangeHandler The change handler that will be used on APIs below 21
*/
public TransitionChangeHandlerCompat(@NonNull TransitionChangeHandler transitionChangeHandler, @NonNull ControllerChangeHandler fallbackChangeHandler) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
changeHandler = transitionChangeHandler;
} else {
changeHandler = fallbackChangeHandler;
}
}
@Override
public void performChange(@NonNull final ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
changeHandler.performChange(container, from, to, isPush, changeListener);
}
@Override
public void saveToBundle(@NonNull Bundle bundle) {
super.saveToBundle(bundle);
bundle.putString(KEY_CHANGE_HANDLER_CLASS, changeHandler.getClass().getName());
Bundle stateBundle = new Bundle();
changeHandler.saveToBundle(stateBundle);
bundle.putBundle(KEY_HANDLER_STATE, stateBundle);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
super.restoreFromBundle(bundle);
String className = bundle.getString(KEY_CHANGE_HANDLER_CLASS);
changeHandler = ClassUtils.newInstance(className);
//noinspection ConstantConditions
changeHandler.restoreFromBundle(bundle.getBundle(KEY_HANDLER_STATE));
}
@Override
public boolean removesFromViewOnPush() {
return changeHandler.removesFromViewOnPush();
}
@Override @NonNull
public ControllerChangeHandler copy() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return new TransitionChangeHandlerCompat((TransitionChangeHandler)changeHandler.copy(), null);
} else {
return new TransitionChangeHandlerCompat(null, changeHandler.copy());
}
}
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
changeHandler.onAbortPush(newHandler, newTop);
}
@Override
public void completeImmediately() {
changeHandler.completeImmediately();
}
@Override
public void setForceRemoveViewOnPush(boolean force) {
changeHandler.setForceRemoveViewOnPush(force);
}
}

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