Compare commits
412 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a87a4140e | |||
| ad81c4819b | |||
| 49018ed84c | |||
| fbfe3ce2e9 | |||
| 5a1746b2d3 | |||
| 2ffafaee79 | |||
| eabfc005d0 | |||
| a90ca5180e | |||
| 4d4d8bfbd1 | |||
| f69fab6062 | |||
| 590debf975 | |||
| c3f7d128f5 | |||
| 5e1f072672 | |||
| b0d15d9f9e | |||
| 8ac2e04c62 | |||
| cdbdee5c42 | |||
| 8488242a26 | |||
| 1fe0187439 | |||
| f78726b916 | |||
| 1f918f10c5 | |||
| bd584727be | |||
| 91db7fe65f | |||
| 2abe2b33f9 | |||
| ac4e09cf67 | |||
| 055532bb21 | |||
| 15037c2217 | |||
| 728f1fb4e9 | |||
| 55c8d64d8a | |||
| 88e0eb882b | |||
| 63a92db540 | |||
| ba98e3b165 | |||
| 966bc1645d | |||
| c8ac58ad6a | |||
| 5f04d9de89 | |||
| d32fc813d0 | |||
| c2bc72c5ce | |||
| 924e4bebfa | |||
| 4ea4aa5c56 | |||
| 3b275d31c2 | |||
| 0e21c8c9c1 | |||
| 8297e0273d | |||
| 46519c2c2c | |||
| 211da8b2ea | |||
| 26db962168 | |||
| f4c1c6ccf5 | |||
| c89caa87e0 | |||
| 2748566437 | |||
| 506c99ed41 | |||
| 4fe0ec5f51 | |||
| afa93f2cc1 | |||
| 836f92b615 | |||
| 94c817bbd9 | |||
| fc1fee3e17 | |||
| 76b7572a01 | |||
| 3fc63b7f5f | |||
| 0ef52211a2 | |||
| 7574131940 | |||
| 1ab9a4c4f6 | |||
| 3bc23bd5cd | |||
| 5f138e5d43 | |||
| 03701d05a9 | |||
| a19968e0c9 | |||
| 5501ab2ac8 | |||
| 804fdb615e | |||
| c01b2a74d6 | |||
| 8a8622c261 | |||
| 6820aa7d6a | |||
| 9ce27e4dee | |||
| 3c8ad0a833 | |||
| a720ac57e8 | |||
| 7d6901389b | |||
| e54e88bf0d | |||
| 010117603c | |||
| cd11ac9d6b | |||
| e78347709b | |||
| 341debc5b9 | |||
| 2346e48154 | |||
| 4174e12958 | |||
| c0abed0813 | |||
| 6fdb1d6ed3 | |||
| 3334b8e21f | |||
| 240424dc63 | |||
| f768e9ab00 | |||
| 4c89124683 | |||
| e0bbd48935 | |||
| 76074d1e3d | |||
| 623ed03df8 | |||
| 28b8810e56 | |||
| 5c397404ce | |||
| 3b81a962b1 | |||
| 4f3662bbf6 | |||
| c53b8d9d49 | |||
| c8f6e552e4 | |||
| c1cc2e2bca | |||
| 9ace20b88b | |||
| d8606498b0 | |||
| f35768393d | |||
| bbb4e8c066 | |||
| 12e66867f1 | |||
| 8eee79dcea | |||
| 5ebd8c9a5d | |||
| 4c9f2e9f30 | |||
| b0340d4c67 | |||
| f6b396f679 | |||
| 589cb91fff | |||
| a51f4192cb | |||
| 266d6a0fd6 | |||
| 7410f7d123 | |||
| 56b5ce3b33 | |||
| a20ee6477a | |||
| 41f14f6ae5 | |||
| a170942ae1 | |||
| 6df6ce8de7 | |||
| 7ddc115918 | |||
| 5f8f3ad98e | |||
| 5205033cc6 | |||
| bbd26995c5 | |||
| 94e4112ece | |||
| 45db55c678 | |||
| 7a00a6777e | |||
| 7a50f12740 | |||
| 530bd551e2 | |||
| e40cdf2fb0 | |||
| 94c9121bb1 | |||
| f72b5bed65 | |||
| 7cb681f6f1 | |||
| f80d78858d | |||
| 6f856453c1 | |||
| a6c50fccca | |||
| 9ad140d00f | |||
| bb173b7cdc | |||
| 5f68328131 | |||
| 61826fb42b | |||
| 6b5495a13b | |||
| 7a24f77b59 | |||
| 562ed171c3 | |||
| 241f052a7b | |||
| f239376b03 | |||
| 9dc5e5def6 | |||
| 23d2bab764 | |||
| 814121d686 | |||
| 4a5442e821 | |||
| 3b67822b25 | |||
| 24754364c8 | |||
| 131589670f | |||
| efca85829b | |||
| 3d64903e54 | |||
| ad3b4911be | |||
| a0d92a631f | |||
| f1b38219b4 | |||
| d59e9ccabf | |||
| 4c81db3a09 | |||
| a8cfd4be9f | |||
| 894601700d | |||
| d72951eb04 | |||
| ca3ec3ac12 | |||
| 5ce857bb24 | |||
| f6512d3e1c | |||
| 52f095d945 | |||
| 91da937d4f | |||
| 9e3095957c | |||
| fac44b74e9 | |||
| ed6ba3a5eb | |||
| df70e983be | |||
| 4d28dcefac | |||
| 8ab528a566 | |||
| 9bbf050cd2 | |||
| 5a545aca20 | |||
| 0987b311d9 | |||
| ff105abf12 | |||
| 422ca7a544 | |||
| 09df937c88 | |||
| ec8ac2a371 | |||
| 66b0458c0e | |||
| bd0cd361f2 | |||
| 60c237bf82 | |||
| bae8a16d8c | |||
| 55e8aebb87 | |||
| 272c507f91 | |||
| 8e945cde52 | |||
| c17287db67 | |||
| 625a3fcfd6 | |||
| dd2ecf2f83 | |||
| 0efc2b3daf | |||
| 4b4c3a82b8 | |||
| 7d0599fc5d | |||
| 0adea89d34 | |||
| 044363517c | |||
| a2f11d5d51 | |||
| bcd8ddbfb5 | |||
| 479e3f0474 | |||
| c298ca905c | |||
| aebb19effa | |||
| e64fe1c610 | |||
| 2357297531 | |||
| 926f7da6ce | |||
| d260dfcf12 | |||
| a84b2fb4c4 | |||
| 6aca3f578a | |||
| 45ab54b5d7 | |||
| ca84419e0c | |||
| a04dec7ec1 | |||
| 81a499d121 | |||
| ff8ab621bc | |||
| cdb5e5a978 | |||
| effa410eae | |||
| df27bfaa3d | |||
| a865c210b6 | |||
| 7820748cce | |||
| 2147b2aa5e | |||
| 19418617dd | |||
| e1924bf8a7 | |||
| 6d3faaebe3 | |||
| 17639129b9 | |||
| 1c809095ec | |||
| 3bc563de38 | |||
| 7beb94f8cc | |||
| 2b32a30c1a | |||
| 893ffc0461 | |||
| 0df11e3224 | |||
| 75ad389424 | |||
| 00577823dc | |||
| 671a117b96 | |||
| 3d1c2d392c | |||
| b7611e1a1b | |||
| d13af316d3 | |||
| 7d5cc26ea4 | |||
| c4d881ac47 | |||
| 0a53b9f07a | |||
| c2ad655af2 | |||
| a888073e1b | |||
| df68655b13 | |||
| 86227ae3b3 | |||
| 97878b1ad6 | |||
| 314ee2b456 | |||
| f4ef47c2d2 | |||
| 769d552e88 | |||
| afa4b69d7a | |||
| a9bdf0dd06 | |||
| 6ffa94ed3a | |||
| 690001ed2a | |||
| 90e015b6b3 | |||
| 44bcd0f977 | |||
| 60d0fabcf4 | |||
| 10a1c8af3e | |||
| 6834df73e6 | |||
| 04d40a5b90 | |||
| be40900e1e | |||
| f16f7b6d2c | |||
| f74f8391b6 | |||
| 2ce8c0a45d | |||
| 77ad6b4512 | |||
| efbdf913bd | |||
| dfb01389f7 | |||
| e390261b53 | |||
| 27f5275172 | |||
| d15f2b68ab | |||
| 44ed19858b | |||
| e7c195d910 | |||
| 23a4dbbb60 | |||
| 0ac81767dd | |||
| 54cdc51557 | |||
| 09ce640d9c | |||
| 01df673a34 | |||
| 553bae0be5 | |||
| 48dc4abcbe | |||
| 4a814afb5f | |||
| 9cd225e704 | |||
| c8640af1ac | |||
| 43c825f7c2 | |||
| 7334ed5300 | |||
| 7ea4872ff8 | |||
| 9655170bd2 | |||
| acce9b1702 | |||
| 95baa8baa3 | |||
| 2388fa2d06 | |||
| 285eb59da0 | |||
| ae42ee1674 | |||
| 638b2ad311 | |||
| 96e068d348 | |||
| db359d906b | |||
| 11185458b3 | |||
| 977db6b5bf | |||
| 803c20e093 | |||
| 9948cb4652 | |||
| 07a579b939 | |||
| 104d96e6e2 | |||
| e0f40a9fce | |||
| bc8e0c5b2c | |||
| 2b6e41f895 | |||
| b633523d0e | |||
| c5eb7fc89e | |||
| cf6837a41a | |||
| e297242264 | |||
| 550e7e0aa1 | |||
| 90f21d99a5 | |||
| 641e0dc43c | |||
| 91c993b005 | |||
| 39ab4723ff | |||
| 3769e706af | |||
| 26efe8f062 | |||
| 8a890644ee | |||
| b2ffa7f7f6 | |||
| 960b931744 | |||
| 47158da05e | |||
| b9c22d267d | |||
| 46e6fac6db | |||
| dc68990bff | |||
| e4f7e9e175 | |||
| 4ab99b68da | |||
| b8bd64e078 | |||
| 812d1f8911 | |||
| ac8288fece | |||
| d2dd786b72 | |||
| 7cf30b820c | |||
| 1120896438 | |||
| 093238cc52 | |||
| c153e29273 | |||
| 2757c7a4b6 | |||
| 46091a7c99 | |||
| b0a5da2b82 | |||
| 0026566ba0 | |||
| 9f640380bf | |||
| a3faaede61 | |||
| 94d8add220 | |||
| 6d4b5a5ef6 | |||
| ae27c5e453 | |||
| e4d23a7c74 | |||
| 778cdcfd58 | |||
| 5e730947aa | |||
| 71b10c7365 | |||
| 8c323b9613 | |||
| af45aae110 | |||
| d4c7e5791e | |||
| dc4c3a9709 | |||
| ef84fbd547 | |||
| 431569763e | |||
| 7b5bab3681 | |||
| 86b32afdcb | |||
| 557e4c2122 | |||
| 8b29c1cd56 | |||
| 30ef5a187b | |||
| ebe69bf98b | |||
| a907263ab8 | |||
| 7528437f65 | |||
| e2e8260876 | |||
| 6dfc5839cc | |||
| 8cba63328f | |||
| 531cc3ff58 | |||
| c7de32584b | |||
| 625d1f15b6 | |||
| 251fc42f67 | |||
| 0bf8e47c48 | |||
| 7784f74102 | |||
| bfacab984b | |||
| f8a05731d9 | |||
| 116b5066c9 | |||
| 011adca579 | |||
| 95de69a006 | |||
| 6cea976d10 | |||
| c573a8961c | |||
| 6c444fecfb | |||
| 29d1fd1c7d | |||
| f6493507f4 | |||
| b012df262d | |||
| e028ed42da | |||
| 43dc561ac2 | |||
| 217b55090a | |||
| 5c8b78e41d | |||
| 899dd70d50 | |||
| d945571d31 | |||
| b7a4386d22 | |||
| b117307340 | |||
| b8ccf3623f | |||
| 495145b72b | |||
| 99e25d65f2 | |||
| 2619d13c8d | |||
| 62a5a81107 | |||
| d234dd4c75 | |||
| a48b49cdbe | |||
| f0a488c711 | |||
| 511c229364 | |||
| b5d0e46740 | |||
| 8f1be7fe21 | |||
| 8a70c09fea | |||
| 343e8c92e5 | |||
| 332788ee01 | |||
| acf77101a7 | |||
| cba372966d | |||
| fff9f76e8a | |||
| bcc8f47856 | |||
| 61d68a9c6c | |||
| 5e7258973e | |||
| 194696e9cb | |||
| 5130909efc | |||
| 7874c4f72d | |||
| 8399868a63 | |||
| 3e97e7f8a8 | |||
| d5e54f33a0 | |||
| 58d1f37db0 | |||
| 7c175e4ce5 | |||
| 328b8a0873 | |||
| db446279bb | |||
| 80042ea71d | |||
| bb6b1c2089 | |||
| f13e7f83b8 | |||
| 633f70f9fb | |||
| 435674075c | |||
| c8f5bb5d4f | |||
| 52f44bb669 | |||
| 7412b5e96d | |||
| a9379d067e |
@@ -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
|
||||
@@ -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 17
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '17'
|
||||
- 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 17
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '17'
|
||||
- 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 }}
|
||||
Executable → Regular
+3
-5
@@ -5,17 +5,17 @@
|
||||
.checkstyle
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
/.idea/*
|
||||
!/.idea/scopes/
|
||||
classes
|
||||
gen-external-apklibs
|
||||
|
||||
# Gradle
|
||||
.gradle
|
||||
build
|
||||
gradle.properties
|
||||
|
||||
# Maven
|
||||
target
|
||||
@@ -36,6 +36,4 @@ pom.xml.*
|
||||
local.properties
|
||||
|
||||
*.prefs
|
||||
|
||||
# The keystore file
|
||||
app/spothero-release.keystore
|
||||
.DS_Store
|
||||
|
||||
Generated
+9
@@ -0,0 +1,9 @@
|
||||
/libraries
|
||||
/runConfigurations.xml
|
||||
/misc.xml
|
||||
/vcs.xml
|
||||
/workspace.xml
|
||||
/caches
|
||||
/gradle.xml
|
||||
/modules.xml
|
||||
/compiler.xml
|
||||
Generated
+6
@@ -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>
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
language: android
|
||||
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- build-tools-23.0.2
|
||||
- android-23
|
||||
- extra-android-m2repository
|
||||
|
||||
script:
|
||||
- ./gradlew test
|
||||
@@ -0,0 +1,70 @@
|
||||
# Community Contribution Guidelines
|
||||
|
||||
As the creators and maintainers of this project, we want to ensure that the project lives and continues to grow. Not blocked by any singular person's computer time. One of the simplest ways of doing this is by encouraging a larger set of shallow contributors. Through this, we hope to mitigate the problems of a project that needs updates, but there is no-one who has the power to do so.
|
||||
|
||||
#### Development Process
|
||||
|
||||
We maintain two permanent, protected branches: `master` and `development`.
|
||||
|
||||
`master` is for the current release. It has already been distributed to jcenter and can't be modified at this point (except for docs that may be incorrect for the current release!).
|
||||
|
||||
`development` is where we stage work for the *next* release. Pull requests should be directed to this branch.
|
||||
|
||||
When working on a new feature or fix that may span multiple commits, please do so in a feature branch (ex: `feature/my_cool_thing`). Please clean up these feature branches once merged into `development`.
|
||||
|
||||
When a new version is ready to be released, please create a pull request to merge `development` into `master`, named something like "Release 10.0". Then we can have some final discussion before we merge it into `master` and push the release out to the public.
|
||||
|
||||
Since `development` is a *shared* branch, it is important not to ever rebase this branch onto `master`. If a bug fix is applied to `master` it can be merged into `development` using good old simple `git checkout development && git merge master`. Yes this will clutter the history a little bit, but it also provides important context to know how/when a patch was applied. Merge commits can be considered necessary historical data, not warts on an idealized history graph.
|
||||
|
||||
#### Testing
|
||||
|
||||
To run tests locally, just run `./gradlew test`. Tests should always be run before pushing anything to the repo.
|
||||
|
||||
#### Ownership
|
||||
|
||||
If you get a merged pull-request of substance (ie not just a typo fix), then you are eligible for push access to this repo. Simply request access via a new GitHub issue.
|
||||
|
||||
Offhand, it is easy to imagine that this would make code quality suffer, but in reality it offers fresh perspectives to the codebase and encourages ownership from people who are depending on the project. If you are building a project that relies on this codebase, then you probably have the skills to improve it and offer valuable feedback. At the end of the day, there isn't too much risk in this, as the `master` branch is still locked, and jcenter access is still being restricted.
|
||||
|
||||
Everyone comes in with their own perspective on what a project could/should look like, and encouraging discussion can help expose good ideas sooner.
|
||||
|
||||
#### Why do we give out push access?
|
||||
|
||||
It can be overwhelming to be offered the chance to wipe the source code for a project. Do not worry, we do not let you push to master. All code is peer-reviewed, and we have the convention that someone other than the submitter should merge non-trivial pull requests.
|
||||
|
||||
As a contributor, you can merge other people's pull requests, or other contributors can merge yours. You will not be assigned a pull request, but you are welcome to jump in and take a code review on topics that interest you.
|
||||
|
||||
This project is not continuously deployed, there is space for debate after review too. Offering everyone the chance to revert, or make an amending pull request. If it feels right, merge.
|
||||
|
||||
#### How can we help you get comfortable contributing?
|
||||
|
||||
It is normal for a first pull request to be a potential fix for a problem, and moving on from there to helping the project's direction can be difficult. We will try to help contributors cross that barrier by offering good first step issues. These issues can be fixed without feeling like you are stepping on toes. Ideally, these are non-critical issues that are well defined. They will be purposely avoided by mature contributors to the project, to make space for others.
|
||||
|
||||
We aim to keep all project discussion inside GitHub issues. This is to make sure valuable discussion is accessible via search. If you have questions about how to use the library, or how the project is running - GitHub issues are the goto tool for this project.
|
||||
|
||||
#### Our expectations on you as a contributor
|
||||
|
||||
To quote [@alloy](https://github.com/alloy) from [this issue](https://github.com/Moya/Moya/issues/135):
|
||||
|
||||
> Do not ever feel bad for not contributing to open source.
|
||||
|
||||
We want contributors to provide ideas, keep the ship shipping and to take some of the load from others. It is non-obligatory; we’re here to get things done in an enjoyable way. :trophy:
|
||||
|
||||
The fact that you will have push access will allow you to:
|
||||
|
||||
- Avoid having to fork the project if you want to submit other pull requests, as you will be able to create branches directly on the project.
|
||||
- Help triage issues and merge pull requests.
|
||||
- Pick up the project if other maintainers move their focus elsewhere.
|
||||
|
||||
It is up to you to use those superpowers or not though 😉
|
||||
|
||||
If someone submits a pull request that is not perfect, and you are reviewing, it is better to think about the PR's motivation rather than the specific implementation. Having braces on the wrong line should not be a blocker. Though we do want to keep test coverage high, we will work with you to figure that out together.
|
||||
|
||||
#### What about if you have problems that cannot be discussed in a public issue?
|
||||
|
||||
[Eric Kuck](https://github.com/erickuck) has a contactable email on his GitHub profile, and is happy to talk about any problems.
|
||||
|
||||
#### This is a different way to handle open source! Where did it come from?
|
||||
|
||||
The original source of this document can be found at [https://github.com/moya/contributors](https://github.com/moya/contributors).
|
||||
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,11 +1,11 @@
|
||||
[](https://travis-ci.org/bluelinelabs/Conductor)
|
||||
[](https://github.com/bluelinelabs/conductor/actions/workflows/main.yml) [](http://android-arsenal.com/details/1/3361) [](http://javadoc.io/doc/com.bluelinelabs/conductor)
|
||||
|
||||
# Conductor
|
||||
|
||||
A small, yet full-featured framework that allows building View-based Android applications. Conductor provides a light-weight wrapper around standard Android Views that does just about everything you'd want:
|
||||
|
||||
| Conductor
|
||||
------|------------------------------
|
||||
| | Conductor |
|
||||
|-----------|-------------|
|
||||
:tada: | Easy integration
|
||||
:point_up: | Single Activity apps without using Fragments
|
||||
:recycle: | Simple but powerful lifecycle management
|
||||
@@ -13,91 +13,95 @@ 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
|
||||
compile 'com.bluelinelabs:conductor:1.0.1'
|
||||
def conductorVersion = '3.2.0'
|
||||
|
||||
// If you want the components that go along with
|
||||
// Android's support libraries (currently just a PagerAdapter):
|
||||
compile 'com.bluelinelabs:conductor-support:1.0.1'
|
||||
implementation "com.bluelinelabs:conductor:$conductorVersion"
|
||||
|
||||
// If you want RxJava/RxAndroid lifecycle support:
|
||||
compile 'com.bluelinelabs:conductor-rxlifecycle:1.0.1'
|
||||
// AndroidX Transition change handlers:
|
||||
implementation "com.bluelinelabs:conductor-androidx-transition:$conductorVersion"
|
||||
|
||||
// ViewPager PagerAdapter:
|
||||
implementation "com.bluelinelabs:conductor-viewpager:$conductorVersion"
|
||||
|
||||
// ViewPager2 Adapter:
|
||||
implementation "com.bluelinelabs:conductor-viewpager2:$conductorVersion"
|
||||
```
|
||||
|
||||
### 4.0 Preview
|
||||
Use `4.0.0-preview-4` as your version number in any of the dependencies above.
|
||||
|
||||
### 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 {
|
||||
repositories {
|
||||
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Components to Know
|
||||
|
||||
### 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
|
||||
|
||||
The Router is responsible for handling navigation and the backstack. Controllers are pushed and popped in order to display and remove them.
|
||||
|
||||
### ControllerChangeHandler
|
||||
|
||||
ControllerChangeHandlers are responsible for performing the logic associated with pushing or popping Controllers. The most common implementation of these will be to animate between Controllers.
|
||||
|
||||
### 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.
|
||||
| | Conductor Components |
|
||||
------|------------------------------
|
||||
__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.
|
||||
__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
|
||||
|
||||
```
|
||||
public class MainActivity extends Activity {
|
||||
```kotlin
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private Router mRouter;
|
||||
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);
|
||||
|
||||
ViewGroup container = (ViewGroup)findViewById(R.id.controller_container)
|
||||
|
||||
mRouter = Conductor.attachRouter(this, container, savedInstanceState);
|
||||
if (!mRouter.hasRootController()) {
|
||||
mRouter.setRoot(new HomeController());
|
||||
}
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
val container = findViewById<ViewGroup>(R.id.controller_container)
|
||||
|
||||
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 (!mRouter.handleBack()) {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### Minimal Controller implementation
|
||||
|
||||
```
|
||||
public class HomeController extends Controller {
|
||||
|
||||
@Override
|
||||
protected int layoutId() {
|
||||
return R.layout.controller_overlay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindView(@NonNull View view) {
|
||||
super.onBindView(view);
|
||||
|
||||
((TextView)view.findViewById(R.id.tv_title)).setText("Hello World");
|
||||
}
|
||||
```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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -105,6 +109,12 @@ public class HomeController extends Controller {
|
||||
|
||||
[Demo app](https://github.com/bluelinelabs/conductor/tree/master/demo) - Shows how to use all basic and most advanced functions of Conductor.
|
||||
|
||||
### Controller Lifecycle
|
||||
|
||||
The lifecycle of a Controller is significantly simpler to understand than that of a Fragment. A lifecycle diagram is shown below:
|
||||
|
||||

|
||||
|
||||
## Advanced Topics
|
||||
|
||||
### Retain View Modes
|
||||
@@ -113,15 +123,23 @@ public class HomeController extends Controller {
|
||||
### Custom Change Handlers
|
||||
`ControllerChangeHandler` can be subclassed in order to perform different functions when changing between two `Controllers`. Two convenience `ControllerChangeHandler` subclasses are included to cover most basic needs: `AnimatorChangeHandler`, which will use an `Animator` object to transition between two views, and `TransitionChangeHandler`, which will use Lollipop's `Transition` framework for transitioning between views.
|
||||
|
||||
### Child Controllers
|
||||
`addChildController` can be called on a `Controller` in order to add nested `Controller`s. Child `Controller`s will receive all lifecycle callbacks that parents get.
|
||||
### Child Routers & Controllers
|
||||
`getChildRouter` can be called on a `Controller` in order to get a nested `Router` into which child `Controller`s can be pushed. This enables creating advanced layouts, such as Master/Detail.
|
||||
|
||||
### RxJava Lifecycle
|
||||
If the RxLifecycle dependency has been added, there is an `RxController` available that can be used along with the standard [RxLifecycle library](https://github.com/trello/RxLifecycle). There is also a `ControllerLifecycleProvider` available if you do not wish to use this subclass.
|
||||
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.
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
apply plugin: 'com.github.dcendents.android-maven'
|
||||
apply plugin: 'com.jfrog.bintray'
|
||||
|
||||
group = 'com.bluelinelabs'
|
||||
version = rootProject.ext.versionName
|
||||
|
||||
task sourcesJar(type: Jar) {
|
||||
from android.sourceSets.main.java.srcDirs
|
||||
classifier = 'sources'
|
||||
}
|
||||
|
||||
task javadoc(type: Javadoc) {
|
||||
source = android.sourceSets.main.java.srcDirs
|
||||
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
|
||||
exclude '**/R.java'
|
||||
failOnError = false
|
||||
}
|
||||
|
||||
task javadocJar(type: Jar, dependsOn: javadoc) {
|
||||
classifier = 'javadoc'
|
||||
from javadoc.destinationDir
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives javadocJar
|
||||
archives sourcesJar
|
||||
}
|
||||
|
||||
if (project.hasProperty('pom_name')) {
|
||||
install {
|
||||
repositories.mavenInstaller {
|
||||
pom.project {
|
||||
name pom_name
|
||||
description pom_description
|
||||
url pom_url
|
||||
packaging pom_packaging
|
||||
groupId 'com.bluelinelabs'
|
||||
artifactId project.hasProperty('artifactId') ? project.ext.artifactId : ''
|
||||
|
||||
organization {
|
||||
name 'BlueLine Labs'
|
||||
url 'http://bluelinelabs.com'
|
||||
}
|
||||
licenses {
|
||||
license {
|
||||
name 'The Apache Software License, Version 2.0'
|
||||
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||
distribution 'repo'
|
||||
}
|
||||
}
|
||||
scm {
|
||||
url pom_url
|
||||
connection pom_git_connection
|
||||
developerConnection pom_git_connection
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id 'erickuck'
|
||||
name 'Eric Kuck'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bintray {
|
||||
user = project.hasProperty('bintray_username') ? bintray_username : ''
|
||||
key = project.hasProperty('bintray_api_key') ? bintray_api_key : ''
|
||||
configurations = ['archives']
|
||||
|
||||
dryRun = false
|
||||
publish = false
|
||||
|
||||
pkg {
|
||||
repo = 'bluelinelabs'
|
||||
userOrg = 'bluelinelabs'
|
||||
name = pom_name
|
||||
desc = pom_description
|
||||
websiteUrl = pom_url
|
||||
issueTrackerUrl = pom_issue_tracker_url
|
||||
vcsUrl = pom_url
|
||||
licenses = ['Apache-2.0']
|
||||
labels = pom_labels
|
||||
|
||||
version {
|
||||
name = project.version
|
||||
gpg {
|
||||
sign = true
|
||||
passphrase = project.hasProperty('bintray_gpg_passphrase') ? bintray_gpg_passphrase : ''
|
||||
}
|
||||
mavenCentralSync {
|
||||
sync = false
|
||||
user = project.hasProperty('maven_central_username') ? maven_central_username : ''
|
||||
password = project.hasProperty('maven_central_password') ? maven_central_password : ''
|
||||
close = '1'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable → Regular
+19
-15
@@ -1,25 +1,29 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:1.5.0'
|
||||
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3'
|
||||
classpath libs.agp
|
||||
classpath libs.kotlin.plugin
|
||||
classpath libs.dokka
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "com.jfrog.bintray" version "1.5"
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task wrapper(type: Wrapper) {
|
||||
gradleVersion = '2.10'
|
||||
}
|
||||
|
||||
apply from: rootProject.file('dependencies.gradle')
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
configurations {
|
||||
libs.lint.checks
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly libs.lint.api
|
||||
compileOnly libs.lint.checks
|
||||
compileOnly libs.kotlin.stdlib
|
||||
|
||||
testImplementation libs.junit
|
||||
testImplementation libs.lint
|
||||
testImplementation libs.lint.tests
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes('Lint-Registry-v2': 'com.bluelinelabs.conductor.lint.IssueRegistry')
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package com.bluelinelabs.conductor.lint;
|
||||
|
||||
import com.android.tools.lint.client.api.JavaEvaluator;
|
||||
import com.android.tools.lint.client.api.UElementHandler;
|
||||
import com.android.tools.lint.detector.api.Category;
|
||||
import com.android.tools.lint.detector.api.Detector;
|
||||
import com.android.tools.lint.detector.api.Implementation;
|
||||
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 org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.uast.UClass;
|
||||
import org.jetbrains.uast.UElement;
|
||||
import org.jetbrains.uast.UMethod;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public final class ControllerChangeHandlerIssueDetector extends Detector implements Detector.UastScanner {
|
||||
|
||||
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.",
|
||||
Category.CORRECTNESS, 6, Severity.FATAL,
|
||||
new Implementation(ControllerChangeHandlerIssueDetector.class, Scope.JAVA_FILE_SCOPE));
|
||||
|
||||
private static final String CLASS_NAME = "com.bluelinelabs.conductor.ControllerChangeHandler";
|
||||
|
||||
@Override
|
||||
public List<Class<? extends UElement>> getApplicableUastTypes() {
|
||||
return Collections.<Class<? extends UElement>>singletonList(UClass.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UElementHandler createUastHandler(final JavaContext context) {
|
||||
final JavaEvaluator evaluator = context.getEvaluator();
|
||||
|
||||
return new UElementHandler() {
|
||||
|
||||
@Override
|
||||
public void visitClass(@NotNull UClass node) {
|
||||
if (evaluator.isAbstract(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean hasSuperType = evaluator.extendsClass(node.getPsi(), 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);
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean hasConstructor = false;
|
||||
boolean hasDefaultConstructor = false;
|
||||
for (UMethod method : node.getMethods()) {
|
||||
if (method.isConstructor()) {
|
||||
hasConstructor = true;
|
||||
if (evaluator.isPublic(method) && method.getUastParameters().size() == 0) {
|
||||
hasDefaultConstructor = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasConstructor && !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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package com.bluelinelabs.conductor.lint;
|
||||
|
||||
import com.android.SdkConstants;
|
||||
import com.android.tools.lint.client.api.JavaEvaluator;
|
||||
import com.android.tools.lint.client.api.UElementHandler;
|
||||
import com.android.tools.lint.detector.api.Category;
|
||||
import com.android.tools.lint.detector.api.Detector;
|
||||
import com.android.tools.lint.detector.api.Implementation;
|
||||
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 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 java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public final class ControllerIssueDetector extends Detector implements Detector.UastScanner {
|
||||
|
||||
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"
|
||||
+ " case of the process being killed.", Category.CORRECTNESS, 6, Severity.FATAL,
|
||||
new Implementation(ControllerIssueDetector.class, Scope.JAVA_FILE_SCOPE));
|
||||
|
||||
private static final String CLASS_NAME = "com.bluelinelabs.conductor.Controller";
|
||||
|
||||
@Override
|
||||
public List<Class<? extends UElement>> getApplicableUastTypes() {
|
||||
return Collections.singletonList(UClass.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UElementHandler createUastHandler(final JavaContext context) {
|
||||
final JavaEvaluator evaluator = context.getEvaluator();
|
||||
|
||||
return new UElementHandler() {
|
||||
@Override
|
||||
public void visitClass(@NotNull UClass node) {
|
||||
if (evaluator.isAbstract(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean hasSuperType = evaluator.extendsClass(node.getPsi(), 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);
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean hasConstructor = false;
|
||||
boolean hasDefaultConstructor = false;
|
||||
boolean hasBundleConstructor = false;
|
||||
for (UMethod method : node.getMethods()) {
|
||||
if (method.isConstructor()) {
|
||||
hasConstructor = true;
|
||||
if (evaluator.isPublic(method)) {
|
||||
List<UParameter> parameters = method.getUastParameters();
|
||||
if (parameters.size() == 0) {
|
||||
hasDefaultConstructor = true;
|
||||
break;
|
||||
} else if (parameters.size() == 1 &&
|
||||
(parameters.get(0).getType().equalsToText(SdkConstants.CLASS_BUNDLE)) ||
|
||||
parameters.get(0).getType().equalsToText("Bundle")) {
|
||||
hasBundleConstructor = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasConstructor && !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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package com.bluelinelabs.conductor.lint;
|
||||
|
||||
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;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public class ControllerChangeHandlerDetectorTest {
|
||||
|
||||
private static final String CONSTRUCTOR =
|
||||
"src/test/SampleHandler.java:2: Error: This ControllerChangeHandler needs to have a public default constructor (test.SampleHandler) [ValidControllerChangeHandler]\n"
|
||||
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
|
||||
+ "^\n"
|
||||
+ "1 errors, 0 warnings\n";
|
||||
|
||||
private final TestFile controllerChangeHandlerStub = java(
|
||||
"package com.bluelinelabs.conductor;\n"
|
||||
+ "abstract class ControllerChangeHandler {}"
|
||||
);
|
||||
|
||||
@Test
|
||||
public void testWithNoConstructor() {
|
||||
@Language("JAVA") String source = ""
|
||||
+ "package test;\n"
|
||||
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
|
||||
+ "}";
|
||||
|
||||
lint()
|
||||
.files(controllerChangeHandlerStub, java(source))
|
||||
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
|
||||
.run()
|
||||
.expectClean();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithEmptyConstructor() {
|
||||
@Language("JAVA") String source = ""
|
||||
+ "package test;\n"
|
||||
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
|
||||
+ " public SampleHandler() { }\n"
|
||||
+ "}";
|
||||
|
||||
lint()
|
||||
.files(controllerChangeHandlerStub, java(source))
|
||||
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
|
||||
.run()
|
||||
.expectClean();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithInvalidConstructor() {
|
||||
@Language("JAVA") String source = ""
|
||||
+ "package test;\n"
|
||||
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
|
||||
+ " public SampleHandler(int number) { }\n"
|
||||
+ "}";
|
||||
|
||||
lint()
|
||||
.files(controllerChangeHandlerStub, java(source))
|
||||
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
|
||||
.run()
|
||||
.expect(CONSTRUCTOR);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithEmptyAndInvalidConstructor() {
|
||||
@Language("JAVA") String source = ""
|
||||
+ "package test;\n"
|
||||
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
|
||||
+ " public SampleHandler() { }\n"
|
||||
+ " public SampleHandler(int number) { }\n"
|
||||
+ "}";
|
||||
|
||||
lint()
|
||||
.files(controllerChangeHandlerStub, java(source))
|
||||
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
|
||||
.run()
|
||||
.expectClean();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithPrivateConstructor() {
|
||||
@Language("JAVA") String source = ""
|
||||
+ "package test;\n"
|
||||
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
|
||||
+ " private SampleHandler() { }\n"
|
||||
+ "}";
|
||||
|
||||
lint()
|
||||
.files(controllerChangeHandlerStub, java(source))
|
||||
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
|
||||
.run()
|
||||
.expect(CONSTRUCTOR);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithPrivateClass() {
|
||||
@Language("JAVA") String source = ""
|
||||
+ "package test;\n"
|
||||
+ "private class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
|
||||
+ " public SampleHandler() { }\n"
|
||||
+ "}";
|
||||
|
||||
lint()
|
||||
.files(controllerChangeHandlerStub, 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 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");
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
package com.bluelinelabs.conductor.lint;
|
||||
|
||||
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;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public class ControllerDetectorTest {
|
||||
|
||||
private static final String CONSTRUCTOR_ERROR =
|
||||
"src/test/SampleController.java:2: Error: This Controller needs to have either a public default constructor or a public single-argument constructor that takes a Bundle. (test.SampleController) [ValidController]\n"
|
||||
+ "public class SampleController extends com.bluelinelabs.conductor.Controller {\n"
|
||||
+ "^\n"
|
||||
+ "1 errors, 0 warnings\n";
|
||||
private static final String CLASS_ERROR =
|
||||
"src/test/SampleController.java:2: Error: This Controller class should be public (test.SampleController) [ValidController]\n"
|
||||
+ "private class SampleController extends com.bluelinelabs.conductor.Controller {\n"
|
||||
+ "^\n"
|
||||
+ "1 errors, 0 warnings\n";
|
||||
|
||||
private final TestFile controllerStub = java(
|
||||
"package com.bluelinelabs.conductor;\n"
|
||||
+ "abstract class Controller {}"
|
||||
);
|
||||
|
||||
|
||||
@Test
|
||||
public void testWithNoConstructor() {
|
||||
@Language("JAVA") String source = ""
|
||||
+ "package test;\n"
|
||||
+ "public class SampleController extends com.bluelinelabs.conductor.Controller {\n"
|
||||
+ "}";
|
||||
|
||||
lint()
|
||||
.files(controllerStub, java(source))
|
||||
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
|
||||
.run()
|
||||
.expectClean();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithEmptyConstructor() {
|
||||
@Language("JAVA") String source = ""
|
||||
+ "package test;\n"
|
||||
+ "public class SampleController extends com.bluelinelabs.conductor.Controller {\n"
|
||||
+ " public SampleController() { }\n"
|
||||
+ "}";
|
||||
|
||||
lint()
|
||||
.files(controllerStub, java(source))
|
||||
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
|
||||
.run()
|
||||
.expectClean();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithInvalidConstructor() {
|
||||
@Language("JAVA") String source = ""
|
||||
+ "package test;\n"
|
||||
+ "public class SampleController extends com.bluelinelabs.conductor.Controller {\n"
|
||||
+ " public SampleController(int number) { }\n"
|
||||
+ "}";
|
||||
|
||||
lint()
|
||||
.files(controllerStub, java(source))
|
||||
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
|
||||
.run()
|
||||
.expect(CONSTRUCTOR_ERROR);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithEmptyAndInvalidConstructor() {
|
||||
@Language("JAVA") String source = ""
|
||||
+ "package test;\n"
|
||||
+ "public class SampleController extends com.bluelinelabs.conductor.Controller {\n"
|
||||
+ " public SampleController() { }\n"
|
||||
+ " public SampleController(int number) { }\n"
|
||||
+ "}";
|
||||
|
||||
lint()
|
||||
.files(controllerStub, java(source))
|
||||
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
|
||||
.run()
|
||||
.expectClean();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithBaseClassAndPrivateConstructor() {
|
||||
@Language("JAVA")
|
||||
String baseClass = ""
|
||||
+ "package test;\n"
|
||||
+ "public class BaseController extends com.bluelinelabs.conductor.Controller {}";
|
||||
|
||||
@Language("JAVA")
|
||||
String source = ""
|
||||
+ "package test;\n"
|
||||
+ "public class SampleController extends BaseController {\n"
|
||||
+ " private SampleController() { }\n"
|
||||
+ "}";
|
||||
|
||||
lint()
|
||||
.files(controllerStub, java(baseClass), java(source))
|
||||
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
|
||||
.run()
|
||||
.expect(
|
||||
"src/test/SampleController.java:2: Error: This Controller needs to have either a public default constructor or a public single-argument constructor that takes a Bundle. (test.SampleController) [ValidController]\n" +
|
||||
"public class SampleController extends BaseController {\n" +
|
||||
"^\n" +
|
||||
"1 errors, 0 warnings"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithPrivateConstructor() {
|
||||
@Language("JAVA") String source = ""
|
||||
+ "package test;\n"
|
||||
+ "public class SampleController extends com.bluelinelabs.conductor.Controller {\n"
|
||||
+ " private SampleController() { }\n"
|
||||
+ "}";
|
||||
|
||||
lint()
|
||||
.files(controllerStub, java(source))
|
||||
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
|
||||
.run()
|
||||
.expect(CONSTRUCTOR_ERROR);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithPrivateClass() {
|
||||
@Language("JAVA") String source = ""
|
||||
+ "package test;\n"
|
||||
+ "private class SampleController extends com.bluelinelabs.conductor.Controller {\n"
|
||||
+ " public SampleController() { }\n"
|
||||
+ "}";
|
||||
|
||||
lint()
|
||||
.files(controllerStub, java(source))
|
||||
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
|
||||
.run()
|
||||
.expect(CLASS_ERROR);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
alias(libs.plugins.mvnpublish)
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk libs.versions.compilesdk.get() as Integer
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion libs.versions.minsdk.get()
|
||||
targetSdkVersion libs.versions.targetsdk.get()
|
||||
versionCode Integer.parseInt(project.VERSION_CODE)
|
||||
versionName project.VERSION_NAME
|
||||
}
|
||||
|
||||
namespace "com.bluelinelabs.conductor.androidxtransition"
|
||||
}
|
||||
|
||||
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
|
||||
+656
@@ -0,0 +1,656 @@
|
||||
package com.bluelinelabs.conductor.changehandler.androidxtransition;
|
||||
|
||||
import android.graphics.Rect;
|
||||
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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A TransitionChangeHandler that facilitates using different Transitions for the entering view, the exiting view,
|
||||
* and shared elements between the two.
|
||||
* <p/>
|
||||
* Note that this class uses 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
|
||||
public abstract class SharedElementTransitionChangeHandler extends TransitionChangeHandler {
|
||||
|
||||
// A map of from -> to names. Generally these will be the same.
|
||||
@NonNull final ArrayMap<String, String> sharedElementNames = new ArrayMap<>();
|
||||
|
||||
@NonNull final List<String> waitForTransitionNames = new ArrayList<>();
|
||||
@NonNull final List<ViewParentPair> removedViews = new ArrayList<>();
|
||||
|
||||
@Nullable Transition exitTransition;
|
||||
@Nullable Transition enterTransition;
|
||||
@Nullable Transition sharedElementTransition;
|
||||
@Nullable private SharedElementCallback exitTransitionCallback;
|
||||
@Nullable private SharedElementCallback enterTransitionCallback;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected final Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
|
||||
exitTransition = getExitTransition(container, from, to, isPush);
|
||||
enterTransition = getEnterTransition(container, from, to, isPush);
|
||||
sharedElementTransition = getSharedElementTransition(container, from, to, isPush);
|
||||
exitTransitionCallback = getExitTransitionCallback(container, from, to, isPush);
|
||||
enterTransitionCallback = getEnterTransitionCallback(container, from, to, isPush);
|
||||
|
||||
if (enterTransition == null && sharedElementTransition == null && exitTransition == null) {
|
||||
throw new IllegalStateException("SharedElementTransitionChangeHandler must have at least one transaction.");
|
||||
}
|
||||
|
||||
return mergeTransitions(isPush);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareForTransition(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, @NonNull final Transition transition, final boolean isPush, @NonNull final OnTransitionPreparedListener onTransitionPreparedListener) {
|
||||
OnTransitionPreparedListener listener = new OnTransitionPreparedListener() {
|
||||
@Override
|
||||
public void onPrepared() {
|
||||
configureTransition(container, from, to, transition, isPush);
|
||||
onTransitionPreparedListener.onPrepared();
|
||||
}
|
||||
};
|
||||
|
||||
configureSharedElements(container, from, to, isPush);
|
||||
|
||||
if (to != null && to.getParent() == null && waitForTransitionNames.size() > 0) {
|
||||
waitOnAllTransitionNames(to, listener);
|
||||
container.addView(to);
|
||||
} else {
|
||||
listener.onPrepared();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @Nullable Transition transition, boolean isPush) {
|
||||
if (to != null && removedViews.size() > 0) {
|
||||
to.setVisibility(View.VISIBLE);
|
||||
|
||||
for (ViewParentPair removedView : removedViews) {
|
||||
removedView.parent.addView(removedView.view);
|
||||
}
|
||||
|
||||
removedViews.clear();
|
||||
}
|
||||
|
||||
super.executePropertyChanges(container, from, to, transition, isPush);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
|
||||
super.onAbortPush(newHandler, newTop);
|
||||
|
||||
removedViews.clear();
|
||||
}
|
||||
|
||||
void configureTransition(@NonNull final ViewGroup container, @Nullable View from, @Nullable View to, @NonNull final Transition transition, boolean isPush) {
|
||||
final View nonExistentView = new View(container.getContext());
|
||||
|
||||
List<View> fromSharedElements = new ArrayList<>();
|
||||
List<View> toSharedElements = new ArrayList<>();
|
||||
|
||||
configureSharedElements(container, nonExistentView, to, from, isPush, fromSharedElements, toSharedElements);
|
||||
|
||||
List<View> exitingViews = exitTransition != null ? configureEnteringExitingViews(exitTransition, from, fromSharedElements, nonExistentView) : null;
|
||||
if (exitingViews == null || exitingViews.isEmpty()) {
|
||||
exitTransition = null;
|
||||
}
|
||||
|
||||
if (enterTransition != null) {
|
||||
enterTransition.addTarget(nonExistentView);
|
||||
}
|
||||
|
||||
final List<View> enteringViews = new ArrayList<>();
|
||||
scheduleRemoveTargets(transition, enterTransition, enteringViews, exitTransition, exitingViews, sharedElementTransition, toSharedElements);
|
||||
scheduleTargetChange(container, to, nonExistentView, toSharedElements, enteringViews, exitingViews);
|
||||
|
||||
setNameOverrides(container, toSharedElements);
|
||||
scheduleNameReset(container, toSharedElements);
|
||||
}
|
||||
|
||||
private void waitOnAllTransitionNames(@NonNull final View to, @NonNull final OnTransitionPreparedListener onTransitionPreparedListener) {
|
||||
OnPreDrawListener onPreDrawListener = new OnPreDrawListener() {
|
||||
boolean addedSubviewListeners;
|
||||
|
||||
@Override
|
||||
public boolean onPreDraw() {
|
||||
List<View> foundViews = new ArrayList<>();
|
||||
boolean allViewsFound = true;
|
||||
for (String transitionName : waitForTransitionNames) {
|
||||
View namedView = TransitionUtils.findNamedView(to, transitionName);
|
||||
if (namedView != null) {
|
||||
foundViews.add(TransitionUtils.findNamedView(to, transitionName));
|
||||
} else {
|
||||
allViewsFound = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allViewsFound && !addedSubviewListeners) {
|
||||
addedSubviewListeners = true;
|
||||
waitOnChildTransitionNames(to, foundViews, this, onTransitionPreparedListener);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
to.getViewTreeObserver().addOnPreDrawListener(onPreDrawListener);
|
||||
}
|
||||
|
||||
void waitOnChildTransitionNames(@NonNull final View to, @NonNull List<View> foundViews, @NonNull final OnPreDrawListener parentPreDrawListener, @NonNull final OnTransitionPreparedListener onTransitionPreparedListener) {
|
||||
for (final View view : foundViews) {
|
||||
OneShotPreDrawListener.add(true, view, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
waitForTransitionNames.remove(ViewCompat.getTransitionName(view));
|
||||
|
||||
removedViews.add(new ViewParentPair(view, (ViewGroup)view.getParent()));
|
||||
((ViewGroup)view.getParent()).removeView(view);
|
||||
|
||||
if (waitForTransitionNames.size() == 0) {
|
||||
to.getViewTreeObserver().removeOnPreDrawListener(parentPreDrawListener);
|
||||
to.setVisibility(View.INVISIBLE);
|
||||
onTransitionPreparedListener.onPrepared();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleTargetChange(@NonNull final ViewGroup container, @Nullable final View to, @NonNull final View nonExistentView,
|
||||
@NonNull final List<View> toSharedElements, @NonNull final List<View> enteringViews, @Nullable final List<View> exitingViews) {
|
||||
OneShotPreDrawListener.add(true, container, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (enterTransition != null) {
|
||||
enterTransition.removeTarget(nonExistentView);
|
||||
List<View> views = configureEnteringExitingViews(enterTransition, to, toSharedElements, nonExistentView);
|
||||
enteringViews.addAll(views);
|
||||
}
|
||||
|
||||
if (exitingViews != null) {
|
||||
if (exitTransition != null) {
|
||||
List<View> tempExiting = new ArrayList<>();
|
||||
tempExiting.add(nonExistentView);
|
||||
TransitionUtils.replaceTargets(exitTransition, exitingViews, tempExiting);
|
||||
}
|
||||
exitingViews.clear();
|
||||
exitingViews.add(nonExistentView);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Transition mergeTransitions(boolean isPush) {
|
||||
boolean overlap = enterTransition == null || exitTransition == null || allowTransitionOverlap(isPush);
|
||||
|
||||
if (overlap) {
|
||||
return TransitionUtils.mergeTransitions(TransitionSet.ORDERING_TOGETHER, exitTransition, enterTransition, sharedElementTransition);
|
||||
} else {
|
||||
Transition staggered = TransitionUtils.mergeTransitions(TransitionSet.ORDERING_SEQUENTIAL, exitTransition, enterTransition);
|
||||
return TransitionUtils.mergeTransitions(TransitionSet.ORDERING_TOGETHER, staggered, sharedElementTransition);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull List<View> configureEnteringExitingViews(@NonNull Transition transition, @Nullable View view, @NonNull List<View> sharedElements, @NonNull View nonExistentView) {
|
||||
List<View> viewList = new ArrayList<>();
|
||||
if (view != null) {
|
||||
captureTransitioningViews(viewList, view);
|
||||
}
|
||||
viewList.removeAll(sharedElements);
|
||||
if (!viewList.isEmpty()) {
|
||||
viewList.add(nonExistentView);
|
||||
TransitionUtils.addTargets(transition, viewList);
|
||||
}
|
||||
return viewList;
|
||||
}
|
||||
|
||||
private void configureSharedElements(@NonNull ViewGroup container, @NonNull final View nonExistentView, @Nullable final View to, @Nullable View from,
|
||||
final boolean isPush, @NonNull final List<View> fromSharedElements, @NonNull final List<View> toSharedElements) {
|
||||
|
||||
if (to == null || from == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ArrayMap<String, View> capturedFromSharedElements = captureFromSharedElements(from);
|
||||
|
||||
if (sharedElementNames.isEmpty()) {
|
||||
sharedElementTransition = null;
|
||||
} else if (capturedFromSharedElements != null) {
|
||||
fromSharedElements.addAll(capturedFromSharedElements.values());
|
||||
}
|
||||
|
||||
if (enterTransition == null && exitTransition == null && sharedElementTransition == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
callSharedElementStartEnd(capturedFromSharedElements, true);
|
||||
|
||||
final Rect toEpicenter;
|
||||
if (sharedElementTransition != null) {
|
||||
toEpicenter = new Rect();
|
||||
TransitionUtils.setTargets(sharedElementTransition, nonExistentView, fromSharedElements);
|
||||
setFromEpicenter(capturedFromSharedElements);
|
||||
if (enterTransition != null) {
|
||||
enterTransition.setEpicenterCallback(new Transition.EpicenterCallback() {
|
||||
@Override
|
||||
public Rect onGetEpicenter(Transition transition) {
|
||||
if (toEpicenter.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return toEpicenter;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
toEpicenter = null;
|
||||
}
|
||||
|
||||
OneShotPreDrawListener.add(true, container, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ArrayMap<String, View> capturedToSharedElements = captureToSharedElements(to, isPush);
|
||||
|
||||
if (capturedToSharedElements != null) {
|
||||
toSharedElements.addAll(capturedToSharedElements.values());
|
||||
toSharedElements.add(nonExistentView);
|
||||
}
|
||||
|
||||
callSharedElementStartEnd(capturedToSharedElements, false);
|
||||
if (sharedElementTransition != null) {
|
||||
sharedElementTransition.getTargets().clear();
|
||||
sharedElementTransition.getTargets().addAll(toSharedElements);
|
||||
TransitionUtils.replaceTargets(sharedElementTransition, fromSharedElements, toSharedElements);
|
||||
|
||||
final View toEpicenterView = getToEpicenterView(capturedToSharedElements);
|
||||
if (toEpicenterView != null && toEpicenter != null) {
|
||||
TransitionUtils.getBoundsOnScreen(toEpicenterView, toEpicenter);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable View getToEpicenterView(@Nullable ArrayMap<String, View> toSharedElements) {
|
||||
if (enterTransition != null && sharedElementNames.size() > 0 && toSharedElements != null) {
|
||||
return toSharedElements.get(sharedElementNames.valueAt(0));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void setFromEpicenter(@Nullable ArrayMap<String, View> fromSharedElements) {
|
||||
if (sharedElementNames.size() > 0 && fromSharedElements != null) {
|
||||
final View fromEpicenterView = fromSharedElements.get(sharedElementNames.keyAt(0));
|
||||
|
||||
if (sharedElementTransition != null) {
|
||||
TransitionUtils.setEpicenter(sharedElementTransition, fromEpicenterView);
|
||||
}
|
||||
|
||||
if (exitTransition != null) {
|
||||
TransitionUtils.setEpicenter(exitTransition, fromEpicenterView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable ArrayMap<String, View> captureToSharedElements(@Nullable final View to, boolean isPush) {
|
||||
if (sharedElementNames.isEmpty() || sharedElementTransition == null || to == null) {
|
||||
sharedElementNames.clear();
|
||||
return null;
|
||||
}
|
||||
|
||||
final ArrayMap<String, View> toSharedElements = new ArrayMap<>();
|
||||
TransitionUtils.findNamedViews(toSharedElements, to);
|
||||
for (ViewParentPair removedView : removedViews) {
|
||||
toSharedElements.put(ViewCompat.getTransitionName(removedView.view), removedView.view);
|
||||
}
|
||||
|
||||
final List<String> names = new ArrayList<>(sharedElementNames.values());
|
||||
|
||||
toSharedElements.retainAll(names);
|
||||
if (enterTransitionCallback != null) {
|
||||
enterTransitionCallback.onMapSharedElements(names, toSharedElements);
|
||||
for (int i = names.size() - 1; i >= 0; i--) {
|
||||
String name = names.get(i);
|
||||
View view = toSharedElements.get(name);
|
||||
if (view == null) {
|
||||
String key = findKeyForValue(sharedElementNames, name);
|
||||
if (key != null) {
|
||||
sharedElementNames.remove(key);
|
||||
}
|
||||
} else if (!name.equals(ViewCompat.getTransitionName(view))) {
|
||||
String key = findKeyForValue(sharedElementNames, name);
|
||||
if (key != null) {
|
||||
sharedElementNames.put(key, ViewCompat.getTransitionName(view));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int i = sharedElementNames.size() - 1; i >= 0; i--) {
|
||||
final String targetName = sharedElementNames.valueAt(i);
|
||||
if (!toSharedElements.containsKey(targetName)) {
|
||||
sharedElementNames.removeAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return toSharedElements;
|
||||
}
|
||||
|
||||
@Nullable String findKeyForValue(@NonNull ArrayMap<String, String> map, @NonNull String value) {
|
||||
final int numElements = map.size();
|
||||
for (int i = 0; i < numElements; i++) {
|
||||
if (value.equals(map.valueAt(i))) {
|
||||
return map.keyAt(i);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ArrayMap<String, View> captureFromSharedElements(@NonNull View from) {
|
||||
if (sharedElementNames.isEmpty() || sharedElementTransition == null) {
|
||||
sharedElementNames.clear();
|
||||
return null;
|
||||
}
|
||||
|
||||
final ArrayMap<String, View> fromSharedElements = new ArrayMap<>();
|
||||
TransitionUtils.findNamedViews(fromSharedElements, from);
|
||||
|
||||
final List<String> names = new ArrayList<>(sharedElementNames.keySet());
|
||||
|
||||
fromSharedElements.retainAll(names);
|
||||
if (exitTransitionCallback != null) {
|
||||
exitTransitionCallback.onMapSharedElements(names, fromSharedElements);
|
||||
for (int i = names.size() - 1; i >= 0; i--) {
|
||||
String name = names.get(i);
|
||||
View view = fromSharedElements.get(name);
|
||||
if (view == null) {
|
||||
sharedElementNames.remove(name);
|
||||
} else if (!name.equals(ViewCompat.getTransitionName(view))) {
|
||||
String targetValue = sharedElementNames.remove(name);
|
||||
sharedElementNames.put(ViewCompat.getTransitionName(view), targetValue);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sharedElementNames.retainAll(fromSharedElements.keySet());
|
||||
}
|
||||
return fromSharedElements;
|
||||
}
|
||||
|
||||
void callSharedElementStartEnd(@Nullable ArrayMap<String, View> sharedElements, boolean isStart) {
|
||||
if (enterTransitionCallback != null) {
|
||||
final int count = sharedElements == null ? 0 : sharedElements.size();
|
||||
List<View> views = new ArrayList<>(count);
|
||||
List<String> names = new ArrayList<>(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
names.add(sharedElements.keyAt(i));
|
||||
views.add(sharedElements.valueAt(i));
|
||||
}
|
||||
if (isStart) {
|
||||
enterTransitionCallback.onSharedElementStart(names, views, null);
|
||||
} else {
|
||||
enterTransitionCallback.onSharedElementEnd(names, views, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void captureTransitioningViews(@NonNull List<View> transitioningViews, @NonNull View view) {
|
||||
if (view.getVisibility() == View.VISIBLE) {
|
||||
if (view instanceof ViewGroup) {
|
||||
ViewGroup viewGroup = (ViewGroup) view;
|
||||
if (ViewGroupCompat.isTransitionGroup(viewGroup)) {
|
||||
transitioningViews.add(viewGroup);
|
||||
} else {
|
||||
int count = viewGroup.getChildCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
View child = viewGroup.getChildAt(i);
|
||||
captureTransitioningViews(transitioningViews, child);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
transitioningViews.add(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleRemoveTargets(@NonNull final Transition overallTransition,
|
||||
@Nullable final Transition enterTransition, @Nullable final List<View> enteringViews,
|
||||
@Nullable final Transition exitTransition, @Nullable final List<View> exitingViews,
|
||||
@Nullable final Transition sharedElementTransition, @Nullable final List<View> toSharedElements) {
|
||||
overallTransition.addListener(new Transition.TransitionListener() {
|
||||
@Override
|
||||
public void onTransitionStart(Transition transition) {
|
||||
if (enterTransition != null && enteringViews != null) {
|
||||
TransitionUtils.replaceTargets(enterTransition, enteringViews, null);
|
||||
}
|
||||
if (exitTransition != null && exitingViews != null) {
|
||||
TransitionUtils.replaceTargets(exitTransition, exitingViews, null);
|
||||
}
|
||||
if (sharedElementTransition != null && toSharedElements != null) {
|
||||
TransitionUtils.replaceTargets(sharedElementTransition, toSharedElements, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransitionEnd(Transition transition) { }
|
||||
|
||||
@Override
|
||||
public void onTransitionCancel(Transition transition) { }
|
||||
|
||||
@Override
|
||||
public void onTransitionPause(Transition transition) { }
|
||||
|
||||
@Override
|
||||
public void onTransitionResume(Transition transition) { }
|
||||
});
|
||||
}
|
||||
|
||||
private void setNameOverrides(@NonNull final View container, @NonNull final List<View> toSharedElements) {
|
||||
OneShotPreDrawListener.add(true, container, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final int numSharedElements = toSharedElements.size();
|
||||
for (int i = 0; i < numSharedElements; i++) {
|
||||
View view = toSharedElements.get(i);
|
||||
String name = ViewCompat.getTransitionName(view);
|
||||
if (name != null) {
|
||||
String inName = findKeyForValue(sharedElementNames, name);
|
||||
ViewCompat.setTransitionName(view, inName);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void scheduleNameReset(@NonNull final ViewGroup container, @NonNull final List<View> toSharedElements) {
|
||||
OneShotPreDrawListener.add(true, container, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final int numSharedElements = toSharedElements.size();
|
||||
for (int i = 0; i < numSharedElements; i++) {
|
||||
final View view = toSharedElements.get(i);
|
||||
final String name = ViewCompat.getTransitionName(view);
|
||||
final String inName = sharedElementNames.get(name);
|
||||
ViewCompat.setTransitionName(view, inName);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be called when views are ready to have their shared elements configured. Within this method one of the addSharedElement methods
|
||||
* should be called for each shared element that will be used. If one or more of these shared elements will not instantly be available in
|
||||
* the incoming view (for ex, in a RecyclerView), waitOnSharedElementNamed can be called to delay the transition until everything is available.
|
||||
*/
|
||||
public abstract void configureSharedElements(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
|
||||
|
||||
/**
|
||||
* Should return the transition that will be used on the exiting ("from") view, if one is desired.
|
||||
*/
|
||||
@Nullable
|
||||
public abstract Transition getExitTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
|
||||
|
||||
/**
|
||||
* Should return the transition that will be used on shared elements between the from and to views.
|
||||
*/
|
||||
@Nullable
|
||||
public abstract Transition getSharedElementTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
|
||||
|
||||
/**
|
||||
* Should return the transition that will be used on the entering ("to") view, if one is desired.
|
||||
*/
|
||||
@Nullable
|
||||
public abstract Transition getEnterTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
|
||||
|
||||
/**
|
||||
* Should return a callback that can be used to customize transition behavior of the shared element transition for the "from" view.
|
||||
*/
|
||||
@Nullable
|
||||
public SharedElementCallback getExitTransitionCallback(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return a callback that can be used to customize transition behavior of the shared element transition for the "to" view.
|
||||
*/
|
||||
@Nullable
|
||||
public SharedElementCallback getEnterTransitionCallback(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return whether or not the the exit transition and enter transition should overlap. If true,
|
||||
* the enter transition will start as soon as possible. Otherwise, the enter transition will wait until the
|
||||
* completion of the exit transition. Defaults to true.
|
||||
*/
|
||||
public boolean allowTransitionOverlap(boolean isPush) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to register an element that will take part in the shared element transition.
|
||||
*
|
||||
* @param name The transition name that is used for both the entering and exiting views.
|
||||
*/
|
||||
protected final void addSharedElement(@NonNull String name) {
|
||||
sharedElementNames.put(name, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to register an element that will take part in the shared element transition. Maps the name used in the
|
||||
* "from" view to the name used in the "to" view if they are not the same.
|
||||
*
|
||||
* @param fromName The transition name used in the "from" view
|
||||
* @param toName The transition name used in the "to" view
|
||||
*/
|
||||
protected final void addSharedElement(@NonNull String fromName, @NonNull String toName) {
|
||||
sharedElementNames.put(fromName, toName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to register an element that will take part in the shared element transition. Maps the name used in the
|
||||
* "from" view to the name used in the "to" view if they are not the same.
|
||||
*
|
||||
* @param sharedElement The view from the "from" view that will take part in the shared element transition
|
||||
* @param toName The transition name used in the "to" view
|
||||
*/
|
||||
protected final void addSharedElement(@NonNull View sharedElement, @NonNull String toName) {
|
||||
String transitionName = ViewCompat.getTransitionName(sharedElement);
|
||||
if (transitionName == null) {
|
||||
throw new IllegalArgumentException("Unique transitionNames are required for all sharedElements");
|
||||
}
|
||||
sharedElementNames.put(transitionName, toName);
|
||||
}
|
||||
|
||||
/**
|
||||
* The transition will be delayed until the view with the name passed in is available in the "to" hierarchy. This is
|
||||
* particularly useful for views that don't load instantly, like RecyclerViews. Note that using this method can
|
||||
* potentially lock up your app indefinitely if the view never loads!
|
||||
*/
|
||||
protected final void waitOnSharedElementNamed(@NonNull String name) {
|
||||
if (!sharedElementNames.values().contains(name)) {
|
||||
throw new IllegalStateException("Can't wait on a shared element that hasn't been registered using addSharedElement");
|
||||
}
|
||||
waitForTransitionNames.add(name);
|
||||
}
|
||||
|
||||
private static class OneShotPreDrawListener implements OnPreDrawListener, View.OnAttachStateChangeListener {
|
||||
|
||||
private final View view;
|
||||
private ViewTreeObserver viewTreeObserver;
|
||||
private final Runnable runnable;
|
||||
private final boolean preDrawReturnValue;
|
||||
|
||||
private OneShotPreDrawListener(boolean preDrawReturnValue, @NonNull View view, @NonNull Runnable runnable) {
|
||||
this.preDrawReturnValue = preDrawReturnValue;
|
||||
this.view = view;
|
||||
viewTreeObserver = view.getViewTreeObserver();
|
||||
this.runnable = runnable;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static OneShotPreDrawListener add(boolean preDrawReturnValue, @NonNull View view, @NonNull Runnable runnable) {
|
||||
OneShotPreDrawListener listener = new OneShotPreDrawListener(preDrawReturnValue, view, runnable);
|
||||
view.getViewTreeObserver().addOnPreDrawListener(listener);
|
||||
view.addOnAttachStateChangeListener(listener);
|
||||
return listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreDraw() {
|
||||
removeListener();
|
||||
runnable.run();
|
||||
return preDrawReturnValue;
|
||||
}
|
||||
|
||||
private void removeListener() {
|
||||
if (viewTreeObserver.isAlive()) {
|
||||
viewTreeObserver.removeOnPreDrawListener(this);
|
||||
} else {
|
||||
view.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
}
|
||||
view.removeOnAttachStateChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAttachedToWindow(View v) {
|
||||
viewTreeObserver = v.getViewTreeObserver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(View v) {
|
||||
removeListener();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class ViewParentPair {
|
||||
@NonNull final View view;
|
||||
@NonNull final ViewGroup parent;
|
||||
|
||||
ViewParentPair(@NonNull View view, ViewGroup parent) {
|
||||
this.view = view;
|
||||
this.parent = parent;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package com.bluelinelabs.conductor.changehandler.androidxtransition;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.transition.Transition;
|
||||
import androidx.transition.TransitionManager;
|
||||
|
||||
import com.bluelinelabs.conductor.Controller;
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler;
|
||||
|
||||
/**
|
||||
* A base {@link ControllerChangeHandler} that facilitates using {@link Transition}s to replace Controller Views.
|
||||
* <p/>
|
||||
* Note that this class uses the <b>androidx</b> {@link Transition}. If you're using Android's platform transitions,
|
||||
* consider using the {@code TransitionChangeHandler} provided by the {@code android-transitions} Conductor module.
|
||||
*/
|
||||
public abstract class TransitionChangeHandler extends ControllerChangeHandler {
|
||||
|
||||
public interface OnTransitionPreparedListener {
|
||||
void onPrepared();
|
||||
}
|
||||
|
||||
boolean canceled;
|
||||
private boolean needsImmediateCompletion;
|
||||
|
||||
/**
|
||||
* Should be overridden to return the Transition to use while replacing Views.
|
||||
*
|
||||
* @param container The container these Views are hosted in
|
||||
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
|
||||
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
|
||||
* @param isPush True if this is a push transaction, false if it's a pop
|
||||
*/
|
||||
@NonNull
|
||||
protected abstract Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
|
||||
|
||||
@Override
|
||||
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
|
||||
super.onAbortPush(newHandler, newTop);
|
||||
|
||||
canceled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void completeImmediately() {
|
||||
super.completeImmediately();
|
||||
|
||||
needsImmediateCompletion = true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ControllerChangeCompletedListener listener;
|
||||
|
||||
@Override
|
||||
public void performChange(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
|
||||
listener = changeListener;
|
||||
if (canceled) {
|
||||
changeListener.onChangeCompleted();
|
||||
return;
|
||||
}
|
||||
if (needsImmediateCompletion) {
|
||||
executePropertyChanges(container, from, to, null, isPush);
|
||||
changeListener.onChangeCompleted();
|
||||
return;
|
||||
}
|
||||
|
||||
final Runnable onTransitionNotStarted = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
changeListener.onChangeCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
final Transition transition = getTransition(container, from, to, isPush);
|
||||
transition.addListener(new Transition.TransitionListener() {
|
||||
@Override
|
||||
public void onTransitionStart(Transition transition) {
|
||||
container.removeCallbacks(onTransitionNotStarted);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransitionEnd(Transition transition) {
|
||||
listener.onChangeCompleted();
|
||||
listener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransitionCancel(Transition transition) {
|
||||
listener.onChangeCompleted();
|
||||
listener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransitionPause(Transition transition) { }
|
||||
|
||||
@Override
|
||||
public void onTransitionResume(Transition transition) { }
|
||||
});
|
||||
|
||||
prepareForTransition(container, from, to, transition, isPush, new OnTransitionPreparedListener() {
|
||||
@Override
|
||||
public void onPrepared() {
|
||||
if (!canceled) {
|
||||
TransitionManager.beginDelayedTransition(container, transition);
|
||||
executePropertyChanges(container, from, to, transition, isPush);
|
||||
container.post(onTransitionNotStarted);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getRemovesFromViewOnPush() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before a transition occurs. This can be used to reorder views, set their transition names, etc. The transition will begin
|
||||
* when {@code onTransitionPreparedListener} is called.
|
||||
*
|
||||
* @param container The container these Views are hosted in
|
||||
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
|
||||
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
|
||||
* @param transition The transition that is being prepared for
|
||||
* @param isPush True if this is a push transaction, false if it's a pop
|
||||
*/
|
||||
public void prepareForTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @NonNull Transition transition, boolean isPush, @NonNull OnTransitionPreparedListener onTransitionPreparedListener) {
|
||||
onTransitionPreparedListener.onPrepared();
|
||||
}
|
||||
|
||||
/**
|
||||
* This should set all view properties needed for the transition to work properly. By default it removes the "from" view
|
||||
* and adds the "to" view.
|
||||
*
|
||||
* @param container The container these Views are hosted in
|
||||
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
|
||||
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
|
||||
* @param transition The transition with which {@code TransitionManager.beginDelayedTransition} has been called. This will be null only if another ControllerChangeHandler immediately overrides this one.
|
||||
* @param isPush True if this is a push transaction, false if it's a pop
|
||||
*/
|
||||
public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @Nullable Transition transition, boolean isPush) {
|
||||
if (from != null && (getRemovesFromViewOnPush() || !isPush) && from.getParent() == container) {
|
||||
container.removeView(from);
|
||||
}
|
||||
if (to != null && to.getParent() == null) {
|
||||
container.addView(to);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
package com.bluelinelabs.conductor.internal;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.transition.Transition;
|
||||
import androidx.transition.TransitionSet;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class TransitionUtils {
|
||||
|
||||
public static void findNamedViews(@NonNull Map<String, View> namedViews, View view) {
|
||||
if (view.getVisibility() == View.VISIBLE) {
|
||||
String transitionName = ViewCompat.getTransitionName(view);
|
||||
if (transitionName != null) {
|
||||
namedViews.put(transitionName, view);
|
||||
}
|
||||
|
||||
if (view instanceof ViewGroup) {
|
||||
ViewGroup viewGroup = (ViewGroup) view;
|
||||
int childCount = viewGroup.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
View child = viewGroup.getChildAt(i);
|
||||
findNamedViews(namedViews, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static View findNamedView(@NonNull View view, @NonNull String transitionName) {
|
||||
if (transitionName.equals(ViewCompat.getTransitionName(view))) {
|
||||
return view;
|
||||
}
|
||||
|
||||
if (view instanceof ViewGroup) {
|
||||
ViewGroup viewGroup = (ViewGroup) view;
|
||||
int childCount = viewGroup.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
View viewWithTransitionName = findNamedView(viewGroup.getChildAt(i), transitionName);
|
||||
if (viewWithTransitionName != null) {
|
||||
return viewWithTransitionName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void setEpicenter(@NonNull Transition transition, @Nullable View view) {
|
||||
if (view != null) {
|
||||
final Rect epicenter = new Rect();
|
||||
getBoundsOnScreen(view, epicenter);
|
||||
transition.setEpicenterCallback(new Transition.EpicenterCallback() {
|
||||
@Override
|
||||
public Rect onGetEpicenter(Transition transition) {
|
||||
return epicenter;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void getBoundsOnScreen(@NonNull View view, @NonNull Rect epicenter) {
|
||||
int[] loc = new int[2];
|
||||
view.getLocationOnScreen(loc);
|
||||
epicenter.set(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight());
|
||||
}
|
||||
|
||||
public static void setTargets(@NonNull Transition transition, @NonNull View nonExistentView, @NonNull List<View> sharedViews) {
|
||||
final List<View> views = transition.getTargets();
|
||||
views.clear();
|
||||
final int count = sharedViews.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final View view = sharedViews.get(i);
|
||||
bfsAddViewChildren(views, view);
|
||||
}
|
||||
views.add(nonExistentView);
|
||||
sharedViews.add(nonExistentView);
|
||||
addTargets(transition, sharedViews);
|
||||
}
|
||||
|
||||
public static void addTargets(@Nullable Transition transition, @NonNull List<View> views) {
|
||||
if (transition == null) {
|
||||
return;
|
||||
}
|
||||
if (transition instanceof TransitionSet) {
|
||||
TransitionSet set = (TransitionSet) transition;
|
||||
int numTransitions = set.getTransitionCount();
|
||||
for (int i = 0; i < numTransitions; i++) {
|
||||
Transition child = set.getTransitionAt(i);
|
||||
addTargets(child, views);
|
||||
}
|
||||
} else if (!hasSimpleTarget(transition)) {
|
||||
List<View> targets = transition.getTargets();
|
||||
if (isNullOrEmpty(targets)) {
|
||||
int numViews = views.size();
|
||||
for (int i = 0; i < numViews; i++) {
|
||||
transition.addTarget(views.get(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void replaceTargets(@NonNull Transition transition, @NonNull List<View> oldTargets, @Nullable List<View> newTargets) {
|
||||
if (transition instanceof TransitionSet) {
|
||||
TransitionSet set = (TransitionSet) transition;
|
||||
int numTransitions = set.getTransitionCount();
|
||||
for (int i = 0; i < numTransitions; i++) {
|
||||
Transition child = set.getTransitionAt(i);
|
||||
replaceTargets(child, oldTargets, newTargets);
|
||||
}
|
||||
} else if (!TransitionUtils.hasSimpleTarget(transition)) {
|
||||
List<View> targets = transition.getTargets();
|
||||
if (targets != null && targets.size() == oldTargets.size() && targets.containsAll(oldTargets)) {
|
||||
final int targetCount = newTargets == null ? 0 : newTargets.size();
|
||||
for (int i = 0; i < targetCount; i++) {
|
||||
transition.addTarget(newTargets.get(i));
|
||||
}
|
||||
for (int i = oldTargets.size() - 1; i >= 0; i--) {
|
||||
transition.removeTarget(oldTargets.get(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void bfsAddViewChildren(@NonNull final List<View> views, @NonNull final View startView) {
|
||||
final int startIndex = views.size();
|
||||
if (containedBeforeIndex(views, startView, startIndex)) {
|
||||
return; // This child is already in the list, so all its children are also.
|
||||
}
|
||||
views.add(startView);
|
||||
for (int index = startIndex; index < views.size(); index++) {
|
||||
final View view = views.get(index);
|
||||
if (view instanceof ViewGroup) {
|
||||
ViewGroup viewGroup = (ViewGroup) view;
|
||||
final int childCount = viewGroup.getChildCount();
|
||||
for (int childIndex = 0; childIndex < childCount; childIndex++) {
|
||||
final View child = viewGroup.getChildAt(childIndex);
|
||||
if (!containedBeforeIndex(views, child, startIndex)) {
|
||||
views.add(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean containedBeforeIndex(@NonNull List<View> views, View view, int maxIndex) {
|
||||
for (int i = 0; i < maxIndex; i++) {
|
||||
if (views.get(i) == view) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean hasSimpleTarget(@NonNull Transition transition) {
|
||||
return !isNullOrEmpty(transition.getTargetIds())
|
||||
|| !isNullOrEmpty(transition.getTargetNames())
|
||||
|| !isNullOrEmpty(transition.getTargetTypes());
|
||||
}
|
||||
|
||||
private static boolean isNullOrEmpty(@Nullable List list) {
|
||||
return list == null || list.isEmpty();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static TransitionSet mergeTransitions(int ordering, Transition... transitions) {
|
||||
TransitionSet transitionSet = new TransitionSet();
|
||||
for (Transition transition : transitions) {
|
||||
if (transition != null) {
|
||||
transitionSet.addTransition(transition);
|
||||
}
|
||||
}
|
||||
transitionSet.setOrdering(ordering);
|
||||
return transitionSet;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
alias(libs.plugins.mvnpublish)
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk libs.versions.compilesdk.get() as Integer
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion libs.versions.minsdk.get()
|
||||
targetSdkVersion libs.versions.targetsdk.get()
|
||||
versionCode Integer.parseInt(project.VERSION_CODE)
|
||||
versionName project.VERSION_NAME
|
||||
}
|
||||
|
||||
namespace "com.bluelinelabs.conductor.viewpager"
|
||||
}
|
||||
|
||||
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
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
package com.bluelinelabs.conductor.viewpager;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
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 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 final SparseArray<Router> visibleRouters = new SparseArray<>();
|
||||
private ArrayList<Integer> savedPageHistory = new ArrayList<>();
|
||||
private Router currentPrimaryRouter;
|
||||
|
||||
/**
|
||||
* Creates a new RouterPagerAdapter using the passed host.
|
||||
*/
|
||||
public RouterPagerAdapter(@NonNull Controller host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public abstract void configureRouter(@NonNull Router router, int position);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public void setMaxPagesToStateSave(int maxPagesToStateSave) {
|
||||
if (maxPagesToStateSave < 0) {
|
||||
throw new IllegalArgumentException("Only positive integers may be passed for maxPagesToStateSave.");
|
||||
}
|
||||
|
||||
this.maxPagesToStateSave = maxPagesToStateSave;
|
||||
|
||||
ensurePagesSaved();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object instantiateItem(ViewGroup container, int position) {
|
||||
final String name = makeRouterName(container.getId(), getItemId(position));
|
||||
|
||||
// Ensure we don't try to restore state for a router with a different ID just because
|
||||
// the position was reused. Fixes https://github.com/bluelinelabs/Conductor/issues/582
|
||||
if (tags.get(position) != null && !tags.get(position).equals(name)) {
|
||||
savedPages.remove(position);
|
||||
}
|
||||
|
||||
Router router = host.getChildRouter(container, name)
|
||||
.setPopRootControllerMode(Router.PopRootControllerMode.NEVER);
|
||||
if (!router.hasRootController()) {
|
||||
Bundle routerSavedState = savedPages.get(position);
|
||||
|
||||
if (routerSavedState != null) {
|
||||
router.restoreInstanceState(routerSavedState);
|
||||
savedPages.remove(position);
|
||||
savedPageHistory.remove((Integer) position);
|
||||
}
|
||||
}
|
||||
|
||||
router.rebindIfNeeded();
|
||||
configureRouter(router, position);
|
||||
|
||||
if (router != currentPrimaryRouter) {
|
||||
for (RouterTransaction transaction : router.getBackstack()) {
|
||||
transaction.controller().setOptionsMenuHidden(true);
|
||||
}
|
||||
}
|
||||
|
||||
tags.put(position, name);
|
||||
visibleRouters.put(position, router);
|
||||
return router;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
Router router = (Router)object;
|
||||
|
||||
Bundle savedState = new Bundle();
|
||||
router.saveInstanceState(savedState);
|
||||
savedPages.put(position, savedState);
|
||||
|
||||
savedPageHistory.remove((Integer) position);
|
||||
savedPageHistory.add(position);
|
||||
|
||||
ensurePagesSaved();
|
||||
|
||||
host.removeChildRouter(router);
|
||||
|
||||
visibleRouters.remove(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
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()) {
|
||||
transaction.controller().setOptionsMenuHidden(true);
|
||||
}
|
||||
}
|
||||
if (router != null) {
|
||||
for (RouterTransaction transaction : router.getBackstack()) {
|
||||
transaction.controller().setOptionsMenuHidden(false);
|
||||
}
|
||||
}
|
||||
currentPrimaryRouter = router;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
||||
Router router = (Router)object;
|
||||
final List<RouterTransaction> backstack = router.getBackstack();
|
||||
for (RouterTransaction transaction : backstack) {
|
||||
if (transaction.controller().getView() == view) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreState(Parcelable state, ClassLoader loader) {
|
||||
Bundle bundle = (Bundle)state;
|
||||
if (state != null) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the already instantiated Router in the specified position or {@code null} if there
|
||||
* is no router associated with this position.
|
||||
*/
|
||||
@Nullable
|
||||
public Router getRouter(int position) {
|
||||
return visibleRouters.get(position);
|
||||
}
|
||||
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
SparseArray<Bundle> getSavedPages() {
|
||||
return savedPages;
|
||||
}
|
||||
|
||||
private void ensurePagesSaved() {
|
||||
while (savedPages.size() > maxPagesToStateSave) {
|
||||
int positionToRemove = savedPageHistory.remove(0);
|
||||
savedPages.remove(positionToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
private static String makeRouterName(int viewId, long id) {
|
||||
return viewId + ":" + id;
|
||||
}
|
||||
|
||||
}
|
||||
+78
@@ -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))
|
||||
}
|
||||
}
|
||||
+85
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
id("kotlin-parcelize")
|
||||
alias(libs.plugins.mvnpublish)
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk libs.versions.compilesdk.get() as Integer
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
namespace "com.bluelinelabs.conductor.viewpager2"
|
||||
}
|
||||
|
||||
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
|
||||
+256
@@ -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
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.bluelinelabs.conductor.viewpager2
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.bluelinelabs.conductor.ChangeHandlerFrameLayout
|
||||
import com.bluelinelabs.conductor.Router
|
||||
|
||||
class RouterViewHolder private constructor(val container: ChangeHandlerFrameLayout) : ViewHolder(container) {
|
||||
var currentRouter: Router? = null
|
||||
var currentItemPosition = 0
|
||||
var currentItemId = 0L
|
||||
var attached = false
|
||||
|
||||
companion object {
|
||||
operator fun invoke(parent: ViewGroup): RouterViewHolder {
|
||||
val container = ChangeHandlerFrameLayout(parent.context)
|
||||
container.id = ViewCompat.generateViewId()
|
||||
container.layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
container.isSaveEnabled = false
|
||||
return RouterViewHolder(container)
|
||||
}
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package com.bluelinelabs.conductor.viewpager2
|
||||
|
||||
import android.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())
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.bluelinelabs.conductor.viewpager2.util;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bluelinelabs.conductor.Controller;
|
||||
|
||||
public class TestController extends Controller {
|
||||
|
||||
@Override @NonNull
|
||||
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) {
|
||||
return new FrameLayout(inflater.getContext());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode rootProject.ext.versionCode
|
||||
versionName rootProject.ext.versionName
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile rootProject.ext.rxJava
|
||||
compile rootProject.ext.rxAndroid
|
||||
compile rootProject.ext.rxLifecycle
|
||||
|
||||
compile project(':conductor')
|
||||
}
|
||||
|
||||
ext.artifactId = 'conductor-rxlifecycle'
|
||||
|
||||
apply from: rootProject.file('dependencies.gradle')
|
||||
apply from: rootProject.file('bll-gradle-push.gradle')
|
||||
@@ -1,3 +0,0 @@
|
||||
<manifest package="com.bluelinelabs.conductor.rxlifecycle">
|
||||
<application />
|
||||
</manifest>
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
package com.bluelinelabs.conductor.rxlifecycle;
|
||||
|
||||
public enum ControllerEvent {
|
||||
|
||||
CREATE,
|
||||
ATTACH,
|
||||
BIND_VIEW,
|
||||
DETACH,
|
||||
UNBIND_VIEW,
|
||||
DESTROY
|
||||
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
package com.bluelinelabs.conductor.rxlifecycle;
|
||||
|
||||
import android.support.annotation.CheckResult;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import rx.Observable;
|
||||
|
||||
/**
|
||||
* Interface used for RxController. Can also be used if writing your own Controller component without subclassing RxController.
|
||||
*/
|
||||
public interface ControllerLifecycleProvider {
|
||||
|
||||
/**
|
||||
* @return An observable that will have all {@link com.bluelinelabs.conductor.Controller} lifecycle events
|
||||
*/
|
||||
@NonNull
|
||||
@CheckResult
|
||||
Observable<ControllerEvent> lifecycle();
|
||||
|
||||
/**
|
||||
* Will bind the source until a specific {@link ControllerEvent} occurs.
|
||||
*
|
||||
* @param event The {@link ControllerEvent} that should cause onComplete to be called
|
||||
* @return A {@link rx.Observable.Transformer} that will call onComplete when the event occurs.
|
||||
*/
|
||||
@NonNull
|
||||
@CheckResult
|
||||
<T> Observable.Transformer<T, T> bindUntilEvent(@NonNull ControllerEvent event);
|
||||
|
||||
/**
|
||||
* Will bind the source until the next reasonable {@link ControllerEvent} occurs.
|
||||
* @return A {@link rx.Observable.Transformer} that will call onComplete when the event occurs.
|
||||
*/
|
||||
@NonNull
|
||||
@CheckResult
|
||||
<T> Observable.Transformer<T, T> bindToLifecycle();
|
||||
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
package com.bluelinelabs.conductor.rxlifecycle;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.View;
|
||||
|
||||
import com.bluelinelabs.conductor.Controller;
|
||||
import com.bluelinelabs.conductor.Controller.LifecycleListener;
|
||||
|
||||
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) {
|
||||
final BehaviorSubject<ControllerEvent> subject = BehaviorSubject.create(ControllerEvent.CREATE);
|
||||
|
||||
controller.addLifecycleListener(new LifecycleListener() {
|
||||
@Override
|
||||
public void preBindView(@NonNull Controller controller, @NonNull View view) {
|
||||
subject.onNext(ControllerEvent.BIND_VIEW);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preAttach(@NonNull Controller controller, @NonNull View view) {
|
||||
subject.onNext(ControllerEvent.ATTACH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preUnbindView(@NonNull Controller controller, @NonNull View view) {
|
||||
subject.onNext(ControllerEvent.UNBIND_VIEW);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preDetach(@NonNull Controller controller, @NonNull View view) {
|
||||
subject.onNext(ControllerEvent.DETACH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preDestroy(@NonNull Controller controller) {
|
||||
subject.onNext(ControllerEvent.DESTROY);
|
||||
}
|
||||
});
|
||||
|
||||
return subject;
|
||||
}
|
||||
|
||||
}
|
||||
-50
@@ -1,50 +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.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 ControllerLifecycleProvider {
|
||||
|
||||
private final BehaviorSubject<ControllerEvent> mLifecycleSubject;
|
||||
|
||||
public RxController() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public RxController(Bundle args) {
|
||||
super(args);
|
||||
mLifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
@CheckResult
|
||||
public final Observable<ControllerEvent> lifecycle() {
|
||||
return mLifecycleSubject.asObservable();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
@CheckResult
|
||||
public final <T> Observable.Transformer<T, T> bindUntilEvent(@NonNull ControllerEvent event) {
|
||||
return RxLifecycle.bindUntilEvent(mLifecycleSubject, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
@CheckResult
|
||||
public final <T> Observable.Transformer<T, T> bindToLifecycle() {
|
||||
return RxControllerLifecycle.bindController(mLifecycleSubject);
|
||||
}
|
||||
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
package com.bluelinelabs.conductor.rxlifecycle;
|
||||
|
||||
import android.support.annotation.CheckResult;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
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.RxLifecycle#bindFragment(Observable)}.
|
||||
*
|
||||
* @param lifecycle the lifecycle sequence of a Controller
|
||||
* @return a reusable {@link Observable.Transformer} that unsubscribes the source during the Controller lifecycle
|
||||
*/
|
||||
@NonNull
|
||||
@CheckResult
|
||||
public static <T> Observable.Transformer<T, 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 ATTACH:
|
||||
return ControllerEvent.DETACH;
|
||||
case BIND_VIEW:
|
||||
return ControllerEvent.UNBIND_VIEW;
|
||||
case DETACH:
|
||||
return ControllerEvent.DESTROY;
|
||||
default:
|
||||
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode rootProject.ext.versionCode
|
||||
versionName rootProject.ext.versionName
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile rootProject.ext.supportAppCompat
|
||||
compile project(':conductor')
|
||||
}
|
||||
|
||||
ext.artifactId = 'conductor-support'
|
||||
|
||||
apply from: rootProject.file('dependencies.gradle')
|
||||
apply from: rootProject.file('bll-gradle-push.gradle')
|
||||
@@ -1,3 +0,0 @@
|
||||
<manifest package="com.bluelinelabs.conductor.support">
|
||||
<application />
|
||||
</manifest>
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
package com.bluelinelabs.conductor.support;
|
||||
|
||||
import android.support.v4.view.PagerAdapter;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.bluelinelabs.conductor.ChildControllerTransaction;
|
||||
import com.bluelinelabs.conductor.Controller;
|
||||
|
||||
/**
|
||||
* An adapter for ViewPagers that will handle adding and removing Controllers
|
||||
*/
|
||||
public abstract class ControllerPagerAdapter extends PagerAdapter {
|
||||
|
||||
private final Controller mHost;
|
||||
|
||||
/**
|
||||
* Creates a new ControllerPagerAdapter using the passed host.
|
||||
*/
|
||||
public ControllerPagerAdapter(Controller host) {
|
||||
mHost = host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Controller associated with a specified position.
|
||||
*/
|
||||
public abstract Controller getItem(int position);
|
||||
|
||||
@Override
|
||||
public Object instantiateItem(ViewGroup container, int position) {
|
||||
final String name = makeControllerName(container.getId(), getItemId(position));
|
||||
|
||||
Controller controller = mHost.getChildController(name);
|
||||
if (controller == null) {
|
||||
controller = getItem(position);
|
||||
|
||||
mHost.addChildController(ChildControllerTransaction.builder(controller, container.getId())
|
||||
.tag(name)
|
||||
.build());
|
||||
}
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||
mHost.removeChildController((Controller)object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewFromObject(View view, Object object) {
|
||||
return ((Controller)object).getView() == view;
|
||||
}
|
||||
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
private static String makeControllerName(int viewId, long id) {
|
||||
return viewId + ":" + id;
|
||||
}
|
||||
|
||||
}
|
||||
Executable → Regular
+32
-19
@@ -1,34 +1,47 @@
|
||||
apply plugin: 'com.android.library'
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
id("kotlin-parcelize")
|
||||
alias(libs.plugins.mvnpublish)
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
compileSdk libs.versions.compilesdk.get() as Integer
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode rootProject.ext.versionCode
|
||||
versionName rootProject.ext.versionName
|
||||
minSdkVersion libs.versions.minsdk.get()
|
||||
targetSdkVersion libs.versions.targetsdk.get()
|
||||
versionCode Integer.parseInt(project.VERSION_CODE)
|
||||
versionName project.VERSION_NAME
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
||||
namespace "com.bluelinelabs.conductor"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testCompile rootProject.ext.junit
|
||||
testCompile 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
|
||||
|
||||
compile rootProject.ext.supportAnnotations
|
||||
api libs.androidx.lifecycle.runtime
|
||||
|
||||
api libs.androidx.annotation
|
||||
api libs.kotlin.stdlib
|
||||
|
||||
lintPublish project(':conductor-lint')
|
||||
}
|
||||
|
||||
ext.artifactId = 'conductor'
|
||||
|
||||
apply from: rootProject.file('dependencies.gradle')
|
||||
apply from: rootProject.file('bll-gradle-push.gradle')
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
POM_NAME=Conductor
|
||||
POM_ARTIFACT_ID=conductor
|
||||
POM_PACKAGING=aar
|
||||
@@ -0,0 +1,8 @@
|
||||
# Retain constructor that is called by using reflection to recreate the Controller
|
||||
-keepclassmembers public class * extends com.bluelinelabs.conductor.Controller {
|
||||
public <init>();
|
||||
public <init>(android.os.Bundle);
|
||||
}
|
||||
-keepclassmembers public class * extends com.bluelinelabs.conductor.ControllerChangeHandler {
|
||||
public <init>();
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
<manifest package="com.bluelinelabs.conductor">
|
||||
<application />
|
||||
</manifest>
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.bluelinelabs.conductor;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentSender;
|
||||
import android.content.IntentSender.SendIntentException;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
|
||||
import com.bluelinelabs.conductor.internal.LifecycleHandler;
|
||||
import com.bluelinelabs.conductor.internal.TransactionIndexer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
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) {
|
||||
removeChangeListener((ControllerChangeListener)this.container);
|
||||
}
|
||||
|
||||
if (container instanceof ControllerChangeListener) {
|
||||
addChangeListener((ControllerChangeListener)container);
|
||||
}
|
||||
|
||||
this.lifecycleHandler = lifecycleHandler;
|
||||
this.container = container;
|
||||
|
||||
watchContainerAttach();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveInstanceState(@NonNull Bundle outState) {
|
||||
super.saveInstanceState(outState);
|
||||
|
||||
transactionIndexer.saveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
super.restoreInstanceState(savedInstanceState);
|
||||
|
||||
transactionIndexer.restoreInstanceState(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override @Nullable
|
||||
public Activity getActivity() {
|
||||
return lifecycleHandler != null ? lifecycleHandler.getLifecycleActivity() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(@NonNull Activity activity, boolean isConfigurationChange) {
|
||||
super.onActivityDestroyed(activity, isConfigurationChange);
|
||||
|
||||
if (!isConfigurationChange) {
|
||||
lifecycleHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void invalidateOptionsMenu() {
|
||||
if (lifecycleHandler != null && getActivity() != null) {
|
||||
getActivity().invalidateOptionsMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
lifecycleHandler.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
void startActivity(@NonNull Intent intent) {
|
||||
lifecycleHandler.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) {
|
||||
lifecycleHandler.startActivityForResult(instanceId, intent, requestCode, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options) {
|
||||
lifecycleHandler.startActivityForResult(instanceId, intent, requestCode, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
void startIntentSenderForResult(@NonNull String instanceId, @NonNull IntentSender intent, int requestCode, @Nullable Intent fillInIntent,
|
||||
int flagsMask, int flagsValues, int extraFlags, @Nullable Bundle options) throws SendIntentException {
|
||||
lifecycleHandler.startIntentSenderForResult(instanceId, intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
void registerForActivityResult(@NonNull String instanceId, int requestCode) {
|
||||
lifecycleHandler.registerForActivityResult(instanceId, requestCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
void unregisterForActivityResults(@NonNull String instanceId) {
|
||||
lifecycleHandler.unregisterForActivityResults(instanceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
|
||||
lifecycleHandler.requestPermissions(instanceId, permissions, requestCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean hasHost() {
|
||||
return lifecycleHandler != null;
|
||||
}
|
||||
|
||||
@Override @NonNull
|
||||
List<Router> getSiblingRouters() {
|
||||
return lifecycleHandler.getRouters();
|
||||
}
|
||||
|
||||
@Override @NonNull
|
||||
Router getRootRouter() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override @NonNull
|
||||
TransactionIndexer getTransactionIndexer() {
|
||||
return transactionIndexer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContextAvailable() {
|
||||
super.onContextAvailable();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package com.bluelinelabs.conductor;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
class Backstack implements Iterable<RouterTransaction> {
|
||||
|
||||
private static final String KEY_ENTRIES = "Backstack.entries";
|
||||
|
||||
private final ArrayDeque<RouterTransaction> mBackStack = new ArrayDeque<>();
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean isEmpty() {
|
||||
return mBackStack.isEmpty();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return mBackStack.size();
|
||||
}
|
||||
|
||||
public RouterTransaction root() {
|
||||
return mBackStack.size() > 0 ? mBackStack.getLast() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<RouterTransaction> iterator() {
|
||||
return mBackStack.iterator();
|
||||
}
|
||||
|
||||
public Iterator<RouterTransaction> reverseIterator() {
|
||||
return mBackStack.descendingIterator();
|
||||
}
|
||||
|
||||
public List<RouterTransaction> popTo(RouterTransaction transaction) {
|
||||
List<RouterTransaction> popped = new ArrayList<>();
|
||||
if (mBackStack.contains(transaction)) {
|
||||
while (mBackStack.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;
|
||||
}
|
||||
|
||||
public RouterTransaction pop() {
|
||||
RouterTransaction popped = mBackStack.pop();
|
||||
popped.getController().destroy();
|
||||
return popped;
|
||||
}
|
||||
|
||||
public RouterTransaction peek() {
|
||||
return mBackStack.peek();
|
||||
}
|
||||
|
||||
public void remove(RouterTransaction transaction) {
|
||||
mBackStack.removeFirstOccurrence(transaction);
|
||||
}
|
||||
|
||||
public void push(RouterTransaction transaction) {
|
||||
mBackStack.push(transaction);
|
||||
}
|
||||
|
||||
public void popAll() {
|
||||
while (!isEmpty()) {
|
||||
pop();
|
||||
}
|
||||
}
|
||||
|
||||
public void saveInstanceState(Bundle outState) {
|
||||
ArrayList<Bundle> entryBundles = new ArrayList<>(mBackStack.size());
|
||||
for (RouterTransaction entry : mBackStack) {
|
||||
entryBundles.add(entry.toBundle());
|
||||
}
|
||||
|
||||
outState.putParcelableArrayList(KEY_ENTRIES, entryBundles);
|
||||
}
|
||||
|
||||
public void restoreInstanceState(Bundle savedInstanceState) {
|
||||
ArrayList<Bundle> entryBundles = savedInstanceState.getParcelableArrayList(KEY_ENTRIES);
|
||||
if (entryBundles != null) {
|
||||
Collections.reverse(entryBundles);
|
||||
for (Bundle transactionBundle : entryBundles) {
|
||||
mBackStack.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,53 +0,0 @@
|
||||
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
|
||||
* {@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 mInProgressTransactionCount;
|
||||
|
||||
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 (mInProgressTransactionCount > 0) || super.onInterceptTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChangeStarted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler) {
|
||||
mInProgressTransactionCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChangeCompleted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler) {
|
||||
mInProgressTransactionCount--;
|
||||
}
|
||||
}
|
||||
@@ -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,79 +0,0 @@
|
||||
package com.bluelinelabs.conductor;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.IdRes;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* A {@link ControllerTransaction} implementation used for adding child {@link Controller}s.
|
||||
*/
|
||||
public class ChildControllerTransaction extends ControllerTransaction {
|
||||
|
||||
private static final String KEY_CONTAINER_ID = "ChildControllerTransaction.containerId";
|
||||
private static final String KEY_ADD_TO_LOCAL_BACKSTACK = "ChildControllerTransaction.addToLocalBackstack";
|
||||
|
||||
/** The ID of the ViewGroup that the child {@link Controller} will be added to */
|
||||
public final int containerId;
|
||||
|
||||
/** If true, the hosting {@link Controller} will be responsible for reversing this transaction if the user presses the back button */
|
||||
public final boolean addToLocalBackstack;
|
||||
|
||||
ChildControllerTransaction(Builder builder) {
|
||||
super(builder);
|
||||
containerId = builder.containerId;
|
||||
addToLocalBackstack = builder.addToLocalBackstack;
|
||||
}
|
||||
|
||||
ChildControllerTransaction(@NonNull Bundle bundle) {
|
||||
super(bundle);
|
||||
containerId = bundle.getInt(KEY_CONTAINER_ID);
|
||||
addToLocalBackstack = bundle.getBoolean(KEY_ADD_TO_LOCAL_BACKSTACK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle toBundle() {
|
||||
Bundle bundle = super.toBundle();
|
||||
bundle.putInt(KEY_CONTAINER_ID, containerId);
|
||||
bundle.putBoolean(KEY_ADD_TO_LOCAL_BACKSTACK, addToLocalBackstack);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Builder
|
||||
*
|
||||
* @param controller The Controller to add as a child
|
||||
* @param containerId The ID of the ViewGroup to which the controller's view should be added
|
||||
*/
|
||||
public static Builder builder(@NonNull Controller controller, @IdRes int containerId) {
|
||||
return new Builder(controller, containerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link ControllerTransaction.Builder} implementation used for adding child {@link Controller}s.
|
||||
*/
|
||||
public static class Builder extends ControllerTransaction.Builder<Builder> {
|
||||
|
||||
@IdRes final int containerId;
|
||||
|
||||
boolean addToLocalBackstack;
|
||||
|
||||
Builder(@NonNull Controller controller, @IdRes int containerId) {
|
||||
super(controller);
|
||||
this.containerId = containerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, the hosting {@link Controller} will be responsible for reversing this transaction if the user presses the back button.
|
||||
*/
|
||||
public Builder addToLocalBackstack(boolean addToLocalBackstack) {
|
||||
this.addToLocalBackstack = addToLocalBackstack;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Creates the transaction */
|
||||
public ChildControllerTransaction build() {
|
||||
return new ChildControllerTransaction(this);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.bluelinelabs.conductor;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.bluelinelabs.conductor.internal.LifecycleHandler;
|
||||
|
||||
/**
|
||||
* Point of initial interaction with Conductor. Used to attach a {@link Router} to your Activity.
|
||||
*/
|
||||
public class 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.
|
||||
*/
|
||||
public static Router attachRouter(@NonNull Activity activity, @NonNull ViewGroup container, Bundle savedInstanceState) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
Executable → Regular
+1151
-403
File diff suppressed because it is too large
Load Diff
@@ -1,172 +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 com.bluelinelabs.conductor.ControllerTransaction.ControllerChangeType;
|
||||
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
|
||||
import com.bluelinelabs.conductor.util.ClassUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 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";
|
||||
|
||||
/**
|
||||
* 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, if any.
|
||||
* @param to The next View that should be put in the container, if any.
|
||||
* @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, View from, 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) { }
|
||||
|
||||
final Bundle toBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(KEY_CLASS_NAME, getClass().getCanonicalName());
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public static void executeChange(final Controller to, final Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler inHandler) {
|
||||
executeChange(to, from, isPush, container, inHandler, new ArrayList<ControllerChangeListener>());
|
||||
}
|
||||
|
||||
public static void executeChange(final Controller to, final Controller from, final boolean isPush, final ViewGroup container, final ControllerChangeHandler inHandler, @NonNull final List<ControllerChangeListener> listeners) {
|
||||
if (container != null) {
|
||||
for (ControllerChangeListener listener : listeners) {
|
||||
listener.onChangeStarted(to, from, isPush, container, inHandler);
|
||||
}
|
||||
|
||||
final ControllerChangeType toChangeType = isPush ? ControllerChangeType.PUSH_ENTER : ControllerChangeType.POP_ENTER;
|
||||
final ControllerChangeType fromChangeType = isPush ? ControllerChangeType.PUSH_EXIT : ControllerChangeType.POP_EXIT;
|
||||
|
||||
final ControllerChangeHandler handler = inHandler != null ? inHandler : new SimpleSwapChangeHandler();
|
||||
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) {
|
||||
to.changeEnded(handler, toChangeType);
|
||||
}
|
||||
|
||||
for (ControllerChangeListener listener : listeners) {
|
||||
listener.onChangeCompleted(to, from, isPush, container, inHandler);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param from The old Controller
|
||||
* @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(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler);
|
||||
|
||||
/**
|
||||
* Called when a {@link ControllerChangeHandler} has completed changing {@link Controller}s
|
||||
* @param to The new Controller
|
||||
* @param from The old Controller
|
||||
* @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(Controller to, Controller from, boolean isPush, ViewGroup container, 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
package com.bluelinelabs.conductor
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.BackEventCompat
|
||||
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
|
||||
|
||||
open val enableOnBackGestureCallbacks = 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, event: BackEventCompat) {}
|
||||
|
||||
open fun handleOnBackProgressed(container: ViewGroup, to: View?, from: View, event: BackEventCompat) {}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package com.bluelinelabs.conductor;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentSender;
|
||||
import android.content.IntentSender.SendIntentException;
|
||||
import android.os.Bundle;
|
||||
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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
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() {
|
||||
popRootControllerMode = PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
hostController = controller;
|
||||
this.container = container;
|
||||
setOnBackPressedDispatcherEnabled(controller.onBackPressedDispatcherEnabled);
|
||||
|
||||
for (RouterTransaction transaction : backstack) {
|
||||
transaction.controller().setParentController(controller);
|
||||
}
|
||||
|
||||
watchContainerAttach();
|
||||
}
|
||||
}
|
||||
|
||||
final void removeHost() {
|
||||
if (container != null && container instanceof ControllerChangeListener) {
|
||||
removeChangeListener((ControllerChangeListener) container);
|
||||
}
|
||||
|
||||
final List<Controller> controllersToDestroy = new ArrayList<>(destroyingControllers);
|
||||
for (Controller controller : controllersToDestroy) {
|
||||
if (controller.getView() != null) {
|
||||
controller.detach(controller.getView(), true, false);
|
||||
}
|
||||
}
|
||||
for (RouterTransaction transaction : backstack) {
|
||||
if (transaction.controller().getView() != null) {
|
||||
transaction.controller().detach(transaction.controller().getView(), true, false);
|
||||
}
|
||||
}
|
||||
|
||||
prepareForContainerRemoval();
|
||||
container = null;
|
||||
}
|
||||
|
||||
final void setDetachFrozen(boolean frozen) {
|
||||
isDetachFrozen = frozen;
|
||||
for (RouterTransaction transaction : backstack) {
|
||||
transaction.controller().setDetachFrozen(frozen);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void destroy(boolean popViews) {
|
||||
setDetachFrozen(false);
|
||||
super.destroy(popViews);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void pushToBackstack(@NonNull RouterTransaction entry) {
|
||||
if (isDetachFrozen) {
|
||||
entry.controller().setDetachFrozen(true);
|
||||
}
|
||||
super.pushToBackstack(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBackstack(@NonNull List<RouterTransaction> newBackstack, @Nullable ControllerChangeHandler changeHandler) {
|
||||
if (isDetachFrozen) {
|
||||
for (RouterTransaction transaction : newBackstack) {
|
||||
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, boolean isConfigurationChange) {
|
||||
super.onActivityDestroyed(activity, isConfigurationChange);
|
||||
|
||||
removeHost();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (hostController != null && hostController.getRouter() != null) {
|
||||
hostController.getRouter().onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateOptionsMenu() {
|
||||
if (hostController != null && hostController.getRouter() != null) {
|
||||
hostController.getRouter().invalidateOptionsMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void startActivity(@NonNull Intent intent) {
|
||||
if (hostController != null && hostController.getRouter() != null) {
|
||||
hostController.getRouter().startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) {
|
||||
if (hostController != null && hostController.getRouter() != null) {
|
||||
hostController.getRouter().startActivityForResult(instanceId, intent, requestCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options) {
|
||||
if (hostController != null && hostController.getRouter() != null) {
|
||||
hostController.getRouter().startActivityForResult(instanceId, intent, requestCode, options);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void startIntentSenderForResult(@NonNull String instanceId, @NonNull IntentSender intent, int requestCode, @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, @Nullable Bundle options) throws SendIntentException {
|
||||
if (hostController != null && hostController.getRouter() != null) {
|
||||
hostController.getRouter().startIntentSenderForResult(instanceId, intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void registerForActivityResult(@NonNull String instanceId, int requestCode) {
|
||||
if (hostController != null && hostController.getRouter() != null) {
|
||||
hostController.getRouter().registerForActivityResult(instanceId, requestCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void unregisterForActivityResults(@NonNull String instanceId) {
|
||||
if (hostController != null && hostController.getRouter() != null) {
|
||||
hostController.getRouter().unregisterForActivityResults(instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
|
||||
if (hostController != null && hostController.getRouter() != null) {
|
||||
hostController.getRouter().requestPermissions(instanceId, permissions, requestCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean hasHost() {
|
||||
return hostController != null && container != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveInstanceState(@NonNull Bundle outState) {
|
||||
super.saveInstanceState(outState);
|
||||
|
||||
outState.putInt(KEY_HOST_ID, hostId);
|
||||
outState.putBoolean(KEY_BOUND_TO_CONTAINER, boundToContainer);
|
||||
outState.putString(KEY_TAG, tag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
super.restoreInstanceState(savedInstanceState);
|
||||
|
||||
hostId = savedInstanceState.getInt(KEY_HOST_ID);
|
||||
boundToContainer = savedInstanceState.getBoolean(KEY_BOUND_TO_CONTAINER);
|
||||
tag = savedInstanceState.getString(KEY_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
void setRouterOnController(@NonNull Controller controller) {
|
||||
controller.setParentController(hostController);
|
||||
super.setRouterOnController(controller);
|
||||
}
|
||||
|
||||
int getHostId() {
|
||||
return hostId;
|
||||
}
|
||||
|
||||
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
|
||||
List<Router> getSiblingRouters() {
|
||||
List<Router> list = new ArrayList<>();
|
||||
list.addAll(hostController.getChildRouters());
|
||||
list.addAll(hostController.getRouter().getSiblingRouters());
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override @NonNull
|
||||
Router getRootRouter() {
|
||||
if (hostController != null && hostController.getRouter() != null) {
|
||||
return hostController.getRouter().getRootRouter();
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@Override @NonNull
|
||||
TransactionIndexer getTransactionIndexer() {
|
||||
Router rootRouter = getRootRouter();
|
||||
if (rootRouter == this) {
|
||||
String debugInfo;
|
||||
if (hostController != null) {
|
||||
debugInfo = String.format(Locale.ENGLISH, "%s (attached? %b, destroyed? %b, parent: %s)",
|
||||
hostController.getClass().getSimpleName(), hostController.isAttached(), hostController.isBeingDestroyed, hostController.getParentController());
|
||||
} else {
|
||||
debugInfo = "null host controller";
|
||||
}
|
||||
throw new IllegalStateException("Unable to retrieve TransactionIndexer from " + debugInfo);
|
||||
} else {
|
||||
return getRootRouter().getTransactionIndexer();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package com.bluelinelabs.conductor;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Metadata used to transition between {@link Controller}s.
|
||||
*/
|
||||
public class ControllerTransaction {
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
/** The Controller is being pushed to the backstack as another Controller is pushed to the host container */
|
||||
PUSH_EXIT,
|
||||
|
||||
/** The Controller is being popped from the backstack and placed in the host container as another Controller is popped */
|
||||
POP_ENTER,
|
||||
|
||||
/** The Controller is being popped from the host contianer */
|
||||
POP_EXIT
|
||||
}
|
||||
|
||||
private static final String KEY_VIEW_CONTROLLER_BUNDLE = "ControllerTransaction.controller.bundle";
|
||||
private static final String KEY_PUSH_TRANSITION = "ControllerTransaction.pushControllerChangeHandler";
|
||||
private static final String KEY_POP_TRANSITION = "ControllerTransaction.popControllerChangeHandler";
|
||||
private static final String KEY_TAG = "ControllerTransaction.tag";
|
||||
|
||||
public final Controller controller;
|
||||
public final String tag;
|
||||
|
||||
private final ControllerChangeHandler mPushControllerChangeHandler;
|
||||
private final ControllerChangeHandler mPopControllerChangeHandler;
|
||||
|
||||
ControllerTransaction(Builder builder) {
|
||||
controller = builder.controller;
|
||||
tag = builder.tag;
|
||||
mPushControllerChangeHandler = builder.pushControllerChangeHandler;
|
||||
mPopControllerChangeHandler = builder.popControllerChangeHandler;
|
||||
}
|
||||
|
||||
ControllerTransaction(@NonNull Bundle bundle) {
|
||||
controller = Controller.newInstance(bundle.getBundle(KEY_VIEW_CONTROLLER_BUNDLE));
|
||||
mPushControllerChangeHandler = ControllerChangeHandler.fromBundle(bundle.getBundle(KEY_PUSH_TRANSITION));
|
||||
mPopControllerChangeHandler = ControllerChangeHandler.fromBundle(bundle.getBundle(KEY_POP_TRANSITION));
|
||||
tag = bundle.getString(KEY_TAG);
|
||||
}
|
||||
|
||||
public Controller getController() {
|
||||
return controller;
|
||||
}
|
||||
|
||||
public String getTag() {
|
||||
return tag;
|
||||
}
|
||||
|
||||
public ControllerChangeHandler getPushControllerChangeHandler() {
|
||||
ControllerChangeHandler handler = controller.getOverriddenPushHandler();
|
||||
if (handler == null) {
|
||||
handler = mPushControllerChangeHandler;
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
public ControllerChangeHandler getPopControllerChangeHandler() {
|
||||
ControllerChangeHandler handler = controller.getOverriddenPopHandler();
|
||||
if (handler == null) {
|
||||
handler = mPopControllerChangeHandler;
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to serialize this transaction into a Bundle
|
||||
*/
|
||||
public Bundle toBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
|
||||
bundle.putBundle(KEY_VIEW_CONTROLLER_BUNDLE, controller.saveInstanceState());
|
||||
|
||||
if (mPushControllerChangeHandler != null) {
|
||||
bundle.putBundle(KEY_PUSH_TRANSITION, mPushControllerChangeHandler.toBundle());
|
||||
}
|
||||
if (mPopControllerChangeHandler != null) {
|
||||
bundle.putBundle(KEY_POP_TRANSITION, mPopControllerChangeHandler.toBundle());
|
||||
}
|
||||
|
||||
bundle.putString(KEY_TAG, tag);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder used to create transactions.
|
||||
*/
|
||||
public static class Builder<T extends Builder<T>> {
|
||||
|
||||
final Controller controller;
|
||||
ControllerChangeHandler pushControllerChangeHandler;
|
||||
ControllerChangeHandler popControllerChangeHandler;
|
||||
String tag;
|
||||
|
||||
public Builder(@NonNull Controller controller) {
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link ControllerChangeHandler} that will be used when the {@link Controller} is pushed
|
||||
* to the screen.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public T pushChangeHandler(ControllerChangeHandler pushControllerChangeHandler) {
|
||||
this.pushControllerChangeHandler = pushControllerChangeHandler;
|
||||
return (T)this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link ControllerChangeHandler} that will be used when the {@link Controller} is popped
|
||||
* from the screen.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public T popChangeHandler(ControllerChangeHandler popControllerChangeHandler) {
|
||||
this.popControllerChangeHandler = popControllerChangeHandler;
|
||||
return (T)this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The tag to use for this transaction. Tags can be used for finding transactions later on.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public T tag(String tag) {
|
||||
this.tag = tag;
|
||||
return (T)this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the transaction.
|
||||
*/
|
||||
public ControllerTransaction build() {
|
||||
return new ControllerTransaction(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Executable → Regular
+901
-177
File diff suppressed because it is too large
Load Diff
@@ -1,44 +0,0 @@
|
||||
package com.bluelinelabs.conductor;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* A {@link ControllerTransaction} implementation used for adding {@link Controller}s to a {@link Router}.
|
||||
*/
|
||||
public class RouterTransaction extends ControllerTransaction {
|
||||
|
||||
private RouterTransaction(Builder builder) {
|
||||
super(builder);
|
||||
}
|
||||
|
||||
RouterTransaction(@NonNull Bundle bundle) {
|
||||
super(bundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Builder
|
||||
*
|
||||
* @param controller The {@link Controller} to add to the {@link Router}
|
||||
*/
|
||||
public static Builder builder(@NonNull Controller controller) {
|
||||
return new Builder(controller);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link ControllerTransaction.Builder} implementation used for adding {@link Controller}s to a {@link Router}.
|
||||
*/
|
||||
public static class Builder extends ControllerTransaction.Builder<Builder> {
|
||||
|
||||
Builder(@NonNull Controller controller) {
|
||||
super(controller);
|
||||
}
|
||||
|
||||
/** Creates the transaction */
|
||||
public RouterTransaction build() {
|
||||
return new RouterTransaction(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
-130
@@ -1,130 +0,0 @@
|
||||
package com.bluelinelabs.conductor.changehandler;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
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";
|
||||
|
||||
public static final long DEFAULT_ANIMATION_DURATION = -1;
|
||||
|
||||
private long mAnimationDuration;
|
||||
private boolean mRemovesFromViewOnPush;
|
||||
|
||||
public AnimatorChangeHandler() {
|
||||
this(DEFAULT_ANIMATION_DURATION, true);
|
||||
}
|
||||
|
||||
public AnimatorChangeHandler(boolean removesFromViewOnPush) {
|
||||
this(DEFAULT_ANIMATION_DURATION, removesFromViewOnPush);
|
||||
}
|
||||
|
||||
public AnimatorChangeHandler(long duration) {
|
||||
this(duration, true);
|
||||
}
|
||||
|
||||
public AnimatorChangeHandler(long duration, boolean removesFromViewOnPush) {
|
||||
mAnimationDuration = duration;
|
||||
mRemovesFromViewOnPush = removesFromViewOnPush;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveToBundle(@NonNull Bundle bundle) {
|
||||
super.saveToBundle(bundle);
|
||||
bundle.putLong(KEY_DURATION, mAnimationDuration);
|
||||
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, mRemovesFromViewOnPush);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreFromBundle(@NonNull Bundle bundle) {
|
||||
super.restoreFromBundle(bundle);
|
||||
mAnimationDuration = bundle.getLong(KEY_DURATION);
|
||||
mRemovesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, if any.
|
||||
* @param to The next View that should be put in the container, if any.
|
||||
* @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 Animator getAnimator(@NonNull ViewGroup container, View from, 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, final View from, 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 {
|
||||
container.addView(to, container.indexOfChild(from));
|
||||
}
|
||||
|
||||
if (to.getWidth() <= 0 && to.getHeight() <= 0) {
|
||||
readyToAnimate = false;
|
||||
to.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
|
||||
@Override
|
||||
public boolean onPreDraw() {
|
||||
final ViewTreeObserver observer = to.getViewTreeObserver();
|
||||
if (observer.isAlive()) {
|
||||
observer.removeOnPreDrawListener(this);
|
||||
}
|
||||
performAnimation(container, from, to, isPush, addingToView, changeListener);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (readyToAnimate) {
|
||||
performAnimation(container, from, to, isPush, addingToView, changeListener);
|
||||
}
|
||||
}
|
||||
|
||||
private void performAnimation(@NonNull final ViewGroup container, final View from, View to, final boolean isPush, final boolean toAddedToContainer, @NonNull final ControllerChangeCompletedListener changeListener) {
|
||||
Animator animator = getAnimator(container, from, to, isPush, toAddedToContainer);
|
||||
|
||||
if (mAnimationDuration > 0) {
|
||||
animator.setDuration(mAnimationDuration);
|
||||
}
|
||||
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
if (from != null && (!isPush || mRemovesFromViewOnPush)) {
|
||||
container.removeView(from);
|
||||
}
|
||||
|
||||
changeListener.onChangeCompleted();
|
||||
|
||||
if (isPush && from != null) {
|
||||
resetFromView(from);
|
||||
}
|
||||
}
|
||||
});
|
||||
animator.start();
|
||||
}
|
||||
|
||||
}
|
||||
+228
@@ -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
|
||||
}
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
package com.bluelinelabs.conductor.changehandler;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.transition.AutoTransition;
|
||||
import android.transition.Transition;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
/**
|
||||
* 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, View from, View to, boolean isPush) {
|
||||
return new AutoTransition();
|
||||
}
|
||||
|
||||
}
|
||||
-47
@@ -1,47 +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.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
/**
|
||||
* 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
|
||||
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
|
||||
AnimatorSet animator = new AnimatorSet();
|
||||
if (to != null && toAddedToContainer) {
|
||||
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, 0, 1));
|
||||
}
|
||||
|
||||
if (from != null) {
|
||||
animator.play(ObjectAnimator.ofFloat(from, View.ALPHA, 0));
|
||||
}
|
||||
|
||||
return animator;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetFromView(@NonNull View from) {
|
||||
from.setAlpha(1);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
-56
@@ -1,56 +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.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
/**
|
||||
* 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
|
||||
protected Animator getAnimator(@NonNull ViewGroup container, View from, 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) {
|
||||
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, -to.getWidth(), 0));
|
||||
}
|
||||
}
|
||||
|
||||
return animatorSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetFromView(@NonNull View from) {
|
||||
from.setTranslationY(0);
|
||||
}
|
||||
}
|
||||
+52
@@ -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)
|
||||
}
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
package com.bluelinelabs.conductor.changehandler;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.View;
|
||||
import android.view.View.OnAttachStateChangeListener;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler;
|
||||
|
||||
/**
|
||||
* A {@link ControllerChangeHandler} that will instantly swap Views with no animations or transitions.
|
||||
*/
|
||||
public class SimpleSwapChangeHandler extends ControllerChangeHandler {
|
||||
|
||||
private static final String KEY_REMOVES_FROM_ON_PUSH = "SimpleSwapChangeHandler.removesFromViewOnPush";
|
||||
|
||||
private boolean mRemovesFromViewOnPush;
|
||||
|
||||
public SimpleSwapChangeHandler() {
|
||||
this(true);
|
||||
}
|
||||
|
||||
public SimpleSwapChangeHandler(boolean removesFromViewOnPush) {
|
||||
mRemovesFromViewOnPush = removesFromViewOnPush;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveToBundle(@NonNull Bundle bundle) {
|
||||
super.saveToBundle(bundle);
|
||||
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, mRemovesFromViewOnPush);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreFromBundle(@NonNull Bundle bundle) {
|
||||
super.restoreFromBundle(bundle);
|
||||
mRemovesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
|
||||
if (from != null && (!isPush || mRemovesFromViewOnPush)) {
|
||||
container.removeView(from);
|
||||
}
|
||||
|
||||
if (to != null && to.getParent() == null) {
|
||||
to.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onViewAttachedToWindow(View view) {
|
||||
view.removeOnAttachStateChangeListener(this);
|
||||
changeListener.onChangeCompleted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(View v) { }
|
||||
});
|
||||
|
||||
container.addView(to);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+88
@@ -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"
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
package com.bluelinelabs.conductor.changehandler;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.transition.Transition;
|
||||
import android.transition.Transition.TransitionListener;
|
||||
import android.transition.TransitionManager;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler;
|
||||
|
||||
/**
|
||||
* A base {@link ControllerChangeHandler} that facilitates using {@link android.transition.Transition}s to replace Controller Views.
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public abstract class TransitionChangeHandler extends ControllerChangeHandler {
|
||||
|
||||
/**
|
||||
* Should be overridden to return the Transition to use while replacing Views.
|
||||
*
|
||||
* @param container The container these Views are hosted in.
|
||||
* @param from The previous View in the container, if any.
|
||||
* @param to The next View that should be put in the container, if any.
|
||||
* @param isPush True if this is a push transaction, false if it's a pop.
|
||||
*/
|
||||
@NonNull
|
||||
protected abstract Transition getTransition(@NonNull ViewGroup container, View from, View to, boolean isPush);
|
||||
|
||||
@Override
|
||||
public void performChange(@NonNull final ViewGroup container, View from, View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
|
||||
Transition transition = getTransition(container, from, to, isPush);
|
||||
transition.addListener(new TransitionListener() {
|
||||
@Override
|
||||
public void onTransitionStart(Transition transition) { }
|
||||
|
||||
@Override
|
||||
public void onTransitionEnd(Transition transition) {
|
||||
changeListener.onChangeCompleted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransitionCancel(Transition transition) {
|
||||
changeListener.onChangeCompleted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransitionPause(Transition transition) { }
|
||||
|
||||
@Override
|
||||
public void onTransitionResume(Transition transition) { }
|
||||
});
|
||||
|
||||
TransitionManager.beginDelayedTransition(container, transition);
|
||||
if (from != null) {
|
||||
container.removeView(from);
|
||||
}
|
||||
if (to != null) {
|
||||
container.addView(to);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
package com.bluelinelabs.conductor.changehandler;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler;
|
||||
import com.bluelinelabs.conductor.util.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_TRANSITION_HANDLER_CLASS = "TransitionChangeHandlerCompat.transitionChangeHandler.class";
|
||||
private static final String KEY_FALLBACK_HANDLER_CLASS = "TransitionChangeHandlerCompat.fallbackChangeHandler.class";
|
||||
private static final String KEY_TRANSITION_HANDLER_STATE = "TransitionChangeHandlerCompat.transitionChangeHandler.state";
|
||||
private static final String KEY_FALLBACK_HANDLER_STATE = "TransitionChangeHandlerCompat.fallbackChangeHandler.state";
|
||||
|
||||
private TransitionChangeHandler mTransitionChangeHandler;
|
||||
private ControllerChangeHandler mFallbackChangeHandler;
|
||||
|
||||
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(TransitionChangeHandler transitionChangeHandler, ControllerChangeHandler fallbackChangeHandler) {
|
||||
mTransitionChangeHandler = transitionChangeHandler;
|
||||
mFallbackChangeHandler = fallbackChangeHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performChange(@NonNull final ViewGroup container, View from, View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
mTransitionChangeHandler.performChange(container, from, to, isPush, changeListener);
|
||||
} else {
|
||||
mFallbackChangeHandler.performChange(container, from, to, isPush, changeListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveToBundle(@NonNull Bundle bundle) {
|
||||
super.saveToBundle(bundle);
|
||||
|
||||
bundle.putString(KEY_TRANSITION_HANDLER_CLASS, mTransitionChangeHandler.getClass().getCanonicalName());
|
||||
bundle.putString(KEY_FALLBACK_HANDLER_CLASS, mFallbackChangeHandler.getClass().getCanonicalName());
|
||||
|
||||
Bundle transitionBundle = new Bundle();
|
||||
mTransitionChangeHandler.saveToBundle(transitionBundle);
|
||||
bundle.putBundle(KEY_TRANSITION_HANDLER_STATE, transitionBundle);
|
||||
|
||||
Bundle fallbackBundle = new Bundle();
|
||||
mFallbackChangeHandler.saveToBundle(fallbackBundle);
|
||||
bundle.putBundle(KEY_FALLBACK_HANDLER_STATE, fallbackBundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreFromBundle(@NonNull Bundle bundle) {
|
||||
super.restoreFromBundle(bundle);
|
||||
|
||||
String transitionClassName = bundle.getString(KEY_TRANSITION_HANDLER_CLASS);
|
||||
mTransitionChangeHandler = ClassUtils.newInstance(transitionClassName);
|
||||
//noinspection ConstantConditions
|
||||
mTransitionChangeHandler.restoreFromBundle(bundle.getBundle(KEY_TRANSITION_HANDLER_STATE));
|
||||
|
||||
String fallbackClassName = bundle.getString(KEY_FALLBACK_HANDLER_CLASS);
|
||||
mFallbackChangeHandler = ClassUtils.newInstance(fallbackClassName);
|
||||
//noinspection ConstantConditions
|
||||
mFallbackChangeHandler.restoreFromBundle(bundle.getBundle(KEY_FALLBACK_HANDLER_STATE));
|
||||
}
|
||||
|
||||
}
|
||||
-51
@@ -1,51 +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.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An {@link AnimatorChangeHandler} that will slide either slide a new View up or slide an old View down,
|
||||
* depending on whether a push or pop change is happening.
|
||||
*/
|
||||
public class VerticalChangeHandler extends AnimatorChangeHandler {
|
||||
|
||||
public VerticalChangeHandler() { }
|
||||
|
||||
public VerticalChangeHandler(boolean removesFromViewOnPush) {
|
||||
super(removesFromViewOnPush);
|
||||
}
|
||||
|
||||
public VerticalChangeHandler(long duration) {
|
||||
super(duration);
|
||||
}
|
||||
|
||||
public VerticalChangeHandler(long duration, boolean removesFromViewOnPush) {
|
||||
super(duration, removesFromViewOnPush);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
|
||||
AnimatorSet animator = new AnimatorSet();
|
||||
List<Animator> viewAnimators = new ArrayList<>();
|
||||
|
||||
if (isPush && to != null) {
|
||||
viewAnimators.add(ObjectAnimator.ofFloat(to, View.TRANSLATION_Y, to.getHeight(), 0));
|
||||
} else if (!isPush && from != null) {
|
||||
viewAnimators.add(ObjectAnimator.ofFloat(from, View.TRANSLATION_Y, from.getHeight()));
|
||||
}
|
||||
|
||||
animator.playTogether(viewAnimators);
|
||||
return animator;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetFromView(@NonNull View from) { }
|
||||
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.bluelinelabs.conductor.changehandler
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
|
||||
/**
|
||||
* An [AnimatorChangeHandler] that will slide either slide a new View up or slide an old View down,
|
||||
* depending on whether a push or pop change is happening.
|
||||
*/
|
||||
class VerticalChangeHandler : AnimatorChangeHandler {
|
||||
constructor() : super()
|
||||
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
|
||||
constructor(duration: Long) : super(duration)
|
||||
constructor(duration: Long, removesFromViewOnPush: Boolean) : super(duration, removesFromViewOnPush)
|
||||
|
||||
override fun getAnimator(
|
||||
container: ViewGroup,
|
||||
from: View?,
|
||||
to: View?,
|
||||
isPush: Boolean,
|
||||
toAddedToContainer: Boolean,
|
||||
): Animator {
|
||||
val animator = AnimatorSet()
|
||||
val viewAnimators: MutableList<Animator> = ArrayList()
|
||||
|
||||
if (isPush && to != null) {
|
||||
viewAnimators.add(ObjectAnimator.ofFloat(to, View.TRANSLATION_Y, to.height.toFloat(), 0f))
|
||||
} else if (!isPush && from != null) {
|
||||
viewAnimators.add(ObjectAnimator.ofFloat(from, View.TRANSLATION_Y, from.height.toFloat()))
|
||||
}
|
||||
|
||||
animator.playTogether(viewAnimators)
|
||||
return animator
|
||||
}
|
||||
|
||||
override fun resetFromView(from: View) = Unit
|
||||
|
||||
override fun copy(): ControllerChangeHandler = VerticalChangeHandler(animationDuration, removesFromViewOnPush)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.bluelinelabs.conductor.internal
|
||||
|
||||
import android.view.View
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
|
||||
class BackGestureViewState(
|
||||
val fromView: View,
|
||||
val toViews: List<BackGestureControllerView>,
|
||||
)
|
||||
|
||||
class BackGestureControllerView(
|
||||
val controller: Controller,
|
||||
val view: View,
|
||||
val inflatedForGesture: Boolean,
|
||||
)
|
||||
+8
-11
@@ -1,16 +1,13 @@
|
||||
package com.bluelinelabs.conductor.util;
|
||||
package com.bluelinelabs.conductor.internal;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
public class ClassUtils {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> Class<? extends T> classForName(String className) {
|
||||
return classForName(className, true);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> Class<? extends T> classForName(String className, boolean allowEmptyName) {
|
||||
@Nullable @SuppressWarnings("unchecked")
|
||||
public static <T> Class<? extends T> classForName(@NonNull String className, boolean allowEmptyName) {
|
||||
if (allowEmptyName && TextUtils.isEmpty(className)) {
|
||||
return null;
|
||||
}
|
||||
@@ -22,10 +19,10 @@ public class ClassUtils {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T newInstance(String className) {
|
||||
@Nullable @SuppressWarnings("unchecked")
|
||||
public static <T> T newInstance(@NonNull String className) {
|
||||
try {
|
||||
Class<? extends T> cls = classForName(className);
|
||||
Class<? extends T> cls = classForName(className, true);
|
||||
return cls != null ? cls.newInstance() : null;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("An exception occurred while creating a new instance of " + className + ". " + e.getMessage());
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.bluelinelabs.conductor.internal
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.Controller.LifecycleListener
|
||||
|
||||
class ControllerLifecycleOwner(lifecycleController: Controller) : LifecycleOwner {
|
||||
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) // --> State.INITIALIZED
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = lifecycleRegistry
|
||||
|
||||
init {
|
||||
lifecycleController.addLifecycleListener(
|
||||
object : LifecycleListener() {
|
||||
override fun postContextAvailable(controller: Controller, context: Context) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) // --> State.CREATED;
|
||||
}
|
||||
|
||||
override fun postCreateView(controller: Controller, view: View) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) // --> State.STARTED;
|
||||
}
|
||||
|
||||
override fun postAttach(controller: Controller, view: View) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) // --> State.RESUMED;
|
||||
}
|
||||
|
||||
override fun preDetach(controller: Controller, view: View) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) // --> State.STARTED;
|
||||
}
|
||||
|
||||
override fun preDestroyView(controller: Controller, view: View) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) // --> State.CREATED;
|
||||
}
|
||||
|
||||
override fun preContextUnavailable(controller: Controller, context: Context) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
override fun preDestroy(controller: Controller) {
|
||||
// Only act on Controllers that have had at least the onContextAvailable call made on them.
|
||||
if (lifecycleRegistry.currentState != Lifecycle.State.INITIALIZED) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) // --> State.DESTROYED;
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
package com.bluelinelabs.conductor.internal;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.app.Application.ActivityLifecycleCallbacks;
|
||||
import android.app.Fragment;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.SparseArray;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.bluelinelabs.conductor.Router;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class LifecycleHandler extends Fragment implements ActivityLifecycleCallbacks {
|
||||
|
||||
private static final String FRAGMENT_TAG = "LifecycleHandler";
|
||||
|
||||
private static final String KEY_PERMISSION_REQUEST_CODES = "LifecycleHandler.permissionRequests";
|
||||
private static final String KEY_ACTIVITY_REQUEST_CODES = "LifecycleHandler.activityRequests";
|
||||
|
||||
private Activity mActivity;
|
||||
private boolean mHasRegisteredCallbacks;
|
||||
|
||||
private SparseArray<String> mPermissionRequestMap = new SparseArray<>();
|
||||
private SparseArray<String> mActivityRequestMap = new SparseArray<>();
|
||||
|
||||
private final Map<Integer, Router> mRouterMap = new HashMap<>();
|
||||
|
||||
public LifecycleHandler() {
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
private static LifecycleHandler findInActivity(Activity activity) {
|
||||
LifecycleHandler lifecycleHandler = (LifecycleHandler)activity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
|
||||
if (lifecycleHandler != null) {
|
||||
lifecycleHandler.registerActivityListener(activity);
|
||||
}
|
||||
return lifecycleHandler;
|
||||
}
|
||||
|
||||
public static LifecycleHandler install(Activity activity) {
|
||||
LifecycleHandler lifecycleHandler = findInActivity(activity);
|
||||
if (lifecycleHandler == null) {
|
||||
lifecycleHandler = new LifecycleHandler();
|
||||
activity.getFragmentManager().beginTransaction().add(lifecycleHandler, FRAGMENT_TAG).commit();
|
||||
}
|
||||
lifecycleHandler.registerActivityListener(activity);
|
||||
return lifecycleHandler;
|
||||
}
|
||||
|
||||
public Router getRouter(ViewGroup container, Bundle savedInstanceState) {
|
||||
Router router = mRouterMap.get(getRouterHashKey(container));
|
||||
if (router == null) {
|
||||
router = new Router();
|
||||
if (savedInstanceState != null) {
|
||||
router.onRestoreInstanceState(savedInstanceState);
|
||||
}
|
||||
mRouterMap.put(getRouterHashKey(container), router);
|
||||
}
|
||||
|
||||
router.setHost(this, container);
|
||||
return router;
|
||||
}
|
||||
|
||||
public Activity getLifecycleActivity() {
|
||||
return mActivity;
|
||||
}
|
||||
|
||||
private static int getRouterHashKey(ViewGroup viewGroup) {
|
||||
return viewGroup.getId();
|
||||
}
|
||||
|
||||
private void registerActivityListener(Activity activity) {
|
||||
mActivity = activity;
|
||||
|
||||
if (!mHasRegisteredCallbacks) {
|
||||
mHasRegisteredCallbacks = true;
|
||||
activity.getApplication().registerActivityLifecycleCallbacks(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
StringSparseArrayParceler permissionParcel = savedInstanceState.getParcelable(KEY_PERMISSION_REQUEST_CODES);
|
||||
mPermissionRequestMap = permissionParcel != null ? permissionParcel.getStringSparseArray() : null;
|
||||
|
||||
StringSparseArrayParceler activityParcel = savedInstanceState.getParcelable(KEY_ACTIVITY_REQUEST_CODES);
|
||||
mActivityRequestMap = activityParcel != null ? activityParcel.getStringSparseArray() : null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
outState.putParcelable(KEY_PERMISSION_REQUEST_CODES, new StringSparseArrayParceler(mPermissionRequestMap));
|
||||
outState.putParcelable(KEY_ACTIVITY_REQUEST_CODES, new StringSparseArrayParceler(mActivityRequestMap));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
if (mActivity != null) {
|
||||
mActivity.getApplication().unregisterActivityLifecycleCallbacks(this);
|
||||
|
||||
for (Router router : mRouterMap.values()) {
|
||||
router.onActivityDestroyed(mActivity);
|
||||
}
|
||||
|
||||
mActivity = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
String instanceId = mActivityRequestMap.get(requestCode);
|
||||
if (instanceId != null) {
|
||||
for (Router router : mRouterMap.values()) {
|
||||
router.onActivityResult(instanceId, requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
String instanceId = mPermissionRequestMap.get(requestCode);
|
||||
if (instanceId != null) {
|
||||
for (Router router : mRouterMap.values()) {
|
||||
router.onRequestPermissionsResult(instanceId, requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldShowRequestPermissionRationale(@NonNull String permission) {
|
||||
for (Router router : mRouterMap.values()) {
|
||||
Boolean handled = router.handleRequestedPermission(permission);
|
||||
if (handled != null) {
|
||||
return handled;
|
||||
}
|
||||
}
|
||||
return super.shouldShowRequestPermissionRationale(permission);
|
||||
}
|
||||
|
||||
public void startActivityForResult(String instanceId, Intent intent, int requestCode) {
|
||||
mActivityRequestMap.put(requestCode, instanceId);
|
||||
startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
public void startActivityForResult(String instanceId, Intent intent, int requestCode, Bundle options) {
|
||||
mActivityRequestMap.put(requestCode, instanceId);
|
||||
startActivityForResult(intent, requestCode, options);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
public void requestPermissions(String instanceId, String[] permissions, int requestCode) {
|
||||
mPermissionRequestMap.put(requestCode, instanceId);
|
||||
requestPermissions(permissions, requestCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
|
||||
if (mActivity == null && findInActivity(activity) == LifecycleHandler.this) {
|
||||
mActivity = activity;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(Activity activity) {
|
||||
if (mActivity == activity) {
|
||||
for (Router router : mRouterMap.values()) {
|
||||
router.onActivityStarted(activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResumed(Activity activity) {
|
||||
if (mActivity == activity) {
|
||||
for (Router router : mRouterMap.values()) {
|
||||
router.onActivityResumed(activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityPaused(Activity activity) {
|
||||
if (mActivity == activity) {
|
||||
for (Router router : mRouterMap.values()) {
|
||||
router.onActivityPaused(activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(Activity activity) {
|
||||
if (mActivity == activity) {
|
||||
for (Router router : mRouterMap.values()) {
|
||||
router.onActivityStopped(activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
|
||||
if (mActivity == activity) {
|
||||
for (Router router : mRouterMap.values()) {
|
||||
router.onActivitySaveInstanceState(activity, outState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(Activity activity) { }
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
package com.bluelinelabs.conductor.internal
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application.ActivityLifecycleCallbacks
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.util.SparseArray
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.bluelinelabs.conductor.ActivityHostedRouter
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
internal interface LifecycleHandler {
|
||||
val routers: List<Router>
|
||||
val lifecycleActivity: Activity?
|
||||
fun getRouter(container: ViewGroup, savedInstanceState: Bundle?): Router
|
||||
fun registerActivityListener(activity: Activity)
|
||||
fun registerForActivityResult(instanceId: String, requestCode: Int)
|
||||
fun unregisterForActivityResults(instanceId: String)
|
||||
|
||||
fun startActivity(intent: Intent?)
|
||||
fun startActivityForResult(instanceId: String, intent: Intent, requestCode: Int, options: Bundle? = null)
|
||||
|
||||
@Throws(IntentSender.SendIntentException::class)
|
||||
fun startIntentSenderForResult(
|
||||
instanceId: String,
|
||||
intent: IntentSender,
|
||||
requestCode: Int,
|
||||
fillInIntent: Intent?,
|
||||
flagsMask: Int,
|
||||
flagsValues: Int,
|
||||
extraFlags: Int,
|
||||
options: Bundle?,
|
||||
)
|
||||
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
|
||||
fun requestPermissions(instanceId: String, permissions: Array<String>, requestCode: Int)
|
||||
|
||||
companion object {
|
||||
fun install(activity: Activity, allowAndroidXBacking: Boolean = true): LifecycleHandler {
|
||||
var lifecycleHandler = findInActivity(activity, allowAndroidXBacking)
|
||||
if (lifecycleHandler == null) {
|
||||
if (allowAndroidXBacking && activity is FragmentActivity) {
|
||||
lifecycleHandler = AndroidXLifecycleHandlerImpl()
|
||||
activity.supportFragmentManager.beginTransaction().add(lifecycleHandler, FRAGMENT_TAG).commit()
|
||||
} else {
|
||||
lifecycleHandler = PlatformLifecycleHandlerImpl()
|
||||
@Suppress("DEPRECATION")
|
||||
activity.fragmentManager.beginTransaction().add(lifecycleHandler, FRAGMENT_TAG).commit()
|
||||
}
|
||||
}
|
||||
lifecycleHandler.registerActivityListener(activity)
|
||||
return lifecycleHandler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class AndroidXLifecycleHandlerImpl : androidx.fragment.app.Fragment(), LifecycleHandler, LifecycleHandlerDelegate {
|
||||
|
||||
override val data: LifecycleHandlerData = LifecycleHandlerData(isAndroidXLifecycleHandler = true)
|
||||
|
||||
override val routers: List<Router>
|
||||
get() = data.routerMap.values.toList()
|
||||
|
||||
override val lifecycleActivity: Activity?
|
||||
get() = data.activity
|
||||
|
||||
init {
|
||||
@Suppress("DEPRECATION")
|
||||
retainInstance = true
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
handleOnCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
handleOnSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
handleOnAttach(context)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
handleOnActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
handleOnRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
|
||||
return handleShouldShowRequestPermissionRationale(permission) {
|
||||
super.shouldShowRequestPermissionRationale(permission)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
handleOnCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
handleOnPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return handleOnOptionsItemSelected(item) {
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRouter(container: ViewGroup, savedInstanceState: Bundle?): Router {
|
||||
return getRouter(container, savedInstanceState, this)
|
||||
}
|
||||
|
||||
override fun registerActivityListener(activity: Activity) {
|
||||
handleRegisterActivityListener(activity, this)
|
||||
}
|
||||
|
||||
override fun registerForActivityResult(instanceId: String, requestCode: Int) {
|
||||
handleRegisterForActivityResult(instanceId, requestCode)
|
||||
}
|
||||
|
||||
override fun unregisterForActivityResults(instanceId: String) {
|
||||
handleUnregisterForActivityResults(instanceId)
|
||||
}
|
||||
|
||||
override fun startActivityForResult(instanceId: String, intent: Intent, requestCode: Int, options: Bundle?) {
|
||||
handleStartActivityForResult(instanceId, intent, requestCode, options)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun startIntentSenderForResult(
|
||||
instanceId: String,
|
||||
intent: IntentSender,
|
||||
requestCode: Int,
|
||||
fillInIntent: Intent?,
|
||||
flagsMask: Int,
|
||||
flagsValues: Int,
|
||||
extraFlags: Int,
|
||||
options: Bundle?,
|
||||
) {
|
||||
handleStartIntentSenderForResult(
|
||||
instanceId = instanceId,
|
||||
intent = intent,
|
||||
requestCode = requestCode,
|
||||
fillInIntent = fillInIntent,
|
||||
flagsMask = flagsMask,
|
||||
flagsValues = flagsValues,
|
||||
extraFlags = extraFlags,
|
||||
options = options,
|
||||
) {
|
||||
startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options)
|
||||
}
|
||||
}
|
||||
|
||||
override fun requestPermissions(instanceId: String, permissions: Array<String>, requestCode: Int) {
|
||||
handleRequestPermissions(instanceId, permissions, requestCode)
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
handleOnDetach()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
handleOnDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
|
||||
internal class PlatformLifecycleHandlerImpl : android.app.Fragment(), LifecycleHandler, LifecycleHandlerDelegate {
|
||||
|
||||
override val data: LifecycleHandlerData = LifecycleHandlerData(isAndroidXLifecycleHandler = false)
|
||||
|
||||
override val routers: List<Router>
|
||||
get() = data.routerMap.values.toList()
|
||||
|
||||
override val lifecycleActivity: Activity?
|
||||
get() = data.activity
|
||||
|
||||
init {
|
||||
retainInstance = true
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
handleOnCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
handleOnSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onAttach(activity: Activity) {
|
||||
super.onAttach(activity)
|
||||
handleOnAttach(activity)
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
handleOnAttach(context)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
handleOnActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
handleOnRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
|
||||
return handleShouldShowRequestPermissionRationale(permission) {
|
||||
super.shouldShowRequestPermissionRationale(permission)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
handleOnCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
handleOnPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return handleOnOptionsItemSelected(item) {
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRouter(container: ViewGroup, savedInstanceState: Bundle?): Router {
|
||||
return getRouter(container, savedInstanceState, this)
|
||||
}
|
||||
|
||||
override fun registerActivityListener(activity: Activity) {
|
||||
handleRegisterActivityListener(activity, this)
|
||||
}
|
||||
|
||||
override fun registerForActivityResult(instanceId: String, requestCode: Int) {
|
||||
handleRegisterForActivityResult(instanceId, requestCode)
|
||||
}
|
||||
|
||||
override fun unregisterForActivityResults(instanceId: String) {
|
||||
handleUnregisterForActivityResults(instanceId)
|
||||
}
|
||||
|
||||
override fun startActivityForResult(instanceId: String, intent: Intent, requestCode: Int, options: Bundle?) {
|
||||
handleStartActivityForResult(instanceId, intent, requestCode, options)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun startIntentSenderForResult(
|
||||
instanceId: String,
|
||||
intent: IntentSender,
|
||||
requestCode: Int,
|
||||
fillInIntent: Intent?,
|
||||
flagsMask: Int,
|
||||
flagsValues: Int,
|
||||
extraFlags: Int,
|
||||
options: Bundle?,
|
||||
) {
|
||||
handleStartIntentSenderForResult(
|
||||
instanceId = instanceId,
|
||||
intent = intent,
|
||||
requestCode = requestCode,
|
||||
fillInIntent = fillInIntent,
|
||||
flagsMask = flagsMask,
|
||||
flagsValues = flagsValues,
|
||||
extraFlags = extraFlags,
|
||||
options = options,
|
||||
) {
|
||||
startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options)
|
||||
}
|
||||
}
|
||||
|
||||
override fun requestPermissions(instanceId: String, permissions: Array<String>, requestCode: Int) {
|
||||
handleRequestPermissions(instanceId, permissions, requestCode)
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
handleOnDetach()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
handleOnDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
private interface LifecycleHandlerDelegate : ActivityLifecycleCallbacks {
|
||||
val data: LifecycleHandlerData
|
||||
|
||||
private val routers: List<ActivityHostedRouter>
|
||||
get() = data.routerMap.values.toList()
|
||||
|
||||
fun handleOnCreate(savedInstanceState: Bundle?) {
|
||||
savedInstanceState ?: return
|
||||
data.permissionRequestMap = savedInstanceState.getParcelable<StringSparseArrayParceler>(KEY_PERMISSION_REQUEST_CODES)
|
||||
?.stringSparseArray
|
||||
?: SparseArray()
|
||||
data.activityRequestMap = savedInstanceState.getParcelable<StringSparseArrayParceler>(KEY_ACTIVITY_REQUEST_CODES)
|
||||
?.stringSparseArray
|
||||
?: SparseArray()
|
||||
data.pendingPermissionRequests = savedInstanceState.getParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS)
|
||||
?: ArrayList()
|
||||
}
|
||||
|
||||
fun handleOnSaveInstanceState(outState: Bundle) {
|
||||
outState.putParcelable(KEY_PERMISSION_REQUEST_CODES, StringSparseArrayParceler(data.permissionRequestMap))
|
||||
outState.putParcelable(KEY_ACTIVITY_REQUEST_CODES, StringSparseArrayParceler(data.activityRequestMap))
|
||||
outState.putParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS, data.pendingPermissionRequests)
|
||||
}
|
||||
|
||||
fun handleOnDestroy() {
|
||||
data.activity?.let { activity ->
|
||||
activity.application.unregisterActivityLifecycleCallbacks(this)
|
||||
activeLifecycleHandlers.remove(activity)
|
||||
destroyRouters(false)
|
||||
data.activity = null
|
||||
}
|
||||
|
||||
data.routerMap.clear()
|
||||
}
|
||||
|
||||
fun getRouter(container: ViewGroup, savedInstanceState: Bundle?, handler: LifecycleHandler): Router {
|
||||
data.routerMap[routerHashKey(container)]?.let {
|
||||
it.setHost(handler, container)
|
||||
return it
|
||||
}
|
||||
|
||||
val router = ActivityHostedRouter()
|
||||
router.setHost(handler, container)
|
||||
savedInstanceState?.getBundle("$KEY_ROUTER_STATE_PREFIX${router.containerId}")?.let {
|
||||
router.restoreInstanceState(it)
|
||||
}
|
||||
data.routerMap[routerHashKey(container)] = router
|
||||
return router
|
||||
}
|
||||
|
||||
fun handleRegisterActivityListener(activity: Activity, handler: LifecycleHandler) {
|
||||
data.activity = activity
|
||||
if (!data.hasRegisteredCallbacks) {
|
||||
data.hasRegisteredCallbacks = true
|
||||
activity.application.registerActivityLifecycleCallbacks(this)
|
||||
|
||||
// Since Fragment transactions are async, we have to keep an <Activity, LifecycleHandler> map in addition
|
||||
// to trying to find the LifecycleHandler fragment in the Activity to handle the case of the developer
|
||||
// trying to immediately get > 1 router in the same Activity. See issue #299.
|
||||
activeLifecycleHandlers[activity] = handler
|
||||
}
|
||||
}
|
||||
|
||||
fun requestPermissions(permissions: Array<String>, requestCode: Int)
|
||||
|
||||
fun handleOnAttach(context: Context) {
|
||||
if (context is Activity) {
|
||||
data.activity = context
|
||||
}
|
||||
|
||||
data.destroyed = false
|
||||
|
||||
if (!data.attached) {
|
||||
data.attached = true
|
||||
|
||||
for (i in data.pendingPermissionRequests.indices.reversed()) {
|
||||
val request = data.pendingPermissionRequests.removeAt(i)
|
||||
handleRequestPermissions(request.instanceId, request.permissions, request.requestCode)
|
||||
}
|
||||
|
||||
routers.forEach { it.onContextAvailable() }
|
||||
}
|
||||
}
|
||||
|
||||
fun handleOnDetach() {
|
||||
data.attached = false
|
||||
data.activity?.let { destroyRouters(it.isChangingConfigurations) }
|
||||
}
|
||||
|
||||
private fun destroyRouters(configurationChange: Boolean) {
|
||||
if (!data.destroyed) {
|
||||
data.destroyed = true
|
||||
data.activity?.let { activity ->
|
||||
routers.forEach { it.onActivityDestroyed(activity, configurationChange) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleOnActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
this.data.activityRequestMap[requestCode]?.let { instanceId ->
|
||||
routers.forEach { it.onActivityResult(instanceId, requestCode, resultCode, data) }
|
||||
}
|
||||
}
|
||||
|
||||
fun handleOnRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
data.permissionRequestMap[requestCode]?.let { instanceId ->
|
||||
routers.forEach { it.onRequestPermissionsResult(instanceId, requestCode, permissions, grantResults) }
|
||||
}
|
||||
}
|
||||
|
||||
fun handleShouldShowRequestPermissionRationale(permission: String, callSuper: () -> Boolean): Boolean {
|
||||
for (router in routers) {
|
||||
val handled = router.handleRequestedPermission(permission)
|
||||
if (handled != null) {
|
||||
return handled
|
||||
}
|
||||
}
|
||||
|
||||
return callSuper()
|
||||
}
|
||||
|
||||
fun handleOnCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
routers.forEach { it.onCreateOptionsMenu(menu, inflater) }
|
||||
}
|
||||
|
||||
fun handleOnPrepareOptionsMenu(menu: Menu) {
|
||||
routers.forEach { it.onPrepareOptionsMenu(menu) }
|
||||
}
|
||||
|
||||
fun handleOnOptionsItemSelected(item: MenuItem, callSuper: () -> Boolean): Boolean {
|
||||
return routers.any { it.onOptionsItemSelected(item) } || callSuper()
|
||||
}
|
||||
|
||||
fun handleRegisterForActivityResult(instanceId: String, requestCode: Int) {
|
||||
data.activityRequestMap.put(requestCode, instanceId)
|
||||
}
|
||||
|
||||
fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?)
|
||||
|
||||
fun handleStartActivityForResult(instanceId: String, intent: Intent, requestCode: Int, options: Bundle?) {
|
||||
handleRegisterForActivityResult(instanceId, requestCode)
|
||||
startActivityForResult(intent, requestCode, options)
|
||||
}
|
||||
|
||||
fun handleStartIntentSenderForResult(
|
||||
instanceId: String,
|
||||
intent: IntentSender,
|
||||
requestCode: Int,
|
||||
fillInIntent: Intent?,
|
||||
flagsMask: Int,
|
||||
flagsValues: Int,
|
||||
extraFlags: Int,
|
||||
options: Bundle?,
|
||||
startIntentSender: () -> Unit,
|
||||
) {
|
||||
handleRegisterForActivityResult(instanceId, requestCode)
|
||||
startIntentSender()
|
||||
}
|
||||
|
||||
fun handleUnregisterForActivityResults(instanceId: String) {
|
||||
for (i in data.activityRequestMap.size() - 1 downTo 0) {
|
||||
if (instanceId == data.activityRequestMap[data.activityRequestMap.keyAt(i)]) {
|
||||
data.activityRequestMap.removeAt(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleRequestPermissions(
|
||||
instanceId: String,
|
||||
permissions: Array<String>,
|
||||
requestCode: Int,
|
||||
) {
|
||||
if (data.attached) {
|
||||
data.permissionRequestMap.put(requestCode, instanceId)
|
||||
requestPermissions(permissions, requestCode)
|
||||
} else {
|
||||
data.pendingPermissionRequests.add(PendingPermissionRequest(instanceId, permissions, requestCode))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
if (findInActivity(activity, data.isAndroidXLifecycleHandler) === this) {
|
||||
data.activity = activity
|
||||
data.routerMap.values.toList().forEach { it.onContextAvailable() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
if (data.activity === activity) {
|
||||
data.hasPreparedForHostDetach = false
|
||||
routers.forEach { it.onActivityStarted(activity) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
if (data.activity === activity) {
|
||||
routers.forEach { it.onActivityResumed(activity) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
if (data.activity === activity) {
|
||||
routers.forEach { it.onActivityPaused(activity) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {
|
||||
if (data.activity === activity) {
|
||||
prepareForHostDetachIfNeeded()
|
||||
routers.forEach { it.onActivityStopped(activity) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
|
||||
if (data.activity === activity) {
|
||||
prepareForHostDetachIfNeeded()
|
||||
|
||||
routers.forEach {
|
||||
val bundle = Bundle()
|
||||
it.saveInstanceState(bundle)
|
||||
outState.putBundle("$KEY_ROUTER_STATE_PREFIX${it.containerId}", bundle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityPreDestroyed(activity: Activity) {
|
||||
if (data.activity === activity && !activity.isChangingConfigurations) {
|
||||
handleOnDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
activeLifecycleHandlers.remove(activity)
|
||||
}
|
||||
|
||||
private fun prepareForHostDetachIfNeeded() {
|
||||
if (!data.hasPreparedForHostDetach) {
|
||||
data.hasPreparedForHostDetach = true
|
||||
routers.forEach { it.prepareForHostDetach() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
internal class PendingPermissionRequest(
|
||||
val instanceId: String,
|
||||
val permissions: Array<String>,
|
||||
val requestCode: Int,
|
||||
) : Parcelable
|
||||
|
||||
internal class LifecycleHandlerData(
|
||||
val isAndroidXLifecycleHandler: Boolean,
|
||||
var activity: Activity? = null,
|
||||
var hasRegisteredCallbacks: Boolean = false,
|
||||
var destroyed: Boolean = false,
|
||||
var attached: Boolean = false,
|
||||
var hasPreparedForHostDetach: Boolean = false,
|
||||
var permissionRequestMap: SparseArray<String> = SparseArray(),
|
||||
var activityRequestMap: SparseArray<String> = SparseArray(),
|
||||
var pendingPermissionRequests: ArrayList<PendingPermissionRequest> = arrayListOf(),
|
||||
val routerMap: MutableMap<Int, ActivityHostedRouter> = mutableMapOf(),
|
||||
)
|
||||
|
||||
private fun findInActivity(activity: Activity, allowAndroidXBacking: Boolean): LifecycleHandler? {
|
||||
var lifecycleHandler = activeLifecycleHandlers[activity]
|
||||
if (lifecycleHandler == null) {
|
||||
lifecycleHandler = if (allowAndroidXBacking && activity is FragmentActivity) {
|
||||
activity.supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? LifecycleHandler
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
activity.fragmentManager.findFragmentByTag(FRAGMENT_TAG) as? LifecycleHandler
|
||||
}
|
||||
}
|
||||
lifecycleHandler?.registerActivityListener(activity)
|
||||
return lifecycleHandler
|
||||
}
|
||||
|
||||
private fun routerHashKey(viewGroup: ViewGroup) = viewGroup.id
|
||||
|
||||
private val activeLifecycleHandlers = mutableMapOf<Activity, LifecycleHandler>()
|
||||
|
||||
private const val FRAGMENT_TAG = "LifecycleHandler"
|
||||
private const val KEY_PENDING_PERMISSION_REQUESTS = "LifecycleHandler.pendingPermissionRequests"
|
||||
private const val KEY_PERMISSION_REQUEST_CODES = "LifecycleHandler.permissionRequests"
|
||||
private const val KEY_ACTIVITY_REQUEST_CODES = "LifecycleHandler.activityRequests"
|
||||
private const val KEY_ROUTER_STATE_PREFIX = "LifecycleHandler.routerState"
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
package com.bluelinelabs.conductor.internal;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler;
|
||||
|
||||
public class NoOpControllerChangeHandler extends ControllerChangeHandler {
|
||||
|
||||
@Override
|
||||
public void performChange(@NonNull ViewGroup container, @NonNull View from, @NonNull View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
|
||||
changeListener.onChangeCompleted();
|
||||
}
|
||||
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.bluelinelabs.conductor.internal
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
|
||||
class NoOpControllerChangeHandler : ControllerChangeHandler() {
|
||||
|
||||
override val isReusable = true
|
||||
|
||||
override fun performChange(
|
||||
container: ViewGroup,
|
||||
from: View?,
|
||||
to: View?,
|
||||
isPush: Boolean,
|
||||
changeListener: ControllerChangeCompletedListener
|
||||
) {
|
||||
changeListener.onChangeCompleted()
|
||||
}
|
||||
|
||||
override fun copy(): ControllerChangeHandler = NoOpControllerChangeHandler()
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
package com.bluelinelabs.conductor.internal
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import androidx.lifecycle.setViewTreeLifecycleOwner
|
||||
import androidx.savedstate.SavedStateRegistry
|
||||
import androidx.savedstate.SavedStateRegistryController
|
||||
import androidx.savedstate.SavedStateRegistryOwner
|
||||
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
|
||||
/**
|
||||
* This class sets the [ViewTreeLifecycleOwner] and [ViewTreeSavedStateRegistryOwner] which is
|
||||
* necessary for Jetpack Compose. By setting these, the view state restoration and compose lifecycle
|
||||
* play together with the lifecycle of the [Controller].
|
||||
*/
|
||||
internal class OwnViewTreeLifecycleAndRegistry private constructor(
|
||||
controller: Controller
|
||||
) : LifecycleOwner, SavedStateRegistryOwner {
|
||||
|
||||
private lateinit var lifecycleRegistry: LifecycleRegistry
|
||||
private lateinit var savedStateRegistryController: SavedStateRegistryController
|
||||
|
||||
private var hasSavedState = false
|
||||
private var savedRegistryState = Bundle.EMPTY
|
||||
override val lifecycle: LifecycleRegistry
|
||||
get() = lifecycleRegistry
|
||||
|
||||
override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry
|
||||
|
||||
init {
|
||||
controller.addLifecycleListener(object : Controller.LifecycleListener() {
|
||||
override fun preCreateView(controller: Controller) {
|
||||
hasSavedState = false
|
||||
|
||||
lifecycleRegistry = LifecycleRegistry(this@OwnViewTreeLifecycleAndRegistry)
|
||||
savedStateRegistryController = SavedStateRegistryController.create(
|
||||
this@OwnViewTreeLifecycleAndRegistry
|
||||
)
|
||||
savedStateRegistryController.performRestore(savedRegistryState)
|
||||
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
}
|
||||
|
||||
override fun postCreateView(controller: Controller, view: View) {
|
||||
/**
|
||||
* If the consumer of the library already has its own [ViewTreeLifecycleOwner] or
|
||||
* [ViewTreeSavedStateRegistryOwner] set, don't overwrite it but assume that they're doing
|
||||
* it on purpose.
|
||||
*/
|
||||
if (
|
||||
view.getTag(androidx.lifecycle.runtime.R.id.view_tree_lifecycle_owner) == null &&
|
||||
view.getTag(androidx.savedstate.R.id.view_tree_saved_state_registry_owner) == null
|
||||
) {
|
||||
view.setViewTreeLifecycleOwner(this@OwnViewTreeLifecycleAndRegistry)
|
||||
view.setViewTreeSavedStateRegistryOwner(this@OwnViewTreeLifecycleAndRegistry)
|
||||
}
|
||||
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
}
|
||||
|
||||
override fun postAttach(controller: Controller, view: View) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
}
|
||||
|
||||
override fun onChangeEnd(
|
||||
changeController: Controller,
|
||||
changeHandler: ControllerChangeHandler,
|
||||
changeType: ControllerChangeType
|
||||
) {
|
||||
// Should only happen if pushing another controller over this one was aborted
|
||||
if (
|
||||
controller === changeController &&
|
||||
changeType.isEnter &&
|
||||
changeHandler.removesFromViewOnPush &&
|
||||
changeController.view?.windowToken != null &&
|
||||
lifecycleRegistry.currentState == Lifecycle.State.STARTED
|
||||
) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChangeStart(
|
||||
changeController: Controller,
|
||||
changeHandler: ControllerChangeHandler,
|
||||
changeType: ControllerChangeType,
|
||||
) {
|
||||
pauseOnChangeStart(
|
||||
targetController = controller,
|
||||
changeController = changeController,
|
||||
changeHandler = changeHandler,
|
||||
changeType = changeType,
|
||||
)
|
||||
|
||||
GlobalChangeStartListener.onChangeStart(changeController, changeHandler, changeType)
|
||||
}
|
||||
|
||||
override fun preDetach(controller: Controller, view: View) {
|
||||
// Should only happen if pushing this controller was aborted
|
||||
if (lifecycleRegistry.currentState == Lifecycle.State.RESUMED) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
}
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(controller: Controller, outState: Bundle) {
|
||||
outState.putBundle(KEY_SAVED_STATE, savedRegistryState)
|
||||
}
|
||||
|
||||
override fun onSaveViewState(controller: Controller, outState: Bundle) {
|
||||
if (!hasSavedState) {
|
||||
savedRegistryState = Bundle()
|
||||
savedStateRegistryController.performSave(savedRegistryState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(controller: Controller, savedInstanceState: Bundle) {
|
||||
savedRegistryState = savedInstanceState.getBundle(KEY_SAVED_STATE)
|
||||
}
|
||||
|
||||
override fun preDestroyView(controller: Controller, view: View) {
|
||||
if (controller.isBeingDestroyed && controller.router.backstackSize == 0) {
|
||||
val parent = view.parent as? View
|
||||
parent?.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(v: View) = Unit
|
||||
override fun onViewDetachedFromWindow(v: View) {
|
||||
parent.removeOnAttachStateChangeListener(this)
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
}
|
||||
}
|
||||
|
||||
override fun postContextAvailable(controller: Controller, context: Context) {
|
||||
listenForAncestorChangeStart(controller)
|
||||
}
|
||||
|
||||
override fun preContextUnavailable(controller: Controller, context: Context) {
|
||||
stopListeningForAncestorChangeStart(controller)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun listenForAncestorChangeStart(controller: Controller) {
|
||||
GlobalChangeStartListener.subscribe(controller, controller.ancestors()) { ancestor, changeHandler, changeType ->
|
||||
// No-op on the case where we (the child controller) hasn't yet created a View as our parent is being
|
||||
// changed out.
|
||||
if (::lifecycleRegistry.isInitialized) {
|
||||
pauseOnChangeStart(
|
||||
targetController = ancestor,
|
||||
changeController = ancestor,
|
||||
changeHandler = changeHandler,
|
||||
changeType = changeType,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopListeningForAncestorChangeStart(controller: Controller) {
|
||||
GlobalChangeStartListener.unsubscribe(controller)
|
||||
}
|
||||
|
||||
// AbstractComposeView adds its own OnAttachStateChangeListener by default. Since it
|
||||
// does this on init, its detach callbacks get called before ours, which prevents us
|
||||
// from saving state in onDetach. The if statement in here should detect upcoming
|
||||
// detachment.
|
||||
private fun pauseOnChangeStart(
|
||||
targetController: Controller,
|
||||
changeController: Controller,
|
||||
changeHandler: ControllerChangeHandler,
|
||||
changeType: ControllerChangeType,
|
||||
) {
|
||||
if (
|
||||
targetController === changeController &&
|
||||
!changeType.isEnter &&
|
||||
changeHandler.removesFromViewOnPush &&
|
||||
changeController.view != null &&
|
||||
lifecycleRegistry.currentState == Lifecycle.State.RESUMED
|
||||
) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
|
||||
savedRegistryState = Bundle()
|
||||
savedStateRegistryController.performSave(savedRegistryState)
|
||||
|
||||
hasSavedState = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun Controller.ancestors(): Collection<String> {
|
||||
return buildList {
|
||||
var ancestor = parentController
|
||||
while (ancestor != null) {
|
||||
add(ancestor.instanceId)
|
||||
ancestor = ancestor.parentController
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_SAVED_STATE = "Registry.savedState"
|
||||
|
||||
fun own(target: Controller): OwnViewTreeLifecycleAndRegistry {
|
||||
return OwnViewTreeLifecycleAndRegistry(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In order to prevent child controllers from having strong references to all of their ancestors, some of which may
|
||||
// break their connection before the child is made aware, this shared listener is used to call all interested parties
|
||||
// when a controller begins transitioning.
|
||||
private object GlobalChangeStartListener {
|
||||
private val listeners = mutableMapOf<String, Listener>()
|
||||
|
||||
fun subscribe(
|
||||
controller: Controller,
|
||||
targetControllers: Collection<String>,
|
||||
listener: (Controller, ControllerChangeHandler, ControllerChangeType) -> Unit,
|
||||
) {
|
||||
listeners[controller.instanceId] = Listener(targetControllers, listener)
|
||||
}
|
||||
|
||||
fun unsubscribe(controller: Controller) {
|
||||
listeners.remove(controller.instanceId)
|
||||
}
|
||||
|
||||
fun onChangeStart(controller: Controller, changeHandler: ControllerChangeHandler, changeType: ControllerChangeType) {
|
||||
listeners.values.forEach { it.call(controller, changeHandler, changeType) }
|
||||
}
|
||||
|
||||
private class Listener(
|
||||
private val targetControllers: Collection<String>,
|
||||
private val listener: (Controller, ControllerChangeHandler, ControllerChangeType) -> Unit,
|
||||
) {
|
||||
fun call(controller: Controller, changeHandler: ControllerChangeHandler, changeType: ControllerChangeType) {
|
||||
if (targetControllers.contains(controller.instanceId)) {
|
||||
listener(controller, changeHandler, changeType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.bluelinelabs.conductor.internal
|
||||
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
|
||||
|
||||
@RestrictTo(LIBRARY_GROUP)
|
||||
interface RouterRequiringFunc {
|
||||
fun execute()
|
||||
}
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
package com.bluelinelabs.conductor.internal;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.SparseArray;
|
||||
|
||||
public class StringSparseArrayParceler implements Parcelable {
|
||||
|
||||
private final SparseArray<String> mStringSparseArray;
|
||||
|
||||
public StringSparseArrayParceler(SparseArray<String> stringSparseArray) {
|
||||
mStringSparseArray = stringSparseArray;
|
||||
}
|
||||
|
||||
private StringSparseArrayParceler(Parcel in) {
|
||||
mStringSparseArray = new SparseArray<>();
|
||||
|
||||
final int size = in.readInt();
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
mStringSparseArray.put(in.readInt(), in.readString());
|
||||
}
|
||||
}
|
||||
|
||||
public SparseArray<String> getStringSparseArray() {
|
||||
return mStringSparseArray;
|
||||
}
|
||||
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
final int size = mStringSparseArray.size();
|
||||
|
||||
out.writeInt(size);
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
int key = mStringSparseArray.keyAt(i);
|
||||
|
||||
out.writeInt(key);
|
||||
out.writeString(mStringSparseArray.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<StringSparseArrayParceler> CREATOR = new Parcelable.Creator<StringSparseArrayParceler>() {
|
||||
public StringSparseArrayParceler createFromParcel(Parcel in) {
|
||||
return new StringSparseArrayParceler(in);
|
||||
}
|
||||
|
||||
public StringSparseArrayParceler[] newArray(int size) {
|
||||
return new StringSparseArrayParceler[size];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.bluelinelabs.conductor.internal
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.util.SparseArray
|
||||
|
||||
internal class StringSparseArrayParceler(val stringSparseArray: SparseArray<String>) : Parcelable {
|
||||
|
||||
override fun describeContents(): Int = 0
|
||||
|
||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||
val size = stringSparseArray.size()
|
||||
out.writeInt(size)
|
||||
for (i in 0 until size) {
|
||||
val key = stringSparseArray.keyAt(i)
|
||||
out.writeInt(key)
|
||||
out.writeString(stringSparseArray[key])
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<StringSparseArrayParceler> =
|
||||
object : Parcelable.Creator<StringSparseArrayParceler> {
|
||||
override fun createFromParcel(parcel: Parcel): StringSparseArrayParceler {
|
||||
val stringSparseArray = SparseArray<String>()
|
||||
val size = parcel.readInt()
|
||||
for (i in 0 until size) {
|
||||
stringSparseArray.put(parcel.readInt(), parcel.readString())
|
||||
}
|
||||
return StringSparseArrayParceler(stringSparseArray)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<StringSparseArrayParceler?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
@file:JvmName("ThreadUtils")
|
||||
|
||||
package com.bluelinelabs.conductor.internal
|
||||
|
||||
import android.os.Looper
|
||||
import android.util.AndroidRuntimeException
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
|
||||
|
||||
@RestrictTo(LIBRARY_GROUP)
|
||||
internal fun ensureMainThread() {
|
||||
if (Looper.getMainLooper().thread !== Thread.currentThread()) {
|
||||
throw CalledFromWrongThreadException("Methods that affect the view hierarchy can can only be called from the main thread.")
|
||||
}
|
||||
}
|
||||
|
||||
private class CalledFromWrongThreadException(msg: String?) : AndroidRuntimeException(msg)
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.bluelinelabs.conductor.internal
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
|
||||
|
||||
@RestrictTo(LIBRARY_GROUP)
|
||||
class TransactionIndexer {
|
||||
|
||||
private var currentIndex = 0
|
||||
|
||||
fun nextIndex(): Int {
|
||||
return ++currentIndex
|
||||
}
|
||||
|
||||
fun saveInstanceState(outState: Bundle) {
|
||||
outState.putInt(KEY_INDEX, currentIndex)
|
||||
}
|
||||
|
||||
fun restoreInstanceState(savedInstanceState: Bundle) {
|
||||
currentIndex = savedInstanceState.getInt(KEY_INDEX)
|
||||
}
|
||||
}
|
||||
|
||||
private const val KEY_INDEX = "TransactionIndexer.currentIndex"
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.bluelinelabs.conductor.internal;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.View.OnAttachStateChangeListener;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
public class ViewAttachHandler implements OnAttachStateChangeListener {
|
||||
|
||||
private enum ReportedState {
|
||||
VIEW_DETACHED,
|
||||
ACTIVITY_STOPPED,
|
||||
ATTACHED
|
||||
}
|
||||
|
||||
public interface ViewAttachListener {
|
||||
void onAttached();
|
||||
void onDetached(boolean fromActivityStop);
|
||||
void onViewDetachAfterStop();
|
||||
}
|
||||
|
||||
private interface ChildAttachListener {
|
||||
void onAttached();
|
||||
}
|
||||
|
||||
private boolean rootAttached = false;
|
||||
boolean childrenAttached = false;
|
||||
private boolean activityStopped = false;
|
||||
private ReportedState reportedState = ReportedState.VIEW_DETACHED;
|
||||
private ViewAttachListener attachListener;
|
||||
OnAttachStateChangeListener childOnAttachStateChangeListener;
|
||||
|
||||
public ViewAttachHandler(ViewAttachListener attachListener) {
|
||||
this.attachListener = attachListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAttachedToWindow(final View v) {
|
||||
if (rootAttached) {
|
||||
return;
|
||||
}
|
||||
|
||||
rootAttached = true;
|
||||
listenForDeepestChildAttach(v, new ChildAttachListener() {
|
||||
@Override
|
||||
public void onAttached() {
|
||||
childrenAttached = true;
|
||||
reportAttached();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(View v) {
|
||||
rootAttached = false;
|
||||
if (childrenAttached) {
|
||||
childrenAttached = false;
|
||||
reportDetached(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void listenForAttach(final View view) {
|
||||
view.addOnAttachStateChangeListener(this);
|
||||
}
|
||||
|
||||
public void unregisterAttachListener(View view) {
|
||||
view.removeOnAttachStateChangeListener(this);
|
||||
|
||||
if (childOnAttachStateChangeListener != null && view instanceof ViewGroup) {
|
||||
findDeepestChild((ViewGroup)view).removeOnAttachStateChangeListener(childOnAttachStateChangeListener);
|
||||
childOnAttachStateChangeListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void onActivityStarted() {
|
||||
activityStopped = false;
|
||||
reportAttached();
|
||||
}
|
||||
|
||||
public void onActivityStopped() {
|
||||
activityStopped = true;
|
||||
reportDetached(true);
|
||||
}
|
||||
|
||||
void reportAttached() {
|
||||
if (rootAttached && childrenAttached && !activityStopped && reportedState != ReportedState.ATTACHED) {
|
||||
reportedState = ReportedState.ATTACHED;
|
||||
attachListener.onAttached();
|
||||
}
|
||||
}
|
||||
|
||||
private void reportDetached(boolean detachedForActivity) {
|
||||
boolean wasDetachedForActivity = reportedState == ReportedState.ACTIVITY_STOPPED;
|
||||
|
||||
if (detachedForActivity) {
|
||||
reportedState = ReportedState.ACTIVITY_STOPPED;
|
||||
} else {
|
||||
reportedState = ReportedState.VIEW_DETACHED;
|
||||
}
|
||||
|
||||
if (wasDetachedForActivity && !detachedForActivity) {
|
||||
attachListener.onViewDetachAfterStop();
|
||||
} else {
|
||||
attachListener.onDetached(detachedForActivity);
|
||||
}
|
||||
}
|
||||
|
||||
private void listenForDeepestChildAttach(final View view, final ChildAttachListener attachListener) {
|
||||
if (!(view instanceof ViewGroup)) {
|
||||
attachListener.onAttached();
|
||||
return;
|
||||
}
|
||||
|
||||
ViewGroup viewGroup = (ViewGroup)view;
|
||||
if (viewGroup.getChildCount() == 0) {
|
||||
attachListener.onAttached();
|
||||
return;
|
||||
}
|
||||
|
||||
childOnAttachStateChangeListener = new OnAttachStateChangeListener() {
|
||||
boolean attached = false;
|
||||
|
||||
@Override
|
||||
public void onViewAttachedToWindow(View v) {
|
||||
if (!attached && childOnAttachStateChangeListener != null) {
|
||||
attached = true;
|
||||
attachListener.onAttached();
|
||||
v.removeOnAttachStateChangeListener(this);
|
||||
childOnAttachStateChangeListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(View v) { }
|
||||
};
|
||||
findDeepestChild(viewGroup).addOnAttachStateChangeListener(childOnAttachStateChangeListener);
|
||||
}
|
||||
|
||||
private View findDeepestChild(ViewGroup viewGroup) {
|
||||
if (viewGroup.getChildCount() == 0) {
|
||||
return viewGroup;
|
||||
}
|
||||
|
||||
View lastChild = viewGroup.getChildAt(viewGroup.getChildCount() - 1);
|
||||
if (lastChild instanceof ViewGroup) {
|
||||
return findDeepestChild((ViewGroup)lastChild);
|
||||
} else {
|
||||
return lastChild;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package com.bluelinelabs.conductor.util;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
@TargetApi(VERSION_CODES.LOLLIPOP)
|
||||
public class ViewUtils {
|
||||
|
||||
public static View findViewWithTransitionName(@NonNull String name, @NonNull View view) {
|
||||
if (name.equals(view.getTransitionName())) {
|
||||
return view;
|
||||
}
|
||||
|
||||
if (view instanceof ViewGroup) {
|
||||
View namedView = findViewWithTransitionNameInGroup(name, (ViewGroup)view);
|
||||
|
||||
if (namedView != null) {
|
||||
return namedView;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static View findViewWithTransitionNameInGroup(@NonNull String name, @NonNull ViewGroup viewGroup) {
|
||||
for (int i = 0; i < viewGroup.getChildCount(); i++) {
|
||||
View namedView = findViewWithTransitionName(name, viewGroup.getChildAt(i));
|
||||
if (namedView != null) {
|
||||
return namedView;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
-keepclassmembers public class * extends com.bluelinelabs.conductor.Controller {
|
||||
public <init>();
|
||||
public <init>(android.os.Bundle);
|
||||
}
|
||||
|
||||
-keepclassmembers public class * extends com.bluelinelabs.conductor.ControllerChangeHandler {
|
||||
public <init>();
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
package com.bluelinelabs.conductor;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class BackstackTests {
|
||||
|
||||
private Backstack mBackstack;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
mBackstack = new Backstack();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPush() {
|
||||
Assert.assertEquals(0, mBackstack.size());
|
||||
mBackstack.push(RouterTransaction.builder(new TestController()).build());
|
||||
Assert.assertEquals(1, mBackstack.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPop() {
|
||||
mBackstack.push(RouterTransaction.builder(new TestController()).build());
|
||||
mBackstack.push(RouterTransaction.builder(new TestController()).build());
|
||||
Assert.assertEquals(2, mBackstack.size());
|
||||
mBackstack.pop();
|
||||
Assert.assertEquals(1, mBackstack.size());
|
||||
mBackstack.pop();
|
||||
Assert.assertEquals(0, mBackstack.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPeek() {
|
||||
RouterTransaction transaction1 = RouterTransaction.builder(new TestController()).build();
|
||||
RouterTransaction transaction2 = RouterTransaction.builder(new TestController()).build();
|
||||
|
||||
mBackstack.push(transaction1);
|
||||
Assert.assertEquals(transaction1, mBackstack.peek());
|
||||
|
||||
mBackstack.push(transaction2);
|
||||
Assert.assertEquals(transaction2, mBackstack.peek());
|
||||
|
||||
mBackstack.pop();
|
||||
Assert.assertEquals(transaction1, mBackstack.peek());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPopTo() {
|
||||
RouterTransaction transaction1 = RouterTransaction.builder(new TestController()).build();
|
||||
RouterTransaction transaction2 = RouterTransaction.builder(new TestController()).build();
|
||||
RouterTransaction transaction3 = RouterTransaction.builder(new TestController()).build();
|
||||
|
||||
mBackstack.push(transaction1);
|
||||
mBackstack.push(transaction2);
|
||||
mBackstack.push(transaction3);
|
||||
|
||||
Assert.assertEquals(3, mBackstack.size());
|
||||
|
||||
mBackstack.popTo(transaction1);
|
||||
|
||||
Assert.assertEquals(1, mBackstack.size());
|
||||
Assert.assertEquals(transaction1, mBackstack.peek());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.bluelinelabs.conductor
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class BackstackTests {
|
||||
|
||||
private val backstack = Backstack()
|
||||
|
||||
@Test
|
||||
fun testPush() {
|
||||
assertEquals(0, backstack.size.toLong())
|
||||
backstack.push(TestController().asTransaction())
|
||||
assertEquals(1, backstack.size.toLong())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPop() {
|
||||
backstack.push(TestController().asTransaction())
|
||||
backstack.push(TestController().asTransaction())
|
||||
assertEquals(2, backstack.size.toLong())
|
||||
|
||||
backstack.pop()
|
||||
assertEquals(1, backstack.size.toLong())
|
||||
|
||||
backstack.pop()
|
||||
assertEquals(0, backstack.size.toLong())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPeek() {
|
||||
val transaction1 = TestController().asTransaction()
|
||||
val transaction2 = TestController().asTransaction()
|
||||
|
||||
backstack.push(transaction1)
|
||||
assertEquals(transaction1, backstack.peek())
|
||||
|
||||
backstack.push(transaction2)
|
||||
assertEquals(transaction2, backstack.peek())
|
||||
|
||||
backstack.pop()
|
||||
assertEquals(transaction1, backstack.peek())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPopTo() {
|
||||
val transaction1 = TestController().asTransaction()
|
||||
val transaction2 = TestController().asTransaction()
|
||||
val transaction3 = TestController().asTransaction()
|
||||
|
||||
backstack.push(transaction1)
|
||||
backstack.push(transaction2)
|
||||
backstack.push(transaction3)
|
||||
assertEquals(3, backstack.size.toLong())
|
||||
|
||||
backstack.popTo(transaction1)
|
||||
assertEquals(1, backstack.size.toLong())
|
||||
assertEquals(transaction1, backstack.peek())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.bluelinelabs.conductor;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
|
||||
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class ControllerChangeHandlerTests {
|
||||
|
||||
@Test
|
||||
public void testSaveRestore() {
|
||||
HorizontalChangeHandler horizontalChangeHandler = new HorizontalChangeHandler();
|
||||
FadeChangeHandler fadeChangeHandler = new FadeChangeHandler(120, false);
|
||||
|
||||
RouterTransaction transaction = RouterTransaction.with(new TestController())
|
||||
.pushChangeHandler(horizontalChangeHandler)
|
||||
.popChangeHandler(fadeChangeHandler);
|
||||
RouterTransaction restoredTransaction = new RouterTransaction(transaction.saveInstanceState());
|
||||
|
||||
ControllerChangeHandler restoredHorizontal = restoredTransaction.pushChangeHandler();
|
||||
ControllerChangeHandler restoredFade = restoredTransaction.popChangeHandler();
|
||||
|
||||
assertEquals(horizontalChangeHandler.getClass(), restoredHorizontal.getClass());
|
||||
assertEquals(fadeChangeHandler.getClass(), restoredFade.getClass());
|
||||
|
||||
HorizontalChangeHandler restoredHorizontalCast = (HorizontalChangeHandler) restoredHorizontal;
|
||||
FadeChangeHandler restoredFadeCast = (FadeChangeHandler) restoredFade;
|
||||
|
||||
assertEquals(horizontalChangeHandler.getAnimationDuration(), restoredHorizontalCast.getAnimationDuration());
|
||||
assertEquals(horizontalChangeHandler.getRemovesFromViewOnPush(), restoredHorizontalCast.getRemovesFromViewOnPush());
|
||||
|
||||
assertEquals(fadeChangeHandler.getAnimationDuration(), restoredFadeCast.getAnimationDuration());
|
||||
assertEquals(fadeChangeHandler.getRemovesFromViewOnPush(), restoredFadeCast.getRemovesFromViewOnPush());
|
||||
}
|
||||
|
||||
}
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
package com.bluelinelabs.conductor
|
||||
|
||||
import android.os.Looper.getMainLooper
|
||||
import android.view.View
|
||||
import com.bluelinelabs.conductor.Controller.LifecycleListener
|
||||
import com.bluelinelabs.conductor.util.MockChangeHandler
|
||||
import com.bluelinelabs.conductor.util.TestActivity
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE)
|
||||
class ControllerLifecycleActivityReferenceTests {
|
||||
|
||||
private val activityController = Robolectric.buildActivity(TestActivity::class.java).setup()
|
||||
private val activity = activityController.get()
|
||||
|
||||
@Test
|
||||
fun testSingleControllerActivityOnPush() {
|
||||
val controller = TestController()
|
||||
Assert.assertNull(controller.activity)
|
||||
|
||||
val listener = ActivityReferencingLifecycleListener()
|
||||
controller.addLifecycleListener(listener)
|
||||
|
||||
activity.router.pushController(
|
||||
controller.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
popChangeHandler = MockChangeHandler.defaultHandler()
|
||||
)
|
||||
)
|
||||
|
||||
Assert.assertEquals(listOf(true), listener.changeEndReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postAttachReferences)
|
||||
Assert.assertEquals(emptyList<Any>(), listener.postDetachReferences)
|
||||
Assert.assertEquals(emptyList<Any>(), listener.postDestroyViewReferences)
|
||||
Assert.assertEquals(emptyList<Any>(), listener.postDestroyReferences)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChildControllerActivityOnPush() {
|
||||
val parent = TestController()
|
||||
activity.router.pushController(
|
||||
parent.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
popChangeHandler = MockChangeHandler.defaultHandler()
|
||||
)
|
||||
)
|
||||
|
||||
val child = TestController()
|
||||
Assert.assertNull(child.activity)
|
||||
|
||||
val listener = ActivityReferencingLifecycleListener()
|
||||
child.addLifecycleListener(listener)
|
||||
|
||||
val childRouter = parent.getChildRouter((parent.view!!.findViewById(TestController.VIEW_ID)))
|
||||
childRouter.pushController(
|
||||
child.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
popChangeHandler = MockChangeHandler.defaultHandler()
|
||||
)
|
||||
)
|
||||
|
||||
Assert.assertEquals(listOf(true), listener.changeEndReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postAttachReferences)
|
||||
Assert.assertEquals(emptyList<Any>(), listener.postDetachReferences)
|
||||
Assert.assertEquals(emptyList<Any>(), listener.postDestroyViewReferences)
|
||||
Assert.assertEquals(emptyList<Any>(), listener.postDestroyReferences)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSingleControllerActivityOnPop() {
|
||||
val controller = TestController()
|
||||
val listener = ActivityReferencingLifecycleListener()
|
||||
controller.addLifecycleListener(listener)
|
||||
|
||||
activity.router.pushController(
|
||||
controller.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
popChangeHandler = MockChangeHandler.defaultHandler()
|
||||
)
|
||||
)
|
||||
activity.router.popCurrentController()
|
||||
|
||||
Assert.assertEquals(listOf(true, true), listener.changeEndReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postAttachReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDetachReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDestroyViewReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDestroyReferences)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChildControllerActivityOnPop() {
|
||||
val parent = TestController()
|
||||
activity.router.pushController(
|
||||
parent.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
popChangeHandler = MockChangeHandler.defaultHandler()
|
||||
)
|
||||
)
|
||||
|
||||
val child = TestController()
|
||||
val listener = ActivityReferencingLifecycleListener()
|
||||
child.addLifecycleListener(listener)
|
||||
|
||||
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
|
||||
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
|
||||
childRouter.pushController(
|
||||
child.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
popChangeHandler = MockChangeHandler.defaultHandler()
|
||||
)
|
||||
)
|
||||
childRouter.popCurrentController()
|
||||
|
||||
shadowOf(getMainLooper()).idle()
|
||||
|
||||
Assert.assertEquals(listOf(true, true), listener.changeEndReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postAttachReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDetachReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDestroyViewReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDestroyReferences)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChildControllerActivityOnParentPop() {
|
||||
val parent = TestController()
|
||||
activity.router.pushController(
|
||||
parent.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
popChangeHandler = MockChangeHandler.defaultHandler()
|
||||
)
|
||||
)
|
||||
|
||||
val child = TestController()
|
||||
val listener = ActivityReferencingLifecycleListener()
|
||||
child.addLifecycleListener(listener)
|
||||
|
||||
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
|
||||
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
|
||||
childRouter.pushController(
|
||||
child.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
popChangeHandler = MockChangeHandler.defaultHandler()
|
||||
)
|
||||
)
|
||||
activity.router.popCurrentController()
|
||||
|
||||
Assert.assertEquals(listOf(true, true), listener.changeEndReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postAttachReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDetachReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDestroyViewReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDestroyReferences)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSingleControllerActivityOnDestroy() {
|
||||
val controller = TestController()
|
||||
val listener = ActivityReferencingLifecycleListener()
|
||||
controller.addLifecycleListener(listener)
|
||||
|
||||
activity.router.pushController(
|
||||
controller.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
popChangeHandler = MockChangeHandler.defaultHandler()
|
||||
)
|
||||
)
|
||||
activityController.pause().stop().destroy()
|
||||
|
||||
Assert.assertEquals(listOf(true), listener.changeEndReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postAttachReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDetachReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDestroyViewReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDestroyReferences)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChildControllerActivityOnDestroy() {
|
||||
val parent = TestController()
|
||||
activity.router.pushController(
|
||||
parent.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
popChangeHandler = MockChangeHandler.defaultHandler()
|
||||
)
|
||||
)
|
||||
|
||||
val child = TestController()
|
||||
val listener = ActivityReferencingLifecycleListener()
|
||||
child.addLifecycleListener(listener)
|
||||
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
|
||||
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
|
||||
childRouter.pushController(
|
||||
child.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
popChangeHandler = MockChangeHandler.defaultHandler()
|
||||
)
|
||||
)
|
||||
activityController.pause().stop().destroy()
|
||||
|
||||
Assert.assertEquals(listOf(true, true), listener.changeEndReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postAttachReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDetachReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDestroyViewReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDestroyReferences)
|
||||
}
|
||||
|
||||
internal class ActivityReferencingLifecycleListener : LifecycleListener() {
|
||||
val changeEndReferences = mutableListOf<Boolean>()
|
||||
val postCreateViewReferences = mutableListOf<Boolean>()
|
||||
val postAttachReferences = mutableListOf<Boolean>()
|
||||
val postDetachReferences = mutableListOf<Boolean>()
|
||||
val postDestroyViewReferences = mutableListOf<Boolean>()
|
||||
val postDestroyReferences = mutableListOf<Boolean>()
|
||||
|
||||
override fun onChangeEnd(
|
||||
controller: Controller,
|
||||
changeHandler: ControllerChangeHandler,
|
||||
changeType: ControllerChangeType
|
||||
) {
|
||||
changeEndReferences.add(controller.activity != null)
|
||||
}
|
||||
|
||||
override fun postCreateView(controller: Controller, view: View) {
|
||||
postCreateViewReferences.add(controller.activity != null)
|
||||
}
|
||||
|
||||
override fun postAttach(controller: Controller, view: View) {
|
||||
postAttachReferences.add(controller.activity != null)
|
||||
}
|
||||
|
||||
override fun postDetach(controller: Controller, view: View) {
|
||||
postDetachReferences.add(controller.activity != null)
|
||||
}
|
||||
|
||||
override fun postDestroyView(controller: Controller) {
|
||||
postDestroyViewReferences.add(controller.activity != null)
|
||||
}
|
||||
|
||||
override fun postDestroy(controller: Controller) {
|
||||
postDestroyReferences.add(controller.activity != null)
|
||||
}
|
||||
}
|
||||
}
|
||||
+704
@@ -0,0 +1,704 @@
|
||||
package com.bluelinelabs.conductor
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.Looper.getMainLooper
|
||||
import android.view.View
|
||||
import com.bluelinelabs.conductor.Controller.LifecycleListener
|
||||
import com.bluelinelabs.conductor.Controller.RetainViewMode
|
||||
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler
|
||||
import com.bluelinelabs.conductor.util.CallState
|
||||
import com.bluelinelabs.conductor.util.MockChangeHandler
|
||||
import com.bluelinelabs.conductor.util.MockChangeHandler.ChangeHandlerListener
|
||||
import com.bluelinelabs.conductor.util.TestActivity
|
||||
import com.bluelinelabs.conductor.util.ViewUtils
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
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.android.controller.ActivityController
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE)
|
||||
class ControllerLifecycleCallbacksTests {
|
||||
|
||||
private lateinit var activityController: ActivityController<TestActivity>
|
||||
private lateinit var currentCallState: CallState
|
||||
|
||||
private fun createActivityController(savedInstanceState: Bundle?, includeStartAndResume: Boolean) {
|
||||
activityController = Robolectric.buildActivity(TestActivity::class.java)
|
||||
|
||||
activityController.create(savedInstanceState)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
activityController.restoreInstanceState(savedInstanceState)
|
||||
}
|
||||
|
||||
if (includeStartAndResume) {
|
||||
activityController
|
||||
.start()
|
||||
.postCreate(savedInstanceState)
|
||||
.resume()
|
||||
.visible()
|
||||
}
|
||||
|
||||
if (!activityController.get().router.hasRootController()) {
|
||||
activityController.get().router.setRoot(TestController().asTransaction())
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
createActivityController(null, true)
|
||||
currentCallState = CallState(false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNormalLifecycle() {
|
||||
val controller = TestController()
|
||||
attachLifecycleListener(controller)
|
||||
val expectedCallState = CallState(false)
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.get().router.pushController(
|
||||
controller.asTransaction(
|
||||
pushChangeHandler = getPushHandler(expectedCallState, controller),
|
||||
popChangeHandler = getPopHandler(expectedCallState, controller)
|
||||
)
|
||||
)
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.get().router.popCurrentController()
|
||||
Assert.assertNull(controller.view)
|
||||
assertCalls(expectedCallState, controller)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLifecycleWithActivityStop() {
|
||||
val controller = TestController()
|
||||
attachLifecycleListener(controller)
|
||||
val expectedCallState = CallState(false)
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.get().router.pushController(
|
||||
controller.asTransaction(
|
||||
pushChangeHandler = getPushHandler(expectedCallState, controller)
|
||||
)
|
||||
)
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.get().destroying = true
|
||||
activityController.pause()
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.stop()
|
||||
expectedCallState.detachCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
Assert.assertNotNull(controller.view)
|
||||
|
||||
ViewUtils.reportAttached(controller.view, false)
|
||||
expectedCallState.saveViewStateCalls++
|
||||
expectedCallState.destroyViewCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLifecycleWithActivityDestroy() {
|
||||
val controller = TestController()
|
||||
attachLifecycleListener(controller)
|
||||
val expectedCallState = CallState(false)
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.get().router.pushController(
|
||||
controller.asTransaction(
|
||||
pushChangeHandler = getPushHandler(expectedCallState, controller)
|
||||
)
|
||||
)
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.get().destroying = true
|
||||
activityController.pause()
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.stop()
|
||||
expectedCallState.detachCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.destroy()
|
||||
expectedCallState.destroyViewCalls++
|
||||
expectedCallState.contextUnavailableCalls++
|
||||
expectedCallState.destroyCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLifecycleWithActivityConfigurationChange() {
|
||||
var controller = TestController()
|
||||
attachLifecycleListener(controller)
|
||||
val expectedCallState = CallState(false)
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.get().router.pushController(
|
||||
RouterTransaction.with(controller)
|
||||
.pushChangeHandler(getPushHandler(expectedCallState, controller))
|
||||
.tag("root")
|
||||
)
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.get().changingConfigurations = true
|
||||
val bundle = Bundle()
|
||||
activityController.saveInstanceState(bundle)
|
||||
expectedCallState.saveViewStateCalls++
|
||||
expectedCallState.saveInstanceStateCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.pause()
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.stop()
|
||||
expectedCallState.detachCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.destroy()
|
||||
expectedCallState.destroyViewCalls++
|
||||
expectedCallState.contextUnavailableCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
createActivityController(bundle, false)
|
||||
controller = activityController.get().router.getControllerWithTag("root") as TestController
|
||||
expectedCallState.contextAvailableCalls++
|
||||
expectedCallState.restoreInstanceStateCalls++
|
||||
expectedCallState.restoreViewStateCalls++
|
||||
expectedCallState.changeStartCalls++
|
||||
expectedCallState.createViewCalls++
|
||||
|
||||
// Lifecycle listener isn't attached during restore, grab the current views from the controller for this stuff...
|
||||
currentCallState.restoreInstanceStateCalls = controller.currentCallState.restoreInstanceStateCalls
|
||||
currentCallState.restoreViewStateCalls = controller.currentCallState.restoreViewStateCalls
|
||||
currentCallState.changeStartCalls = controller.currentCallState.changeStartCalls
|
||||
currentCallState.changeEndCalls = controller.currentCallState.changeEndCalls
|
||||
currentCallState.createViewCalls = controller.currentCallState.createViewCalls
|
||||
currentCallState.attachCalls = controller.currentCallState.attachCalls
|
||||
currentCallState.contextAvailableCalls = controller.currentCallState.contextAvailableCalls
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController
|
||||
.start()
|
||||
.postCreate(bundle)
|
||||
.resume()
|
||||
.visible()
|
||||
|
||||
currentCallState.changeEndCalls = controller.currentCallState.changeEndCalls
|
||||
currentCallState.attachCalls = controller.currentCallState.attachCalls
|
||||
expectedCallState.changeEndCalls++
|
||||
expectedCallState.attachCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.resume()
|
||||
assertCalls(expectedCallState, controller)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLifecycleWithActivityBackground() {
|
||||
val controller = TestController()
|
||||
attachLifecycleListener(controller)
|
||||
val expectedCallState = CallState(false)
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.get().router.pushController(
|
||||
controller.asTransaction(
|
||||
pushChangeHandler = getPushHandler(expectedCallState, controller)
|
||||
)
|
||||
)
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.pause()
|
||||
val bundle = Bundle()
|
||||
activityController.saveInstanceState(bundle)
|
||||
expectedCallState.saveInstanceStateCalls++
|
||||
expectedCallState.saveViewStateCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
activityController.resume()
|
||||
assertCalls(expectedCallState, controller)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLifecycleCallOrder() {
|
||||
val testController = TestController()
|
||||
val callState = CallState(false)
|
||||
testController.addLifecycleListener(object : LifecycleListener() {
|
||||
override fun preCreateView(controller: Controller) {
|
||||
callState.createViewCalls++
|
||||
Assert.assertEquals(1, callState.createViewCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.createViewCalls)
|
||||
Assert.assertEquals(0, callState.attachCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.attachCalls)
|
||||
Assert.assertEquals(0, callState.detachCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.detachCalls)
|
||||
Assert.assertEquals(0, callState.destroyViewCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls)
|
||||
Assert.assertEquals(0, callState.destroyCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
|
||||
}
|
||||
|
||||
override fun postCreateView(controller: Controller, view: View) {
|
||||
callState.createViewCalls++
|
||||
Assert.assertEquals(2, callState.createViewCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
|
||||
Assert.assertEquals(0, callState.attachCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.attachCalls)
|
||||
Assert.assertEquals(0, callState.detachCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.detachCalls)
|
||||
Assert.assertEquals(0, callState.destroyViewCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls)
|
||||
Assert.assertEquals(0, callState.destroyCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
|
||||
}
|
||||
|
||||
override fun preAttach(controller: Controller, view: View) {
|
||||
callState.attachCalls++
|
||||
Assert.assertEquals(2, callState.createViewCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
|
||||
Assert.assertEquals(1, callState.attachCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.attachCalls)
|
||||
Assert.assertEquals(0, callState.detachCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.detachCalls)
|
||||
Assert.assertEquals(0, callState.destroyViewCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls)
|
||||
Assert.assertEquals(0, callState.destroyCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
|
||||
}
|
||||
|
||||
override fun postAttach(controller: Controller, view: View) {
|
||||
callState.attachCalls++
|
||||
Assert.assertEquals(2, callState.createViewCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
|
||||
Assert.assertEquals(2, callState.attachCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.attachCalls)
|
||||
Assert.assertEquals(0, callState.detachCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.detachCalls)
|
||||
Assert.assertEquals(0, callState.destroyViewCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls)
|
||||
Assert.assertEquals(0, callState.destroyCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
|
||||
}
|
||||
|
||||
override fun preDetach(controller: Controller, view: View) {
|
||||
callState.detachCalls++
|
||||
Assert.assertEquals(2, callState.createViewCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
|
||||
Assert.assertEquals(2, callState.attachCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.attachCalls)
|
||||
Assert.assertEquals(1, callState.detachCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.detachCalls)
|
||||
Assert.assertEquals(0, callState.destroyViewCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls)
|
||||
Assert.assertEquals(0, callState.destroyCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
|
||||
}
|
||||
|
||||
override fun postDetach(controller: Controller, view: View) {
|
||||
callState.detachCalls++
|
||||
Assert.assertEquals(2, callState.createViewCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
|
||||
Assert.assertEquals(2, callState.attachCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.attachCalls)
|
||||
Assert.assertEquals(2, callState.detachCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.detachCalls)
|
||||
Assert.assertEquals(0, callState.destroyViewCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls)
|
||||
Assert.assertEquals(0, callState.destroyCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
|
||||
}
|
||||
|
||||
override fun preDestroyView(controller: Controller, view: View) {
|
||||
callState.destroyViewCalls++
|
||||
Assert.assertEquals(2, callState.createViewCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
|
||||
Assert.assertEquals(2, callState.attachCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.attachCalls)
|
||||
Assert.assertEquals(2, callState.detachCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.detachCalls)
|
||||
Assert.assertEquals(1, callState.destroyViewCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls)
|
||||
Assert.assertEquals(0, callState.destroyCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
|
||||
}
|
||||
|
||||
override fun postDestroyView(controller: Controller) {
|
||||
callState.destroyViewCalls++
|
||||
Assert.assertEquals(2, callState.createViewCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
|
||||
Assert.assertEquals(2, callState.attachCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.attachCalls)
|
||||
Assert.assertEquals(2, callState.detachCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.detachCalls)
|
||||
Assert.assertEquals(2, callState.destroyViewCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.destroyViewCalls)
|
||||
Assert.assertEquals(0, callState.destroyCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
|
||||
}
|
||||
|
||||
override fun preDestroy(controller: Controller) {
|
||||
callState.destroyCalls++
|
||||
Assert.assertEquals(2, callState.createViewCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
|
||||
Assert.assertEquals(2, callState.attachCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.attachCalls)
|
||||
Assert.assertEquals(2, callState.detachCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.detachCalls)
|
||||
Assert.assertEquals(2, callState.destroyViewCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.destroyViewCalls)
|
||||
Assert.assertEquals(1, callState.destroyCalls)
|
||||
Assert.assertEquals(0, testController.currentCallState.destroyCalls)
|
||||
}
|
||||
|
||||
override fun postDestroy(controller: Controller) {
|
||||
callState.destroyCalls++
|
||||
Assert.assertEquals(2, callState.createViewCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.createViewCalls)
|
||||
Assert.assertEquals(2, callState.attachCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.attachCalls)
|
||||
Assert.assertEquals(2, callState.detachCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.detachCalls)
|
||||
Assert.assertEquals(2, callState.destroyViewCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.destroyViewCalls)
|
||||
Assert.assertEquals(2, callState.destroyCalls)
|
||||
Assert.assertEquals(1, testController.currentCallState.destroyCalls)
|
||||
}
|
||||
})
|
||||
activityController.get().router.pushController(
|
||||
testController.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
popChangeHandler = MockChangeHandler.defaultHandler()
|
||||
)
|
||||
)
|
||||
activityController.get().router.popController(testController)
|
||||
Assert.assertEquals(2, callState.createViewCalls)
|
||||
Assert.assertEquals(2, callState.attachCalls)
|
||||
Assert.assertEquals(2, callState.detachCalls)
|
||||
Assert.assertEquals(2, callState.destroyViewCalls)
|
||||
Assert.assertEquals(2, callState.destroyCalls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLifecycleWhenPopNonCurrentController() {
|
||||
val controller1Tag = "controller1"
|
||||
val controller2Tag = "controller2"
|
||||
val controller3Tag = "controller3"
|
||||
val controller1 = TestController()
|
||||
val controller2 = TestController()
|
||||
val controller3 = TestController()
|
||||
activityController.get().router.pushController(
|
||||
RouterTransaction.with(controller1).tag(controller1Tag)
|
||||
)
|
||||
activityController.get().router.pushController(
|
||||
RouterTransaction.with(controller2).tag(controller2Tag)
|
||||
)
|
||||
activityController.get().router.pushController(
|
||||
RouterTransaction.with(controller3).tag(controller3Tag)
|
||||
)
|
||||
activityController.get().router.popController(controller2)
|
||||
Assert.assertEquals(1, controller2.currentCallState.attachCalls)
|
||||
Assert.assertEquals(1, controller2.currentCallState.createViewCalls)
|
||||
Assert.assertEquals(1, controller2.currentCallState.detachCalls)
|
||||
Assert.assertEquals(1, controller2.currentCallState.destroyViewCalls)
|
||||
Assert.assertEquals(1, controller2.currentCallState.destroyCalls)
|
||||
Assert.assertEquals(1, controller2.currentCallState.contextAvailableCalls)
|
||||
Assert.assertEquals(1, controller2.currentCallState.contextUnavailableCalls)
|
||||
Assert.assertEquals(1, controller2.currentCallState.saveViewStateCalls)
|
||||
Assert.assertEquals(0, controller2.currentCallState.restoreViewStateCalls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChildLifecycle() {
|
||||
val parent = TestController()
|
||||
activityController.get().router.pushController(
|
||||
parent.asTransaction(pushChangeHandler = MockChangeHandler.defaultHandler())
|
||||
)
|
||||
|
||||
val child = TestController()
|
||||
attachLifecycleListener(child)
|
||||
val expectedCallState = CallState(false)
|
||||
assertCalls(expectedCallState, child)
|
||||
|
||||
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
|
||||
childRouter.setRoot(
|
||||
child.asTransaction(
|
||||
pushChangeHandler = getPushHandler(expectedCallState, child),
|
||||
popChangeHandler = getPopHandler(expectedCallState, child)
|
||||
)
|
||||
)
|
||||
assertCalls(expectedCallState, child)
|
||||
|
||||
parent.removeChildRouter(childRouter)
|
||||
assertCalls(expectedCallState, child)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChildLifecycle2() {
|
||||
val parent = TestController()
|
||||
activityController.get().router.pushController(
|
||||
parent.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
popChangeHandler = MockChangeHandler.defaultHandler()
|
||||
)
|
||||
)
|
||||
|
||||
val child = TestController()
|
||||
attachLifecycleListener(child)
|
||||
val expectedCallState = CallState(false)
|
||||
assertCalls(expectedCallState, child)
|
||||
|
||||
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
|
||||
childRouter.setRoot(
|
||||
child.asTransaction(
|
||||
pushChangeHandler = getPushHandler(expectedCallState, child),
|
||||
popChangeHandler = getPopHandler(expectedCallState, child)
|
||||
)
|
||||
)
|
||||
assertCalls(expectedCallState, child)
|
||||
|
||||
activityController.get().router.popCurrentController()
|
||||
expectedCallState.changeStartCalls++
|
||||
expectedCallState.changeEndCalls++
|
||||
expectedCallState.detachCalls++
|
||||
expectedCallState.destroyViewCalls++
|
||||
expectedCallState.contextUnavailableCalls++
|
||||
expectedCallState.destroyCalls++
|
||||
assertCalls(expectedCallState, child)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChildLifecycleOrderingAfterUnexpectedAttach() {
|
||||
val parent = TestController()
|
||||
parent.retainViewMode = RetainViewMode.RETAIN_DETACH
|
||||
activityController.get().router.pushController(
|
||||
parent.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
popChangeHandler = MockChangeHandler.defaultHandler()
|
||||
)
|
||||
)
|
||||
|
||||
val child = TestController()
|
||||
child.retainViewMode = RetainViewMode.RETAIN_DETACH
|
||||
val childRouter = parent.getChildRouter(parent.getView()!!.findViewById(TestController.VIEW_ID))
|
||||
childRouter.setRoot(
|
||||
child.asTransaction(
|
||||
pushChangeHandler = SimpleSwapChangeHandler(),
|
||||
popChangeHandler = SimpleSwapChangeHandler()
|
||||
)
|
||||
)
|
||||
Assert.assertTrue(parent.isAttached)
|
||||
Assert.assertTrue(child.isAttached)
|
||||
|
||||
ViewUtils.reportAttached(parent.view, false, true)
|
||||
Assert.assertFalse(parent.isAttached)
|
||||
Assert.assertFalse(child.isAttached)
|
||||
|
||||
ViewUtils.reportAttached(child.view, true)
|
||||
Assert.assertFalse(parent.isAttached)
|
||||
Assert.assertFalse(child.isAttached)
|
||||
|
||||
ViewUtils.reportAttached(parent.view, true)
|
||||
Assert.assertTrue(parent.isAttached)
|
||||
Assert.assertTrue(child.isAttached)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChildLifecycleAfterPushAndPop() {
|
||||
val parent = TestController()
|
||||
parent.retainViewMode = RetainViewMode.RETAIN_DETACH
|
||||
activityController.get().router.pushController(
|
||||
parent.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
popChangeHandler = MockChangeHandler.defaultHandler()
|
||||
)
|
||||
)
|
||||
|
||||
val child = TestController()
|
||||
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
|
||||
childRouter.setRoot(
|
||||
child.asTransaction(
|
||||
pushChangeHandler = SimpleSwapChangeHandler(),
|
||||
popChangeHandler = SimpleSwapChangeHandler()
|
||||
)
|
||||
)
|
||||
|
||||
val nextController = TestController()
|
||||
activityController.get().router.pushController(nextController.asTransaction())
|
||||
activityController.get().router.popCurrentController()
|
||||
|
||||
shadowOf(getMainLooper()).idle()
|
||||
|
||||
Assert.assertTrue(parent.isAttached)
|
||||
Assert.assertTrue(child.isAttached)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChildLifecycleAfterPushPopPush() {
|
||||
val parent = TestController()
|
||||
parent.retainViewMode = RetainViewMode.RETAIN_DETACH
|
||||
activityController.get().router.pushController(
|
||||
parent.asTransaction(
|
||||
pushChangeHandler = MockChangeHandler.defaultHandler(),
|
||||
popChangeHandler = MockChangeHandler.defaultHandler()
|
||||
)
|
||||
)
|
||||
|
||||
val child = TestController()
|
||||
val childRouter = parent.getChildRouter(parent.getView()!!.findViewById(TestController.VIEW_ID))
|
||||
childRouter.setRoot(
|
||||
child.asTransaction(
|
||||
pushChangeHandler = SimpleSwapChangeHandler(),
|
||||
popChangeHandler = SimpleSwapChangeHandler()
|
||||
)
|
||||
)
|
||||
|
||||
val nextController = TestController()
|
||||
activityController.get().router.pushController(nextController.asTransaction())
|
||||
|
||||
val child2 = TestController()
|
||||
childRouter.pushController(child2.asTransaction())
|
||||
activityController.get().router.popCurrentController()
|
||||
|
||||
shadowOf(getMainLooper()).idle()
|
||||
|
||||
Assert.assertTrue(parent.isAttached)
|
||||
Assert.assertFalse(child.isAttached)
|
||||
Assert.assertTrue(child2.isAttached)
|
||||
}
|
||||
|
||||
private fun getPushHandler(
|
||||
expectedCallState: CallState,
|
||||
controller: TestController
|
||||
): MockChangeHandler {
|
||||
return MockChangeHandler.listeningChangeHandler(object : ChangeHandlerListener() {
|
||||
override fun willStartChange() {
|
||||
expectedCallState.contextAvailableCalls++
|
||||
expectedCallState.changeStartCalls++
|
||||
expectedCallState.createViewCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
}
|
||||
|
||||
override fun didAttachOrDetach() {
|
||||
expectedCallState.attachCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
}
|
||||
|
||||
override fun didEndChange() {
|
||||
expectedCallState.changeEndCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun getPopHandler(
|
||||
expectedCallState: CallState,
|
||||
controller: TestController
|
||||
): MockChangeHandler {
|
||||
return MockChangeHandler.listeningChangeHandler(object : ChangeHandlerListener() {
|
||||
override fun willStartChange() {
|
||||
expectedCallState.changeStartCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
}
|
||||
|
||||
override fun didAttachOrDetach() {
|
||||
expectedCallState.destroyViewCalls++
|
||||
expectedCallState.detachCalls++
|
||||
expectedCallState.contextUnavailableCalls++
|
||||
expectedCallState.destroyCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
}
|
||||
|
||||
override fun didEndChange() {
|
||||
expectedCallState.changeEndCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun assertCalls(callState: CallState, controller: TestController) {
|
||||
shadowOf(getMainLooper()).idle()
|
||||
|
||||
Assert.assertEquals(
|
||||
"Expected call counts and controller call counts do not match.",
|
||||
callState,
|
||||
controller.currentCallState
|
||||
)
|
||||
Assert.assertEquals(
|
||||
"Expected call counts and lifecycle call counts do not match.",
|
||||
callState,
|
||||
currentCallState
|
||||
)
|
||||
}
|
||||
|
||||
private fun attachLifecycleListener(controller: Controller?) {
|
||||
controller!!.addLifecycleListener(object : LifecycleListener() {
|
||||
override fun onChangeStart(
|
||||
controller: Controller,
|
||||
changeHandler: ControllerChangeHandler,
|
||||
changeType: ControllerChangeType
|
||||
) {
|
||||
currentCallState.changeStartCalls++
|
||||
}
|
||||
|
||||
override fun onChangeEnd(
|
||||
controller: Controller,
|
||||
changeHandler: ControllerChangeHandler,
|
||||
changeType: ControllerChangeType
|
||||
) {
|
||||
currentCallState.changeEndCalls++
|
||||
}
|
||||
|
||||
override fun postContextAvailable(controller: Controller, context: Context) {
|
||||
currentCallState.contextAvailableCalls++
|
||||
}
|
||||
|
||||
override fun postContextUnavailable(controller: Controller) {
|
||||
currentCallState.contextUnavailableCalls++
|
||||
}
|
||||
|
||||
override fun postCreateView(controller: Controller, view: View) {
|
||||
currentCallState.createViewCalls++
|
||||
}
|
||||
|
||||
override fun postAttach(controller: Controller, view: View) {
|
||||
currentCallState.attachCalls++
|
||||
}
|
||||
|
||||
override fun postDestroyView(controller: Controller) {
|
||||
currentCallState.destroyViewCalls++
|
||||
}
|
||||
|
||||
override fun postDetach(controller: Controller, view: View) {
|
||||
currentCallState.detachCalls++
|
||||
}
|
||||
|
||||
override fun postDestroy(controller: Controller) {
|
||||
currentCallState.destroyCalls++
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(controller: Controller, outState: Bundle) {
|
||||
currentCallState.saveInstanceStateCalls++
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(controller: Controller, savedInstanceState: Bundle) {
|
||||
currentCallState.restoreInstanceStateCalls++
|
||||
}
|
||||
|
||||
override fun onSaveViewState(controller: Controller, outState: Bundle) {
|
||||
currentCallState.saveViewStateCalls++
|
||||
}
|
||||
|
||||
override fun onRestoreViewState(controller: Controller, savedViewState: Bundle) {
|
||||
currentCallState.restoreViewStateCalls++
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
package com.bluelinelabs.conductor;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.bluelinelabs.conductor.Controller.LifecycleListener;
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeCompletedListener;
|
||||
import com.bluelinelabs.conductor.ControllerTransaction.ControllerChangeType;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.util.ActivityController;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE)
|
||||
public class ControllerTests {
|
||||
|
||||
private ActivityController<TestActivity> mActivityController;
|
||||
private Router mRouter;
|
||||
|
||||
private int mChangeStartCalls;
|
||||
private int mChangeEndCalls;
|
||||
private int mBindViewCalls;
|
||||
private int mAttachCalls;
|
||||
private int mUnbindViewCalls;
|
||||
private int mDetachCalls;
|
||||
private int mDestroyCalls;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
mActivityController = Robolectric.buildActivity(TestActivity.class).create();
|
||||
Activity activity = mActivityController.get();
|
||||
mRouter = Conductor.attachRouter(activity, new FrameLayout(activity), null);
|
||||
mRouter.setRoot(new TestController());
|
||||
|
||||
mChangeStartCalls = 0;
|
||||
mChangeEndCalls = 0;
|
||||
mBindViewCalls = 0;
|
||||
mAttachCalls = 0;
|
||||
mUnbindViewCalls = 0;
|
||||
mDestroyCalls = 0;
|
||||
mDestroyCalls = 0;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalLifecycle() {
|
||||
Controller controller = new TestController();
|
||||
attachLifecycleListener(controller);
|
||||
|
||||
assertCalls(0, 0, 0, 0, 0, 0, 0);
|
||||
mRouter.pushController(RouterTransaction.builder(controller)
|
||||
.pushChangeHandler(getPushHandler(0, 0, 0, 0, 0, 0, 0))
|
||||
.popChangeHandler(getPopHandler(1, 1, 1, 1, 0, 0, 0))
|
||||
.build()
|
||||
);
|
||||
|
||||
assertCalls(1, 1, 1, 1, 0, 0, 0);
|
||||
|
||||
mRouter.popCurrentController();
|
||||
|
||||
assertCalls(2, 2, 1, 1, 1, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLifecycleWithActivityDestroy() {
|
||||
Controller controller = new TestController();
|
||||
attachLifecycleListener(controller);
|
||||
|
||||
assertCalls(0, 0, 0, 0, 0, 0, 0);
|
||||
mRouter.pushController(RouterTransaction.builder(controller)
|
||||
.pushChangeHandler(getPushHandler(0, 0, 0, 0, 0, 0, 0))
|
||||
.build()
|
||||
);
|
||||
|
||||
assertCalls(1, 1, 1, 1, 0, 0, 0);
|
||||
|
||||
mActivityController.pause();
|
||||
|
||||
assertCalls(1, 1, 1, 1, 0, 0, 0);
|
||||
|
||||
mActivityController.stop();
|
||||
|
||||
assertCalls(1, 1, 1, 1, 0, 0, 0);
|
||||
|
||||
mActivityController.destroy();
|
||||
|
||||
assertCalls(1, 1, 1, 1, 0, 0, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChildLifecycle() {
|
||||
Controller parent = new TestController();
|
||||
mRouter.pushController(RouterTransaction.builder(parent)
|
||||
.pushChangeHandler(new ChangeHandler(new ChangeHandlerListener() {
|
||||
@Override
|
||||
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
|
||||
container.addView(to);
|
||||
ViewUtils.setAttached(to, true);
|
||||
changeListener.onChangeCompleted();
|
||||
}
|
||||
}))
|
||||
.build());
|
||||
|
||||
Controller child = new TestController();
|
||||
attachLifecycleListener(child);
|
||||
|
||||
assertCalls(0, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
parent.addChildController(ChildControllerTransaction.builder(child, TestController.VIEW_ID)
|
||||
.pushChangeHandler(getPushHandler(0, 0, 0, 0, 0, 0, 0))
|
||||
.popChangeHandler(getPopHandler(1, 1, 1, 1, 0, 0, 0))
|
||||
.build()
|
||||
);
|
||||
|
||||
assertCalls(1, 1, 1, 1, 0, 0, 0);
|
||||
|
||||
parent.removeChildController(child);
|
||||
|
||||
assertCalls(2, 2, 1, 1, 1, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChildLifecycle2() {
|
||||
Controller parent = new TestController();
|
||||
mRouter.pushController(RouterTransaction.builder(parent)
|
||||
.pushChangeHandler(new ChangeHandler(new ChangeHandlerListener() {
|
||||
@Override
|
||||
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
|
||||
container.addView(to);
|
||||
ViewUtils.setAttached(to, true);
|
||||
changeListener.onChangeCompleted();
|
||||
}
|
||||
}))
|
||||
.popChangeHandler(new ChangeHandler(new ChangeHandlerListener() {
|
||||
@Override
|
||||
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
|
||||
container.removeView(from);
|
||||
ViewUtils.setAttached(from, false);
|
||||
changeListener.onChangeCompleted();
|
||||
}
|
||||
}))
|
||||
.build());
|
||||
|
||||
Controller child = new TestController();
|
||||
attachLifecycleListener(child);
|
||||
|
||||
assertCalls(0, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
parent.addChildController(ChildControllerTransaction.builder(child, TestController.VIEW_ID)
|
||||
.pushChangeHandler(getPushHandler(0, 0, 0, 0, 0, 0, 0))
|
||||
.popChangeHandler(getPopHandler(1, 1, 1, 1, 0, 0, 0))
|
||||
.build()
|
||||
);
|
||||
|
||||
assertCalls(1, 1, 1, 1, 0, 0, 0);
|
||||
|
||||
mRouter.popCurrentController();
|
||||
ViewUtils.setAttached(child.getView(), false);
|
||||
|
||||
assertCalls(1, 1, 1, 1, 1, 1, 1);
|
||||
}
|
||||
|
||||
private ChangeHandler getPushHandler(final int changeStart, final int changeEnd, final int bindView, final int attach, final int unbindView, final int detach, final int destroy) {
|
||||
return new ChangeHandler(new ChangeHandlerListener() {
|
||||
@Override
|
||||
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
|
||||
assertCalls(changeStart + 1, changeEnd, bindView + 1, attach, unbindView, detach, destroy);
|
||||
container.addView(to);
|
||||
ViewUtils.setAttached(to, true);
|
||||
assertCalls(changeStart + 1, changeEnd, bindView + 1, attach + 1, unbindView, detach, destroy);
|
||||
changeListener.onChangeCompleted();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ChangeHandler getPopHandler(final int changeStart, final int changeEnd, final int bindView, final int attach, final int unbindView, final int detach, final int destroy) {
|
||||
return new ChangeHandler(new ChangeHandlerListener() {
|
||||
@Override
|
||||
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
|
||||
assertCalls(changeStart + 1, changeEnd, bindView, attach, unbindView, detach, destroy + 1);
|
||||
container.removeView(from);
|
||||
ViewUtils.setAttached(from, false);
|
||||
assertCalls(changeStart + 1, changeEnd, bindView, attach, unbindView + 1, detach + 1, destroy + 1);
|
||||
changeListener.onChangeCompleted();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void assertCalls(int changeStart, int changeEnd, int bindView, int attach, int unbindView, int detach, int destroy) {
|
||||
Assert.assertEquals(changeStart, mChangeStartCalls);
|
||||
Assert.assertEquals(changeEnd, mChangeEndCalls);
|
||||
Assert.assertEquals(bindView, mBindViewCalls);
|
||||
Assert.assertEquals(attach, mAttachCalls);
|
||||
Assert.assertEquals(unbindView, mUnbindViewCalls);
|
||||
Assert.assertEquals(detach, mDetachCalls);
|
||||
Assert.assertEquals(destroy, mDestroyCalls);
|
||||
}
|
||||
|
||||
private void attachLifecycleListener(Controller controller) {
|
||||
controller.addLifecycleListener(new LifecycleListener() {
|
||||
@Override
|
||||
public void onChangeStart(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
|
||||
mChangeStartCalls++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
|
||||
mChangeEndCalls++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postBindView(@NonNull Controller controller, @NonNull View view) {
|
||||
mBindViewCalls++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postAttach(@NonNull Controller controller, @NonNull View view) {
|
||||
mAttachCalls++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postUnbindView(@NonNull Controller controller) {
|
||||
mUnbindViewCalls++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postDetach(@NonNull Controller controller, @NonNull View view) {
|
||||
mDetachCalls++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postDestroy(@NonNull Controller controller) {
|
||||
mDestroyCalls++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
interface ChangeHandlerListener {
|
||||
void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener);
|
||||
}
|
||||
|
||||
static class ChangeHandler extends ControllerChangeHandler {
|
||||
|
||||
private ChangeHandlerListener mListener;
|
||||
|
||||
public ChangeHandler() { }
|
||||
|
||||
public ChangeHandler(ChangeHandlerListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
|
||||
mListener.performChange(container, from, to, isPush, changeListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
package com.bluelinelabs.conductor
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Looper
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.SubMenu
|
||||
import com.bluelinelabs.conductor.Controller.RetainViewMode
|
||||
import com.bluelinelabs.conductor.util.AttachFakingFrameLayout
|
||||
import com.bluelinelabs.conductor.util.CallState
|
||||
import com.bluelinelabs.conductor.util.TestActivity
|
||||
import com.bluelinelabs.conductor.util.ViewUtils
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE)
|
||||
class ControllerTests {
|
||||
|
||||
private val router = Robolectric.buildActivity(TestActivity::class.java)
|
||||
.setup()
|
||||
.get()
|
||||
.router
|
||||
|
||||
@Test
|
||||
fun testViewRetention() {
|
||||
val controller = TestController()
|
||||
controller.setRouter(router)
|
||||
|
||||
// Test View getting released w/ RELEASE_DETACH
|
||||
controller.retainViewMode = RetainViewMode.RELEASE_DETACH
|
||||
Assert.assertNull(controller.getView())
|
||||
var view = controller.inflate(router.container)
|
||||
Assert.assertNotNull(controller.getView())
|
||||
ViewUtils.reportAttached(view, true)
|
||||
Assert.assertNotNull(controller.getView())
|
||||
ViewUtils.reportAttached(view, false)
|
||||
Assert.assertNull(controller.getView())
|
||||
|
||||
// Test View getting retained w/ RETAIN_DETACH
|
||||
controller.retainViewMode = RetainViewMode.RETAIN_DETACH
|
||||
view = controller.inflate(router.container)
|
||||
Assert.assertNotNull(controller.getView())
|
||||
ViewUtils.reportAttached(view, true)
|
||||
Assert.assertNotNull(controller.getView())
|
||||
ViewUtils.reportAttached(view, false)
|
||||
Assert.assertNotNull(controller.getView())
|
||||
|
||||
// Ensure re-setting RELEASE_DETACH releases
|
||||
controller.retainViewMode = RetainViewMode.RELEASE_DETACH
|
||||
Assert.assertNull(controller.getView())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testActivityResult() {
|
||||
val controller = TestController()
|
||||
val expectedCallState = CallState(true)
|
||||
router.pushController(controller.asTransaction())
|
||||
|
||||
// Ensure that calling onActivityResult w/o requesting a result doesn't do anything
|
||||
router.onActivityResult(1, Activity.RESULT_OK, null)
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
// Ensure starting an activity for result gets us the result back
|
||||
controller.startActivityForResult(Intent("action"), 1)
|
||||
router.onActivityResult(1, Activity.RESULT_OK, null)
|
||||
expectedCallState.onActivityResultCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
// Ensure requesting a result w/o calling startActivityForResult works
|
||||
controller.registerForActivityResult(2)
|
||||
router.onActivityResult(2, Activity.RESULT_OK, null)
|
||||
expectedCallState.onActivityResultCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testActivityResultForChild() {
|
||||
val parent = TestController()
|
||||
val child = TestController()
|
||||
router.pushController(parent.asTransaction())
|
||||
val childContainer = parent.view!!.findViewById<AttachFakingFrameLayout>(TestController.VIEW_ID)
|
||||
childContainer.setAttached(true)
|
||||
parent.getChildRouter(childContainer)
|
||||
.setRoot(child.asTransaction())
|
||||
val childExpectedCallState = CallState(true)
|
||||
val parentExpectedCallState = CallState(true)
|
||||
|
||||
// Ensure that calling onActivityResult w/o requesting a result doesn't do anything
|
||||
router.onActivityResult(1, Activity.RESULT_OK, null)
|
||||
assertCalls(childExpectedCallState, child)
|
||||
assertCalls(parentExpectedCallState, parent)
|
||||
|
||||
// Ensure starting an activity for result gets us the result back
|
||||
child.startActivityForResult(Intent("action"), 1)
|
||||
router.onActivityResult(1, Activity.RESULT_OK, null)
|
||||
childExpectedCallState.onActivityResultCalls++
|
||||
assertCalls(childExpectedCallState, child)
|
||||
assertCalls(parentExpectedCallState, parent)
|
||||
|
||||
// Ensure requesting a result w/o calling startActivityForResult works
|
||||
child.registerForActivityResult(2)
|
||||
router.onActivityResult(2, Activity.RESULT_OK, null)
|
||||
childExpectedCallState.onActivityResultCalls++
|
||||
assertCalls(childExpectedCallState, child)
|
||||
assertCalls(parentExpectedCallState, parent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPermissionResult() {
|
||||
val requestedPermissions = arrayOf("test")
|
||||
val controller = TestController()
|
||||
val expectedCallState = CallState(true)
|
||||
router.pushController(controller.asTransaction())
|
||||
|
||||
// Ensure that calling handleRequestedPermission w/o requesting a result doesn't do anything
|
||||
router.onRequestPermissionsResult("anotherId", 1, requestedPermissions, intArrayOf(1))
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
// Ensure requesting the permission gets us the result back
|
||||
controller.requestPermissions(requestedPermissions, 1)
|
||||
|
||||
expectedCallState.onRequestPermissionsResultCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPermissionResultForChild() {
|
||||
val requestedPermissions = arrayOf("test")
|
||||
val parent = TestController()
|
||||
val child = TestController()
|
||||
router.pushController(parent.asTransaction())
|
||||
val childContainer = parent.view!!.findViewById<AttachFakingFrameLayout>(TestController.VIEW_ID)
|
||||
childContainer.setAttached(true)
|
||||
parent.getChildRouter(childContainer)
|
||||
.setRoot(child.asTransaction())
|
||||
val childExpectedCallState = CallState(true)
|
||||
val parentExpectedCallState = CallState(true)
|
||||
|
||||
// Ensure that calling handleRequestedPermission w/o requesting a result doesn't do anything
|
||||
router.onRequestPermissionsResult("anotherId", 1, requestedPermissions, intArrayOf(1))
|
||||
assertCalls(childExpectedCallState, child)
|
||||
assertCalls(parentExpectedCallState, parent)
|
||||
|
||||
// Ensure requesting the permission gets us the result back
|
||||
child.requestPermissions(requestedPermissions, 1)
|
||||
|
||||
childExpectedCallState.onRequestPermissionsResultCalls++
|
||||
assertCalls(childExpectedCallState, child)
|
||||
assertCalls(parentExpectedCallState, parent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOptionsMenu() {
|
||||
val controller = TestController()
|
||||
val expectedCallState = CallState(true)
|
||||
router.pushController(controller.asTransaction())
|
||||
|
||||
// Ensure that calling onCreateOptionsMenu w/o declaring that we have one doesn't do anything
|
||||
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
// Ensure calling onCreateOptionsMenu with a menu works
|
||||
controller.setHasOptionsMenu(true)
|
||||
expectedCallState.createOptionsMenuCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
// Ensure it'll still get called back next time onCreateOptionsMenu is called
|
||||
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
|
||||
expectedCallState.createOptionsMenuCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
// Ensure we stop getting them when we hide it
|
||||
controller.setOptionsMenuHidden(true)
|
||||
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
// Ensure we get the callback them when we un-hide it
|
||||
controller.setOptionsMenuHidden(false)
|
||||
expectedCallState.createOptionsMenuCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
|
||||
expectedCallState.createOptionsMenuCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
// Ensure we don't get the callback when we no longer have a menu
|
||||
controller.setHasOptionsMenu(false)
|
||||
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
|
||||
assertCalls(expectedCallState, controller)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOptionsMenuForChild() {
|
||||
val parent = TestController()
|
||||
val child = TestController()
|
||||
router.pushController(parent.asTransaction())
|
||||
val childContainer = parent.view!!.findViewById<AttachFakingFrameLayout>(TestController.VIEW_ID)
|
||||
childContainer.setAttached(true)
|
||||
parent.getChildRouter(childContainer)
|
||||
.setRoot(child.asTransaction())
|
||||
val childExpectedCallState = CallState(true)
|
||||
val parentExpectedCallState = CallState(true)
|
||||
|
||||
// Ensure that calling onCreateOptionsMenu w/o declaring that we have one doesn't do anything
|
||||
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
|
||||
assertCalls(childExpectedCallState, child)
|
||||
assertCalls(parentExpectedCallState, parent)
|
||||
|
||||
// Ensure calling onCreateOptionsMenu with a menu works
|
||||
child.setHasOptionsMenu(true)
|
||||
childExpectedCallState.createOptionsMenuCalls++
|
||||
assertCalls(childExpectedCallState, child)
|
||||
assertCalls(parentExpectedCallState, parent)
|
||||
|
||||
// Ensure it'll still get called back next time onCreateOptionsMenu is called
|
||||
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
|
||||
childExpectedCallState.createOptionsMenuCalls++
|
||||
assertCalls(childExpectedCallState, child)
|
||||
assertCalls(parentExpectedCallState, parent)
|
||||
|
||||
// Ensure we stop getting them when we hide it
|
||||
child.setOptionsMenuHidden(true)
|
||||
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
|
||||
assertCalls(childExpectedCallState, child)
|
||||
assertCalls(parentExpectedCallState, parent)
|
||||
|
||||
// Ensure we get the callback them when we un-hide it
|
||||
child.setOptionsMenuHidden(false)
|
||||
childExpectedCallState.createOptionsMenuCalls++
|
||||
assertCalls(childExpectedCallState, child)
|
||||
assertCalls(parentExpectedCallState, parent)
|
||||
|
||||
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
|
||||
childExpectedCallState.createOptionsMenuCalls++
|
||||
assertCalls(childExpectedCallState, child)
|
||||
assertCalls(parentExpectedCallState, parent)
|
||||
|
||||
// Ensure we don't get the callback when we no longer have a menu
|
||||
child.setHasOptionsMenu(false)
|
||||
router.onCreateOptionsMenu(menu(), menuInflater(router.activity!!))
|
||||
assertCalls(childExpectedCallState, child)
|
||||
assertCalls(parentExpectedCallState, parent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddRemoveChildControllers() {
|
||||
val parent = TestController()
|
||||
val child1 = TestController()
|
||||
val child2 = TestController()
|
||||
router.pushController(parent.asTransaction())
|
||||
Assert.assertEquals(0, parent.childRouters.size)
|
||||
Assert.assertNull(child1.parentController)
|
||||
Assert.assertNull(child2.parentController)
|
||||
|
||||
var childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
|
||||
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
|
||||
childRouter.setRoot(child1.asTransaction())
|
||||
Assert.assertEquals(1, parent.childRouters.size)
|
||||
Assert.assertEquals(childRouter, parent.childRouters[0])
|
||||
Assert.assertEquals(1, childRouter.backstackSize)
|
||||
Assert.assertEquals(child1, childRouter.controllers[0])
|
||||
Assert.assertEquals(parent, child1.parentController)
|
||||
Assert.assertNull(child2.parentController)
|
||||
|
||||
childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
|
||||
childRouter.pushController(child2.asTransaction())
|
||||
Assert.assertEquals(1, parent.childRouters.size)
|
||||
Assert.assertEquals(childRouter, parent.childRouters[0])
|
||||
Assert.assertEquals(2, childRouter.backstackSize)
|
||||
Assert.assertEquals(child1, childRouter.controllers[0])
|
||||
Assert.assertEquals(child2, childRouter.controllers[1])
|
||||
Assert.assertEquals(parent, child1.parentController)
|
||||
Assert.assertEquals(parent, child2.parentController)
|
||||
|
||||
childRouter.popController(child2)
|
||||
Assert.assertEquals(1, parent.childRouters.size)
|
||||
Assert.assertEquals(childRouter, parent.childRouters[0])
|
||||
Assert.assertEquals(1, childRouter.backstackSize)
|
||||
Assert.assertEquals(child1, childRouter.controllers[0])
|
||||
Assert.assertEquals(parent, child1.parentController)
|
||||
Assert.assertNull(child2.parentController)
|
||||
|
||||
childRouter.popController(child1)
|
||||
shadowOf(Looper.getMainLooper()).idle()
|
||||
Assert.assertEquals(1, parent.childRouters.size)
|
||||
Assert.assertEquals(childRouter, parent.childRouters[0])
|
||||
Assert.assertEquals(0, childRouter.backstackSize)
|
||||
Assert.assertNull(child1.parentController)
|
||||
Assert.assertNull(child2.parentController)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddRemoveChildRouters() {
|
||||
val parent = TestController()
|
||||
val child1 = TestController()
|
||||
val child2 = TestController()
|
||||
router.pushController(parent.asTransaction())
|
||||
Assert.assertEquals(0, parent.childRouters.size)
|
||||
Assert.assertNull(child1.parentController)
|
||||
Assert.assertNull(child2.parentController)
|
||||
|
||||
val childRouter1 = parent.getChildRouter(parent.view!!.findViewById(TestController.CHILD_VIEW_ID_1))
|
||||
val childRouter2 = parent.getChildRouter(parent.view!!.findViewById(TestController.CHILD_VIEW_ID_2))
|
||||
childRouter1.setRoot(child1.asTransaction())
|
||||
childRouter2.setRoot(child2.asTransaction())
|
||||
Assert.assertEquals(2, parent.childRouters.size)
|
||||
Assert.assertEquals(childRouter1, parent.childRouters[0])
|
||||
Assert.assertEquals(childRouter2, parent.childRouters[1])
|
||||
Assert.assertEquals(1, childRouter1.backstackSize)
|
||||
Assert.assertEquals(1, childRouter2.backstackSize)
|
||||
Assert.assertEquals(child1, childRouter1.controllers[0])
|
||||
Assert.assertEquals(child2, childRouter2.controllers[0])
|
||||
Assert.assertEquals(parent, child1.parentController)
|
||||
Assert.assertEquals(parent, child2.parentController)
|
||||
|
||||
parent.removeChildRouter(childRouter2)
|
||||
shadowOf(Looper.getMainLooper()).idle()
|
||||
Assert.assertEquals(1, parent.childRouters.size)
|
||||
Assert.assertEquals(childRouter1, parent.childRouters[0])
|
||||
Assert.assertEquals(1, childRouter1.backstackSize)
|
||||
Assert.assertEquals(0, childRouter2.backstackSize)
|
||||
Assert.assertEquals(child1, childRouter1.controllers[0])
|
||||
Assert.assertEquals(parent, child1.parentController)
|
||||
Assert.assertNull(child2.parentController)
|
||||
parent.removeChildRouter(childRouter1)
|
||||
Assert.assertEquals(0, parent.childRouters.size)
|
||||
Assert.assertEquals(0, childRouter1.backstackSize)
|
||||
Assert.assertEquals(0, childRouter2.backstackSize)
|
||||
Assert.assertNull(child1.parentController)
|
||||
Assert.assertNull(child2.parentController)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRestoredChildRouterBackstack() {
|
||||
val parent = TestController()
|
||||
router.pushController(parent.asTransaction())
|
||||
ViewUtils.reportAttached(parent.view, true)
|
||||
|
||||
val childTransaction1 = TestController().asTransaction()
|
||||
val childTransaction2 = TestController().asTransaction()
|
||||
var childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.CHILD_VIEW_ID_1))
|
||||
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
|
||||
childRouter.setRoot(childTransaction1)
|
||||
childRouter.pushController(childTransaction2)
|
||||
val savedState = Bundle()
|
||||
childRouter.saveInstanceState(savedState)
|
||||
parent.removeChildRouter(childRouter)
|
||||
childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.CHILD_VIEW_ID_1))
|
||||
Assert.assertEquals(0, childRouter.backstackSize)
|
||||
|
||||
childRouter.restoreInstanceState(savedState)
|
||||
childRouter.rebindIfNeeded()
|
||||
Assert.assertEquals(2, childRouter.backstackSize)
|
||||
val restoredChildTransaction1 = childRouter.getBackstack()[0]
|
||||
val restoredChildTransaction2 = childRouter.getBackstack()[1]
|
||||
Assert.assertEquals(
|
||||
childTransaction1.transactionIndex,
|
||||
restoredChildTransaction1.transactionIndex
|
||||
)
|
||||
Assert.assertEquals(
|
||||
childTransaction1.controller.getInstanceId(),
|
||||
restoredChildTransaction1.controller.getInstanceId()
|
||||
)
|
||||
Assert.assertEquals(
|
||||
childTransaction2.transactionIndex,
|
||||
restoredChildTransaction2.transactionIndex
|
||||
)
|
||||
Assert.assertEquals(
|
||||
childTransaction2.controller.getInstanceId(),
|
||||
restoredChildTransaction2.controller.getInstanceId()
|
||||
)
|
||||
Assert.assertTrue(parent.handleBack())
|
||||
Assert.assertEquals(1, childRouter.backstackSize)
|
||||
Assert.assertEquals(restoredChildTransaction1, childRouter.getBackstack()[0])
|
||||
Assert.assertTrue(parent.handleBack())
|
||||
Assert.assertEquals(0, childRouter.backstackSize)
|
||||
}
|
||||
|
||||
private fun assertCalls(callState: CallState, controller: TestController) {
|
||||
shadowOf(Looper.getMainLooper()).idle()
|
||||
|
||||
Assert.assertEquals(
|
||||
"Expected call counts and controller call counts do not match.",
|
||||
callState,
|
||||
controller.currentCallState
|
||||
)
|
||||
}
|
||||
|
||||
private fun menu(): Menu {
|
||||
return object : Menu {
|
||||
override fun add(p0: CharSequence?): MenuItem {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun add(p0: Int): MenuItem {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun add(p0: Int, p1: Int, p2: Int, p3: CharSequence?): MenuItem {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun add(p0: Int, p1: Int, p2: Int, p3: Int): MenuItem {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun addSubMenu(p0: CharSequence?): SubMenu {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun addSubMenu(p0: Int): SubMenu {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun addSubMenu(p0: Int, p1: Int, p2: Int, p3: CharSequence?): SubMenu {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun addSubMenu(p0: Int, p1: Int, p2: Int, p3: Int): SubMenu {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun addIntentOptions(
|
||||
p0: Int,
|
||||
p1: Int,
|
||||
p2: Int,
|
||||
p3: ComponentName?,
|
||||
p4: Array<out Intent>?,
|
||||
p5: Intent?,
|
||||
p6: Int,
|
||||
p7: Array<out MenuItem>?
|
||||
): Int {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun removeItem(p0: Int) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun removeGroup(p0: Int) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setGroupCheckable(p0: Int, p1: Boolean, p2: Boolean) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setGroupVisible(p0: Int, p1: Boolean) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setGroupEnabled(p0: Int, p1: Boolean) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun hasVisibleItems(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun findItem(p0: Int): MenuItem {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun size(): Int {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getItem(p0: Int): MenuItem {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun performShortcut(p0: Int, p1: KeyEvent?, p2: Int): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isShortcutKey(p0: Int, p1: KeyEvent?): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun performIdentifierAction(p0: Int, p1: Int): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setQwertyMode(p0: Boolean) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun menuInflater(context: Context): MenuInflater {
|
||||
return MenuInflater(context)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user