Compare commits

...

159 Commits

Author SHA1 Message Date
Eric Kuck 241f052a7b Version bump 2018-08-01 16:19:16 -05:00
Eric Kuck f239376b03 Added a more helpful exception when TransactionIndexer object can't be found. 2018-08-01 12:32:00 -05:00
Eric Kuck 9dc5e5def6 setBackstack with an empty list will now remove all visible views when it's supposed to 2018-07-31 15:43:19 -05:00
Eric Kuck 23d2bab764 Fixes potential out-of-order attach calls for children if the framework itself calls them out of order. (Fixes #416) 2018-07-29 22:42:42 -05:00
Eric Kuck 814121d686 Ensures getTransactionIndexer() will never throw a StackOverflowException 2018-07-29 21:37:05 -05:00
Eric Kuck 4a5442e821 Adds more Autodispose options (fixes #452) 2018-07-29 21:33:15 -05:00
Eric Kuck 3b67822b25 Fixed another lifecycle call order issue with Android P 2018-07-19 14:01:31 -05:00
Eric Kuck 24754364c8 Fix rotation issue for SDK 28 (fixes #457) 2018-07-18 13:32:24 -05:00
Eric Kuck 131589670f Now removes savedPageHistory for pages that are reinstated (and therefore no longer saved) 2018-06-20 15:36:34 -05:00
Dhi Aurrahman efca85829b Fix unresolved dep, failed to resolve: common (#443)
Thanks :)
2018-06-12 13:11:26 +02:00
inorichi 3d64903e54 Fix demo not compiling after dependency updates (#437)
LGTM 👍
2018-06-12 10:08:56 +02:00
Eric Kuck ad3b4911be Fixes #367 2018-06-06 14:58:58 -05:00
Eric Kuck a0d92a631f Fixes #432 2018-05-30 13:53:07 -05:00
Eric Kuck f1b38219b4 Fixes #425 2018-05-30 10:58:18 -05:00
Eric Kuck d59e9ccabf Fixes #429 2018-05-30 10:46:54 -05:00
Eric Kuck 4c81db3a09 Fixes #394 2018-05-29 16:01:12 -05:00
Eric Kuck a8cfd4be9f Added a first draft of contribution guidelines 2018-05-29 15:39:08 -05:00
Eric Kuck 894601700d Fixed potential ConcurrentModificationExceptions if developers added or removed routers during lifecycle callbacks 2018-05-29 15:05:36 -05:00
Eric Kuck d72951eb04 Fixed up lint calls and related tests 2018-05-29 14:43:16 -05:00
Eric Kuck ca3ec3ac12 Fixed false positives for new UAST lint checks 2018-05-29 12:13:51 -05:00
Paul Woitaschek 5ce857bb24 Dependency update & Migrate Lint to UAST (#433)
* Use Android Sdk 27

* Updated the unmock plugin and use the default configuration.

* Removed the explicit buildTools and use the one bundled with the plugin.

* Updated the support library, butterknife, leakCanary, rxjava 1 & 2,
rxlifecycle 1 & 2, junit and robolectric.

* Migrated the lint checks to UAST.

* Rename the UClass "declaration" to "field" to be consistent to the
other check and the default name.

* Moved the evaluator of the ControllerIssueDetector to the handler too.
2018-05-29 11:31:32 -05:00
Eric Kuck f6512d3e1c Fixed build 2018-05-29 11:14:28 -05:00
Eric Kuck 52f095d945 Codestyle updates 2018-05-25 14:45:58 -05:00
Stephan Schuster 91da937d4f Improved lifecycle integration of Conductor and Architecture Components (#383)
Previous mapping was:
- () -> ON_CREATE -> CREATED
- preCreateView -> ON_START -> STARTED
- preAttach -> ON_RESUME -> RESUMED
- preDetach -> ON_PAUSE -> STARTED
- preDestroyView -> ON_STOP -> CREATED
- preDestroy -> ON_DESTROY -> DESTROYED

New mapping is:
- () -> INITIALIZED
- postContextAvailable -> ON_CREATE -> CREATED
- postCreateView -> ON_START -> STARTED
- postAttach -> ON_RESUME -> RESUMED
- preDetach -> ON_PAUSE -> STARTED
- preDestroyView -> ON_STOP -> CREATED
- preContextUnavailable -> // do nothing
- preDestroy -> ON_DESTROY -> DESTROYED

Change-Id: I9f65480018dc1204dc89a8065fa7c7f554ad7dd2
2018-05-25 14:41:06 -05:00
Eric Kuck 9e3095957c Autodispose dependency update, fixed build 2018-05-25 14:38:23 -05:00
Eric Kuck fac44b74e9 build.gradle updates 2018-05-25 13:26:48 -05:00
Daniel Stout ed6ba3a5eb Fix ConcurrentModificationException thrown in Router.popController (#406) 2018-05-25 13:15:25 -05:00
Paul Woitaschek df70e983be Removed synthetic accessors (#347)
* Removed synthetic accessors.

* Upon copying child routers, init the list with the new size and use
addAll.

* Initialize some lists with an appropriate initial capacity.

* Don't wrap methods.
2017-10-09 14:55:35 -05:00
Stefan M 4d28dcefac Styles snapshot section (#364)
* Add missing curly braces
* Make snapshot bold and remove double point
* Fix typo
2017-10-09 11:46:22 -05:00
Eric Kuck 8ab528a566 Fixed ordering of onContextAvailable calls when setting the backstack 2017-10-03 16:42:23 -05:00
Eric Kuck 9bbf050cd2 setBackstack now delays destroying old controllers until after the change handler that removes them has completed 2017-08-04 10:59:19 -05:00
Eric Kuck 5a545aca20 Fixes removeAllExceptVisibleAndUnowned when there is more than 1 View that needs to be removed. Fixes #336 2017-08-01 16:13:40 -05:00
inorichi 0987b311d9 Only show options menu of the active item in a ViewPager (#335) 2017-07-28 10:54:12 -05:00
Eric Kuck ff105abf12 Fixes waitOnSharedElementNamed 2017-07-27 13:42:55 -05:00
Gustavo Pagani 422ca7a544 Expose parent constructor with Bundle as parameter (#328)
* Expose constructor with Bundle as parameter
* Add Nullable annotation to Bundle parameter
2017-07-21 11:58:20 -05:00
Eric Kuck 09df937c88 Fixes evaluation of isPush for several cases of setBackstack. Fixes #327 2017-07-17 14:00:11 -05:00
Eric Kuck ec8ac2a371 setBackstack will now make context available to all new controllers as soon as possible 2017-07-17 11:56:41 -05:00
Eric Kuck 66b0458c0e Fixed potential ConcurrentModificationException when listeners are removed during change handler events 2017-07-14 13:10:05 -05:00
Eric Kuck bd0cd361f2 Readme update 2017-07-11 14:54:19 -05:00
Eric Kuck 60c237bf82 Travis yml update 2017-07-10 17:08:03 -05:00
Eric Kuck bae8a16d8c Added SharedElementTransitionChangeHandler, which handles transitions much more robustly 2017-07-10 16:02:51 -05:00
Eric Kuck 55e8aebb87 Version bump 2017-07-05 13:02:07 -05:00
Eric Kuck 272c507f91 Args are now restored to a controller after process death even if there is only a no-arg constructor. Fixes #313 2017-07-05 10:26:56 -05:00
Hannes Struß 8e945cde52 Fixes ControllerScopeProvider.from return type (#319) 2017-06-25 14:23:28 -05:00
Eric Kuck c17287db67 shouldShowRequestPermissionRationale now returns the correct result. Fixes #317 2017-06-24 12:29:35 -05:00
Eric Kuck 625a3fcfd6 Fixes fade change handler animation when popping and removesFromViewOnPush is false 2017-06-21 17:11:37 -05:00
Eric Kuck dd2ecf2f83 onContextUnavailable now correctly called before onDestroy even if the host Activity is still around 2017-06-16 08:37:21 -05:00
Eric Kuck 0efc2b3daf Fixed rx lifecycle handling for subscriptions in the onContextAvailable callback 2017-06-15 10:27:05 -05:00
Eric Kuck 4b4c3a82b8 Added context available/unavailable to lifecycle listeners. Now supported by all rx lifecycle modules as well. 2017-06-12 12:14:18 -05:00
Eric Kuck 7d0599fc5d Fixed tests 2017-06-02 16:15:37 -05:00
Eric Kuck 0adea89d34 Arch components lifecycle module now labeled as an alpha 2017-06-02 15:55:22 -05:00
Eric Kuck 044363517c Added modules for Autodispose and architecture components's Lifecycle
Cleaned up the demo and directory structure a bit now that there are so many external modules
2017-06-02 15:47:56 -05:00
Eric Kuck a2f11d5d51 The final controller popped in a router now has onDestroy properly called in it, even if the pop change handler is a no-op. 2017-05-26 16:09:56 -05:00
Eric Kuck bcd8ddbfb5 - Added ability to query current controller lifecycle state (needed for things like Google's Lifecycle and Uber's AutoDispose)
- Added onContextAvailable and onContextUnavailable callback to controllers (helpful for dependency injection)
2017-05-26 13:42:29 -05:00
Eric Kuck 479e3f0474 Pop changes that happen rapidly are now properly immediately completed instead of aborted. 2017-05-26 07:01:24 -05:00
Eric Kuck c298ca905c Fixes leaked activity introduced in e64fe1c610 2017-05-26 06:14:05 -05:00
Eric Kuck aebb19effa Fixes attach state management when a controller is pushed while the Activity is paused. (Fixes #303) 2017-05-24 03:05:42 -05:00
Eric Kuck e64fe1c610 Fixes issue when immediately creating two routers in the same Activity caused by the async nature of fragment transactions (fixes #299) 2017-05-24 03:01:36 -05:00
Frieder Bluemle 2357297531 Project updates (#264)
* Update Gradle wrapper to 3.5

* Fix whitespace errors

* Remove executable bit from regular files

* Fix DrawableRes annotation

* Update Android Gradle plugin to 2.3.2

* Update build tools to 25.0.3

* Do not ignore Lint errors

* Replace android-apt with annotationProcessor

* Update unmock to 0.6.0

* Update support libs to 25.3.1
2017-05-18 10:35:30 -05:00
Eric Kuck 926f7da6ce Now ensures that parent controller is restored after process death (fixes #300) 2017-05-18 10:29:12 -05:00
Eric Kuck d260dfcf12 Change handlers that occur after a deferred change handler are now deferred until the preceding handler completes to ensure correct ordering. Handlers will only be deferred when the containing view isn’t yet fully attached and they could cause a NPE in ViewGroup.java. 2017-05-18 07:36:02 -05:00
Eric Kuck a84b2fb4c4 Reverts 81a499d and attempts to fix #274, #285, and #287 in a more reliable way. 2017-05-10 12:19:00 +03:00
Eric Kuck 6aca3f578a Fixes a case that caused the incorrect change handler to execute when setting the backstack (fixes #286) 2017-05-10 11:37:10 +03:00
Eric Kuck 45ab54b5d7 Fixes issue where calling setBackstack repeatedly could result in a situation where multiple ControllerChangeHandlers are executing at the same time and interfering with each others states (#263) 2017-05-04 11:26:18 -05:00
Eric Kuck ca84419e0c Version bump 2017-05-03 16:28:23 -05:00
Eric Kuck a04dec7ec1 Fixes views never attaching if the host activity is stopped before inflation. Fixes #273 2017-05-03 16:03:58 -05:00
Eric Kuck 81a499d121 Now handles sequences of immediate pushing and popping of controllers much better. Also guards against NPEs due to popping a controller during onAttach. Fixes #274 2017-05-03 14:42:17 -05:00
Eric Kuck ff8ab621bc From controller’s initial position now correctly based on translationX in HorizontalChangeHandler. Fixes #279 2017-05-02 12:31:26 -05:00
Eric Kuck cdb5e5a978 Ensures AnimatorChangeHandlers can’t animate more than once per change 2017-05-02 11:27:01 -05:00
Eric Kuck effa410eae Fixes #269 in the demo app 2017-04-24 07:43:35 -05:00
Paul Woitaschek df27bfaa3d Made removeLifecycleListener final too (#268) 2017-04-13 13:45:33 -05:00
Eric Kuck a865c210b6 addLifecycleListener method now final. Fixes #267 2017-04-13 13:39:03 -05:00
Leonardo Ferrari 7820748cce Update README.md (#261)
fix latest version value
2017-04-06 09:30:02 -05:00
Eric Kuck 2147b2aa5e Version bump 2017-04-05 17:09:26 -05:00
fergusonm 19418617dd Pass along the correct view state (#258) 2017-04-04 07:59:12 -05:00
Eric Kuck e1924bf8a7 Args and savedInstanceState bundles now have classLoaders properly set. Fixes #246. 2017-03-31 09:38:20 -05:00
Eric Kuck 6d3faaebe3 Now ensures view hierarchy-affecting calls are made on the main thread. Fixes #255 2017-03-31 09:24:18 -05:00
Eric Kuck 17639129b9 Fixed issue where child controllers added while the parent is in the process of transitioning off the screen would not be properly restored - Fixes #256. 2017-03-31 09:01:13 -05:00
Valery 1c809095ec Fix image display (#251)
Github doesn't understand spaces in file name
2017-03-21 10:16:00 -05:00
Valery 3bc563de38 Fixed readme table (#250) 2017-03-21 09:58:21 -05:00
Eric Kuck 7beb94f8cc Fixed readme table 2017-03-20 16:56:19 -05:00
Eric Kuck 2b32a30c1a Version bump 2017-03-03 11:53:29 -06:00
Eric Kuck 893ffc0461 Version bump 2017-03-03 11:50:59 -06:00
Eric Kuck 0df11e3224 Build tools updates 2017-03-03 09:22:37 -06:00
Eric Kuck 75ad389424 Fixed a few warnings 2017-03-01 16:51:45 -06:00
Eric Kuck 00577823dc Moved popToTransaction calls over to use setBackstack internally. Fixes #239 2017-03-01 16:33:19 -06:00
Eric Kuck 671a117b96 Added ability to specify the maximum number of pages for which states should be saved. Fixes #236 2017-02-23 16:52:50 -06:00
Eric Kuck 3d1c2d392c Added startIntentSenderForResult method to controller. Fixes #235 2017-02-23 12:01:47 -06:00
Eric Kuck b7611e1a1b Added a RecyclerView -> RecyclerView shared element transition demo 2017-02-20 16:57:56 -06:00
Eric Kuck d13af316d3 Dialog background no longer fades out over the top of the outgoing dialog 2017-02-17 12:52:51 -06:00
Eric Kuck 7d5cc26ea4 Replaced png icon with vector drawable 2017-02-17 10:47:58 -06:00
Eric Kuck c4d881ac47 Added some better shared element demos 2017-02-17 10:40:38 -06:00
Eduardo Alejandro Pool Aké 0a53b9f07a Dependency updates 2017-02-16 11:20:25 -06:00
Eric Kuck c2ad655af2 Added fade to dialog’s immersive background
Moved FabToDialogTransitionChangeHandler over to the library’s TransitionChangeHandler
2017-02-16 11:13:04 -06:00
Simon Vergauwen a888073e1b DialogToFabTransition (#229) 2017-02-16 10:24:49 -06:00
Eric Kuck df68655b13 Added missing comments to new methods 2017-02-14 13:52:20 -06:00
Eric Kuck 86227ae3b3 TransitionChangeHandler is now much more flexible (doesn’t force you to add/remove views) 2017-02-14 13:50:13 -06:00
Paul Woitaschek 97878b1ad6 List simplification (#225)
* Use a singleton list for transactions
* Use built-in java list functions
2017-02-08 10:37:07 -06:00
Eric Kuck 314ee2b456 Routers now properly remove views of all owned controllers on destroy (fixes #221) 2017-02-07 10:22:33 -06:00
Paul Woitaschek f4ef47c2d2 Added missing nullity annotations for the pager adapters (#219) 2017-02-02 08:21:38 -06:00
Eric Kuck 769d552e88 Fixed typo in lifecycle diagram 2017-02-01 19:13:50 -06:00
Eric Kuck afa4b69d7a Version bump 2017-02-01 19:04:01 -06:00
Eric Kuck a9bdf0dd06 Revamped how child backtacks are handled to be more reliable with unforseen uses of state restoration. Fixes #194 and #217 2017-02-01 18:44:33 -06:00
Eric Kuck 6ffa94ed3a Added documentation for @Nullable’s. Fixes #218 2017-02-01 11:01:29 -06:00
blazeroni 690001ed2a Fixes issue when retrieving an existing controller from a ControllerPagerAdapter (#216) 2017-02-01 10:12:36 -06:00
Eric Kuck 90e015b6b3 Now correctly calls onDetach when the host activity is stopped, even if the view is not detached from the window. Fixes #213 2017-01-31 18:06:23 -06:00
Eric Kuck 44bcd0f977 Fixes #205 2017-01-20 17:35:15 -06:00
Eric Kuck 60d0fabcf4 Fixes #208 2017-01-20 17:15:33 -06:00
Eric Kuck 10a1c8af3e Version bump 2017-01-19 14:21:48 -06:00
Eric Kuck 6834df73e6 Fixes #203 2017-01-19 14:13:26 -06:00
Eric Kuck 04d40a5b90 Minor ViewAttachHandler optimizations 2017-01-19 13:21:02 -06:00
Allan Hasegawa be40900e1e #Fixes 206 (#207) 2017-01-19 13:20:20 -06:00
Eric Kuck f16f7b6d2c Fixes #205 2017-01-19 12:02:31 -06:00
Eric Kuck f74f8391b6 Fixes #204 (also mentioned in #199) 2017-01-19 11:50:07 -06:00
Eric Kuck 2ce8c0a45d Version bump 2017-01-18 13:00:24 -06:00
Eric Kuck 77ad6b4512 Now sets ClassLoader for view state Bundle - potential fix for #198 2017-01-18 12:18:06 -06:00
Eric Kuck efbdf913bd Fixes #201 2017-01-18 11:54:50 -06:00
Eric Kuck dfb01389f7 Fixes #199 2017-01-17 16:45:56 -06:00
Eric Kuck e390261b53 Fixes #197 2017-01-15 17:33:40 -06:00
Eric Kuck 27f5275172 Now throws an exception when a destroyed controller is pushed in order to make it more obvious that controllers can not be reused. 2016-12-26 15:25:16 -06:00
Eric Kuck d15f2b68ab Added missing files from e7c195d910 2016-12-16 11:35:45 -06:00
Eric Kuck 44ed19858b Fixes #185 2016-12-15 15:22:26 -06:00
Eric Kuck e7c195d910 Now internally ensures that ControllerChangeHandlers aren't reused, unless they're specifically designed to be safe for reuse
Made ControllerChangeHandler's copy() method public
Fixes #179
2016-12-15 15:11:25 -06:00
Eric Kuck 23a4dbbb60 Travis yml fix 2016-12-14 20:20:04 -06:00
Eric Kuck 0ac81767dd Travis.yml licenses fix 2016-12-14 20:05:38 -06:00
Eric Kuck 54cdc51557 Added license to travis.yml 2016-12-14 19:48:55 -06:00
Eric Kuck 09ce640d9c Fixes #183 2016-12-14 19:42:10 -06:00
Eric Kuck 01df673a34 Switched lint checks to the new PSI based api (closes #184) 2016-12-14 19:41:41 -06:00
Eric Kuck 553bae0be5 Updated readme to include mention of RxLifecycle2 2016-12-14 17:53:45 -06:00
Eric Kuck 48dc4abcbe Version bump 2016-12-13 17:09:19 -06:00
Eric Kuck 4a814afb5f Fixes #166 2016-12-13 16:56:22 -06:00
Eric Kuck 9cd225e704 Controllers now throw an exception when the user forgets to pass false for LayoutInflater.inflate's attachToRoot parameter. 2016-12-12 14:31:22 -06:00
Eric Kuck c8640af1ac Remove saveState option for RouterPagerAdapters, as users can configure the page before display anyway. 2016-12-12 13:25:57 -06:00
Eric Kuck 43c825f7c2 - Child backstack is now properly restored when Android kills the process
- Added a RouterPagerAdapter, which allows the use of Routers as pages
2016-12-12 13:09:29 -06:00
Eric Kuck 7334ed5300 ControllerPagerAdapter updates to enable using a per-page router if needed. 2016-12-12 12:24:26 -06:00
Eric Kuck 7ea4872ff8 Updated ordering of calls in backstack to be in line with other backstack-affecting calls 2016-12-08 13:25:11 -06:00
Eric Kuck 9655170bd2 Fixes tests 2016-12-07 16:34:38 -06:00
Eric Kuck acce9b1702 Simplified setRoot implementation 2016-12-07 16:17:32 -06:00
Eric Kuck 95baa8baa3 Fixes #172 2016-12-01 18:00:06 -06:00
Eric Kuck 2388fa2d06 Added a demo for the new RxLifecycle2Controller 2016-12-01 17:56:43 -06:00
Vishnu Rajeevan 285eb59da0 add rxlifecycle2 support, fixes #148 (#171) 2016-12-01 17:31:52 -06:00
Eric Kuck ae42ee1674 Attempted fix for #165 2016-12-01 17:28:15 -06:00
Eric Kuck 638b2ad311 Version bump 2016-11-11 11:10:35 -06:00
Eric Kuck 96e068d348 Fixes a controller's internal backstack when setBackstack is used on a child router 2016-11-11 10:44:37 -06:00
Eric Kuck db359d906b Un-deprecated getChildRouter with a tag. Fixes #160. 2016-11-10 13:34:13 -06:00
Eric Kuck 11185458b3 Fixed nullable annotations for menu callbacks 2016-11-09 16:03:20 -06:00
Eric Kuck 977db6b5bf Merge branch 'develop' of github.com:bluelinelabs/Conductor into develop 2016-11-09 15:32:01 -06:00
Eric Kuck 803c20e093 Added UiThread annotations - closes #145 2016-11-09 15:25:28 -06:00
TMTron 9948cb4652 Navigation Demos: "GO UP" is hidden in "Controller #0" - closes #158 (#159)
* Navigation Demos: "GO UP" is hidden in "Controller #0" - this closes #158

* Multiple Child Routers: "GO UP" is hidden for all - #158
2016-11-09 14:54:27 -06:00
Eric Kuck 07a579b939 Fixed incorrect child router backstack handling if controllers were VERY rapidly added 2016-11-09 11:07:41 -06:00
Eric Kuck 104d96e6e2 Filled out @Nullable and @NonNull annotations throughout the library 2016-11-09 10:09:00 -06:00
Eric Kuck e0f40a9fce Fixes tests 2016-11-02 12:29:58 -05:00
Eric Kuck bc8e0c5b2c Fixes #113 2016-11-02 12:04:53 -05:00
Eric Kuck 2b6e41f895 Fixes #140 2016-10-17 17:32:10 -05:00
Eric Kuck b633523d0e Fixes #138 2016-10-12 12:22:22 -05:00
Eric Kuck c5eb7fc89e Fixes #136 2016-10-12 09:44:26 -05:00
Eric Kuck cf6837a41a Fixes #137 2016-10-12 09:24:29 -05:00
Eric Kuck e297242264 Now handles requesting permissions while now yet fully attached to the host activity 2016-10-10 15:14:06 -05:00
Eric Kuck 550e7e0aa1 Added missing constructor to RestoreViewOnCreateController 2016-10-05 15:16:04 -05:00
192 changed files with 9234 additions and 1705 deletions
Executable → Regular
View File
+6 -4
View File
@@ -3,9 +3,11 @@ language: android
android:
components:
- tools
- build-tools-23.0.2
- android-23
- build-tools-27.0.3
- android-27
- extra-android-m2repository
licenses:
- '.+'
script:
- ./gradlew test
@@ -29,5 +31,5 @@ sudo: false
env:
global:
- secure: SoRbxmNTxnGY9qPR5Z8HraRLhHrq7eJ2UaHMuiXDSxpLwUI0IMw+8+l59PNdy/C5ZXXrr6jo6cj+sn/4u6VNg74e2h9i//O+kYGvbGJUnBx8uo1INOrVenpzSnIgxbRLxyN3ZDp8YgwJMl8MSCLM1nj2OMzjNRY5EBnEw5h3qNXBs4Hyhpp2FxVk7dA2yLMUZdOpFKJIsqhH1ZHnCEnYrLlx5cVM9yoefFmJ3PptgumtV8ciBnp0lgDGy5nTykPh6zJBz4rAXgOr95WHvoqpyBRAUZIUEgw/vB5aF8/g+CX2gvTlJYF2N9LgJTNHMEwd+zJtmjM8JzkuCfTT3uMDD3JK5O8eNU03a/+9AkbKpK2+Pt829ZPdkObavXi+oJykCmD5IirukVXE9ushR2J+fM4VOvJinsANSI0zjzFpjZMplX63lfhNu/4lj3AWV2G4rkZd3vZQU+4AuhGQ469RA9BFqUJDIsiQQJwHEAWIqo9WNi6H4H8OhferACd2T3d5Y0O3s0EG5JfdADBPh9YDIkB2zEtGc3gGdxFzxVmH48BJViubAHlH4SgJn7gn69T9wyKmJ1M8F9ph/CdhSHT3kADRDELPEEVXCcANG/verCbyxMlAMXvLNKIGgHD+A0/z9QS1WduOOZwWd1mAuNuEg/rq2OB8SoDTv/BseHrXOpc=
- secure: WpqrbdAvNUFn5cM/Iu3zJOaDvT3jWGHCRwvxQCzX9F8iJeTggB5dB2rjgUDCx8LJ8UAt0VCeOcGtR1RT3EHyaHorN3NWeLcBFAHSz2sXv+2xGkspsXwjfygghZTCdYEzhhvmWlz9Ln4s4QJ2fBFZA07pG0jw4Cp1hSQiJ1WlKfDQezldj8D3pPwg1oOq4b5+HVucQ6+PPVwzGk2c3etwb5205L8H8flRjZrP95mFa5n/H3b/HFIsKX5p+CPNIKCrjBEmX0nHXiV0+g6lBQBV1iCwT56vfmN8Urm4KLId71iMpmvstDxlBBRQx3sz41vxIWGFn/oN7iXJI6XfzVFkyvrd9XAQLQFffq4KpN0REy1L3rjO46sYRXu1ycCP5VFVAAwKZn+o1q6xRjCuma2Qj4tqY754pwPNyzXnndFLO7hoN8KjOgV2nk75+XlRG8LhP356CHET62QBZgJ+sl+aFM3hhknsaEuDQywo8Uz4WZL0lPmYqm5BImQT9sTEF6uQNofg4gMy/uqgGhpLtseQW3PoJXB6dmD5JdNxlOalkGSQ+aI/q5QvR6ruIiuap66o4Bu+YTvHiS2hVzmldvMmLFsU1/zECSI6Fs/vkwRN55R9mbPROWi8SzvftYk9shkFMC5QC1FXA/CHqX1W5nl/HpMrs8R9uPhdZ1lifCiW8Rk=
- secure: Px0uj7aMtKUVZVBnoEjHrEHh4cwO2qKJrHHPvwBiDqhwLBaWQIVXYOy0njaYc5o8p96Fv7bMu7NZx/72vMu1+nmTKxgzrMIxvMW6kczBvJUpv6xd1NuC34x+5Xq5gBNOFvb8JarWStcKgIvnFqvBsRUeI1Hsz7Olb8HF+fEo1kShuP18ezSsBkXruw8JuGiU9x0kq4YhZ7vRvFnc3sJX2FL6heuvQsnUWrolUOsKRadNkCibo+Euuls7ExvbbAXN4LEO3rs0G2eBUBbi2wXvTMG9symtItEHTMPO7K+aQfNQnHsY91TYveH/IJM1u5p6OldsUSOUigzpDmpVYW94aLuJaYqc6Ibq3eUws+tv2didOHXZW5zOCFjldDFBIQFPA3fih/wK/JP0taQ0uIu1+2eifvuERarMkGsYlOFe5tJd10ipi+kK5vNxoRwS9kGv5WwP5fVPX2m5XbD2y1LnugCCcAumfNX7NyNBIRqTy7BP34O3EMLZpMxjwSLnUBnYd4V/0LEvoVmbYmrLhWwpojBJmdwe2QknrPuvRErxNujRA1uEVupbU5A6RW1BmrtzSahJYoROI+ayG7UTOSbFN8+DorER1SUXsrOFlawak8yWsoi6OIynTKucrFM3YcBdJ3Su5AIhfBAOASZa6CUa6sn6Zo8mHmDVGKeckvXnLCU=
- secure: 3Dj6roVTO2a9Z8lwlTGzJJ+QGeyIYSuQ/Z6YsYnW3wB9Mw36uWqw9rOmMNIGjjyAlER8bKgalHr90Pus87oaNaIlbEyvq+L+I0FVAwognViFjo/a2apCcq81THoKjT1l0sgGRzvLNDynQe1q2L0tOu21wtBhMLb7FKQYB9+oD3H+rcU63xD2tv3ToJz+j+dccZ9nrtgk0MQ1xAeMtEb4tdq3flKATKhIuDkp9chaDxx/ZGwyhdE0UP29UUyP5Np1QvpFAlAJIZloZPvde2e05fwxTh4rwUCetkfJknDK6WrGiq97WGRXJpfORNuwGn7jxDCtgxcAm9nGF8qmI/v78BhjJ857CfJBTLGv4QI0RszlhXyezJqqRYjCn9S4yx8UAOixVJfJfFNHLqD4MFn41b7j8J3HDJPxNt0t/qYhUMrgrZVosNOUqhwCyQTKDqtrpvmSUbhHpk2+fxZF1GEL5N540rA0OjLmFUUEDSvRQVaa/waeqXrRefOhsXIx20dHs93C6XDffjnxVMKlqtPab2MlHV/23QCuSa6eW+lKyOZ6ZkWLwwDsLbnuD0WJfJpiL1xaTdPJNIDb6kB/Bno4V1O4xrYncEOaA4Vr4amO8le9Q33/QeNrUZSXs/eye5t0f02wCzubuiEDVEeoR/qj5+5CNWv3AWr/f8AgXp80Pzw=
+70
View File
@@ -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; were 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).
+23 -21
View File
@@ -4,8 +4,8 @@
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
@@ -20,38 +20,40 @@ Conductor is architecture-agnostic and does not try to force any design decision
## Installation
```gradle
compile 'com.bluelinelabs:conductor:2.0.3'
implementation 'com.bluelinelabs:conductor:2.1.5'
// If you want the components that go along with
// Android's support libraries (currently just a PagerAdapter):
compile 'com.bluelinelabs:conductor-support:2.0.3'
implementation 'com.bluelinelabs:conductor-support:2.1.5'
// If you want RxJava/RxAndroid lifecycle support:
compile 'com.bluelinelabs:conductor-rxlifecycle:2.0.3'
// If you want RxJava lifecycle support:
implementation 'com.bluelinelabs:conductor-rxlifecycle:2.1.5'
// If you want RxJava2 lifecycle support:
implementation 'com.bluelinelabs:conductor-rxlifecycle2:2.1.5'
// If you want RxJava2 Autodispose support:
implementation 'com.bluelinelabs:conductor-autodispose:2.1.5'
// If you want Controllers that are Lifecycle-aware (architecture components):
implementation 'com.bluelinelabs:conductor-archlifecycle:2.1.5'
```
SNAPSHOT:
**SNAPSHOT**
```gradle
compile 'com.bluelinelabs:conductor:2.0.4-SNAPSHOT'
compile 'com.bluelinelabs:conductor-support:2.0.4-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle:2.0.4-SNAPSHOT'
```
You also have to add the url to the snapshot repository:
Just use `2.1.6-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
| Conductor Components
| | 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.
@@ -73,7 +75,7 @@ public class MainActivity extends Activity {
setContentView(R.layout.activity_main);
ViewGroup container = (ViewGroup)findViewById(R.id.controller_container);
ViewGroup container = (ViewGroup) findViewById(R.id.controller_container);
router = Conductor.attachRouter(this, container, savedInstanceState);
if (!router.hasRootController()) {
@@ -99,7 +101,7 @@ public class HomeController extends Controller {
@Override
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
View view = inflater.inflate(R.layout.controller_home, container, false);
((TextView)view.findViewById(R.id.tv_title)).setText("Hello World");
((TextView) view.findViewById(R.id.tv_title)).setText("Hello World");
return view;
}
@@ -114,7 +116,7 @@ public class HomeController extends Controller {
The lifecycle of a Controller is significantly simpler to understand than that of a Fragment. A lifecycle diagram is shown below:
![Controller Lifecycle](docs/Controller Lifecycle.jpg)
![Controller Lifecycle](docs/Controller%20Lifecycle.jpg)
## Advanced Topics
@@ -128,7 +130,7 @@ The lifecycle of a Controller is significantly simpler to understand than that o
`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).
## License
```
Executable → Regular
+4 -6
View File
@@ -1,21 +1,19 @@
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.0'
classpath 'com.android.tools.build:gradle:3.1.3'
}
}
allprojects {
repositories {
mavenCentral()
maven { url 'https://maven.google.com' }
jcenter()
mavenLocal()
}
}
task wrapper(type: Wrapper) {
gradleVersion = '2.10'
}
apply from: rootProject.file('dependencies.gradle')
+8 -2
View File
@@ -1,12 +1,18 @@
apply plugin: 'java'
targetCompatibility = JavaVersion.VERSION_1_7
sourceCompatibility = JavaVersion.VERSION_1_7
configurations {
lintChecks
}
dependencies {
compile rootProject.ext.lintapi
compile rootProject.ext.lintchecks
implementation rootProject.ext.lintapi
implementation rootProject.ext.lintchecks
testImplementation rootProject.ext.lint
testImplementation rootProject.ext.lintTests
lintChecks files(jar)
}
@@ -1,7 +1,7 @@
package com.bluelinelabs.conductor.lint;
import com.android.annotations.NonNull;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
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;
@@ -9,21 +9,16 @@ import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import java.lang.reflect.Modifier;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UTypeReferenceExpression;
import java.util.Collections;
import java.util.List;
import lombok.ast.ClassDeclaration;
import lombok.ast.ConstructorDeclaration;
import lombok.ast.Node;
import lombok.ast.NormalTypeBody;
import lombok.ast.StrictListAccessor;
import lombok.ast.TypeMember;
import lombok.ast.VariableDefinition;
public final class ControllerChangeHandlerIssueDetector extends Detector implements Detector.JavaScanner, Detector.ClassScanner {
public final class ControllerChangeHandlerIssueDetector extends Detector implements Detector.UastScanner {
public static final Issue ISSUE =
Issue.create("ValidControllerChangeHandler", "ControllerChangeHandler not instantiatable",
@@ -32,69 +27,67 @@ public final class ControllerChangeHandlerIssueDetector extends Detector impleme
Category.CORRECTNESS, 6, Severity.FATAL,
new Implementation(ControllerChangeHandlerIssueDetector.class, Scope.JAVA_FILE_SCOPE));
public ControllerChangeHandlerIssueDetector() { }
private static final String CLASS_NAME = "com.bluelinelabs.conductor.ControllerChangeHandler";
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.<Class<? extends UElement>>singletonList(UClass.class);
}
@Override
public List<String> applicableSuperClasses() {
return Collections.singletonList("com.bluelinelabs.conductor.ControllerChangeHandler");
}
public UElementHandler createUastHandler(final JavaContext context) {
final JavaEvaluator evaluator = context.getEvaluator();
@Override
public void checkClass(@NonNull JavaContext context, ClassDeclaration node,
@NonNull Node declarationOrAnonymous, @NonNull ResolvedClass cls) {
return new UElementHandler() {
if (node == null) {
return;
}
@Override
public void visitClass(UClass node) {
if (evaluator.isAbstract(node)) {
return;
}
final int flags = node.astModifiers().getEffectiveModifierFlags();
if ((flags & Modifier.ABSTRACT) != 0) {
return;
}
boolean hasSuperType = false;
for (UTypeReferenceExpression superType : node.getUastSuperTypes()) {
if (CLASS_NAME.equals(superType.asRenderString())) {
hasSuperType = true;
break;
}
}
if (!hasSuperType) {
return;
}
if ((flags & Modifier.PUBLIC) == 0) {
String message = String.format("This ControllerChangeHandler class should be public (%1$s)", cls.getName());
context.report(ISSUE, node, context.getLocation(node.astName()), message);
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 (cls.getContainingClass() != null && (flags & Modifier.STATIC) == 0) {
String message = String.format("This ControllerChangeHandler inner class should be static (%1$s)", cls.getName());
context.report(ISSUE, node, context.getLocation(node.astName()), 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;
NormalTypeBody body = node.astBody();
if (body != null) {
for (TypeMember member : body.astMembers()) {
if (member instanceof ConstructorDeclaration) {
hasConstructor = true;
ConstructorDeclaration constructor = (ConstructorDeclaration)member;
if (constructor.astModifiers().isPublic()) {
StrictListAccessor<VariableDefinition, ConstructorDeclaration> params = constructor.astParameters();
if (params.isEmpty()) {
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`)",
cls.getName());
context.report(ISSUE, node, context.getLocation(node.astName()), message);
}
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);
}
}
};
}
}
}
@@ -1,8 +1,8 @@
package com.bluelinelabs.conductor.lint;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
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;
@@ -10,21 +10,17 @@ import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import java.lang.reflect.Modifier;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UParameter;
import org.jetbrains.uast.UTypeReferenceExpression;
import java.util.Collections;
import java.util.List;
import lombok.ast.ClassDeclaration;
import lombok.ast.ConstructorDeclaration;
import lombok.ast.Node;
import lombok.ast.NormalTypeBody;
import lombok.ast.StrictListAccessor;
import lombok.ast.TypeMember;
import lombok.ast.VariableDefinition;
public final class ControllerIssueDetector extends Detector implements Detector.JavaScanner, Detector.ClassScanner {
public final class ControllerIssueDetector extends Detector implements Detector.UastScanner {
public static final Issue ISSUE =
Issue.create("ValidController", "Controller not instantiatable",
@@ -33,76 +29,76 @@ public final class ControllerIssueDetector extends Detector implements Detector.
+ " case of the process being killed.", Category.CORRECTNESS, 6, Severity.FATAL,
new Implementation(ControllerIssueDetector.class, Scope.JAVA_FILE_SCOPE));
public ControllerIssueDetector() { }
private static final String CLASS_NAME = "com.bluelinelabs.conductor.Controller";
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.<Class<? extends UElement>>singletonList(UClass.class);
}
@Override
public List<String> applicableSuperClasses() {
return Collections.singletonList("com.bluelinelabs.conductor.Controller");
}
public UElementHandler createUastHandler(final JavaContext context) {
final JavaEvaluator evaluator = context.getEvaluator();
@Override
public void checkClass(@NonNull JavaContext context, ClassDeclaration node,
@NonNull Node declarationOrAnonymous, @NonNull ResolvedClass cls) {
return new UElementHandler() {
@Override
public void visitClass(UClass node) {
if (evaluator.isAbstract(node)) {
return;
}
if (node == null) {
return;
}
boolean hasSuperType = false;
for (UTypeReferenceExpression superType : node.getUastSuperTypes()) {
if (CLASS_NAME.equals(superType.asRenderString())) {
hasSuperType = true;
break;
}
}
if (!hasSuperType) {
return;
}
final int flags = node.astModifiers().getEffectiveModifierFlags();
if ((flags & Modifier.ABSTRACT) != 0) {
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 ((flags & Modifier.PUBLIC) == 0) {
String message = String.format("This Controller class should be public (%1$s)", cls.getName());
context.report(ISSUE, node, context.getLocation(node.astName()), 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;
}
if (cls.getContainingClass() != null && (flags & Modifier.STATIC) == 0) {
String message = String.format("This Controller inner class should be static (%1$s)", cls.getName());
context.report(ISSUE, node, context.getLocation(node.astName()), message);
return;
}
boolean hasConstructor = false;
boolean hasDefaultConstructor = false;
boolean hasBundleConstructor = false;
NormalTypeBody body = node.astBody();
if (body != null) {
for (TypeMember member : body.astMembers()) {
if (member instanceof ConstructorDeclaration) {
hasConstructor = true;
ConstructorDeclaration constructor = (ConstructorDeclaration)member;
if (constructor.astModifiers().isPublic()) {
StrictListAccessor<VariableDefinition, ConstructorDeclaration> params = constructor.astParameters();
if (params.isEmpty()) {
hasDefaultConstructor = true;
break;
} else if (params.size() == 1 &&
(params.first().astTypeReference().getTypeName().equals(SdkConstants.CLASS_BUNDLE)) ||
params.first().astTypeReference().getTypeName().equals("Bundle")) {
hasBundleConstructor = true;
break;
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`)",
cls.getName());
context.report(ISSUE, node, context.getLocation(node.astName()), message);
}
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);
}
}
};
}
}
}
@@ -11,4 +11,4 @@ public final class IssueRegistry extends com.android.tools.lint.client.api.Issue
ControllerIssueDetector.ISSUE,
ControllerChangeHandlerIssueDetector.ISSUE);
}
}
}
@@ -0,0 +1,112 @@
package com.bluelinelabs.conductor.lint;
import org.intellij.lang.annotations.Language;
import org.junit.Test;
import static com.android.tools.lint.checks.infrastructure.TestFiles.java;
import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint;
public class ControllerChangeHandlerDetectorTest {
private static final String CONSTRUCTOR =
"src/test/SampleHandler.java:2: Error: This ControllerChangeHandler needs to have a public default constructor (test.SampleHandler) [ValidControllerChangeHandler]\n"
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ "^\n"
+ "1 errors, 0 warnings\n";
private static final String PRIVATE_CLASS_ERROR =
"src/test/SampleHandler.java:2: Error: This ControllerChangeHandler class should be public (test.SampleHandler) [ValidControllerChangeHandler]\n"
+ "private class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ "^\n"
+ "1 errors, 0 warnings\n";
@Test
public void testWithNoConstructor() {
@Language("JAVA") String source = ""
+ "package test;\n"
+ "public class SampleHandler extends com.bluelinelabs.conductor.ControllerChangeHandler {\n"
+ "}";
lint()
.files(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(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(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(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(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(java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(PRIVATE_CLASS_ERROR);
}
}
@@ -0,0 +1,112 @@
package com.bluelinelabs.conductor.lint;
import org.intellij.lang.annotations.Language;
import org.junit.Test;
import static com.android.tools.lint.checks.infrastructure.TestFiles.java;
import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint;
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";
@Test
public void testWithNoConstructor() {
@Language("JAVA") String source = ""
+ "package test;\n"
+ "public class SampleController extends com.bluelinelabs.conductor.Controller {\n"
+ "}";
lint()
.files(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(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(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(java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expectClean();
}
@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(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(java(source))
.issues(ControllerIssueDetector.ISSUE, ControllerChangeHandlerIssueDetector.ISSUE)
.run()
.expect(CLASS_ERROR);
}
}
@@ -5,11 +5,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
@@ -25,9 +20,9 @@ android {
}
dependencies {
compile rootProject.ext.supportAppCompat
compile project(':conductor')
api rootProject.ext.archComponentsLifecycle
implementation project(':conductor')
}
ext.artifactId = 'conductor-support'
ext.artifactId = 'conductor-arch-components-lifecycle'
@@ -0,0 +1,3 @@
POM_NAME=Conductor Architecture Components Lifecycle Extensions
POM_ARTIFACT_ID=conductor-archlifecycle
POM_PACKAGING=aar
@@ -0,0 +1,3 @@
<manifest package="com.bluelinelabs.conductor.archlifecycle">
<application />
</manifest>
@@ -0,0 +1,65 @@
package com.bluelinelabs.conductor.archlifecycle;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.Lifecycle.Event;
import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.LifecycleRegistry;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
public class ControllerLifecycleOwner implements LifecycleOwner {
private final LifecycleRegistry lifecycleRegistry;
public <T extends Controller & LifecycleOwner> ControllerLifecycleOwner(@NonNull T lifecycleController) {
lifecycleRegistry = new LifecycleRegistry(lifecycleController); // --> State.INITIALIZED
lifecycleController.addLifecycleListener(new LifecycleListener() {
@Override
public void postContextAvailable(@NonNull Controller controller, @NonNull Context context) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_CREATE); // --> State.CREATED;
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_START); // --> State.STARTED;
}
@Override
public void postAttach(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_RESUME); // --> State.RESUMED;
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_PAUSE); // --> State.STARTED;
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_STOP); // --> State.CREATED;
}
@Override
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
// do nothing
}
@Override
public void preDestroy(@NonNull Controller controller) {
lifecycleRegistry.handleLifecycleEvent(Event.ON_DESTROY); // --> State.DESTROYED;
}
});
}
@Override @NonNull
public Lifecycle getLifecycle() {
return lifecycleRegistry;
}
}
@@ -0,0 +1,28 @@
package com.bluelinelabs.conductor.archlifecycle;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleOwner;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
public abstract class LifecycleController extends Controller implements LifecycleOwner {
private final ControllerLifecycleOwner lifecycleOwner = new ControllerLifecycleOwner(this);
public LifecycleController() {
super();
}
public LifecycleController(@Nullable Bundle args) {
super(args);
}
@Override @NonNull
public Lifecycle getLifecycle() {
return lifecycleOwner.getLifecycle();
}
}
@@ -0,0 +1,28 @@
package com.bluelinelabs.conductor.archlifecycle;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleOwner;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bluelinelabs.conductor.RestoreViewOnCreateController;
public abstract class LifecycleRestoreViewOnCreateController extends RestoreViewOnCreateController implements LifecycleOwner {
private final ControllerLifecycleOwner mLifecycleOwner = new ControllerLifecycleOwner(this);
public LifecycleRestoreViewOnCreateController() {
super();
}
public LifecycleRestoreViewOnCreateController(@Nullable Bundle args) {
super(args);
}
@Override @NonNull
public Lifecycle getLifecycle() {
return mLifecycleOwner.getLifecycle();
}
}
@@ -0,0 +1,29 @@
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
api rootProject.ext.rxJava2
api rootProject.ext.autodispose
implementation project(':conductor')
}
ext.artifactId = 'conductor-autodispose'
@@ -0,0 +1,3 @@
POM_NAME=Conductor AutoDispose Extensions
POM_ARTIFACT_ID=conductor-autodispose
POM_PACKAGING=aar
@@ -0,0 +1,3 @@
<manifest package="com.bluelinelabs.conductor.autodispose">
<application />
</manifest>
@@ -0,0 +1,14 @@
package com.bluelinelabs.conductor.autodispose;
public enum ControllerEvent {
CREATE,
CONTEXT_AVAILABLE,
CREATE_VIEW,
ATTACH,
DETACH,
DESTROY_VIEW,
CONTEXT_UNAVAILABLE,
DESTROY
}
@@ -0,0 +1,71 @@
package com.bluelinelabs.conductor.autodispose;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.uber.autodispose.OutsideLifecycleException;
import io.reactivex.subjects.BehaviorSubject;
public class ControllerLifecycleSubjectHelper {
private ControllerLifecycleSubjectHelper() { }
@NonNull
public static BehaviorSubject<ControllerEvent> create(@NonNull Controller controller) {
ControllerEvent initialState;
if (controller.isBeingDestroyed() || controller.isDestroyed()) {
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
} else if (controller.isAttached()) {
initialState = ControllerEvent.ATTACH;
} else if (controller.getView() != null) {
initialState = ControllerEvent.CREATE_VIEW;
} else if (controller.getActivity() != null) {
initialState = ControllerEvent.CONTEXT_AVAILABLE;
} else {
initialState = ControllerEvent.CREATE;
}
final BehaviorSubject<ControllerEvent> subject = BehaviorSubject.createDefault(initialState);
controller.addLifecycleListener(new Controller.LifecycleListener() {
@Override
public void preContextAvailable(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CONTEXT_AVAILABLE);
}
@Override
public void preCreateView(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CREATE_VIEW);
}
@Override
public void preAttach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.ATTACH);
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DETACH);
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DESTROY_VIEW);
}
@Override
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
subject.onNext(ControllerEvent.CONTEXT_UNAVAILABLE);
}
@Override
public void preDestroy(@NonNull Controller controller) {
subject.onNext(ControllerEvent.DESTROY);
}
});
return subject;
}
}
@@ -0,0 +1,74 @@
package com.bluelinelabs.conductor.autodispose;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.Controller;
import com.uber.autodispose.LifecycleScopeProvider;
import com.uber.autodispose.OutsideLifecycleException;
import io.reactivex.Observable;
import io.reactivex.functions.Function;
import io.reactivex.subjects.BehaviorSubject;
public class ControllerScopeProvider implements LifecycleScopeProvider<ControllerEvent> {
private static final CorrespondingEventsFunction CORRESPONDING_EVENTS =
new CorrespondingEventsFunction() {
@Override
public ControllerEvent apply(ControllerEvent lastEvent) throws OutsideLifecycleException {
switch (lastEvent) {
case CREATE:
return ControllerEvent.DESTROY;
case CONTEXT_AVAILABLE:
return ControllerEvent.CONTEXT_UNAVAILABLE;
case CREATE_VIEW:
return ControllerEvent.DESTROY_VIEW;
case ATTACH:
return ControllerEvent.DETACH;
case DETACH:
return ControllerEvent.DESTROY;
default:
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
}
}
};
@NonNull private final BehaviorSubject<ControllerEvent> lifecycleSubject;
@NonNull private final Function<ControllerEvent, ControllerEvent> correspondingEventsFunction;
public static ControllerScopeProvider from(@NonNull Controller controller) {
return new ControllerScopeProvider(controller, CORRESPONDING_EVENTS);
}
public static ControllerScopeProvider from(@NonNull Controller controller, @NonNull final ControllerEvent untilEvent) {
return new ControllerScopeProvider(controller, new CorrespondingEventsFunction() {
@Override
public ControllerEvent apply(ControllerEvent controllerEvent) {
return untilEvent;
}
});
}
public static ControllerScopeProvider from(@NonNull Controller controller, @NonNull final CorrespondingEventsFunction correspondingEventsFunction) {
return new ControllerScopeProvider(controller, correspondingEventsFunction);
}
private ControllerScopeProvider(@NonNull Controller controller, @NonNull CorrespondingEventsFunction correspondingEventsFunction) {
lifecycleSubject = ControllerLifecycleSubjectHelper.create(controller);
this.correspondingEventsFunction = correspondingEventsFunction;
}
@Override
public Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
public Function<ControllerEvent, ControllerEvent> correspondingEvents() {
return correspondingEventsFunction;
}
@Override
public ControllerEvent peekLifecycle() {
return lifecycleSubject.getValue();
}
}
@@ -0,0 +1,24 @@
package com.bluelinelabs.conductor.autodispose;
import com.uber.autodispose.OutsideLifecycleException;
import io.reactivex.functions.Function;
/**
* Based on https://github.com/uber/AutoDispose/blob/master/lifecycle/autodispose-lifecycle/src/main/java/com/uber/autodispose/lifecycle/CorrespondingEventsFunction.java
*
* A corresponding events function that acts as a normal {@link Function} but ensures ControllerEvent event
* types are used in the generic and tightens the possible exception thrown to {@link OutsideLifecycleException}.
*/
public interface CorrespondingEventsFunction extends Function<ControllerEvent, ControllerEvent> {
/**
* Given an event {@code event}, returns the next corresponding event that this lifecycle should
* dispose on.
*
* @param event the source or start event.
* @return the target event that should signal disposal.
* @throws OutsideLifecycleException if the lifecycle exceeds its scope boundaries.
*/
@Override ControllerEvent apply(ControllerEvent event) throws OutsideLifecycleException;
}
@@ -5,11 +5,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
@@ -25,13 +20,11 @@ android {
}
dependencies {
compile rootProject.ext.rxJava
compile rootProject.ext.rxAndroid
compile rootProject.ext.rxLifecycle
compile rootProject.ext.rxLifecycleAndroid
api rootProject.ext.rxJava
api rootProject.ext.rxLifecycle
api rootProject.ext.rxLifecycleAndroid
compile project(':conductor')
implementation project(':conductor')
}
ext.artifactId = 'conductor-rxlifecycle'
@@ -1,3 +1,3 @@
<manifest package="com.bluelinelabs.conductor.rxlifecycle">
<application />
</manifest>
</manifest>
@@ -3,10 +3,12 @@ package com.bluelinelabs.conductor.rxlifecycle;
public enum ControllerEvent {
CREATE,
CONTEXT_AVAILABLE,
CREATE_VIEW,
ATTACH,
DETACH,
DESTROY_VIEW,
CONTEXT_UNAVAILABLE,
DESTROY
}
@@ -1,10 +1,12 @@
package com.bluelinelabs.conductor.rxlifecycle;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.trello.rxlifecycle.OutsideLifecycleException;
import rx.subjects.BehaviorSubject;
@@ -17,9 +19,27 @@ public class ControllerLifecycleSubjectHelper {
private ControllerLifecycleSubjectHelper() { }
public static BehaviorSubject<ControllerEvent> create(Controller controller) {
final BehaviorSubject<ControllerEvent> subject = BehaviorSubject.create(ControllerEvent.CREATE);
ControllerEvent initialState;
if (controller.isBeingDestroyed() || controller.isDestroyed()) {
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
} else if (controller.isAttached()) {
initialState = ControllerEvent.ATTACH;
} else if (controller.getView() != null) {
initialState = ControllerEvent.CREATE_VIEW;
} else if (controller.getActivity() != null) {
initialState = ControllerEvent.CONTEXT_AVAILABLE;
} else {
initialState = ControllerEvent.CREATE;
}
final BehaviorSubject<ControllerEvent> subject = BehaviorSubject.create(initialState);
controller.addLifecycleListener(new LifecycleListener() {
@Override
public void preContextAvailable(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CONTEXT_AVAILABLE);
}
@Override
public void preCreateView(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CREATE_VIEW);
@@ -40,6 +60,11 @@ public class ControllerLifecycleSubjectHelper {
subject.onNext(ControllerEvent.DESTROY_VIEW);
}
@Override
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
subject.onNext(ControllerEvent.CONTEXT_UNAVAILABLE);
}
@Override
public void preDestroy(@NonNull Controller controller) {
subject.onNext(ControllerEvent.DESTROY);
@@ -49,4 +49,4 @@ public abstract class RxController extends Controller implements LifecycleProvid
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
}
@@ -17,7 +17,7 @@ public class RxControllerLifecycle {
* {@link com.trello.rxlifecycle.android.RxLifecycleAndroid#bindFragment(Observable)}.
*
* @param lifecycle the lifecycle sequence of a Controller
* @return a reusable {@link Observable.Transformer} that unsubscribes the source during the Controller lifecycle
* @return a reusable {@link rx.Observable.Transformer} that unsubscribes the source during the Controller lifecycle
*/
@NonNull
@CheckResult
@@ -32,6 +32,8 @@ public class RxControllerLifecycle {
switch (lastEvent) {
case CREATE:
return ControllerEvent.DESTROY;
case CONTEXT_AVAILABLE:
return ControllerEvent.CONTEXT_UNAVAILABLE;
case ATTACH:
return ControllerEvent.DETACH;
case CREATE_VIEW:
@@ -0,0 +1,52 @@
package com.bluelinelabs.conductor.rxlifecycle;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.RestoreViewOnCreateController;
import com.trello.rxlifecycle.LifecycleProvider;
import com.trello.rxlifecycle.LifecycleTransformer;
import com.trello.rxlifecycle.RxLifecycle;
import rx.Observable;
import rx.subjects.BehaviorSubject;
/**
* A base {@link RestoreViewOnCreateController} that can be used to expose lifecycle events using RxJava
*/
public abstract class RxRestoreViewOnCreateController extends RestoreViewOnCreateController implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxRestoreViewOnCreateController() {
this(null);
}
public RxRestoreViewOnCreateController(Bundle args) {
super(args);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.asObservable();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
@@ -0,0 +1,30 @@
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
api rootProject.ext.rxJava2
api rootProject.ext.rxLifecycle2
api rootProject.ext.rxLifecycleAndroid2
implementation project(':conductor')
}
ext.artifactId = 'conductor-rxlifecycle2'
@@ -0,0 +1,3 @@
POM_NAME=Conductor RxLifecycle2 Extensions
POM_ARTIFACT_ID=conductor-rxlifecycle2
POM_PACKAGING=aar
@@ -0,0 +1,3 @@
<manifest package="com.bluelinelabs.conductor.rxlifecycle2">
<application />
</manifest>
@@ -0,0 +1,14 @@
package com.bluelinelabs.conductor.rxlifecycle2;
public enum ControllerEvent {
CREATE,
CONTEXT_AVAILABLE,
CREATE_VIEW,
ATTACH,
DETACH,
DESTROY_VIEW,
CONTEXT_UNAVAILABLE,
DESTROY
}
@@ -0,0 +1,71 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.trello.rxlifecycle2.OutsideLifecycleException;
import io.reactivex.subjects.BehaviorSubject;
public class ControllerLifecycleSubjectHelper {
private ControllerLifecycleSubjectHelper() {
}
public static BehaviorSubject<ControllerEvent> create(Controller controller) {
ControllerEvent initialState;
if (controller.isBeingDestroyed() || controller.isDestroyed()) {
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
} else if (controller.isAttached()) {
initialState = ControllerEvent.ATTACH;
} else if (controller.getView() != null) {
initialState = ControllerEvent.CREATE_VIEW;
} else if (controller.getActivity() != null) {
initialState = ControllerEvent.CONTEXT_AVAILABLE;
} else {
initialState = ControllerEvent.CREATE;
}
final BehaviorSubject<ControllerEvent> subject = BehaviorSubject.createDefault(initialState);
controller.addLifecycleListener(new Controller.LifecycleListener() {
@Override
public void preContextAvailable(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CONTEXT_AVAILABLE);
}
@Override
public void preCreateView(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CREATE_VIEW);
}
@Override
public void preAttach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.ATTACH);
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DETACH);
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DESTROY_VIEW);
}
@Override
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
subject.onNext(ControllerEvent.CONTEXT_UNAVAILABLE);
}
@Override
public void preDestroy(@NonNull Controller controller) {
subject.onNext(ControllerEvent.DESTROY);
}
});
return subject;
}
}
@@ -0,0 +1,49 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
import com.trello.rxlifecycle2.LifecycleProvider;
import com.trello.rxlifecycle2.LifecycleTransformer;
import com.trello.rxlifecycle2.RxLifecycle;
import io.reactivex.Observable;
import io.reactivex.subjects.BehaviorSubject;
/**
* A base {@link Controller} that can be used to expose lifecycle events using RxJava
*/
public abstract class RxController extends Controller implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxController(){
this(null);
}
public RxController(@Nullable Bundle args) {
super(args);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
@@ -0,0 +1,43 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.support.annotation.NonNull;
import com.trello.rxlifecycle2.LifecycleTransformer;
import com.trello.rxlifecycle2.OutsideLifecycleException;
import com.trello.rxlifecycle2.RxLifecycle;
import io.reactivex.Observable;
import io.reactivex.functions.Function;
public class RxControllerLifecycle {
/**
* Binds the given source to a Controller lifecycle. This is the Controller version of
* {@link com.trello.rxlifecycle2.android.RxLifecycleAndroid#bindFragment(Observable)}.
*
* @param lifecycle the lifecycle sequence of a Controller
* @return a reusable {@link io.reactivex.ObservableTransformer} that unsubscribes the source during the Controller lifecycle
*/
public static <T> LifecycleTransformer<T> bindController(@NonNull final Observable<ControllerEvent> lifecycle) {
return RxLifecycle.bind(lifecycle, CONTROLLER_LIFECYCLE);
}
private static final Function<ControllerEvent, ControllerEvent> CONTROLLER_LIFECYCLE =
new Function<ControllerEvent, ControllerEvent>() {
@Override
public ControllerEvent apply(ControllerEvent lastEvent) {
switch (lastEvent) {
case CREATE:
return ControllerEvent.DESTROY;
case CONTEXT_AVAILABLE:
return ControllerEvent.CONTEXT_UNAVAILABLE;
case ATTACH:
return ControllerEvent.DETACH;
case CREATE_VIEW:
return ControllerEvent.DESTROY_VIEW;
case DETACH:
return ControllerEvent.DESTROY;
default:
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
}
}
};
}
@@ -0,0 +1,45 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import com.bluelinelabs.conductor.RestoreViewOnCreateController;
import com.trello.rxlifecycle2.LifecycleProvider;
import com.trello.rxlifecycle2.LifecycleTransformer;
import com.trello.rxlifecycle2.RxLifecycle;
import io.reactivex.Observable;
import io.reactivex.subjects.BehaviorSubject;
public abstract class RxRestoreViewOnCreateController extends RestoreViewOnCreateController implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxRestoreViewOnCreateController() {
this(null);
}
public RxRestoreViewOnCreateController(Bundle args) {
super(args);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
+40
View File
@@ -0,0 +1,40 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'de.mobilej.unmock:UnMockPlugin:0.6.5'
}
}
apply plugin: 'com.android.library'
apply plugin: 'de.mobilej.unmock'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.roboelectric
implementation rootProject.ext.supportV4
implementation project(':conductor')
}
ext.artifactId = 'conductor-support'
apply from: rootProject.file('dependencies.gradle')
apply from: rootProject.file('gradle-mvn-push.gradle')
@@ -1,3 +1,3 @@
<manifest package="com.bluelinelabs.conductor.support">
<application />
</manifest>
</manifest>
@@ -2,6 +2,8 @@ package com.bluelinelabs.conductor.support;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.util.SparseArray;
import android.view.View;
@@ -12,21 +14,28 @@ import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
/**
* @deprecated Use RouterPagerAdapter instead! This implementation was too limited and had too many
* gotchas associated with it.
*
* An adapter for ViewPagers that will handle adding and removing Controllers
*/
@Deprecated
public abstract class ControllerPagerAdapter extends PagerAdapter {
private static final String KEY_SAVED_PAGES = "ControllerPagerAdapter.savedStates";
private static final String KEY_SAVES_STATE = "ControllerPagerAdapter.savesState";
private static final String KEY_VISIBLE_PAGE_IDS_KEYS = "ControllerPagerAdapter.visiblePageIds.keys";
private static final String KEY_VISIBLE_PAGE_IDS_VALUES = "ControllerPagerAdapter.visiblePageIds.values";
private final Controller host;
private boolean savesState;
private SparseArray<Bundle> savedPages = new SparseArray<>();
private SparseArray<String> visiblePageIds = new SparseArray<>();
/**
* Creates a new ControllerPagerAdapter using the passed host.
*/
public ControllerPagerAdapter(Controller host, boolean saveControllerState) {
public ControllerPagerAdapter(@NonNull Controller host, boolean saveControllerState) {
this.host = host;
savesState = saveControllerState;
}
@@ -34,6 +43,7 @@ public abstract class ControllerPagerAdapter extends PagerAdapter {
/**
* Return the Controller associated with a specified position.
*/
@NonNull
public abstract Controller getItem(int position);
@Override
@@ -49,11 +59,17 @@ public abstract class ControllerPagerAdapter extends PagerAdapter {
}
}
final Controller controller;
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(getItem(position))
.tag(name));
controller = getItem(position);
router.setRoot(RouterTransaction.with(controller).tag(name));
} else {
router.rebindIfNeeded();
controller = router.getControllerWithTag(name);
}
if (controller != null) {
visiblePageIds.put(position, controller.getInstanceId());
}
return router.getControllerWithTag(name);
@@ -69,6 +85,8 @@ public abstract class ControllerPagerAdapter extends PagerAdapter {
savedPages.put(position, savedState);
}
visiblePageIds.remove(position);
host.removeChildRouter(router);
}
@@ -82,6 +100,16 @@ public abstract class ControllerPagerAdapter extends PagerAdapter {
Bundle bundle = new Bundle();
bundle.putBoolean(KEY_SAVES_STATE, savesState);
bundle.putSparseParcelableArray(KEY_SAVED_PAGES, savedPages);
int[] visiblePageIdsKeys = new int[visiblePageIds.size()];
String[] visiblePageIdsValues = new String[visiblePageIds.size()];
for (int i = 0; i < visiblePageIds.size(); i++) {
visiblePageIdsKeys[i] = visiblePageIds.keyAt(i);
visiblePageIdsValues[i] = visiblePageIds.valueAt(i);
}
bundle.putIntArray(KEY_VISIBLE_PAGE_IDS_KEYS, visiblePageIdsKeys);
bundle.putStringArray(KEY_VISIBLE_PAGE_IDS_VALUES, visiblePageIdsValues);
return bundle;
}
@@ -91,6 +119,27 @@ public abstract class ControllerPagerAdapter extends PagerAdapter {
if (state != null) {
savesState = bundle.getBoolean(KEY_SAVES_STATE, false);
savedPages = bundle.getSparseParcelableArray(KEY_SAVED_PAGES);
int[] visiblePageIdsKeys = bundle.getIntArray(KEY_VISIBLE_PAGE_IDS_KEYS);
String[] visiblePageIdsValues = bundle.getStringArray(KEY_VISIBLE_PAGE_IDS_VALUES);
visiblePageIds = new SparseArray<>(visiblePageIdsKeys.length);
for (int i = 0; i < visiblePageIdsKeys.length; i++) {
visiblePageIds.put(visiblePageIdsKeys[i], visiblePageIdsValues[i]);
}
}
}
/**
* Returns the already instantiated Controller in the specified position or {@code null} if
* this position does not yet have a controller.
*/
@Nullable
public Controller getController(int position) {
String instanceId = visiblePageIds.get(position);
if (instanceId != null) {
return host.getRouter().getControllerWithInstanceId(instanceId);
} else {
return null;
}
}
@@ -102,4 +151,4 @@ public abstract class ControllerPagerAdapter extends PagerAdapter {
return viewId + ":" + id;
}
}
}
@@ -0,0 +1,187 @@
package com.bluelinelabs.conductor.support;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import java.util.ArrayList;
import java.util.List;
/**
* An adapter for ViewPagers 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_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 SparseArray<Bundle> savedPages = new SparseArray<>();
private 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));
Router router = host.getChildRouter(container, name);
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);
}
}
visibleRouters.put(position, router);
return router;
}
@Override
public void destroyItem(ViewGroup container, int position, 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(ViewGroup container, int position, 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(View view, 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.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);
}
}
/**
* 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;
}
}
@@ -0,0 +1,113 @@
package com.bluelinelabs.conductor.support;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.util.SparseArray;
import android.widget.FrameLayout;
import com.bluelinelabs.conductor.Conductor;
import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.support.util.FakePager;
import com.bluelinelabs.conductor.support.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class StateSaveTests {
private FakePager pager;
private RouterPagerAdapter pagerAdapter;
public void createActivityController(Bundle savedInstanceState) {
ActivityController<Activity> activityController = Robolectric.buildActivity(Activity.class).create().start().resume();
Router router = Conductor.attachRouter(activityController.get(), new FrameLayout(activityController.get()), savedInstanceState);
TestController controller = new TestController();
router.setRoot(RouterTransaction.with(controller));
pager = new FakePager(new FrameLayout(activityController.get()));
pager.setOffscreenPageLimit(1);
pagerAdapter = new RouterPagerAdapter(controller) {
@Override
public void configureRouter(@NonNull Router router, int position) {
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Override
public int getCount() {
return 20;
}
};
pager.setAdapter(pagerAdapter);
}
@Before
public void setup() {
createActivityController(null);
}
@Test
public void testNoMaxSaves() {
// Load all pages
for (int i = 0; i < pagerAdapter.getCount(); i++) {
pager.pageTo(i);
}
pager.pageTo(pagerAdapter.getCount() / 2);
// Ensure all non-visible pages are saved
assertEquals(pagerAdapter.getCount() - 1 - pager.getOffscreenPageLimit() * 2, pagerAdapter.getSavedPages().size());
}
@Test
public void testMaxSavedSet() {
final int maxPages = 3;
pagerAdapter.setMaxPagesToStateSave(maxPages);
// Load all pages
for (int i = 0; i < pagerAdapter.getCount(); i++) {
pager.pageTo(i);
}
final int firstSelectedItem = pagerAdapter.getCount() / 2;
pager.pageTo(firstSelectedItem);
SparseArray<Bundle> savedPages = pagerAdapter.getSavedPages();
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size());
// Ensure correct pages are saved
assertEquals(pagerAdapter.getCount() - 3, savedPages.keyAt(0));
assertEquals(pagerAdapter.getCount() - 2, savedPages.keyAt(1));
assertEquals(pagerAdapter.getCount() - 1, savedPages.keyAt(2));
final int secondSelectedItem = 1;
pager.pageTo(secondSelectedItem);
savedPages = pagerAdapter.getSavedPages();
// Ensure correct number of pages are saved
assertEquals(maxPages, savedPages.size());
// Ensure correct pages are saved
assertEquals(firstSelectedItem - 1, savedPages.keyAt(0));
assertEquals(firstSelectedItem, savedPages.keyAt(1));
assertEquals(firstSelectedItem + 1, savedPages.keyAt(2));
}
}
@@ -0,0 +1,61 @@
package com.bluelinelabs.conductor.support.util;
import android.util.SparseArray;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.support.RouterPagerAdapter;
import java.util.ArrayList;
import java.util.List;
public class FakePager {
private ViewGroup container;
private int offscreenPageLimit;
private final SparseArray<Object> pages = new SparseArray<>();
private RouterPagerAdapter adapter;
public FakePager(ViewGroup container) {
this.container = container;
}
public void setAdapter(RouterPagerAdapter adapter) {
this.adapter = adapter;
}
public void pageTo(int page) {
int firstPage = Math.max(0, page - offscreenPageLimit);
int lastPage = Math.min(adapter.getCount() - 1, page + offscreenPageLimit);
List<Integer> pagesI = new ArrayList<>();
for (int i = 0; i < pages.size(); i++) {
pagesI.add(pages.keyAt(i));
}
for (int i = pages.size() - 1; i >= 0; i--) {
int key = pages.keyAt(i);
if (key < firstPage || key > lastPage) {
adapter.destroyItem(container, key, pages.get(key));
pages.remove(key);
}
}
for (int key = firstPage; key <= lastPage; key++) {
if (pages.get(key) == null) {
pages.put(key, adapter.instantiateItem(container, key));
}
}
adapter.setPrimaryItem(container, page, pages.get(page));
}
public int getOffscreenPageLimit() {
return offscreenPageLimit;
}
public void setOffscreenPageLimit(int offscreenPageLimit) {
this.offscreenPageLimit = offscreenPageLimit;
}
}
@@ -0,0 +1,18 @@
package com.bluelinelabs.conductor.support.util;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.bluelinelabs.conductor.Controller;
public class TestController extends Controller {
@Override @NonNull
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return new FrameLayout(inflater.getContext());
}
}
Executable → Regular
+4 -17
View File
@@ -3,7 +3,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'de.mobilej.unmock:UnMockPlugin:0.3.6'
classpath 'de.mobilej.unmock:UnMockPlugin:0.6.5'
}
}
@@ -12,11 +12,6 @@ apply plugin: 'de.mobilej.unmock'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
@@ -37,22 +32,14 @@ configurations {
}
dependencies {
testCompile rootProject.ext.junit
testCompile rootProject.ext.roboelectric
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.roboelectric
compile rootProject.ext.supportAnnotations
api rootProject.ext.supportAnnotations
lintChecks project(path: ':conductor-lint', configuration: 'lintChecks')
}
unMock {
downloadFrom 'https://oss.sonatype.org/content/groups/public/org/robolectric/android-all/4.3_r2-robolectric-0/android-all-4.3_r2-robolectric-0.jar'
keep "android.os.Bundle"
keep "android.os.BaseBundle"
keep "android.text.TextUtils"
}
task copyLintJar(type: Copy) {
from(configurations.lintChecks) {
rename { 'lint.jar' }
+1 -1
View File
@@ -2,4 +2,4 @@
-keepclassmembers public class * extends com.bluelinelabs.conductor.Controller {
public <init>();
public <init>(android.os.Bundle);
}
}
+1 -1
View File
@@ -1,3 +1,3 @@
<manifest package="com.bluelinelabs.conductor">
<application />
</manifest>
</manifest>
@@ -2,18 +2,23 @@ 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.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.ViewGroup;
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 final void setHost(@NonNull LifecycleHandler lifecycleHandler, @NonNull ViewGroup container) {
if (this.lifecycleHandler != lifecycleHandler || this.container != container) {
@@ -27,16 +32,32 @@ public class ActivityHostedRouter extends Router {
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(Activity activity) {
public void onActivityDestroyed(@NonNull Activity activity) {
super.onActivityDestroyed(activity);
lifecycleHandler = null;
}
@@ -49,37 +70,43 @@ public class ActivityHostedRouter extends Router {
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
lifecycleHandler.onActivityResult(requestCode, resultCode, data);
}
@Override
void startActivity(Intent intent) {
void startActivity(@NonNull Intent intent) {
lifecycleHandler.startActivity(intent);
}
@Override
void startActivityForResult(String instanceId, Intent intent, int requestCode) {
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) {
lifecycleHandler.startActivityForResult(instanceId, intent, requestCode);
}
@Override
void startActivityForResult(String instanceId, Intent intent, int requestCode, Bundle options) {
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options) {
lifecycleHandler.startActivityForResult(instanceId, intent, requestCode, options);
}
@Override
void registerForActivityResult(String instanceId, int requestCode) {
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(String instanceId) {
void unregisterForActivityResults(@NonNull String instanceId) {
lifecycleHandler.unregisterForActivityResults(instanceId);
}
@Override
void requestPermissions(String instanceId, @NonNull String[] permissions, int requestCode) {
void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
lifecycleHandler.requestPermissions(instanceId, permissions, requestCode);
}
@@ -88,13 +115,24 @@ public class ActivityHostedRouter extends Router {
return lifecycleHandler != null;
}
@Override
@Override @NonNull
List<Router> getSiblingRouters() {
return lifecycleHandler.getRouters();
}
@Override
@Override @NonNull
Router getRootRouter() {
return this;
}
@Override @NonNull
TransactionIndexer getTransactionIndexer() {
return transactionIndexer;
}
@Override
public void onContextAvailable() {
super.onContextAvailable();
}
}
+46 -47
View File
@@ -1,6 +1,8 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -13,34 +15,37 @@ class Backstack implements Iterable<RouterTransaction> {
private static final String KEY_ENTRIES = "Backstack.entries";
private final Deque<RouterTransaction> backStack = new ArrayDeque<>();
private final Deque<RouterTransaction> backstack = new ArrayDeque<>();
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isEmpty() {
return backStack.isEmpty();
boolean isEmpty() {
return backstack.isEmpty();
}
public int size() {
return backStack.size();
int size() {
return backstack.size();
}
public RouterTransaction root() {
return backStack.size() > 0 ? backStack.getLast() : null;
@Nullable
RouterTransaction root() {
return backstack.size() > 0 ? backstack.getLast() : null;
}
@Override
@Override @NonNull
public Iterator<RouterTransaction> iterator() {
return backStack.iterator();
return backstack.iterator();
}
public Iterator<RouterTransaction> reverseIterator() {
return backStack.descendingIterator();
@NonNull
Iterator<RouterTransaction> reverseIterator() {
return backstack.descendingIterator();
}
public List<RouterTransaction> popTo(RouterTransaction transaction) {
@NonNull
List<RouterTransaction> popTo(@NonNull RouterTransaction transaction) {
List<RouterTransaction> popped = new ArrayList<>();
if (backStack.contains(transaction)) {
while (backStack.peek() != transaction) {
if (backstack.contains(transaction)) {
while (backstack.peek() != transaction) {
RouterTransaction poppedTransaction = pop();
popped.add(poppedTransaction);
}
@@ -50,25 +55,24 @@ class Backstack implements Iterable<RouterTransaction> {
return popped;
}
public RouterTransaction pop() {
RouterTransaction popped = backStack.pop();
@NonNull
RouterTransaction pop() {
RouterTransaction popped = backstack.pop();
popped.controller.destroy();
return popped;
}
public RouterTransaction peek() {
return backStack.peek();
@Nullable
RouterTransaction peek() {
return backstack.peek();
}
public void remove(RouterTransaction transaction) {
backStack.removeFirstOccurrence(transaction);
void push(@NonNull RouterTransaction transaction) {
backstack.push(transaction);
}
public void push(RouterTransaction transaction) {
backStack.push(transaction);
}
public List<RouterTransaction> popAll() {
@NonNull
List<RouterTransaction> popAll() {
List<RouterTransaction> list = new ArrayList<>();
while (!isEmpty()) {
list.add(pop());
@@ -76,42 +80,37 @@ class Backstack implements Iterable<RouterTransaction> {
return list;
}
public void setBackstack(List<RouterTransaction> backstack) {
for (RouterTransaction existingTransaction : backStack) {
boolean contains = false;
for (RouterTransaction newTransaction : backstack) {
if (existingTransaction.controller == newTransaction.controller) {
contains = true;
break;
}
}
if (!contains) {
existingTransaction.controller.destroy();
}
}
backStack.clear();
void setBackstack(@NonNull List<RouterTransaction> backstack) {
this.backstack.clear();
for (RouterTransaction transaction : backstack) {
backStack.push(transaction);
this.backstack.push(transaction);
}
}
public void saveInstanceState(Bundle outState) {
ArrayList<Bundle> entryBundles = new ArrayList<>(backStack.size());
for (RouterTransaction entry : backStack) {
boolean contains(@NonNull Controller controller) {
for (RouterTransaction transaction : backstack) {
if (controller == transaction.controller) {
return true;
}
}
return false;
}
void saveInstanceState(@NonNull Bundle outState) {
ArrayList<Bundle> entryBundles = new ArrayList<>(backstack.size());
for (RouterTransaction entry : backstack) {
entryBundles.add(entry.saveInstanceState());
}
outState.putParcelableArrayList(KEY_ENTRIES, entryBundles);
}
public void restoreInstanceState(Bundle savedInstanceState) {
void restoreInstanceState(@NonNull Bundle savedInstanceState) {
ArrayList<Bundle> entryBundles = savedInstanceState.getParcelableArrayList(KEY_ENTRIES);
if (entryBundles != null) {
Collections.reverse(entryBundles);
for (Bundle transactionBundle : entryBundles) {
backStack.push(new RouterTransaction(transactionBundle));
backstack.push(new RouterTransaction(transactionBundle));
}
}
}
@@ -3,6 +3,8 @@ package com.bluelinelabs.conductor;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewGroup;
@@ -42,12 +44,12 @@ public class ChangeHandlerFrameLayout extends FrameLayout implements ControllerC
}
@Override
public void onChangeStarted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler) {
public void onChangeStarted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) {
inProgressTransactionCount++;
}
@Override
public void onChangeCompleted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler) {
public void onChangeCompleted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) {
inProgressTransactionCount--;
}
@@ -3,17 +3,20 @@ package com.bluelinelabs.conductor;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.internal.LifecycleHandler;
import com.bluelinelabs.conductor.internal.ThreadUtils;
/**
* Point of initial interaction with Conductor. Used to attach a {@link Router} to your Activity.
*/
public final class Conductor {
private Conductor() {}
/**
* Conductor will create a {@link Router} that has been initialized for your Activity and containing ViewGroup.
* If an existing {@link Router} is already associated with this Activity/ViewGroup pair, either in memory
@@ -26,7 +29,10 @@ public final class Conductor {
* 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) {
@NonNull @UiThread
public static Router attachRouter(@NonNull Activity activity, @NonNull ViewGroup container, @Nullable Bundle savedInstanceState) {
ThreadUtils.ensureMainThread();
LifecycleHandler lifecycleHandler = LifecycleHandler.install(activity);
Router router = lifecycleHandler.getRouter(container, savedInstanceState);
File diff suppressed because it is too large Load Diff
@@ -2,6 +2,7 @@ package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
@@ -9,7 +10,6 @@ import android.view.ViewParent;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import com.bluelinelabs.conductor.internal.ClassUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -24,20 +24,21 @@ public abstract class ControllerChangeHandler {
private static final String KEY_CLASS_NAME = "ControllerChangeHandler.className";
private static final String KEY_SAVED_STATE = "ControllerChangeHandler.savedState";
private static final Map<String, ControllerChangeHandler> inProgressPushHandlers = new HashMap<>();
static final Map<String, ChangeHandlerData> inProgressChangeHandlers = new HashMap<>();
private boolean forceRemoveViewOnPush;
boolean forceRemoveViewOnPush;
private boolean hasBeenUsed;
/**
* Responsible for swapping Views from one Controller to another.
*
* @param container The container these Views are hosted in.
* @param from The previous View in the container, if any.
* @param to The next View that should be put in the container, if any.
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop.
* @param changeListener This listener must be called when any transitions or animations are completed.
*/
public abstract void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener);
public abstract void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener);
public ControllerChangeHandler() {
ensureDefaultConstructor();
@@ -61,10 +62,11 @@ public abstract class ControllerChangeHandler {
* 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
* @param newHandler The change handler that has caused this push to be aborted
* @param newTop The Controller that will now be at the top of the backstack or {@code null}
* if there will be no new Controller at the top
*/
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, Controller newTop) { }
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) { }
/**
* Will be called on change handlers that push a controller if the controller being pushed is
@@ -72,6 +74,27 @@ public abstract class ControllerChangeHandler {
*/
public void completeImmediately() { }
/**
* Returns a copy of this ControllerChangeHandler. This method is internally used by the library, so
* ensure it will return an exact copy of your handler if overriding. If not overriding, the handler
* will be saved and restored from the Bundle format.
*/
@NonNull
public ControllerChangeHandler copy() {
return fromBundle(toBundle());
}
/**
* Returns whether or not this is a reusable ControllerChangeHandler. Defaults to false and should
* ONLY be overridden if there are absolutely no side effects to using this handler more than once.
* In the case that a handler is not reusable, it will be copied using the {@link #copy()} method
* prior to use.
*/
public boolean isReusable() {
return false;
}
@NonNull
final Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putString(KEY_CLASS_NAME, getClass().getName());
@@ -83,10 +106,6 @@ public abstract class ControllerChangeHandler {
return bundle;
}
final ControllerChangeHandler copy() {
return fromBundle(toBundle());
}
private void ensureDefaultConstructor() {
try {
getClass().getConstructor();
@@ -95,7 +114,8 @@ public abstract class ControllerChangeHandler {
}
}
public static ControllerChangeHandler fromBundle(Bundle bundle) {
@Nullable
public static ControllerChangeHandler fromBundle(@Nullable Bundle bundle) {
if (bundle != null) {
String className = bundle.getString(KEY_CLASS_NAME);
ControllerChangeHandler changeHandler = ClassUtils.newInstance(className);
@@ -107,40 +127,59 @@ public abstract class ControllerChangeHandler {
}
}
static boolean completePushImmediately(String controllerInstanceId) {
ControllerChangeHandler changeHandler = inProgressPushHandlers.get(controllerInstanceId);
if (changeHandler != null) {
changeHandler.completeImmediately();
inProgressPushHandlers.remove(controllerInstanceId);
static boolean completeHandlerImmediately(@NonNull String controllerInstanceId) {
ChangeHandlerData changeHandlerData = inProgressChangeHandlers.get(controllerInstanceId);
if (changeHandlerData != null) {
changeHandlerData.changeHandler.completeImmediately();
inProgressChangeHandlers.remove(controllerInstanceId);
return true;
}
return false;
}
public static void abortPush(Controller toAbort, Controller newController, ControllerChangeHandler newChangeHandler) {
ControllerChangeHandler handlerForPush = inProgressPushHandlers.get(toAbort.getInstanceId());
if (handlerForPush != null) {
handlerForPush.onAbortPush(newChangeHandler, newController);
inProgressPushHandlers.remove(toAbort.getInstanceId());
static void abortOrComplete(@NonNull Controller toAbort, @Nullable Controller newController, @NonNull ControllerChangeHandler newChangeHandler) {
ChangeHandlerData changeHandlerData = inProgressChangeHandlers.get(toAbort.getInstanceId());
if (changeHandlerData != null) {
if (changeHandlerData.isPush) {
changeHandlerData.changeHandler.onAbortPush(newChangeHandler, newController);
} else {
changeHandlerData.changeHandler.completeImmediately();
}
inProgressChangeHandlers.remove(toAbort.getInstanceId());
}
}
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>());
static void executeChange(@NonNull final ChangeTransaction transaction) {
executeChange(transaction.to, transaction.from, transaction.isPush, transaction.container, transaction.changeHandler, transaction.listeners);
}
public static void executeChange(final Controller to, final Controller from, final boolean isPush, final ViewGroup container, final ControllerChangeHandler inHandler, @NonNull final List<ControllerChangeListener> listeners) {
private static void executeChange(@Nullable final Controller to, @Nullable final Controller from, final boolean isPush, @Nullable final ViewGroup container, @Nullable final ControllerChangeHandler inHandler, @NonNull final List<ControllerChangeListener> listeners) {
if (container != null) {
final ControllerChangeHandler handler = inHandler != null ? inHandler : new SimpleSwapChangeHandler();
final ControllerChangeHandler handler;
if (inHandler == null) {
handler = new SimpleSwapChangeHandler();
} else if (inHandler.hasBeenUsed && !inHandler.isReusable()) {
handler = inHandler.copy();
} else {
handler = inHandler;
}
handler.hasBeenUsed = true;
if (isPush && to != null) {
inProgressPushHandlers.put(to.getInstanceId(), handler);
} else if (!isPush && from != null) {
abortPush(from, to, handler);
if (from != null) {
if (isPush) {
completeHandlerImmediately(from.getInstanceId());
} else {
abortOrComplete(from, to, handler);
}
}
if (to != null) {
inProgressChangeHandlers.put(to.getInstanceId(), new ChangeHandlerData(handler, isPush));
}
for (ControllerChangeListener listener : listeners) {
listener.onChangeStarted(to, from, isPush, container, inHandler);
listener.onChangeStarted(to, from, isPush, container, handler);
}
final ControllerChangeType toChangeType = isPush ? ControllerChangeType.PUSH_ENTER : ControllerChangeType.POP_ENTER;
@@ -170,12 +209,12 @@ public abstract class ControllerChangeHandler {
}
if (to != null) {
inProgressPushHandlers.remove(to.getInstanceId());
inProgressChangeHandlers.remove(to.getInstanceId());
to.changeEnded(handler, toChangeType);
}
for (ControllerChangeListener listener : listeners) {
listener.onChangeCompleted(to, from, isPush, container, inHandler);
listener.onChangeCompleted(to, from, isPush, container, handler);
}
if (handler.forceRemoveViewOnPush && fromView != null) {
@@ -184,6 +223,10 @@ public abstract class ControllerChangeHandler {
((ViewGroup)fromParent).removeView(fromView);
}
}
if (handler.removesFromViewOnPush() && from != null) {
from.setNeedsAttach(false);
}
}
});
}
@@ -197,6 +240,24 @@ public abstract class ControllerChangeHandler {
forceRemoveViewOnPush = force;
}
static class ChangeTransaction {
@Nullable final Controller to;
@Nullable final Controller from;
final boolean isPush;
@Nullable final ViewGroup container;
@Nullable final ControllerChangeHandler changeHandler;
@NonNull final List<ControllerChangeListener> listeners;
public ChangeTransaction(@Nullable Controller to, @Nullable Controller from, boolean isPush, @Nullable ViewGroup container, @Nullable ControllerChangeHandler changeHandler, @NonNull List<ControllerChangeListener> listeners) {
this.to = to;
this.from = from;
this.isPush = isPush;
this.container = container;
this.changeHandler = changeHandler;
this.listeners = listeners;
}
}
/**
* A listener interface useful for allowing external classes to be notified of change events.
*/
@@ -204,24 +265,24 @@ public abstract class ControllerChangeHandler {
/**
* Called when a {@link ControllerChangeHandler} has started changing {@link Controller}s
*
* @param to The new Controller
* @param from The old Controller
* @param to The new Controller or {@code null} if no Controller is being transitioned to
* @param from The old Controller or {@code null} if there was no Controller before this transition
* @param isPush True if this is a push operation, or false if it's a pop.
* @param container The containing ViewGroup
* @param handler The change handler being used.
*/
void onChangeStarted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler);
void onChangeStarted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler);
/**
* Called when a {@link ControllerChangeHandler} has completed changing {@link Controller}s
*
* @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 to The new Controller or {@code null} if no Controller is being transitioned to
* @param from The old Controller or {@code null} if there was no Controller before this transition
* @param isPush True if this was a push operation, or false if it's a pop
* @param container The containing ViewGroup
* @param handler The change handler that was used.
*/
void onChangeCompleted(Controller to, Controller from, boolean isPush, ViewGroup container, ControllerChangeHandler handler);
void onChangeCompleted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler);
}
/**
@@ -235,4 +296,14 @@ public abstract class ControllerChangeHandler {
void onChangeCompleted();
}
private static class ChangeHandlerData {
public final ControllerChangeHandler changeHandler;
public final boolean isPush;
public ChangeHandlerData(ControllerChangeHandler changeHandler, boolean isPush) {
this.changeHandler = changeHandler;
this.isPush = isPush;
}
}
}
@@ -2,15 +2,20 @@ 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.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.ViewGroup;
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 {
@@ -21,10 +26,11 @@ class ControllerHostedRouter extends Router {
@IdRes private int hostId;
private String tag;
private boolean isDetachFrozen;
ControllerHostedRouter() { }
ControllerHostedRouter(int hostId, String tag) {
ControllerHostedRouter(int hostId, @Nullable String tag) {
this.hostId = hostId;
this.tag = tag;
}
@@ -39,6 +45,12 @@ class ControllerHostedRouter extends Router {
hostController = controller;
this.container = container;
for (RouterTransaction transaction : backstack) {
transaction.controller.setParentController(controller);
}
watchContainerAttach();
}
}
@@ -50,12 +62,12 @@ class ControllerHostedRouter extends Router {
final List<Controller> controllersToDestroy = new ArrayList<>(destroyingControllers);
for (Controller controller : controllersToDestroy) {
if (controller.getView() != null) {
controller.detach(controller.getView(), true);
controller.detach(controller.getView(), true, false);
}
}
for (RouterTransaction transaction : backstack) {
if (transaction.controller.getView() != null) {
transaction.controller.detach(transaction.controller.getView(), true);
transaction.controller.detach(transaction.controller.getView(), true, false);
}
}
@@ -65,31 +77,50 @@ class ControllerHostedRouter extends Router {
}
final void setDetachFrozen(boolean frozen) {
isDetachFrozen = frozen;
for (RouterTransaction transaction : backstack) {
transaction.controller.setDetachFrozen(frozen);
}
}
@Override
void destroy() {
void destroy(boolean popViews) {
setDetachFrozen(false);
super.destroy();
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 @Nullable
public Activity getActivity() {
return hostController != null ? hostController.getActivity() : null;
}
@Override
public void onActivityDestroyed(Activity activity) {
public void onActivityDestroyed(@NonNull Activity activity) {
super.onActivityDestroyed(activity);
removeHost();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().onActivityResult(requestCode, resultCode, data);
}
@@ -103,42 +134,49 @@ class ControllerHostedRouter extends Router {
}
@Override
void startActivity(Intent intent) {
void startActivity(@NonNull Intent intent) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().startActivity(intent);
}
}
@Override
void startActivityForResult(String instanceId, Intent intent, int requestCode) {
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(String instanceId, Intent intent, int requestCode, Bundle options) {
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 registerForActivityResult(String instanceId, int requestCode) {
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(String instanceId) {
void unregisterForActivityResults(@NonNull String instanceId) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().unregisterForActivityResults(instanceId);
}
}
@Override
void requestPermissions(String instanceId, String[] permissions, int requestCode) {
void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
if (hostController != null && hostController.getRouter() != null) {
hostController.getRouter().requestPermissions(instanceId, permissions, requestCode);
}
@@ -150,7 +188,7 @@ class ControllerHostedRouter extends Router {
}
@Override
public void saveInstanceState(Bundle outState) {
public void saveInstanceState(@NonNull Bundle outState) {
super.saveInstanceState(outState);
outState.putInt(KEY_HOST_ID, hostId);
@@ -158,7 +196,7 @@ class ControllerHostedRouter extends Router {
}
@Override
public void restoreInstanceState(Bundle savedInstanceState) {
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
super.restoreInstanceState(savedInstanceState);
hostId = savedInstanceState.getInt(KEY_HOST_ID);
@@ -166,20 +204,21 @@ class ControllerHostedRouter extends Router {
}
@Override
void setControllerRouter(Controller controller) {
super.setControllerRouter(controller);
void setControllerRouter(@NonNull Controller controller) {
controller.setParentController(hostController);
super.setControllerRouter(controller);
}
int getHostId() {
return hostId;
}
@Nullable
String getTag() {
return tag;
}
@Override
@Override @NonNull
List<Router> getSiblingRouters() {
List<Router> list = new ArrayList<>();
list.addAll(hostController.getChildRouters());
@@ -187,7 +226,7 @@ class ControllerHostedRouter extends Router {
return list;
}
@Override
@Override @NonNull
Router getRootRouter() {
if (hostController != null && hostController.getRouter() != null) {
return hostController.getRouter().getRootRouter();
@@ -195,4 +234,22 @@ class ControllerHostedRouter extends Router {
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();
}
}
}
@@ -14,10 +14,25 @@ import android.view.ViewGroup;
*/
abstract public class RestoreViewOnCreateController extends Controller {
@NonNull
@Override
/**
* Convenience constructor for use when no arguments are needed.
*/
protected RestoreViewOnCreateController() {
super(null);
}
/**
* Constructor that takes arguments that need to be retained across restarts.
*
* @param args Any arguments that need to be retained.
*/
protected RestoreViewOnCreateController(@Nullable Bundle args) {
super(args);
}
@Override @NonNull
protected final View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return onCreateView(inflater, container, viewState);
return onCreateView(inflater, container, viewState == null ? null : viewState.getBundle(KEY_VIEW_STATE_BUNDLE));
}
/**
@@ -30,7 +45,7 @@ abstract public class RestoreViewOnCreateController extends Controller {
* This Controller's view should NOT be added in this method. It is simply passed in
* so that valid LayoutParams can be used during inflation.
* @param savedViewState A bundle for the view's state, which would have been created in {@link #onSaveViewState(View, Bundle)},
* or null if no saved state exists.
* or {@code null} if no saved state exists.
*/
@NonNull
protected abstract View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState);
+384 -143
View File
@@ -2,8 +2,11 @@ package com.bluelinelabs.conductor;
import android.app.Activity;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -11,9 +14,12 @@ import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.bluelinelabs.conductor.ControllerChangeHandler.ChangeTransaction;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import com.bluelinelabs.conductor.internal.NoOpControllerChangeHandler;
import com.bluelinelabs.conductor.internal.ThreadUtils;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
import java.util.ArrayList;
import java.util.Collections;
@@ -30,17 +36,22 @@ public abstract class Router {
private static final String KEY_BACKSTACK = "Router.backstack";
private static final String KEY_POPS_LAST_VIEW = "Router.popsLastView";
protected final Backstack backstack = new Backstack();
final Backstack backstack = new Backstack();
private final List<ControllerChangeListener> changeListeners = new ArrayList<>();
private final List<ChangeTransaction> pendingControllerChanges = new ArrayList<>();
final List<Controller> destroyingControllers = new ArrayList<>();
private boolean popsLastView = false;
boolean containerFullyAttached = false;
boolean isActivityStopped = false;
ViewGroup container;
/**
* Returns this Router's host Activity
* Returns this Router's host Activity or {@code null} if it has either not yet been attached to
* an Activity or if the Activity has been destroyed.
*/
@Nullable
public abstract Activity getActivity();
/**
@@ -48,21 +59,21 @@ public abstract class Router {
* of the controller that called startActivityForResult is not known.
*
* @param requestCode The Activity's onActivityResult requestCode
* @param resultCode The Activity's onActivityResult resultCode
* @param data The Activity's onActivityResult data
* @param resultCode The Activity's onActivityResult resultCode
* @param data The Activity's onActivityResult data
*/
public abstract void onActivityResult(int requestCode, int resultCode, Intent data);
public abstract void onActivityResult(int requestCode, int resultCode, @Nullable Intent data);
/**
* This should be called by the host Activity when its onRequestPermissionsResult method is called. The call will be forwarded
* to the {@link Controller} with the instanceId passed in.
*
* @param instanceId The instanceId of the Controller to which this result should be forwarded
* @param requestCode The Activity's onRequestPermissionsResult requestCode
* @param permissions The Activity's onRequestPermissionsResult permissions
* @param instanceId The instanceId of the Controller to which this result should be forwarded
* @param requestCode The Activity's onRequestPermissionsResult requestCode
* @param permissions The Activity's onRequestPermissionsResult permissions
* @param grantResults The Activity's onRequestPermissionsResult grantResults
*/
public void onRequestPermissionsResult(String instanceId, int requestCode, String[] permissions, int[] grantResults) {
public void onRequestPermissionsResult(@NonNull String instanceId, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Controller controller = getControllerWithInstanceId(instanceId);
if (controller != null) {
controller.requestPermissionsResult(requestCode, permissions, grantResults);
@@ -75,8 +86,12 @@ public abstract class Router {
*
* @return Whether or not a back action was handled by the Router
*/
@UiThread
public boolean handleBack() {
ThreadUtils.ensureMainThread();
if (!backstack.isEmpty()) {
//noinspection ConstantConditions
if (backstack.peek().controller.handleBack()) {
return true;
} else if (popCurrentController()) {
@@ -92,8 +107,16 @@ public abstract class Router {
*
* @return Whether or not this Router still has controllers remaining on it after popping.
*/
@SuppressWarnings("WeakerAccess")
@UiThread
public boolean popCurrentController() {
return popController(backstack.peek().controller);
ThreadUtils.ensureMainThread();
RouterTransaction transaction = backstack.peek();
if (transaction == null) {
throw new IllegalStateException("Trying to pop the current controller when there are none on the backstack.");
}
return popController(transaction.controller);
}
/**
@@ -102,27 +125,43 @@ public abstract class Router {
* @param controller The controller that should be popped from this Router
* @return Whether or not this Router still has controllers remaining on it after popping.
*/
public boolean popController(Controller controller) {
RouterTransaction topController = backstack.peek();
boolean poppingTopController = topController != null && topController.controller == controller;
@UiThread
public boolean popController(@NonNull Controller controller) {
ThreadUtils.ensureMainThread();
RouterTransaction topTransaction = backstack.peek();
boolean poppingTopController = topTransaction != null && topTransaction.controller == controller;
if (poppingTopController) {
trackDestroyingController(backstack.pop());
performControllerChange(backstack.peek(), topTransaction, false);
} else {
for (RouterTransaction transaction : backstack) {
RouterTransaction removedTransaction = null;
RouterTransaction nextTransaction = null;
Iterator<RouterTransaction> iterator = backstack.iterator();
while (iterator.hasNext()) {
RouterTransaction transaction = iterator.next();
if (transaction.controller == controller) {
backstack.remove(transaction);
if (controller.isAttached()) {
trackDestroyingController(transaction);
}
iterator.remove();
removedTransaction = transaction;
} else if (removedTransaction != null) {
if (!transaction.controller.isAttached()) {
nextTransaction = transaction;
}
break;
}
}
}
if (poppingTopController) {
performControllerChange(backstack.peek(), topController, false);
if (removedTransaction != null) {
performControllerChange(nextTransaction, removedTransaction, false);
}
}
if (popsLastView) {
return topController != null;
return topTransaction != null;
} else {
return !backstack.isEmpty();
}
@@ -134,7 +173,10 @@ public abstract class Router {
* @param transaction The transaction detailing what should be pushed, including the {@link Controller},
* and its push and pop {@link ControllerChangeHandler}, and its tag.
*/
@UiThread
public void pushController(@NonNull RouterTransaction transaction) {
ThreadUtils.ensureMainThread();
RouterTransaction from = backstack.peek();
pushToBackstack(transaction);
performControllerChange(transaction, from, true);
@@ -146,18 +188,25 @@ public abstract class Router {
* @param transaction The transaction detailing what should be pushed, including the {@link Controller},
* and its push and pop {@link ControllerChangeHandler}, and its tag.
*/
@SuppressWarnings("WeakerAccess")
@UiThread
public void replaceTopController(@NonNull RouterTransaction transaction) {
ThreadUtils.ensureMainThread();
RouterTransaction topTransaction = backstack.peek();
if (!backstack.isEmpty()) {
trackDestroyingController(backstack.pop());
}
final ControllerChangeHandler handler = transaction.pushChangeHandler();
final boolean oldHandlerRemovedViews = topTransaction.pushChangeHandler() == null || topTransaction.pushChangeHandler().removesFromViewOnPush();
final boolean newHandlerRemovesViews = handler == null || handler.removesFromViewOnPush();
if (!oldHandlerRemovedViews && newHandlerRemovesViews) {
for (RouterTransaction visibleTransaction : getVisibleTransactions(backstack.iterator())) {
performControllerChange(null, visibleTransaction.controller, true, handler != null ? handler.copy() : new SimpleSwapChangeHandler());
if (topTransaction != null) {
//noinspection ConstantConditions
final boolean oldHandlerRemovedViews = topTransaction.pushChangeHandler() == null || topTransaction.pushChangeHandler().removesFromViewOnPush();
final boolean newHandlerRemovesViews = handler == null || handler.removesFromViewOnPush();
if (!oldHandlerRemovedViews && newHandlerRemovesViews) {
for (RouterTransaction visibleTransaction : getVisibleTransactions(backstack.iterator())) {
performControllerChange(null, visibleTransaction, true, handler);
}
}
}
@@ -169,14 +218,26 @@ public abstract class Router {
performControllerChange(transaction.pushChangeHandler(handler), topTransaction, true);
}
void destroy() {
void destroy(boolean popViews) {
popsLastView = true;
List<RouterTransaction> poppedControllers = backstack.popAll();
final List<RouterTransaction> poppedControllers = backstack.popAll();
trackDestroyingControllers(poppedControllers);
if (poppedControllers.size() > 0) {
trackDestroyingControllers(poppedControllers);
if (popViews && poppedControllers.size() > 0) {
RouterTransaction topTransaction = poppedControllers.get(0);
topTransaction.controller().addLifecycleListener(new LifecycleListener() {
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
if (changeType == ControllerChangeType.POP_EXIT) {
for (int i = poppedControllers.size() - 1; i > 0; i--) {
RouterTransaction transaction = poppedControllers.get(i);
performControllerChange(null, transaction, true, new SimpleSwapChangeHandler());
}
}
}
});
performControllerChange(null, poppedControllers.get(0).controller, false, poppedControllers.get(0).popChangeHandler());
performControllerChange(null, topTransaction, false, topTransaction.popChangeHandler());
}
}
@@ -189,6 +250,7 @@ public abstract class Router {
* in the stack. This defaults to false so that the developer can either finish its containing Activity or otherwise
* hide its parent view without any strange artifacting.
*/
@NonNull
public Router setPopsLastView(boolean popsLastView) {
this.popsLastView = popsLastView;
return this;
@@ -199,7 +261,10 @@ public abstract class Router {
*
* @return Whether or not any {@link Controller}s were popped in order to get to the root transaction
*/
@UiThread
public boolean popToRoot() {
ThreadUtils.ensureMainThread();
return popToRoot(null);
}
@@ -209,8 +274,13 @@ public abstract class Router {
* @param changeHandler The {@link ControllerChangeHandler} to handle this transaction
* @return Whether or not any {@link Controller}s were popped in order to get to the root transaction
*/
public boolean popToRoot(ControllerChangeHandler changeHandler) {
@SuppressWarnings("WeakerAccess")
@UiThread
public boolean popToRoot(@Nullable ControllerChangeHandler changeHandler) {
ThreadUtils.ensureMainThread();
if (backstack.size() > 1) {
//noinspection ConstantConditions
popToTransaction(backstack.root(), changeHandler);
return true;
} else {
@@ -224,18 +294,25 @@ public abstract class Router {
* @param tag The tag being popped to
* @return Whether or not any {@link Controller}s were popped in order to get to the transaction with the passed tag
*/
@UiThread
public boolean popToTag(@NonNull String tag) {
ThreadUtils.ensureMainThread();
return popToTag(tag, null);
}
/**
* Pops all {@link Controller}s until the {@link Controller} with the passed tag is at the top
*
* @param tag The tag being popped to
* @param tag The tag being popped to
* @param changeHandler The {@link ControllerChangeHandler} to handle this transaction
* @return Whether or not the {@link Controller} with the passed tag is now at the top
*/
public boolean popToTag(@NonNull String tag, ControllerChangeHandler changeHandler) {
@SuppressWarnings("WeakerAccess")
@UiThread
public boolean popToTag(@NonNull String tag, @Nullable ControllerChangeHandler changeHandler) {
ThreadUtils.ensureMainThread();
for (RouterTransaction transaction : backstack) {
if (tag.equals(transaction.tag())) {
popToTransaction(transaction, changeHandler);
@@ -251,39 +328,22 @@ public abstract class Router {
* @param transaction The transaction detailing what should be pushed, including the {@link Controller},
* and its push and pop {@link ControllerChangeHandler}, and its tag.
*/
@UiThread
public void setRoot(@NonNull RouterTransaction transaction) {
ControllerChangeHandler newHandler = transaction.pushChangeHandler() != null ? transaction.pushChangeHandler() : new SimpleSwapChangeHandler();
ThreadUtils.ensureMainThread();
List<RouterTransaction> visibleTransactions = getVisibleTransactions(backstack.iterator());
RouterTransaction rootTransaction = visibleTransactions.size() > 0 ? visibleTransactions.get(0) : null;
removeAllExceptVisibleAndUnowned();
trackDestroyingControllers(backstack.popAll());
pushToBackstack(transaction);
for (int i = visibleTransactions.size() - 1; i > 0; i--) {
if (visibleTransactions.get(i).controller.getView() == null) {
ControllerChangeHandler.abortPush(visibleTransactions.get(i).controller, transaction.controller, newHandler);
} else {
performControllerChange(null, visibleTransactions.get(i).controller, true, newHandler);
}
}
if (rootTransaction != null && rootTransaction.controller.getView() == null) {
ControllerChangeHandler.abortPush(rootTransaction.controller, transaction.controller, newHandler);
}
performControllerChange(transaction, rootTransaction, true);
List<RouterTransaction> transactions = Collections.singletonList(transaction);
setBackstack(transactions, transaction.pushChangeHandler());
}
/**
* Returns the hosted Controller with the given instance id, if available.
* Returns the hosted Controller with the given instance id or {@code null} if no such
* Controller exists in this Router.
*
* @param instanceId The instance ID being searched for
* @return The matching Controller, if one exists
*/
public Controller getControllerWithInstanceId(String instanceId) {
@Nullable
public Controller getControllerWithInstanceId(@NonNull String instanceId) {
for (RouterTransaction transaction : backstack) {
Controller controllerWithId = transaction.controller.findController(instanceId);
if (controllerWithId != null) {
@@ -294,12 +354,13 @@ public abstract class Router {
}
/**
* Returns the hosted Controller that was pushed with the given tag, if available.
* Returns the hosted Controller that was pushed with the given tag or {@code null} if no
* such Controller exists in this Router.
*
* @param tag The tag being searched for
* @return The matching Controller, if one exists
*/
public Controller getControllerWithTag(String tag) {
@Nullable
public Controller getControllerWithTag(@NonNull String tag) {
for (RouterTransaction transaction : backstack) {
if (tag.equals(transaction.tag())) {
return transaction.controller;
@@ -311,6 +372,7 @@ public abstract class Router {
/**
* Returns the number of {@link Controller}s currently in the backstack
*/
@SuppressWarnings("WeakerAccess")
public int getBackstackSize() {
return backstack.size();
}
@@ -318,8 +380,9 @@ public abstract class Router {
/**
* Returns the current backstack, ordered from root to most recently pushed.
*/
@NonNull
public List<RouterTransaction> getBackstack() {
List<RouterTransaction> list = new ArrayList<>();
List<RouterTransaction> list = new ArrayList<>(backstack.size());
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
list.add(backstackIterator.next());
@@ -331,55 +394,104 @@ public abstract class Router {
* Sets the backstack, transitioning from the current top controller to the top of the new stack (if different)
* using the passed {@link ControllerChangeHandler}
*
* @param newBackstack The new backstack
* @param newBackstack The new backstack
* @param changeHandler An optional change handler to be used to handle the root view of transition
*/
public void setBackstack(@NonNull List<RouterTransaction> newBackstack, ControllerChangeHandler changeHandler) {
@SuppressWarnings("WeakerAccess")
@UiThread
public void setBackstack(@NonNull List<RouterTransaction> newBackstack, @Nullable ControllerChangeHandler changeHandler) {
ThreadUtils.ensureMainThread();
List<RouterTransaction> oldTransactions = getBackstack();
List<RouterTransaction> oldVisibleTransactions = getVisibleTransactions(backstack.iterator());
removeAllExceptVisibleAndUnowned();
ensureOrderedTransactionIndices(newBackstack);
ensureNoDuplicateControllers(newBackstack);
backstack.setBackstack(newBackstack);
List<RouterTransaction> transactionsToBeRemoved = new ArrayList<>();
for (RouterTransaction oldTransaction : oldTransactions) {
boolean contains = false;
for (RouterTransaction newTransaction : newBackstack) {
if (oldTransaction.controller == newTransaction.controller) {
contains = true;
break;
}
}
if (!contains) {
// Inform the controller that it will be destroyed soon
oldTransaction.controller.isBeingDestroyed = true;
transactionsToBeRemoved.add(oldTransaction);
}
}
// Ensure all new controllers have a valid router set
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
transaction.onAttachedToRouter();
setControllerRouter(transaction.controller);
}
if (newBackstack.size() > 0) {
List<RouterTransaction> reverseNewBackstack = new ArrayList<>(newBackstack);
Collections.reverse(reverseNewBackstack);
List<RouterTransaction> newVisibleTransactions = getVisibleTransactions(reverseNewBackstack.iterator());
boolean newRootRequiresPush = !(newVisibleTransactions.size() > 0 && oldTransactions.contains(newVisibleTransactions.get(0)));
boolean visibleTransactionsChanged = newVisibleTransactions.size() != oldVisibleTransactions.size();
if (!visibleTransactionsChanged) {
for (int i = 0; i < oldVisibleTransactions.size(); i++) {
if (oldVisibleTransactions.get(i).controller != newVisibleTransactions.get(i).controller) {
visibleTransactionsChanged = true;
break;
boolean visibleTransactionsChanged = !backstacksAreEqual(newVisibleTransactions, oldVisibleTransactions);
if (visibleTransactionsChanged) {
RouterTransaction oldRootTransaction = oldVisibleTransactions.size() > 0 ? oldVisibleTransactions.get(0) : null;
RouterTransaction newRootTransaction = newVisibleTransactions.get(0);
// Replace the old root with the new one
if (oldRootTransaction == null || oldRootTransaction.controller != newRootTransaction.controller) {
// Ensure the existing root controller is fully pushed to the view hierarchy
if (oldRootTransaction != null) {
ControllerChangeHandler.completeHandlerImmediately(oldRootTransaction.controller.getInstanceId());
}
performControllerChange(newRootTransaction, oldRootTransaction, newRootRequiresPush, changeHandler);
}
// Remove all visible controllers that were previously on the backstack
for (int i = oldVisibleTransactions.size() - 1; i > 0; i--) {
RouterTransaction transaction = oldVisibleTransactions.get(i);
if (!newVisibleTransactions.contains(transaction)) {
ControllerChangeHandler localHandler = changeHandler != null ? changeHandler.copy() : new SimpleSwapChangeHandler();
localHandler.setForceRemoveViewOnPush(true);
ControllerChangeHandler.completeHandlerImmediately(transaction.controller.getInstanceId());
performControllerChange(null, transaction, newRootRequiresPush, localHandler);
}
}
// Add any new controllers to the backstack
for (int i = 1; i < newVisibleTransactions.size(); i++) {
RouterTransaction transaction = newVisibleTransactions.get(i);
if (!oldVisibleTransactions.contains(transaction)) {
performControllerChange(transaction, newVisibleTransactions.get(i - 1), true, transaction.pushChangeHandler());
}
}
}
if (visibleTransactionsChanged) {
ControllerChangeHandler handler = changeHandler != null ? changeHandler : new SimpleSwapChangeHandler();
Controller rootController = oldVisibleTransactions.size() > 0 ? oldVisibleTransactions.get(0).controller : null;
performControllerChange(newVisibleTransactions.get(0).controller, rootController, true, handler.copy());
for (int i = oldVisibleTransactions.size() - 1; i > 0; i--) {
RouterTransaction transaction = oldVisibleTransactions.get(i);
ControllerChangeHandler localHandler = handler.copy();
localHandler.setForceRemoveViewOnPush(true);
performControllerChange(null, transaction.controller, true, localHandler);
}
for (int i = 1; i < newVisibleTransactions.size(); i++) {
RouterTransaction transaction = newVisibleTransactions.get(i);
handler = transaction.pushChangeHandler() != null ? transaction.pushChangeHandler().copy() : new SimpleSwapChangeHandler();
performControllerChange(transaction.controller, newVisibleTransactions.get(i - 1).controller, true, handler.copy());
}
} else {
// Remove all visible controllers that were previously on the backstack
for (int i = oldVisibleTransactions.size() - 1; i >= 0; i--) {
RouterTransaction transaction = oldVisibleTransactions.get(i);
ControllerChangeHandler localHandler = changeHandler != null ? changeHandler.copy() : new SimpleSwapChangeHandler();
ControllerChangeHandler.completeHandlerImmediately(transaction.controller.getInstanceId());
performControllerChange(null, transaction, false, localHandler);
}
}
for (RouterTransaction transaction : backstack) {
transaction.onAttachedToRouter();
// Destroy all old controllers that are no longer on the backstack. We don't do this when we initially
// set the backstack to prevent the possibility that they'll be destroyed before the controller
// change handler runs.
for (RouterTransaction removedTransaction : transactionsToBeRemoved) {
removedTransaction.controller.destroy();
}
backstack.setBackstack(newBackstack);
}
/**
@@ -394,7 +506,8 @@ public abstract class Router {
*
* @param changeListener The listener
*/
public void addChangeListener(ControllerChangeListener changeListener) {
@SuppressWarnings("WeakerAccess")
public void addChangeListener(@NonNull ControllerChangeListener changeListener) {
if (!changeListeners.contains(changeListener)) {
changeListeners.add(changeListener);
}
@@ -405,32 +518,38 @@ public abstract class Router {
*
* @param changeListener The listener to be removed
*/
public void removeChangeListener(ControllerChangeListener changeListener) {
@SuppressWarnings("WeakerAccess")
public void removeChangeListener(@NonNull ControllerChangeListener changeListener) {
changeListeners.remove(changeListener);
}
/**
* Attaches this Router's existing backstack to its container if one exists.
*/
@UiThread
public void rebindIfNeeded() {
ThreadUtils.ensureMainThread();
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
if (transaction.controller.getNeedsAttach()) {
performControllerChange(transaction.controller, null, true, new SimpleSwapChangeHandler(false));
performControllerChange(transaction, null, true, new SimpleSwapChangeHandler(false));
}
}
}
public final void onActivityResult(String instanceId, int requestCode, int resultCode, Intent data) {
public final void onActivityResult(@NonNull String instanceId, int requestCode, int resultCode, @Nullable Intent data) {
Controller controller = getControllerWithInstanceId(instanceId);
if (controller != null) {
controller.onActivityResult(requestCode, resultCode, data);
}
}
public final void onActivityStarted(Activity activity) {
public final void onActivityStarted(@NonNull Activity activity) {
isActivityStopped = false;
for (RouterTransaction transaction : backstack) {
transaction.controller.activityStarted(activity);
@@ -440,7 +559,7 @@ public abstract class Router {
}
}
public final void onActivityResumed(Activity activity) {
public final void onActivityResumed(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityResumed(activity);
@@ -450,7 +569,7 @@ public abstract class Router {
}
}
public final void onActivityPaused(Activity activity) {
public final void onActivityPaused(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityPaused(activity);
@@ -460,7 +579,7 @@ public abstract class Router {
}
}
public final void onActivityStopped(Activity activity) {
public final void onActivityStopped(@NonNull Activity activity) {
for (RouterTransaction transaction : backstack) {
transaction.controller.activityStopped(activity);
@@ -468,14 +587,16 @@ public abstract class Router {
childRouter.onActivityStopped(activity);
}
}
isActivityStopped = true;
}
public void onActivityDestroyed(Activity activity) {
public void onActivityDestroyed(@NonNull Activity activity) {
prepareForContainerRemoval();
changeListeners.clear();
for (RouterTransaction transaction : backstack) {
transaction.controller.activityDestroyed(activity.isChangingConfigurations());
transaction.controller.activityDestroyed(activity);
for (Router childRouter : transaction.controller.getChildRouters()) {
childRouter.onActivityDestroyed(activity);
@@ -484,7 +605,7 @@ public abstract class Router {
for (int index = destroyingControllers.size() - 1; index >= 0; index--) {
Controller controller = destroyingControllers.get(index);
controller.activityDestroyed(activity.isChangingConfigurations());
controller.activityDestroyed(activity);
for (Router childRouter : controller.getChildRouters()) {
childRouter.onActivityDestroyed(activity);
@@ -496,16 +617,14 @@ public abstract class Router {
public void prepareForHostDetach() {
for (RouterTransaction transaction : backstack) {
if (ControllerChangeHandler.completePushImmediately(transaction.controller.getInstanceId())) {
transaction.controller.setNeedsAttach();
if (ControllerChangeHandler.completeHandlerImmediately(transaction.controller.getInstanceId())) {
transaction.controller.setNeedsAttach(true);
}
transaction.controller.prepareForHostDetach();
}
}
public void saveInstanceState(Bundle outState) {
prepareForHostDetach();
public void saveInstanceState(@NonNull Bundle outState) {
Bundle backstackState = new Bundle();
backstack.saveInstanceState(backstackState);
@@ -513,8 +632,9 @@ public abstract class Router {
outState.putBoolean(KEY_POPS_LAST_VIEW, popsLastView);
}
public void restoreInstanceState(Bundle savedInstanceState) {
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
Bundle backstackBundle = savedInstanceState.getParcelable(KEY_BACKSTACK);
//noinspection ConstantConditions
backstack.restoreInstanceState(backstackBundle);
popsLastView = savedInstanceState.getBoolean(KEY_POPS_LAST_VIEW);
@@ -524,7 +644,7 @@ public abstract class Router {
}
}
public final void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
public final void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
for (RouterTransaction transaction : backstack) {
transaction.controller.createOptionsMenu(menu, inflater);
@@ -534,7 +654,7 @@ public abstract class Router {
}
}
public final void onPrepareOptionsMenu(Menu menu) {
public final void onPrepareOptionsMenu(@NonNull Menu menu) {
for (RouterTransaction transaction : backstack) {
transaction.controller.prepareOptionsMenu(menu);
@@ -544,7 +664,7 @@ public abstract class Router {
}
}
public final boolean onOptionsItemSelected(MenuItem item) {
public final boolean onOptionsItemSelected(@NonNull MenuItem item) {
for (RouterTransaction transaction : backstack) {
if (transaction.controller.optionsItemSelected(item)) {
return true;
@@ -559,28 +679,55 @@ public abstract class Router {
return false;
}
private void popToTransaction(@NonNull RouterTransaction transaction, ControllerChangeHandler changeHandler) {
RouterTransaction topTransaction = backstack.peek();
List<RouterTransaction> poppedTransactions = backstack.popTo(transaction);
trackDestroyingControllers(poppedTransactions);
private void popToTransaction(@NonNull RouterTransaction transaction, @Nullable ControllerChangeHandler changeHandler) {
if (backstack.size() > 0) {
RouterTransaction topTransaction = backstack.peek();
List<RouterTransaction> updatedBackstack = new ArrayList<>();
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
RouterTransaction existingTransaction = backstackIterator.next();
updatedBackstack.add(existingTransaction);
if (existingTransaction == transaction) {
break;
}
}
if (poppedTransactions.size() > 0) {
if (changeHandler == null) {
//noinspection ConstantConditions
changeHandler = topTransaction.popChangeHandler();
}
performControllerChange(backstack.peek().controller, topTransaction.controller, false, changeHandler);
setBackstack(updatedBackstack, changeHandler);
}
}
void watchContainerAttach() {
container.post(new Runnable() {
@Override
public void run() {
containerFullyAttached = true;
}
});
}
void prepareForContainerRemoval() {
containerFullyAttached = false;
if (container != null) {
container.setOnHierarchyChangeListener(null);
}
}
void onContextAvailable() {
for (RouterTransaction transaction : backstack) {
transaction.controller.onContextAvailable();
}
}
@NonNull
final List<Controller> getControllers() {
List<Controller> controllers = new ArrayList<>();
List<Controller> controllers = new ArrayList<>(backstack.size());
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
@@ -590,6 +737,7 @@ public abstract class Router {
return controllers;
}
@Nullable
public final Boolean handleRequestedPermission(@NonNull String permission) {
for (RouterTransaction transaction : backstack) {
if (transaction.controller.didRequestPermission(permission)) {
@@ -599,7 +747,7 @@ public abstract class Router {
return null;
}
private void performControllerChange(RouterTransaction to, RouterTransaction from, boolean isPush) {
private void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush) {
if (isPush && to != null) {
to.onAttachedToRouter();
}
@@ -611,32 +759,78 @@ public abstract class Router {
} else if (from != null) {
changeHandler = from.popChangeHandler();
} else {
changeHandler = new SimpleSwapChangeHandler();
changeHandler = null;
}
performControllerChange(to, from, isPush, changeHandler);
}
void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush, @Nullable ControllerChangeHandler changeHandler) {
Controller toController = to != null ? to.controller : null;
Controller fromController = from != null ? from.controller : null;
boolean forceDetachDestroy = false;
performControllerChange(toController, fromController, isPush, changeHandler);
}
private void performControllerChange(final Controller to, final Controller from, boolean isPush, @NonNull ControllerChangeHandler changeHandler) {
if (to != null) {
setControllerRouter(to);
to.ensureValidIndex(getTransactionIndexer());
setControllerRouter(toController);
} else if (backstack.size() == 0 && !popsLastView) {
// We're emptying out the backstack. Views get weird if you transition them out, so just no-op it. The hosting
// Activity should be handling this by finishing or at least hiding this view.
// We're emptying out the backstack. Views get weird if you transition them out, so just no-op it. The host
// Activity or controller should be handling this by finishing or at least hiding this view.
changeHandler = new NoOpControllerChangeHandler();
forceDetachDestroy = true;
}
ControllerChangeHandler.executeChange(to, from, isPush, container, changeHandler, changeListeners);
performControllerChange(toController, fromController, isPush, changeHandler);
if (forceDetachDestroy && fromController != null && fromController.getView() != null) {
fromController.detach(fromController.getView(), true, false);
}
}
private void pushToBackstack(@NonNull RouterTransaction entry) {
private void performControllerChange(@Nullable final Controller to, @Nullable final Controller from, final boolean isPush, @Nullable final ControllerChangeHandler changeHandler) {
if (isPush && to != null && to.isDestroyed()) {
throw new IllegalStateException("Trying to push a controller that has already been destroyed. (" + to.getClass().getSimpleName() + ")");
}
final ChangeTransaction transaction = new ChangeTransaction(to, from, isPush, container, changeHandler, new ArrayList<>(changeListeners));
if (pendingControllerChanges.size() > 0) {
// If we already have changes queued up (awaiting full container attach), queue this one up as well so they don't happen
// out of order.
pendingControllerChanges.add(transaction);
} else if (from != null && (changeHandler == null || changeHandler.removesFromViewOnPush()) && !containerFullyAttached) {
// If the change handler will remove the from view, we have to make sure the container is fully attached first so we avoid NPEs
// within ViewGroup (details on issue #287). Post this to the container to ensure the attach is complete before we try to remove
// anything.
pendingControllerChanges.add(transaction);
container.post(new Runnable() {
@Override
public void run() {
performPendingControllerChanges();
}
});
} else {
ControllerChangeHandler.executeChange(transaction);
}
}
void performPendingControllerChanges() {
// We're intentionally using dynamic size checking (list.size()) here so we can account for changes
// that occur during this loop (ex: if a controller is popped from within onAttach)
for (int i = 0; i < pendingControllerChanges.size(); i++) {
ControllerChangeHandler.executeChange(pendingControllerChanges.get(i));
}
pendingControllerChanges.clear();
}
protected void pushToBackstack(@NonNull RouterTransaction entry) {
if (backstack.contains(entry.controller)) {
throw new IllegalStateException("Trying to push a controller that already exists on the backstack.");
}
backstack.push(entry);
}
private void trackDestroyingController(RouterTransaction transaction) {
private void trackDestroyingController(@NonNull RouterTransaction transaction) {
if (!transaction.controller.isDestroyed()) {
destroyingControllers.add(transaction.controller);
@@ -649,7 +843,7 @@ public abstract class Router {
}
}
private void trackDestroyingControllers(List<RouterTransaction> transactions) {
private void trackDestroyingControllers(@NonNull List<RouterTransaction> transactions) {
for (RouterTransaction transaction : transactions) {
trackDestroyingController(transaction);
}
@@ -671,7 +865,7 @@ public abstract class Router {
}
final int childCount = container.getChildCount();
for (int i = 0; i < childCount; i++) {
for (int i = childCount - 1; i >= 0; i--) {
final View child = container.getChildAt(i);
if (!views.contains(child)) {
container.removeView(child);
@@ -679,7 +873,34 @@ public abstract class Router {
}
}
private void addRouterViewsToList(Router router, List<View> list) {
// Swap around transaction indices to ensure they don't get thrown out of order by the
// developer rearranging the backstack at runtime.
private void ensureOrderedTransactionIndices(List<RouterTransaction> backstack) {
List<Integer> indices = new ArrayList<>(backstack.size());
for (RouterTransaction transaction : backstack) {
transaction.ensureValidIndex(getTransactionIndexer());
indices.add(transaction.transactionIndex);
}
Collections.sort(indices);
for (int i = 0; i < backstack.size(); i++) {
backstack.get(i).transactionIndex = indices.get(i);
}
}
private void ensureNoDuplicateControllers(List<RouterTransaction> backstack) {
for (int i = 0; i < backstack.size(); i++) {
Controller controller = backstack.get(i).controller;
for (int j = i + 1; j < backstack.size(); j++) {
if (backstack.get(j).controller == controller) {
throw new IllegalStateException("Trying to push the same controller to the backstack more than once.");
}
}
}
}
private void addRouterViewsToList(@NonNull Router router, @NonNull List<View> list) {
for (Controller controller : router.getControllers()) {
if (controller.getView() != null) {
list.add(controller.getView());
@@ -691,12 +912,13 @@ public abstract class Router {
}
}
private List<RouterTransaction> getVisibleTransactions(Iterator<RouterTransaction> backstackIterator) {
private List<RouterTransaction> getVisibleTransactions(@NonNull Iterator<RouterTransaction> backstackIterator) {
List<RouterTransaction> transactions = new ArrayList<>();
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
transactions.add(transaction);
//noinspection ConstantConditions
if (transaction.pushChangeHandler() == null || transaction.pushChangeHandler().removesFromViewOnPush()) {
break;
}
@@ -706,18 +928,37 @@ public abstract class Router {
return transactions;
}
void setControllerRouter(Controller controller) {
private boolean backstacksAreEqual(List<RouterTransaction> lhs, List<RouterTransaction> rhs) {
if (lhs.size() != rhs.size()) {
return false;
}
for (int i = 0; i < rhs.size(); i++) {
if (rhs.get(i).controller() != lhs.get(i).controller()) {
return false;
}
}
return true;
}
void setControllerRouter(@NonNull Controller controller) {
controller.setRouter(this);
controller.onContextAvailable();
}
abstract void invalidateOptionsMenu();
abstract void startActivity(Intent intent);
abstract void startActivityForResult(String instanceId, Intent intent, int requestCode);
abstract void startActivityForResult(String instanceId, Intent intent, int requestCode, Bundle options);
abstract void registerForActivityResult(String instanceId, int requestCode);
abstract void unregisterForActivityResults(String instanceId);
abstract void requestPermissions(String instanceId, String[] permissions, int requestCode);
abstract void startActivity(@NonNull Intent intent);
abstract void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode);
abstract void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options);
abstract void startIntentSenderForResult(@NonNull String instanceId, @NonNull IntentSender intent, int requestCode, @Nullable Intent fillInIntent, int flagsMask,
int flagsValues, int extraFlags, @Nullable Bundle options) throws IntentSender.SendIntentException;
abstract void registerForActivityResult(@NonNull String instanceId, int requestCode);
abstract void unregisterForActivityResults(@NonNull String instanceId);
abstract void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode);
abstract boolean hasHost();
abstract List<Router> getSiblingRouters();
abstract Router getRootRouter();
@NonNull abstract List<Router> getSiblingRouters();
@NonNull abstract Router getRootRouter();
@NonNull abstract TransactionIndexer getTransactionIndexer();
}
@@ -2,16 +2,22 @@ package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
/**
* Metadata used for adding {@link Controller}s to a {@link Router}.
*/
public class RouterTransaction {
private static int INVALID_INDEX = -1;
private static final String KEY_VIEW_CONTROLLER_BUNDLE = "RouterTransaction.controller.bundle";
private static final String KEY_PUSH_TRANSITION = "RouterTransaction.pushControllerChangeHandler";
private static final String KEY_POP_TRANSITION = "RouterTransaction.popControllerChangeHandler";
private static final String KEY_TAG = "RouterTransaction.tag";
private static final String KEY_INDEX = "RouterTransaction.transactionIndex";
private static final String KEY_ATTACHED_TO_ROUTER = "RouterTransaction.attachedToRouter";
@NonNull final Controller controller;
@@ -20,7 +26,9 @@ public class RouterTransaction {
private ControllerChangeHandler pushControllerChangeHandler;
private ControllerChangeHandler popControllerChangeHandler;
private boolean attachedToRouter;
int transactionIndex = INVALID_INDEX;
@NonNull
public static RouterTransaction with(@NonNull Controller controller) {
return new RouterTransaction(controller);
}
@@ -34,6 +42,7 @@ public class RouterTransaction {
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);
}
@@ -41,15 +50,18 @@ public class RouterTransaction {
attachedToRouter = true;
}
@NonNull
public Controller controller() {
return controller;
}
@Nullable
public String tag() {
return tag;
}
public RouterTransaction tag(String tag) {
@NonNull
public RouterTransaction tag(@Nullable String tag) {
if (!attachedToRouter) {
this.tag = tag;
return this;
@@ -58,6 +70,7 @@ public class RouterTransaction {
}
}
@Nullable
public ControllerChangeHandler pushChangeHandler() {
ControllerChangeHandler handler = controller.getOverriddenPushHandler();
if (handler == null) {
@@ -66,7 +79,8 @@ public class RouterTransaction {
return handler;
}
public RouterTransaction pushChangeHandler(ControllerChangeHandler handler) {
@NonNull
public RouterTransaction pushChangeHandler(@Nullable ControllerChangeHandler handler) {
if (!attachedToRouter) {
pushControllerChangeHandler = handler;
return this;
@@ -75,6 +89,7 @@ public class RouterTransaction {
}
}
@Nullable
public ControllerChangeHandler popChangeHandler() {
ControllerChangeHandler handler = controller.getOverriddenPopHandler();
if (handler == null) {
@@ -83,7 +98,8 @@ public class RouterTransaction {
return handler;
}
public RouterTransaction popChangeHandler(ControllerChangeHandler handler) {
@NonNull
public RouterTransaction popChangeHandler(@Nullable ControllerChangeHandler handler) {
if (!attachedToRouter) {
popControllerChangeHandler = handler;
return this;
@@ -92,9 +108,16 @@ public class RouterTransaction {
}
}
void ensureValidIndex(@NonNull TransactionIndexer indexer) {
if (transactionIndex == INVALID_INDEX) {
transactionIndex = indexer.nextIndex();
}
}
/**
* Used to serialize this transaction into a Bundle
*/
@NonNull
public Bundle saveInstanceState() {
Bundle bundle = new Bundle();
@@ -108,9 +131,10 @@ public class RouterTransaction {
}
bundle.putString(KEY_TAG, tag);
bundle.putInt(KEY_INDEX, transactionIndex);
bundle.putBoolean(KEY_ATTACHED_TO_ROUTER, attachedToRouter);
return bundle;
}
}
}
@@ -1,9 +1,11 @@
package com.bluelinelabs.conductor.changehandler;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
@@ -19,22 +21,28 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
private static final String KEY_DURATION = "AnimatorChangeHandler.duration";
private static final String KEY_REMOVES_FROM_ON_PUSH = "AnimatorChangeHandler.removesFromViewOnPush";
@SuppressWarnings("WeakerAccess")
public static final long DEFAULT_ANIMATION_DURATION = -1;
private long animationDuration;
private boolean removesFromViewOnPush;
private boolean canceled;
private boolean needsImmediateCompletion;
private Animator animator;
boolean removesFromViewOnPush;
boolean canceled;
boolean needsImmediateCompletion;
private boolean completed;
Animator animator;
private OnAnimationReadyOrAbortedListener onAnimationReadyOrAbortedListener;
@SuppressWarnings("WeakerAccess")
public AnimatorChangeHandler() {
this(DEFAULT_ANIMATION_DURATION, true);
}
@SuppressWarnings("WeakerAccess")
public AnimatorChangeHandler(boolean removesFromViewOnPush) {
this(DEFAULT_ANIMATION_DURATION, removesFromViewOnPush);
}
@SuppressWarnings("WeakerAccess")
public AnimatorChangeHandler(long duration) {
this(duration, true);
}
@@ -59,12 +67,14 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
}
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, Controller newTop) {
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
canceled = true;
if (animator != null) {
animator.cancel();
} else if (onAnimationReadyOrAbortedListener != null) {
onAnimationReadyOrAbortedListener.onReadyOrAborted();
}
}
@@ -74,7 +84,9 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
needsImmediateCompletion = true;
if (animator != null) {
animator.cancel();
animator.end();
} else if (onAnimationReadyOrAbortedListener != null) {
onAnimationReadyOrAbortedListener.onReadyOrAborted();
}
}
@@ -91,12 +103,13 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
* 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 from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param isPush True if this is a push transaction, false if it's a pop.
* @param toAddedToContainer True if the "to" view was added to the container as a part of this ChangeHandler. False if it was already in the hierarchy.
*/
protected abstract Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer);
@NonNull
protected abstract Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer);
/**
* Will be called after the animation is complete to reset the View that was removed to its pre-animation state.
@@ -104,7 +117,7 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
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) {
public final void performChange(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
boolean readyToAnimate = true;
final boolean addingToView = to != null && to.getParent() == null;
@@ -117,17 +130,8 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
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;
}
});
onAnimationReadyOrAbortedListener = new OnAnimationReadyOrAbortedListener(container, from, to, isPush, true, changeListener);
to.getViewTreeObserver().addOnPreDrawListener(onAnimationReadyOrAbortedListener);
}
}
@@ -136,9 +140,36 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
}
}
private void performAnimation(@NonNull final ViewGroup container, final View from, final View to, final boolean isPush, final boolean toAddedToContainer, @NonNull final ControllerChangeCompletedListener changeListener) {
if (canceled) {
void complete(@NonNull ControllerChangeCompletedListener changeListener, @Nullable AnimatorListener animatorListener) {
if (!completed) {
completed = true;
changeListener.onChangeCompleted();
}
if (animator != null) {
if (animatorListener != null) {
animator.removeListener(animatorListener);
}
animator.cancel();
animator = null;
}
onAnimationReadyOrAbortedListener = null;
}
void performAnimation(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, final boolean toAddedToContainer, @NonNull final ControllerChangeCompletedListener changeListener) {
if (canceled) {
complete(changeListener, null);
return;
}
if (needsImmediateCompletion) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from);
}
complete(changeListener, null);
if (isPush && from != null) {
resetFromView(from);
}
return;
}
@@ -151,13 +182,15 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(Animator animation) {
if (from != null && (!isPush || removesFromViewOnPush) && needsImmediateCompletion) {
container.removeView(from);
if (from != null) {
resetFromView(from);
}
changeListener.onChangeCompleted();
animator.removeListener(this);
animator = null;
if (to != null && to.getParent() == container) {
container.removeView(to);
}
complete(changeListener, this);
}
@Override
@@ -167,14 +200,11 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
container.removeView(from);
}
changeListener.onChangeCompleted();
animator.removeListener(this);
complete(changeListener, this);
if (isPush && from != null) {
resetFromView(from);
}
animator = null;
}
}
});
@@ -182,4 +212,46 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
animator.start();
}
private class OnAnimationReadyOrAbortedListener implements ViewTreeObserver.OnPreDrawListener {
@NonNull final ViewGroup container;
@Nullable final View from;
@Nullable final View to;
final boolean isPush;
final boolean addingToView;
@NonNull final ControllerChangeCompletedListener changeListener;
private boolean hasRun;
OnAnimationReadyOrAbortedListener(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean addingToView, @NonNull ControllerChangeCompletedListener changeListener) {
this.container = container;
this.from = from;
this.to = to;
this.isPush = isPush;
this.addingToView = addingToView;
this.changeListener = changeListener;
}
@Override
public boolean onPreDraw() {
onReadyOrAborted();
return true;
}
void onReadyOrAborted() {
if (!hasRun) {
hasRun = true;
if (to != null) {
final ViewTreeObserver observer = to.getViewTreeObserver();
if (observer.isAlive()) {
observer.removeOnPreDrawListener(this);
}
}
performAnimation(container, from, to, isPush, addingToView, changeListener);
}
}
}
}
@@ -3,21 +3,31 @@ package com.bluelinelabs.conductor.changehandler;
import android.annotation.TargetApi;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.AutoTransition;
import android.transition.Transition;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* @deprecated It's very rare that a simple AutoTransition is what you want when changing controllers. This class
* is deprecated simply because it was often a red herring for people trying to make nice transitions.
*
* A change handler that will use an AutoTransition.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class AutoTransitionChangeHandler extends TransitionChangeHandler {
@Override
@NonNull
protected Transition getTransition(@NonNull ViewGroup container, View from, View to, boolean isPush) {
@Override @NonNull
protected Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
return new AutoTransition();
}
@Override @NonNull
public ControllerChangeHandler copy() {
return new AutoTransitionChangeHandler();
}
}
@@ -4,9 +4,12 @@ import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* An {@link AnimatorChangeHandler} that will cross fade two views
*/
@@ -26,14 +29,15 @@ public class FadeChangeHandler extends AnimatorChangeHandler {
super(duration, removesFromViewOnPush);
}
@Override
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animator = new AnimatorSet();
if (to != null && toAddedToContainer) {
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, 0, 1));
if (to != null) {
float start = toAddedToContainer ? 0 : to.getAlpha();
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1));
}
if (from != null) {
if (from != null && (!isPush || removesFromViewOnPush())) {
animator.play(ObjectAnimator.ofFloat(from, View.ALPHA, 0));
}
@@ -44,4 +48,10 @@ public class FadeChangeHandler extends AnimatorChangeHandler {
protected void resetFromView(@NonNull View from) {
from.setAlpha(1);
}
@Override @NonNull
public ControllerChangeHandler copy() {
return new FadeChangeHandler(getAnimationDuration(), removesFromViewOnPush());
}
}
@@ -4,9 +4,12 @@ import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
/**
* An {@link AnimatorChangeHandler} that will slide the views left or right, depending on if it's a push or pop.
*/
@@ -26,8 +29,8 @@ public class HorizontalChangeHandler extends AnimatorChangeHandler {
super(duration, removesFromViewOnPush);
}
@Override
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animatorSet = new AnimatorSet();
if (isPush) {
@@ -42,7 +45,9 @@ public class HorizontalChangeHandler extends AnimatorChangeHandler {
animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, from.getWidth()));
}
if (to != null) {
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, -to.getWidth(), 0));
// Allow this to have a nice transition when coming off an aborted push animation
float fromLeft = from != null ? from.getTranslationX() : 0;
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, fromLeft - to.getWidth(), 0));
}
}
@@ -53,4 +58,10 @@ public class HorizontalChangeHandler extends AnimatorChangeHandler {
protected void resetFromView(@NonNull View from) {
from.setTranslationX(0);
}
@Override @NonNull
public ControllerChangeHandler copy() {
return new HorizontalChangeHandler(getAnimationDuration(), removesFromViewOnPush());
}
}
@@ -0,0 +1,654 @@
package com.bluelinelabs.conductor.changehandler;
import android.annotation.TargetApi;
import android.app.SharedElementCallback;
import android.graphics.Rect;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.Transition;
import android.transition.Transition.TransitionListener;
import android.transition.TransitionSet;
import android.util.ArrayMap;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnPreDrawListener;
import 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.
*/
// Much of this class is based on FragmentTransition.java and FragmentTransitionCompat21.java from the Android support library
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class SharedElementTransitionChangeHandler extends TransitionChangeHandler {
// A map of from -> to names. Generally these will be the same.
@NonNull final ArrayMap<String, String> sharedElementNames = new ArrayMap<>();
@NonNull final List<String> waitForTransitionNames = new ArrayList<>();
@NonNull final List<ViewParentPair> removedViews = new ArrayList<>();
@Nullable Transition exitTransition;
@Nullable Transition enterTransition;
@Nullable Transition sharedElementTransition;
@Nullable private SharedElementCallback exitTransitionCallback;
@Nullable private SharedElementCallback enterTransitionCallback;
@NonNull
@Override
protected final Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
exitTransition = getExitTransition(container, from, to, isPush);
enterTransition = getEnterTransition(container, from, to, isPush);
sharedElementTransition = getSharedElementTransition(container, from, to, isPush);
exitTransitionCallback = getExitTransitionCallback(container, from, to, isPush);
enterTransitionCallback = getEnterTransitionCallback(container, from, to, isPush);
if (enterTransition == null && sharedElementTransition == null && exitTransition == null) {
throw new IllegalStateException("SharedElementTransitionChangeHandler must have at least one transaction.");
}
return mergeTransitions(isPush);
}
@Override
public void prepareForTransition(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, @NonNull final Transition transition, final boolean isPush, @NonNull final OnTransitionPreparedListener onTransitionPreparedListener) {
OnTransitionPreparedListener listener = 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(view.getTransitionName());
removedViews.add(new ViewParentPair(view, (ViewGroup)view.getParent()));
((ViewGroup)view.getParent()).removeView(view);
if (waitForTransitionNames.size() == 0) {
to.getViewTreeObserver().removeOnPreDrawListener(parentPreDrawListener);
to.setVisibility(View.INVISIBLE);
onTransitionPreparedListener.onPrepared();
}
}
});
}
}
private void scheduleTargetChange(@NonNull final ViewGroup container, @Nullable final View to, @NonNull final View nonExistentView,
@NonNull final List<View> toSharedElements, @NonNull final List<View> enteringViews, @Nullable final List<View> exitingViews) {
OneShotPreDrawListener.add(true, container, 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(removedView.view.getTransitionName(), removedView.view);
}
final List<String> names = new ArrayList<>(sharedElementNames.values());
toSharedElements.retainAll(names);
if (enterTransitionCallback != null) {
enterTransitionCallback.onMapSharedElements(names, toSharedElements);
for (int i = names.size() - 1; i >= 0; i--) {
String name = names.get(i);
View view = toSharedElements.get(name);
if (view == null) {
String key = findKeyForValue(sharedElementNames, name);
if (key != null) {
sharedElementNames.remove(key);
}
} else if (!name.equals(view.getTransitionName())) {
String key = findKeyForValue(sharedElementNames, name);
if (key != null) {
sharedElementNames.put(key, view.getTransitionName());
}
}
}
} else {
for (int i = sharedElementNames.size() - 1; i >= 0; i--) {
final String targetName = sharedElementNames.valueAt(i);
if (!toSharedElements.containsKey(targetName)) {
sharedElementNames.removeAt(i);
}
}
}
return toSharedElements;
}
@Nullable String findKeyForValue(@NonNull ArrayMap<String, String> map, @NonNull String value) {
final int numElements = map.size();
for (int i = 0; i < numElements; i++) {
if (value.equals(map.valueAt(i))) {
return map.keyAt(i);
}
}
return null;
}
@Nullable
private ArrayMap<String, View> captureFromSharedElements(@NonNull View from) {
if (sharedElementNames.isEmpty() || sharedElementTransition == null) {
sharedElementNames.clear();
return null;
}
final ArrayMap<String, View> fromSharedElements = new ArrayMap<>();
TransitionUtils.findNamedViews(fromSharedElements, from);
final List<String> names = new ArrayList<>(sharedElementNames.keySet());
fromSharedElements.retainAll(names);
if (exitTransitionCallback != null) {
exitTransitionCallback.onMapSharedElements(names, fromSharedElements);
for (int i = names.size() - 1; i >= 0; i--) {
String name = names.get(i);
View view = fromSharedElements.get(name);
if (view == null) {
sharedElementNames.remove(name);
} else if (!name.equals(view.getTransitionName())) {
String targetValue = sharedElementNames.remove(name);
sharedElementNames.put(view.getTransitionName(), targetValue);
}
}
} else {
sharedElementNames.retainAll(fromSharedElements.keySet());
}
return fromSharedElements;
}
void callSharedElementStartEnd(@Nullable ArrayMap<String, View> sharedElements, boolean isStart) {
if (enterTransitionCallback != null) {
final int count = sharedElements == null ? 0 : sharedElements.size();
List<View> views = new ArrayList<>(count);
List<String> names = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
names.add(sharedElements.keyAt(i));
views.add(sharedElements.valueAt(i));
}
if (isStart) {
enterTransitionCallback.onSharedElementStart(names, views, null);
} else {
enterTransitionCallback.onSharedElementEnd(names, views, null);
}
}
}
private void captureTransitioningViews(@NonNull List<View> transitioningViews, @NonNull View view) {
if (view.getVisibility() == View.VISIBLE) {
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
if (viewGroup.isTransitionGroup()) {
transitioningViews.add(viewGroup);
} else {
int count = viewGroup.getChildCount();
for (int i = 0; i < count; i++) {
View child = viewGroup.getChildAt(i);
captureTransitioningViews(transitioningViews, child);
}
}
} else {
transitioningViews.add(view);
}
}
}
private void scheduleRemoveTargets(@NonNull final Transition overallTransition,
@Nullable final Transition enterTransition, @Nullable final List<View> enteringViews,
@Nullable final Transition exitTransition, @Nullable final List<View> exitingViews,
@Nullable final Transition sharedElementTransition, @Nullable final List<View> toSharedElements) {
overallTransition.addListener(new TransitionListener() {
@Override
public void onTransitionStart(Transition transition) {
if (enterTransition != null && enteringViews != null) {
TransitionUtils.replaceTargets(enterTransition, enteringViews, null);
}
if (exitTransition != null && exitingViews != null) {
TransitionUtils.replaceTargets(exitTransition, exitingViews, null);
}
if (sharedElementTransition != null && toSharedElements != null) {
TransitionUtils.replaceTargets(sharedElementTransition, toSharedElements, null);
}
}
@Override
public void onTransitionEnd(Transition transition) { }
@Override
public void onTransitionCancel(Transition transition) { }
@Override
public void onTransitionPause(Transition transition) { }
@Override
public void onTransitionResume(Transition transition) { }
});
}
private void setNameOverrides(@NonNull final View container, @NonNull final List<View> toSharedElements) {
OneShotPreDrawListener.add(true, container, 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 = view.getTransitionName();
if (name != null) {
String inName = findKeyForValue(sharedElementNames, name);
view.setTransitionName(inName);
}
}
}
});
}
private void scheduleNameReset(@NonNull final ViewGroup container, @NonNull final List<View> toSharedElements) {
OneShotPreDrawListener.add(true, container, 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 = view.getTransitionName();
final String inName = sharedElementNames.get(name);
view.setTransitionName(inName);
}
}
});
}
/**
* Will be called when views are ready to have their shared elements configured. Within this method one of the addSharedElement methods
* should be called for each shared element that will be used. If one or more of these shared elements will not instantly be available in
* the incoming view (for ex, in a RecyclerView), waitOnSharedElementNamed can be called to delay the transition until everything is available.
*/
public abstract void configureSharedElements(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
/**
* Should return the transition that will be used on the exiting ("from") view, if one is desired.
*/
@Nullable
public abstract Transition getExitTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
/**
* Should return the transition that will be used on shared elements between the from and to views.
*/
@Nullable
public abstract Transition getSharedElementTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
/**
* Should return the transition that will be used on the entering ("to") view, if one is desired.
*/
@Nullable
public abstract Transition getEnterTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
/**
* Should return a callback that can be used to customize transition behavior of the shared element transition for the "from" view.
*/
@Nullable
public SharedElementCallback getExitTransitionCallback(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
return null;
}
/**
* Should return a callback that can be used to customize transition behavior of the shared element transition for the "to" view.
*/
@Nullable
public SharedElementCallback getEnterTransitionCallback(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush) {
return null;
}
/**
* Should return whether or not the the exit transition and enter transition should overlap. If true,
* the enter transition will start as soon as possible. Otherwise, the enter transition will wait until the
* completion of the exit transition. Defaults to true.
*/
public boolean allowTransitionOverlap(boolean isPush) {
return true;
}
/**
* Used to register an element that will take part in the shared element transition.
*
* @param name The transition name that is used for both the entering and exiting views.
*/
protected final void addSharedElement(@NonNull String name) {
sharedElementNames.put(name, name);
}
/**
* Used to register an element that will take part in the shared element transition. Maps the name used in the
* "from" view to the name used in the "to" view if they are not the same.
*
* @param fromName The transition name used in the "from" view
* @param toName The transition name used in the "to" view
*/
protected final void addSharedElement(@NonNull String fromName, @NonNull String toName) {
sharedElementNames.put(fromName, toName);
}
/**
* Used to register an element that will take part in the shared element transition. Maps the name used in the
* "from" view to the name used in the "to" view if they are not the same.
*
* @param sharedElement The view from the "from" view that will take part in the shared element transition
* @param toName The transition name used in the "to" view
*/
protected final void addSharedElement(@NonNull View sharedElement, @NonNull String toName) {
String transitionName = sharedElement.getTransitionName();
if (transitionName == null) {
throw new IllegalArgumentException("Unique transitionNames are required for all sharedElements");
}
sharedElementNames.put(transitionName, toName);
}
/**
* The transition will be delayed until the view with the name passed in is available in the "to" hierarchy. This is
* particularly useful for views that don't load instantly, like RecyclerViews. Note that using this method can
* potentially lock up your app indefinitely if the view never loads!
*/
protected final void waitOnSharedElementNamed(@NonNull String name) {
if (!sharedElementNames.values().contains(name)) {
throw new IllegalStateException("Can't wait on a shared element that hasn't been registered using addSharedElement");
}
waitForTransitionNames.add(name);
}
private static class OneShotPreDrawListener implements OnPreDrawListener, View.OnAttachStateChangeListener {
private final View view;
private ViewTreeObserver viewTreeObserver;
private final Runnable runnable;
private final boolean preDrawReturnValue;
private OneShotPreDrawListener(boolean preDrawReturnValue, @NonNull View view, @NonNull Runnable runnable) {
this.preDrawReturnValue = preDrawReturnValue;
this.view = view;
viewTreeObserver = view.getViewTreeObserver();
this.runnable = runnable;
}
@NonNull
public static OneShotPreDrawListener add(boolean preDrawReturnValue, @NonNull View view, @NonNull Runnable runnable) {
OneShotPreDrawListener listener = new OneShotPreDrawListener(preDrawReturnValue, view, runnable);
view.getViewTreeObserver().addOnPreDrawListener(listener);
view.addOnAttachStateChangeListener(listener);
return listener;
}
@Override
public boolean onPreDraw() {
removeListener();
runnable.run();
return preDrawReturnValue;
}
private void removeListener() {
if (viewTreeObserver.isAlive()) {
viewTreeObserver.removeOnPreDrawListener(this);
} else {
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
view.removeOnAttachStateChangeListener(this);
}
@Override
public void onViewAttachedToWindow(View v) {
viewTreeObserver = v.getViewTreeObserver();
}
@Override
public void onViewDetachedFromWindow(View v) {
removeListener();
}
}
private static class ViewParentPair {
@NonNull final View view;
@NonNull final ViewGroup parent;
ViewParentPair(@NonNull View view, ViewGroup parent) {
this.view = view;
this.parent = parent;
}
}
}
@@ -2,6 +2,7 @@ package com.bluelinelabs.conductor.changehandler;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
@@ -44,7 +45,7 @@ public class SimpleSwapChangeHandler extends ControllerChangeHandler implements
}
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, Controller newTop) {
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
canceled = true;
@@ -62,7 +63,7 @@ public class SimpleSwapChangeHandler extends ControllerChangeHandler implements
}
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
if (!canceled) {
if (from != null && (!isPush || removesFromViewOnPush)) {
container.removeView(from);
@@ -89,7 +90,7 @@ public class SimpleSwapChangeHandler extends ControllerChangeHandler implements
}
@Override
public void onViewAttachedToWindow(View v) {
public void onViewAttachedToWindow(@NonNull View v) {
v.removeOnAttachStateChangeListener(this);
if (changeListener != null) {
@@ -100,5 +101,15 @@ public class SimpleSwapChangeHandler extends ControllerChangeHandler implements
}
@Override
public void onViewDetachedFromWindow(View v) { }
public void onViewDetachedFromWindow(@NonNull View v) { }
@Override @NonNull
public ControllerChangeHandler copy() {
return new SimpleSwapChangeHandler(removesFromViewOnPush());
}
@Override
public boolean isReusable() {
return true;
}
}
@@ -3,6 +3,7 @@ package com.bluelinelabs.conductor.changehandler;
import android.annotation.TargetApi;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.Transition;
import android.transition.Transition.TransitionListener;
import android.transition.TransitionManager;
@@ -18,34 +19,51 @@ import com.bluelinelabs.conductor.ControllerChangeHandler;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class TransitionChangeHandler extends ControllerChangeHandler {
private boolean canceled;
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, 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 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, View from, View to, boolean isPush);
protected abstract Transition getTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush);
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, Controller newTop) {
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
super.onAbortPush(newHandler, newTop);
canceled = true;
}
@Override
public void performChange(@NonNull final ViewGroup container, View from, View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
public void completeImmediately() {
super.completeImmediately();
needsImmediateCompletion = true;
}
@Override
public void performChange(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
if (canceled) {
changeListener.onChangeCompleted();
return;
}
if (needsImmediateCompletion) {
executePropertyChanges(container, from, to, null, isPush);
changeListener.onChangeCompleted();
return;
}
Transition transition = getTransition(container, from, to, isPush);
final Transition transition = getTransition(container, from, to, isPush);
transition.addListener(new TransitionListener() {
@Override
public void onTransitionStart(Transition transition) { }
@@ -67,17 +85,53 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
public void onTransitionResume(Transition transition) { }
});
TransitionManager.beginDelayedTransition(container, transition);
if (from != null) {
prepareForTransition(container, from, to, transition, isPush, new OnTransitionPreparedListener() {
@Override
public void onPrepared() {
if (!canceled) {
TransitionManager.beginDelayedTransition(container, transition);
executePropertyChanges(container, from, to, transition, isPush);
}
}
});
}
@Override
public boolean removesFromViewOnPush() {
return true;
}
/**
* Called before a transition occurs. This can be used to reorder views, set their transition names, etc. The transition will begin
* when {@code onTransitionPreparedListener} is called.
*
* @param container The container these Views are hosted in
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param transition The transition that is being prepared for
* @param isPush True if this is a push transaction, false if it's a pop
*/
public void prepareForTransition(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @NonNull Transition transition, boolean isPush, @NonNull OnTransitionPreparedListener onTransitionPreparedListener) {
onTransitionPreparedListener.onPrepared();
}
/**
* This should set all view properties needed for the transition to work properly. By default it removes the "from" view
* and adds the "to" view.
*
* @param container The container these Views are hosted in
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param transition The transition with which {@code TransitionManager.beginDelayedTransition} has been called. This will be null only if another ControllerChangeHandler immediately overrides this one.
* @param isPush True if this is a push transaction, false if it's a pop
*/
public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @Nullable Transition transition, boolean isPush) {
if (from != null && (removesFromViewOnPush() || !isPush) && from.getParent() == container) {
container.removeView(from);
}
if (to != null) {
if (to != null && to.getParent() == null) {
container.addView(to);
}
}
@Override
public final boolean removesFromViewOnPush() {
return true;
}
}
@@ -3,9 +3,11 @@ package com.bluelinelabs.conductor.changehandler;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.internal.ClassUtils;
@@ -15,13 +17,10 @@ import com.bluelinelabs.conductor.internal.ClassUtils;
*/
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 static final String KEY_CHANGE_HANDLER_CLASS = "TransitionChangeHandlerCompat.changeHandler.class";
private static final String KEY_HANDLER_STATE = "TransitionChangeHandlerCompat.changeHandler.state";
private TransitionChangeHandler transitionChangeHandler;
private ControllerChangeHandler fallbackChangeHandler;
private ControllerChangeHandler changeHandler;
public TransitionChangeHandlerCompat() { }
@@ -32,57 +31,67 @@ public class TransitionChangeHandlerCompat extends ControllerChangeHandler {
* @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) {
this.transitionChangeHandler = transitionChangeHandler;
this.fallbackChangeHandler = fallbackChangeHandler;
public TransitionChangeHandlerCompat(@NonNull TransitionChangeHandler transitionChangeHandler, @NonNull ControllerChangeHandler fallbackChangeHandler) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
changeHandler = transitionChangeHandler;
} else {
changeHandler = fallbackChangeHandler;
}
}
@Override
public void performChange(@NonNull final ViewGroup container, View from, View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
transitionChangeHandler.performChange(container, from, to, isPush, changeListener);
} else {
fallbackChangeHandler.performChange(container, from, to, isPush, changeListener);
}
public void performChange(@NonNull final ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
changeHandler.performChange(container, from, to, isPush, changeListener);
}
@Override
public void saveToBundle(@NonNull Bundle bundle) {
super.saveToBundle(bundle);
bundle.putString(KEY_TRANSITION_HANDLER_CLASS, transitionChangeHandler.getClass().getName());
bundle.putString(KEY_FALLBACK_HANDLER_CLASS, fallbackChangeHandler.getClass().getName());
bundle.putString(KEY_CHANGE_HANDLER_CLASS, changeHandler.getClass().getName());
Bundle transitionBundle = new Bundle();
transitionChangeHandler.saveToBundle(transitionBundle);
bundle.putBundle(KEY_TRANSITION_HANDLER_STATE, transitionBundle);
Bundle fallbackBundle = new Bundle();
fallbackChangeHandler.saveToBundle(fallbackBundle);
bundle.putBundle(KEY_FALLBACK_HANDLER_STATE, fallbackBundle);
Bundle stateBundle = new Bundle();
changeHandler.saveToBundle(stateBundle);
bundle.putBundle(KEY_HANDLER_STATE, stateBundle);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
super.restoreFromBundle(bundle);
String transitionClassName = bundle.getString(KEY_TRANSITION_HANDLER_CLASS);
transitionChangeHandler = ClassUtils.newInstance(transitionClassName);
String className = bundle.getString(KEY_CHANGE_HANDLER_CLASS);
changeHandler = ClassUtils.newInstance(className);
//noinspection ConstantConditions
transitionChangeHandler.restoreFromBundle(bundle.getBundle(KEY_TRANSITION_HANDLER_STATE));
String fallbackClassName = bundle.getString(KEY_FALLBACK_HANDLER_CLASS);
fallbackChangeHandler = ClassUtils.newInstance(fallbackClassName);
//noinspection ConstantConditions
fallbackChangeHandler.restoreFromBundle(bundle.getBundle(KEY_FALLBACK_HANDLER_STATE));
changeHandler.restoreFromBundle(bundle.getBundle(KEY_HANDLER_STATE));
}
@Override
public boolean removesFromViewOnPush() {
return changeHandler.removesFromViewOnPush();
}
@Override @NonNull
public ControllerChangeHandler copy() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return transitionChangeHandler.removesFromViewOnPush();
return new TransitionChangeHandlerCompat((TransitionChangeHandler)changeHandler.copy(), null);
} else {
return fallbackChangeHandler.removesFromViewOnPush();
return new TransitionChangeHandlerCompat(null, changeHandler.copy());
}
}
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
changeHandler.onAbortPush(newHandler, newTop);
}
@Override
public void completeImmediately() {
changeHandler.completeImmediately();
}
@Override
public void setForceRemoveViewOnPush(boolean force) {
changeHandler.setForceRemoveViewOnPush(force);
}
}
@@ -4,9 +4,12 @@ import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import java.util.ArrayList;
import java.util.List;
@@ -30,8 +33,8 @@ public class VerticalChangeHandler extends AnimatorChangeHandler {
super(duration, removesFromViewOnPush);
}
@Override
protected Animator getAnimator(@NonNull ViewGroup container, View from, View to, boolean isPush, boolean toAddedToContainer) {
@Override @NonNull
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
AnimatorSet animator = new AnimatorSet();
List<Animator> viewAnimators = new ArrayList<>();
@@ -48,4 +51,9 @@ public class VerticalChangeHandler extends AnimatorChangeHandler {
@Override
protected void resetFromView(@NonNull View from) { }
@Override @NonNull
public ControllerChangeHandler copy() {
return new VerticalChangeHandler(getAnimationDuration(), removesFromViewOnPush());
}
}
@@ -1,16 +1,13 @@
package com.bluelinelabs.conductor.internal;
import android.support.annotation.NonNull;
import android.support.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());
@@ -6,9 +6,13 @@ import android.app.Application.ActivityLifecycleCallbacks;
import android.app.Fragment;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuInflater;
@@ -27,6 +31,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
private static final String FRAGMENT_TAG = "LifecycleHandler";
private static final String KEY_PENDING_PERMISSION_REQUESTS = "LifecycleHandler.pendingPermissionRequests";
private static final String KEY_PERMISSION_REQUEST_CODES = "LifecycleHandler.permissionRequests";
private static final String KEY_ACTIVITY_REQUEST_CODES = "LifecycleHandler.activityRequests";
private static final String KEY_ROUTER_STATE_PREFIX = "LifecycleHandler.routerState";
@@ -34,9 +39,13 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
private Activity activity;
private boolean hasRegisteredCallbacks;
private boolean destroyed;
private boolean attached;
private boolean hasPreparedForHostDetach;
private static final Map<Activity, LifecycleHandler> activeLifecycleHandlers = new HashMap<>();
private SparseArray<String> permissionRequestMap = new SparseArray<>();
private SparseArray<String> activityRequestMap = new SparseArray<>();
private ArrayList<PendingPermissionRequest> pendingPermissionRequests = new ArrayList<>();
private final Map<Integer, ActivityHostedRouter> routerMap = new HashMap<>();
@@ -45,15 +54,20 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
setHasOptionsMenu(true);
}
private static LifecycleHandler findInActivity(Activity activity) {
LifecycleHandler lifecycleHandler = (LifecycleHandler)activity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
@Nullable
private static LifecycleHandler findInActivity(@NonNull Activity activity) {
LifecycleHandler lifecycleHandler = activeLifecycleHandlers.get(activity);
if (lifecycleHandler == null) {
lifecycleHandler = (LifecycleHandler)activity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
}
if (lifecycleHandler != null) {
lifecycleHandler.registerActivityListener(activity);
}
return lifecycleHandler;
}
public static LifecycleHandler install(Activity activity) {
@NonNull
public static LifecycleHandler install(@NonNull Activity activity) {
LifecycleHandler lifecycleHandler = findInActivity(activity);
if (lifecycleHandler == null) {
lifecycleHandler = new LifecycleHandler();
@@ -63,7 +77,8 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
return lifecycleHandler;
}
public Router getRouter(ViewGroup container, Bundle savedInstanceState) {
@NonNull
public Router getRouter(@NonNull ViewGroup container, @Nullable Bundle savedInstanceState) {
ActivityHostedRouter router = routerMap.get(getRouterHashKey(container));
if (router == null) {
router = new ActivityHostedRouter();
@@ -83,24 +98,31 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
return router;
}
@NonNull
public List<Router> getRouters() {
return new ArrayList<Router>(routerMap.values());
}
@Nullable
public Activity getLifecycleActivity() {
return activity;
}
private static int getRouterHashKey(ViewGroup viewGroup) {
private static int getRouterHashKey(@NonNull ViewGroup viewGroup) {
return viewGroup.getId();
}
private void registerActivityListener(Activity activity) {
private void registerActivityListener(@NonNull Activity activity) {
this.activity = activity;
if (!hasRegisteredCallbacks) {
hasRegisteredCallbacks = true;
activity.getApplication().registerActivityLifecycleCallbacks(this);
// Since Fragment transactions are async, we have to keep an <Activity, LifecycleHandler> map in addition
// to trying to find the LifecycleHandler fragment in the Activity to handle the case of the developer
// trying to immediately get > 1 router in the same Activity. See issue #299.
activeLifecycleHandlers.put(activity, this);
}
}
@@ -110,10 +132,13 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
if (savedInstanceState != null) {
StringSparseArrayParceler permissionParcel = savedInstanceState.getParcelable(KEY_PERMISSION_REQUEST_CODES);
permissionRequestMap = permissionParcel != null ? permissionParcel.getStringSparseArray() : null;
permissionRequestMap = permissionParcel != null ? permissionParcel.getStringSparseArray() : new SparseArray<String>();
StringSparseArrayParceler activityParcel = savedInstanceState.getParcelable(KEY_ACTIVITY_REQUEST_CODES);
activityRequestMap = activityParcel != null ? activityParcel.getStringSparseArray() : null;
activityRequestMap = activityParcel != null ? activityParcel.getStringSparseArray() : new SparseArray<String>();
ArrayList<PendingPermissionRequest> pendingRequests = savedInstanceState.getParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS);
pendingPermissionRequests = pendingRequests != null ? pendingRequests : new ArrayList<PendingPermissionRequest>();
}
}
@@ -123,6 +148,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
outState.putParcelable(KEY_PERMISSION_REQUEST_CODES, new StringSparseArrayParceler(permissionRequestMap));
outState.putParcelable(KEY_ACTIVITY_REQUEST_CODES, new StringSparseArrayParceler(activityRequestMap));
outState.putParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS, pendingPermissionRequests);
}
@Override
@@ -131,6 +157,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
if (activity != null) {
activity.getApplication().unregisterActivityLifecycleCallbacks(this);
activeLifecycleHandlers.remove(activity);
destroyRouters();
activity = null;
}
@@ -141,27 +168,45 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
public void onAttach(Activity activity) {
super.onAttach(activity);
destroyed = false;
setAttached();
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
destroyed = false;
setAttached();
}
@Override
public void onDetach() {
super.onDetach();
attached = false;
destroyRouters();
}
private void setAttached() {
if (!attached) {
attached = true;
for (int i = pendingPermissionRequests.size() - 1; i >= 0; i--) {
PendingPermissionRequest request = pendingPermissionRequests.remove(i);
requestPermissions(request.instanceId, request.permissions, request.requestCode);
}
}
for (ActivityHostedRouter router : new ArrayList<>(routerMap.values())) {
router.onContextAvailable();
}
}
private void destroyRouters() {
if (!destroyed) {
destroyed = true;
if (activity != null) {
for (Router router : routerMap.values()) {
for (Router router : getRouters()) {
router.onActivityDestroyed(activity);
}
}
@@ -174,7 +219,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
String instanceId = activityRequestMap.get(requestCode);
if (instanceId != null) {
for (Router router : routerMap.values()) {
for (Router router : getRouters()) {
router.onActivityResult(instanceId, requestCode, resultCode, data);
}
}
@@ -186,7 +231,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
String instanceId = permissionRequestMap.get(requestCode);
if (instanceId != null) {
for (Router router : routerMap.values()) {
for (Router router : getRouters()) {
router.onRequestPermissionsResult(instanceId, requestCode, permissions, grantResults);
}
}
@@ -194,7 +239,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
@Override
public boolean shouldShowRequestPermissionRationale(@NonNull String permission) {
for (Router router : routerMap.values()) {
for (Router router : getRouters()) {
Boolean handled = router.handleRequestedPermission(permission);
if (handled != null) {
return handled;
@@ -207,7 +252,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
for (Router router : routerMap.values()) {
for (Router router : getRouters()) {
router.onCreateOptionsMenu(menu, inflater);
}
}
@@ -216,14 +261,14 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
for (Router router : routerMap.values()) {
for (Router router : getRouters()) {
router.onPrepareOptionsMenu(menu);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
for (Router router : routerMap.values()) {
for (Router router : getRouters()) {
if (router.onOptionsItemSelected(item)) {
return true;
}
@@ -232,11 +277,11 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
return super.onOptionsItemSelected(item);
}
public void registerForActivityResult(String instanceId, int requestCode) {
public void registerForActivityResult(@NonNull String instanceId, int requestCode) {
activityRequestMap.put(requestCode, instanceId);
}
public void unregisterForActivityResults(String instanceId) {
public void unregisterForActivityResults(@NonNull String instanceId) {
for (int i = activityRequestMap.size() - 1; i >= 0; i--) {
if (instanceId.equals(activityRequestMap.get(activityRequestMap.keyAt(i)))) {
activityRequestMap.removeAt(i);
@@ -244,33 +289,51 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
}
}
public void startActivityForResult(String instanceId, Intent intent, int requestCode) {
public void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) {
registerForActivityResult(instanceId, requestCode);
startActivityForResult(intent, requestCode);
}
public void startActivityForResult(String instanceId, Intent intent, int requestCode, Bundle options) {
public void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options) {
registerForActivityResult(instanceId, requestCode);
startActivityForResult(intent, requestCode, options);
}
@TargetApi(Build.VERSION_CODES.N)
public void startIntentSenderForResult(@NonNull String instanceId, @NonNull IntentSender intent, int requestCode,
@Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,
@Nullable Bundle options) throws IntentSender.SendIntentException {
registerForActivityResult(instanceId, requestCode);
startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options);
}
@TargetApi(Build.VERSION_CODES.M)
public void requestPermissions(String instanceId, String[] permissions, int requestCode) {
permissionRequestMap.put(requestCode, instanceId);
requestPermissions(permissions, requestCode);
public void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
if (attached) {
permissionRequestMap.put(requestCode, instanceId);
requestPermissions(permissions, requestCode);
} else {
pendingPermissionRequests.add(new PendingPermissionRequest(instanceId, permissions, requestCode));
}
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (this.activity == null && findInActivity(activity) == LifecycleHandler.this) {
this.activity = activity;
for (ActivityHostedRouter router : new ArrayList<>(routerMap.values())) {
router.onContextAvailable();
}
}
}
@Override
public void onActivityStarted(Activity activity) {
if (this.activity == activity) {
for (Router router : routerMap.values()) {
hasPreparedForHostDetach = false;
for (Router router : getRouters()) {
router.onActivityStarted(activity);
}
}
@@ -279,7 +342,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
@Override
public void onActivityResumed(Activity activity) {
if (this.activity == activity) {
for (Router router : routerMap.values()) {
for (Router router : getRouters()) {
router.onActivityResumed(activity);
}
}
@@ -288,7 +351,7 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
@Override
public void onActivityPaused(Activity activity) {
if (this.activity == activity) {
for (Router router : routerMap.values()) {
for (Router router : getRouters()) {
router.onActivityPaused(activity);
}
}
@@ -297,7 +360,9 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
@Override
public void onActivityStopped(Activity activity) {
if (this.activity == activity) {
for (Router router : routerMap.values()) {
prepareForHostDetachIfNeeded();
for (Router router : getRouters()) {
router.onActivityStopped(activity);
}
}
@@ -306,7 +371,9 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
if (this.activity == activity) {
for (Router router : routerMap.values()) {
prepareForHostDetachIfNeeded();
for (Router router : getRouters()) {
Bundle bundle = new Bundle();
router.saveInstanceState(bundle);
outState.putBundle(KEY_ROUTER_STATE_PREFIX + router.getContainerId(), bundle);
@@ -315,5 +382,60 @@ public class LifecycleHandler extends Fragment implements ActivityLifecycleCallb
}
@Override
public void onActivityDestroyed(Activity activity) { }
public void onActivityDestroyed(Activity activity) {
activeLifecycleHandlers.remove(activity);
}
private void prepareForHostDetachIfNeeded() {
if (!hasPreparedForHostDetach) {
hasPreparedForHostDetach = true;
for (Router router : getRouters()) {
router.prepareForHostDetach();
}
}
}
private static class PendingPermissionRequest implements Parcelable {
final String instanceId;
final String[] permissions;
final int requestCode;
PendingPermissionRequest(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
this.instanceId = instanceId;
this.permissions = permissions;
this.requestCode = requestCode;
}
PendingPermissionRequest(Parcel in) {
instanceId = in.readString();
permissions = in.createStringArray();
requestCode = in.readInt();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeString(instanceId);
out.writeStringArray(permissions);
out.writeInt(requestCode);
}
public static final Parcelable.Creator<PendingPermissionRequest> CREATOR = new Parcelable.Creator<PendingPermissionRequest>() {
@Override
public PendingPermissionRequest createFromParcel(Parcel in) {
return new PendingPermissionRequest(in);
}
@Override
public PendingPermissionRequest[] newArray(int size) {
return new PendingPermissionRequest[size];
}
};
}
}
@@ -1,6 +1,7 @@
package com.bluelinelabs.conductor.internal;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
@@ -9,8 +10,18 @@ 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) {
public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
changeListener.onChangeCompleted();
}
}
@NonNull
@Override
public ControllerChangeHandler copy() {
return new NoOpControllerChangeHandler();
}
@Override
public boolean isReusable() {
return true;
}
}
@@ -2,17 +2,18 @@ package com.bluelinelabs.conductor.internal;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.util.SparseArray;
public class StringSparseArrayParceler implements Parcelable {
private final SparseArray<String> stringSparseArray;
public StringSparseArrayParceler(SparseArray<String> stringSparseArray) {
public StringSparseArrayParceler(@NonNull SparseArray<String> stringSparseArray) {
this.stringSparseArray = stringSparseArray;
}
private StringSparseArrayParceler(Parcel in) {
StringSparseArrayParceler(@NonNull Parcel in) {
stringSparseArray = new SparseArray<>();
final int size = in.readInt();
@@ -22,14 +23,17 @@ public class StringSparseArrayParceler implements Parcelable {
}
}
@NonNull
public SparseArray<String> getStringSparseArray() {
return stringSparseArray;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
final int size = stringSparseArray.size();
@@ -44,10 +48,12 @@ public class StringSparseArrayParceler implements Parcelable {
}
public static final Parcelable.Creator<StringSparseArrayParceler> CREATOR = new Parcelable.Creator<StringSparseArrayParceler>() {
@Override
public StringSparseArrayParceler createFromParcel(Parcel in) {
return new StringSparseArrayParceler(in);
}
@Override
public StringSparseArrayParceler[] newArray(int size) {
return new StringSparseArrayParceler[size];
}
@@ -0,0 +1,20 @@
package com.bluelinelabs.conductor.internal;
import android.os.Looper;
import android.util.AndroidRuntimeException;
public class ThreadUtils {
public static void ensureMainThread() {
if (Looper.getMainLooper().getThread() != Thread.currentThread()) {
throw new CalledFromWrongThreadException("Methods that affect the view hierarchy can can only be called from the main thread.");
}
}
private static final class CalledFromWrongThreadException extends AndroidRuntimeException {
CalledFromWrongThreadException(String msg) {
super(msg);
}
}
}
@@ -0,0 +1,24 @@
package com.bluelinelabs.conductor.internal;
import android.os.Bundle;
import android.support.annotation.NonNull;
public class TransactionIndexer {
private static final String KEY_INDEX = "TransactionIndexer.currentIndex";
private int currentIndex;
public int nextIndex() {
return ++currentIndex;
}
public void saveInstanceState(@NonNull Bundle outState) {
outState.putInt(KEY_INDEX, currentIndex);
}
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
currentIndex = savedInstanceState.getInt(KEY_INDEX);
}
}
@@ -0,0 +1,185 @@
package com.bluelinelabs.conductor.internal;
import android.annotation.TargetApi;
import android.graphics.Rect;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.Transition;
import android.transition.TransitionSet;
import android.view.View;
import android.view.ViewGroup;
import java.util.List;
import java.util.Map;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class TransitionUtils {
public static void findNamedViews(@NonNull Map<String, View> namedViews, View view) {
if (view.getVisibility() == View.VISIBLE) {
String transitionName = view.getTransitionName();
if (transitionName != null) {
namedViews.put(transitionName, view);
}
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = viewGroup.getChildAt(i);
findNamedViews(namedViews, child);
}
}
}
}
@Nullable
public static View findNamedView(@NonNull View view, @NonNull String transitionName) {
if (transitionName.equals(view.getTransitionName())) {
return view;
}
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View viewWithTransitionName = findNamedView(viewGroup.getChildAt(i), transitionName);
if (viewWithTransitionName != null) {
return viewWithTransitionName;
}
}
}
return null;
}
public static void setEpicenter(@NonNull Transition transition, @Nullable View view) {
if (view != null) {
final Rect epicenter = new Rect();
getBoundsOnScreen(view, epicenter);
transition.setEpicenterCallback(new Transition.EpicenterCallback() {
@Override
public Rect onGetEpicenter(Transition transition) {
return epicenter;
}
});
}
}
public static void getBoundsOnScreen(@NonNull View view, @NonNull Rect epicenter) {
int[] loc = new int[2];
view.getLocationOnScreen(loc);
epicenter.set(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight());
}
public static void setTargets(@NonNull Transition transition, @NonNull View nonExistentView, @NonNull List<View> sharedViews) {
final List<View> views = transition.getTargets();
views.clear();
final int count = sharedViews.size();
for (int i = 0; i < count; i++) {
final View view = sharedViews.get(i);
bfsAddViewChildren(views, view);
}
views.add(nonExistentView);
sharedViews.add(nonExistentView);
addTargets(transition, sharedViews);
}
public static void addTargets(@Nullable Transition transition, @NonNull List<View> views) {
if (transition == null) {
return;
}
if (transition instanceof TransitionSet) {
TransitionSet set = (TransitionSet) transition;
int numTransitions = set.getTransitionCount();
for (int i = 0; i < numTransitions; i++) {
Transition child = set.getTransitionAt(i);
addTargets(child, views);
}
} else if (!hasSimpleTarget(transition)) {
List<View> targets = transition.getTargets();
if (isNullOrEmpty(targets)) {
int numViews = views.size();
for (int i = 0; i < numViews; i++) {
transition.addTarget(views.get(i));
}
}
}
}
public static void replaceTargets(@NonNull Transition transition, @NonNull List<View> oldTargets, @Nullable List<View> newTargets) {
if (transition instanceof TransitionSet) {
TransitionSet set = (TransitionSet) transition;
int numTransitions = set.getTransitionCount();
for (int i = 0; i < numTransitions; i++) {
Transition child = set.getTransitionAt(i);
replaceTargets(child, oldTargets, newTargets);
}
} else if (!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,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);
}
}
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) {
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,9 +1,12 @@
package com.bluelinelabs.conductor;
import org.junit.Assert;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class BackstackTests {
private Backstack backstack;
@@ -15,20 +18,20 @@ public class BackstackTests {
@Test
public void testPush() {
Assert.assertEquals(0, backstack.size());
assertEquals(0, backstack.size());
backstack.push(RouterTransaction.with(new TestController()));
Assert.assertEquals(1, backstack.size());
assertEquals(1, backstack.size());
}
@Test
public void testPop() {
backstack.push(RouterTransaction.with(new TestController()));
backstack.push(RouterTransaction.with(new TestController()));
Assert.assertEquals(2, backstack.size());
assertEquals(2, backstack.size());
backstack.pop();
Assert.assertEquals(1, backstack.size());
assertEquals(1, backstack.size());
backstack.pop();
Assert.assertEquals(0, backstack.size());
assertEquals(0, backstack.size());
}
@Test
@@ -37,13 +40,13 @@ public class BackstackTests {
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
backstack.push(transaction1);
Assert.assertEquals(transaction1, backstack.peek());
assertEquals(transaction1, backstack.peek());
backstack.push(transaction2);
Assert.assertEquals(transaction2, backstack.peek());
assertEquals(transaction2, backstack.peek());
backstack.pop();
Assert.assertEquals(transaction1, backstack.peek());
assertEquals(transaction1, backstack.peek());
}
@Test
@@ -56,11 +59,11 @@ public class BackstackTests {
backstack.push(transaction2);
backstack.push(transaction3);
Assert.assertEquals(3, backstack.size());
assertEquals(3, backstack.size());
backstack.popTo(transaction1);
Assert.assertEquals(1, backstack.size());
Assert.assertEquals(transaction1, backstack.peek());
assertEquals(1, backstack.size());
assertEquals(transaction1, backstack.peek());
}
}
@@ -2,10 +2,12 @@ package com.bluelinelabs.conductor;
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Assert;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class ControllerChangeHandlerTests {
@Test
@@ -21,17 +23,17 @@ public class ControllerChangeHandlerTests {
ControllerChangeHandler restoredHorizontal = restoredTransaction.pushChangeHandler();
ControllerChangeHandler restoredFade = restoredTransaction.popChangeHandler();
Assert.assertEquals(horizontalChangeHandler.getClass(), restoredHorizontal.getClass());
Assert.assertEquals(fadeChangeHandler.getClass(), restoredFade.getClass());
assertEquals(horizontalChangeHandler.getClass(), restoredHorizontal.getClass());
assertEquals(fadeChangeHandler.getClass(), restoredFade.getClass());
HorizontalChangeHandler restoredHorizontalCast = (HorizontalChangeHandler)restoredHorizontal;
FadeChangeHandler restoredFadeCast = (FadeChangeHandler)restoredFade;
Assert.assertEquals(horizontalChangeHandler.getAnimationDuration(), restoredHorizontalCast.getAnimationDuration());
Assert.assertEquals(horizontalChangeHandler.removesFromViewOnPush(), restoredHorizontalCast.removesFromViewOnPush());
assertEquals(horizontalChangeHandler.getAnimationDuration(), restoredHorizontalCast.getAnimationDuration());
assertEquals(horizontalChangeHandler.removesFromViewOnPush(), restoredHorizontalCast.removesFromViewOnPush());
Assert.assertEquals(fadeChangeHandler.getAnimationDuration(), restoredFadeCast.getAnimationDuration());
Assert.assertEquals(fadeChangeHandler.removesFromViewOnPush(), restoredFadeCast.removesFromViewOnPush());
assertEquals(fadeChangeHandler.getAnimationDuration(), restoredFadeCast.getAnimationDuration());
assertEquals(fadeChangeHandler.removesFromViewOnPush(), restoredFadeCast.removesFromViewOnPush());
}
}
@@ -0,0 +1,269 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ControllerLifecycleActivityReferenceTests {
private Router router;
private ActivityProxy activityProxy;
public void createActivityController(Bundle savedInstanceState, boolean includeStartAndResume) {
activityProxy = new ActivityProxy().create(savedInstanceState);
if (includeStartAndResume) {
activityProxy.start().resume();
}
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
router.setPopsLastView(true);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Before
public void setup() {
createActivityController(null, true);
}
@Test
public void testSingleControllerActivityOnPush() {
Controller controller = new TestController();
assertNull(controller.getActivity());
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
controller.addLifecycleListener(listener);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertEquals(Collections.singletonList(true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.emptyList(), listener.postDetachReferences);
assertEquals(Collections.emptyList(), listener.postDestroyViewReferences);
assertEquals(Collections.emptyList(), listener.postDestroyReferences);
}
@Test
public void testChildControllerActivityOnPush() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
assertNull(child.getActivity());
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
assertEquals(Collections.singletonList(true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.emptyList(), listener.postDetachReferences);
assertEquals(Collections.emptyList(), listener.postDestroyViewReferences);
assertEquals(Collections.emptyList(), listener.postDestroyReferences);
}
@Test
public void testSingleControllerActivityOnPop() {
Controller controller = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
controller.addLifecycleListener(listener);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
router.popCurrentController();
assertEquals(Arrays.asList(true, true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.singletonList(true), listener.postDetachReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
@Test
public void testChildControllerActivityOnPop() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
childRouter.popCurrentController();
assertEquals(Arrays.asList(true, true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.singletonList(true), listener.postDetachReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
@Test
public void testChildControllerActivityOnParentPop() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
router.popCurrentController();
assertEquals(Collections.singletonList(true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.singletonList(true), listener.postDetachReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
@Test
public void testSingleControllerActivityOnDestroy() {
Controller controller = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
controller.addLifecycleListener(listener);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
activityProxy.pause().stop(false).destroy();
assertEquals(Collections.singletonList(true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.singletonList(true), listener.postDetachReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
@Test
public void testChildControllerActivityOnDestroy() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener();
child.addLifecycleListener(listener);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(child)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
activityProxy.pause().stop(false).destroy();
assertEquals(Collections.singletonList(true), listener.changeEndReferences);
assertEquals(Collections.singletonList(true), listener.postCreateViewReferences);
assertEquals(Collections.singletonList(true), listener.postAttachReferences);
assertEquals(Collections.singletonList(true), listener.postDetachReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyViewReferences);
assertEquals(Collections.singletonList(true), listener.postDestroyReferences);
}
static class ActivityReferencingLifecycleListener extends Controller.LifecycleListener {
final List<Boolean> changeEndReferences = new ArrayList<>();
final List<Boolean> postCreateViewReferences = new ArrayList<>();
final List<Boolean> postAttachReferences = new ArrayList<>();
final List<Boolean> postDetachReferences = new ArrayList<>();
final List<Boolean> postDestroyViewReferences = new ArrayList<>();
final List<Boolean> postDestroyReferences = new ArrayList<>();
@Override
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
changeEndReferences.add(controller.getActivity() != null);
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
postCreateViewReferences.add(controller.getActivity() != null);
}
@Override
public void postAttach(@NonNull Controller controller, @NonNull View view) {
postAttachReferences.add(controller.getActivity() != null);
}
@Override
public void postDetach(@NonNull Controller controller, @NonNull View view) {
postDetachReferences.add(controller.getActivity() != null);
}
@Override
public void postDestroyView(@NonNull Controller controller) {
postDestroyViewReferences.add(controller.getActivity() != null);
}
@Override
public void postDestroy(@NonNull Controller controller) {
postDestroyReferences.add(controller.getActivity() != null);
}
}
}
@@ -1,23 +1,36 @@
package com.bluelinelabs.conductor;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.bluelinelabs.conductor.MockChangeHandler.ChangeHandlerListener;
import com.bluelinelabs.conductor.Controller.RetainViewMode;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.CallState;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.MockChangeHandler.ChangeHandlerListener;
import com.bluelinelabs.conductor.util.TestController;
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.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ControllerLifecycleTests {
public class ControllerLifecycleCallbacksTests {
private Router router;
@@ -41,7 +54,7 @@ public class ControllerLifecycleTests {
public void setup() {
createActivityController(null, true);
currentCallState = new CallState();
currentCallState = new CallState(false);
}
@Test
@@ -49,7 +62,7 @@ public class ControllerLifecycleTests {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
@@ -60,17 +73,17 @@ public class ControllerLifecycleTests {
router.popCurrentController();
Assert.assertNull(controller.getView());
assertNull(controller.getView());
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleWithActivityDestroy() {
public void testLifecycleWithActivityStop() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
@@ -83,7 +96,38 @@ public class ControllerLifecycleTests {
assertCalls(expectedCallState, controller);
activityProxy.stop();
activityProxy.stop(false);
expectedCallState.detachCalls++;
assertCalls(expectedCallState, controller);
assertNotNull(controller.getView());
ViewUtils.reportAttached(controller.getView(), false);
expectedCallState.saveViewStateCalls++;
expectedCallState.destroyViewCalls++;
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleWithActivityDestroy() {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(getPushHandler(expectedCallState, controller)));
assertCalls(expectedCallState, controller);
activityProxy.getActivity().isDestroying = true;
activityProxy.pause();
assertCalls(expectedCallState, controller);
activityProxy.stop(true);
expectedCallState.saveViewStateCalls++;
expectedCallState.detachCalls++;
@@ -92,6 +136,7 @@ public class ControllerLifecycleTests {
activityProxy.destroy();
expectedCallState.contextUnavailableCalls++;
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, controller);
}
@@ -101,7 +146,7 @@ public class ControllerLifecycleTests {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
@@ -122,17 +167,19 @@ public class ControllerLifecycleTests {
activityProxy.pause();
assertCalls(expectedCallState, controller);
activityProxy.stop();
activityProxy.stop(true);
expectedCallState.detachCalls++;
expectedCallState.destroyViewCalls++;
assertCalls(expectedCallState, controller);
activityProxy.destroy();
expectedCallState.contextUnavailableCalls++;
assertCalls(expectedCallState, controller);
createActivityController(bundle, false);
controller = (TestController)router.getControllerWithTag("root");
expectedCallState.contextAvailableCalls++;
expectedCallState.restoreInstanceStateCalls++;
expectedCallState.restoreViewStateCalls++;
expectedCallState.changeStartCalls++;
@@ -145,6 +192,7 @@ public class ControllerLifecycleTests {
currentCallState.changeEndCalls = controller.currentCallState.changeEndCalls;
currentCallState.createViewCalls = controller.currentCallState.createViewCalls;
currentCallState.attachCalls = controller.currentCallState.attachCalls;
currentCallState.contextAvailableCalls = controller.currentCallState.contextAvailableCalls;
assertCalls(expectedCallState, controller);
@@ -165,7 +213,7 @@ public class ControllerLifecycleTests {
TestController controller = new TestController();
attachLifecycleListener(controller);
CallState expectedCallState = new CallState();
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, controller);
router.pushController(RouterTransaction.with(controller)
@@ -183,232 +231,234 @@ public class ControllerLifecycleTests {
assertCalls(expectedCallState, controller);
activityProxy.resume();
assertCalls(expectedCallState, controller);
}
@Test
public void testLifecycleCallOrder() {
final TestController testController = new TestController();
final CallState callState = new CallState();
final CallState callState = new CallState(false);
testController.addLifecycleListener(new LifecycleListener() {
@Override
public void preCreateView(@NonNull Controller controller) {
callState.createViewCalls++;
Assert.assertEquals(1, callState.createViewCalls);
Assert.assertEquals(0, testController.currentCallState.createViewCalls);
assertEquals(1, callState.createViewCalls);
assertEquals(0, testController.currentCallState.createViewCalls);
Assert.assertEquals(0, callState.attachCalls);
Assert.assertEquals(0, testController.currentCallState.attachCalls);
assertEquals(0, callState.attachCalls);
assertEquals(0, testController.currentCallState.attachCalls);
Assert.assertEquals(0, callState.detachCalls);
Assert.assertEquals(0, testController.currentCallState.detachCalls);
assertEquals(0, callState.detachCalls);
assertEquals(0, testController.currentCallState.detachCalls);
Assert.assertEquals(0, callState.destroyViewCalls);
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(0, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
callState.createViewCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(0, callState.attachCalls);
Assert.assertEquals(0, testController.currentCallState.attachCalls);
assertEquals(0, callState.attachCalls);
assertEquals(0, testController.currentCallState.attachCalls);
Assert.assertEquals(0, callState.detachCalls);
Assert.assertEquals(0, testController.currentCallState.detachCalls);
assertEquals(0, callState.detachCalls);
assertEquals(0, testController.currentCallState.detachCalls);
Assert.assertEquals(0, callState.destroyViewCalls);
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(0, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void preAttach(@NonNull Controller controller, @NonNull View view) {
callState.attachCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(1, callState.attachCalls);
Assert.assertEquals(0, testController.currentCallState.attachCalls);
assertEquals(1, callState.attachCalls);
assertEquals(0, testController.currentCallState.attachCalls);
Assert.assertEquals(0, callState.detachCalls);
Assert.assertEquals(0, testController.currentCallState.detachCalls);
assertEquals(0, callState.detachCalls);
assertEquals(0, testController.currentCallState.detachCalls);
Assert.assertEquals(0, callState.destroyViewCalls);
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(0, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postAttach(@NonNull Controller controller, @NonNull View view) {
callState.attachCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(2, callState.attachCalls);
Assert.assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
Assert.assertEquals(0, callState.detachCalls);
Assert.assertEquals(0, testController.currentCallState.detachCalls);
assertEquals(0, callState.detachCalls);
assertEquals(0, testController.currentCallState.detachCalls);
Assert.assertEquals(0, callState.destroyViewCalls);
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(0, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
callState.detachCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(2, callState.attachCalls);
Assert.assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
Assert.assertEquals(1, callState.detachCalls);
Assert.assertEquals(0, testController.currentCallState.detachCalls);
assertEquals(1, callState.detachCalls);
assertEquals(0, testController.currentCallState.detachCalls);
Assert.assertEquals(0, callState.destroyViewCalls);
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(0, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postDetach(@NonNull Controller controller, @NonNull View view) {
callState.detachCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(2, callState.attachCalls);
Assert.assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
Assert.assertEquals(2, callState.detachCalls);
Assert.assertEquals(1, testController.currentCallState.detachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(1, testController.currentCallState.detachCalls);
Assert.assertEquals(0, callState.destroyViewCalls);
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(0, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(0, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
callState.destroyViewCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(2, callState.attachCalls);
Assert.assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
Assert.assertEquals(2, callState.detachCalls);
Assert.assertEquals(1, testController.currentCallState.detachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(1, testController.currentCallState.detachCalls);
Assert.assertEquals(1, callState.destroyViewCalls);
Assert.assertEquals(0, testController.currentCallState.destroyViewCalls);
assertEquals(1, callState.destroyViewCalls);
assertEquals(0, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(0, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postDestroyView(@NonNull Controller controller) {
callState.destroyViewCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(2, callState.attachCalls);
Assert.assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
Assert.assertEquals(2, callState.detachCalls);
Assert.assertEquals(1, testController.currentCallState.detachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(1, testController.currentCallState.detachCalls);
Assert.assertEquals(2, callState.destroyViewCalls);
Assert.assertEquals(1, testController.currentCallState.destroyViewCalls);
assertEquals(2, callState.destroyViewCalls);
assertEquals(1, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(0, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
assertEquals(0, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void preDestroy(@NonNull Controller controller) {
callState.destroyCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(2, callState.attachCalls);
Assert.assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
Assert.assertEquals(2, callState.detachCalls);
Assert.assertEquals(1, testController.currentCallState.detachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(1, testController.currentCallState.detachCalls);
Assert.assertEquals(2, callState.destroyViewCalls);
Assert.assertEquals(1, testController.currentCallState.destroyViewCalls);
assertEquals(2, callState.destroyViewCalls);
assertEquals(1, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(1, callState.destroyCalls);
Assert.assertEquals(0, testController.currentCallState.destroyCalls);
assertEquals(1, callState.destroyCalls);
assertEquals(0, testController.currentCallState.destroyCalls);
}
@Override
public void postDestroy(@NonNull Controller controller) {
callState.destroyCalls++;
Assert.assertEquals(2, callState.createViewCalls);
Assert.assertEquals(1, testController.currentCallState.createViewCalls);
assertEquals(2, callState.createViewCalls);
assertEquals(1, testController.currentCallState.createViewCalls);
Assert.assertEquals(2, callState.attachCalls);
Assert.assertEquals(1, testController.currentCallState.attachCalls);
assertEquals(2, callState.attachCalls);
assertEquals(1, testController.currentCallState.attachCalls);
Assert.assertEquals(2, callState.detachCalls);
Assert.assertEquals(1, testController.currentCallState.detachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(1, testController.currentCallState.detachCalls);
Assert.assertEquals(2, callState.destroyViewCalls);
Assert.assertEquals(1, testController.currentCallState.destroyViewCalls);
assertEquals(2, callState.destroyViewCalls);
assertEquals(1, testController.currentCallState.destroyViewCalls);
Assert.assertEquals(2, callState.destroyCalls);
Assert.assertEquals(1, testController.currentCallState.destroyCalls);
assertEquals(2, callState.destroyCalls);
assertEquals(1, testController.currentCallState.destroyCalls);
}
});
router.pushController(RouterTransaction.with(testController)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
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);
assertEquals(2, callState.createViewCalls);
assertEquals(2, callState.attachCalls);
assertEquals(2, callState.detachCalls);
assertEquals(2, callState.destroyViewCalls);
assertEquals(2, callState.destroyCalls);
}
@Test
public void testChildLifecycle() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
attachLifecycleListener(child);
CallState expectedCallState = new CallState();
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, child);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter
.setRoot(RouterTransaction.with(child)
.pushChangeHandler(getPushHandler(expectedCallState, child))
@@ -425,17 +475,17 @@ public class ControllerLifecycleTests {
public void testChildLifecycle2() {
Controller parent = new TestController();
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
attachLifecycleListener(child);
CallState expectedCallState = new CallState();
CallState expectedCallState = new CallState(false);
assertCalls(expectedCallState, child);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter
.setRoot(RouterTransaction.with(child)
.pushChangeHandler(getPushHandler(expectedCallState, child))
@@ -445,26 +495,64 @@ public class ControllerLifecycleTests {
router.popCurrentController();
expectedCallState.detachCalls++;
expectedCallState.destroyViewCalls++;
expectedCallState.contextUnavailableCalls++;
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, child);
}
@Test
public void testChildLifecycleOrderingAfterUnexpectedAttach() {
Controller parent = new TestController();
parent.setRetainViewMode(RetainViewMode.RETAIN_DETACH);
router.pushController(RouterTransaction.with(parent)
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
TestController child = new TestController();
child.setRetainViewMode(RetainViewMode.RETAIN_DETACH);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter
.setRoot(RouterTransaction.with(child)
.pushChangeHandler(new SimpleSwapChangeHandler())
.popChangeHandler(new SimpleSwapChangeHandler()));
assertTrue(parent.isAttached());
assertTrue(child.isAttached());
ViewUtils.reportAttached(parent.getView(), false, true);
assertFalse(parent.isAttached());
assertFalse(child.isAttached());
ViewUtils.reportAttached(child.getView(), true);
assertFalse(parent.isAttached());
assertFalse(child.isAttached());
ViewUtils.reportAttached(parent.getView(), true);
assertTrue(parent.isAttached());
assertTrue(child.isAttached());
}
private MockChangeHandler getPushHandler(final CallState expectedCallState, final TestController controller) {
return new MockChangeHandler(new ChangeHandlerListener() {
return MockChangeHandler.listeningChangeHandler(new ChangeHandlerListener() {
@Override
void willStartChange() {
public void willStartChange() {
expectedCallState.contextAvailableCalls++;
expectedCallState.changeStartCalls++;
expectedCallState.createViewCalls++;
assertCalls(expectedCallState, controller);
}
@Override
void didAttachOrDetach() {
public void didAttachOrDetach() {
expectedCallState.attachCalls++;
assertCalls(expectedCallState, controller);
}
@Override
void didEndChange() {
public void didEndChange() {
expectedCallState.changeEndCalls++;
assertCalls(expectedCallState, controller);
}
@@ -472,23 +560,24 @@ public class ControllerLifecycleTests {
}
private MockChangeHandler getPopHandler(final CallState expectedCallState, final TestController controller) {
return new MockChangeHandler(new ChangeHandlerListener() {
return MockChangeHandler.listeningChangeHandler(new ChangeHandlerListener() {
@Override
void willStartChange() {
public void willStartChange() {
expectedCallState.changeStartCalls++;
assertCalls(expectedCallState, controller);
}
@Override
void didAttachOrDetach() {
public void didAttachOrDetach() {
expectedCallState.destroyViewCalls++;
expectedCallState.detachCalls++;
expectedCallState.contextUnavailableCalls++;
expectedCallState.destroyCalls++;
assertCalls(expectedCallState, controller);
}
@Override
void didEndChange() {
public void didEndChange() {
expectedCallState.changeEndCalls++;
assertCalls(expectedCallState, controller);
}
@@ -496,8 +585,8 @@ public class ControllerLifecycleTests {
}
private void assertCalls(CallState callState, TestController controller) {
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);
assertEquals("Expected call counts and controller call counts do not match.", callState, controller.currentCallState);
assertEquals("Expected call counts and lifecycle call counts do not match.", callState, currentCallState);
}
private void attachLifecycleListener(Controller controller) {
@@ -512,6 +601,16 @@ public class ControllerLifecycleTests {
currentCallState.changeEndCalls++;
}
@Override
public void postContextAvailable(@NonNull Controller controller, @NonNull Context context) {
currentCallState.contextAvailableCalls++;
}
@Override
public void postContextUnavailable(@NonNull Controller controller) {
currentCallState.contextUnavailableCalls++;
}
@Override
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
currentCallState.createViewCalls++;
@@ -7,14 +7,22 @@ import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller.RetainViewMode;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.CallState;
import com.bluelinelabs.conductor.util.TestController;
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.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ControllerTests {
@@ -38,29 +46,30 @@ public class ControllerTests {
@Test
public void testViewRetention() {
Controller controller = new TestController();
controller.setRouter(router);
// Test View getting released w/ RELEASE_DETACH
controller.setRetainViewMode(RetainViewMode.RELEASE_DETACH);
Assert.assertNull(controller.getView());
View view = controller.inflate(new AttachFakingFrameLayout(router.getActivity()));
Assert.assertNotNull(controller.getView());
assertNull(controller.getView());
View view = controller.inflate(router.container);
assertNotNull(controller.getView());
ViewUtils.reportAttached(view, true);
Assert.assertNotNull(controller.getView());
assertNotNull(controller.getView());
ViewUtils.reportAttached(view, false);
Assert.assertNull(controller.getView());
assertNull(controller.getView());
// Test View getting retained w/ RETAIN_DETACH
controller.setRetainViewMode(RetainViewMode.RETAIN_DETACH);
view = controller.inflate(new AttachFakingFrameLayout(router.getActivity()));
Assert.assertNotNull(controller.getView());
view = controller.inflate(router.container);
assertNotNull(controller.getView());
ViewUtils.reportAttached(view, true);
Assert.assertNotNull(controller.getView());
assertNotNull(controller.getView());
ViewUtils.reportAttached(view, false);
Assert.assertNotNull(controller.getView());
assertNotNull(controller.getView());
// Ensure re-setting RELEASE_DETACH releases
controller.setRetainViewMode(RetainViewMode.RELEASE_DETACH);
Assert.assertNull(controller.getView());
assertNull(controller.getView());
}
@Test
@@ -93,7 +102,7 @@ public class ControllerTests {
TestController child = new TestController();
router.pushController(RouterTransaction.with(parent));
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null)
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID))
.setRoot(RouterTransaction.with(child));
CallState childExpectedCallState = new CallState(true);
@@ -150,7 +159,7 @@ public class ControllerTests {
TestController child = new TestController();
router.pushController(RouterTransaction.with(parent));
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null)
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID))
.setRoot(RouterTransaction.with(child));
CallState childExpectedCallState = new CallState(true);
@@ -214,7 +223,7 @@ public class ControllerTests {
TestController child = new TestController();
router.pushController(RouterTransaction.with(parent));
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null)
parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID))
.setRoot(RouterTransaction.with(child));
CallState childExpectedCallState = new CallState(true);
@@ -262,48 +271,48 @@ public class ControllerTests {
router.pushController(RouterTransaction.with(parent));
Assert.assertEquals(0, parent.getChildRouters().size());
Assert.assertNull(child1.getParentController());
Assert.assertNull(child2.getParentController());
assertEquals(0, parent.getChildRouters().size());
assertNull(child1.getParentController());
assertNull(child2.getParentController());
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null);
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.setRoot(RouterTransaction.with(child1));
Assert.assertEquals(1, parent.getChildRouters().size());
Assert.assertEquals(childRouter, parent.getChildRouters().get(0));
Assert.assertEquals(1, childRouter.getBackstackSize());
Assert.assertEquals(child1, childRouter.getControllers().get(0));
Assert.assertEquals(parent, child1.getParentController());
Assert.assertNull(child2.getParentController());
assertEquals(1, parent.getChildRouters().size());
assertEquals(childRouter, parent.getChildRouters().get(0));
assertEquals(1, childRouter.getBackstackSize());
assertEquals(child1, childRouter.getControllers().get(0));
assertEquals(parent, child1.getParentController());
assertNull(child2.getParentController());
childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID), null);
childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(child2));
Assert.assertEquals(1, parent.getChildRouters().size());
Assert.assertEquals(childRouter, parent.getChildRouters().get(0));
Assert.assertEquals(2, childRouter.getBackstackSize());
Assert.assertEquals(child1, childRouter.getControllers().get(0));
Assert.assertEquals(child2, childRouter.getControllers().get(1));
Assert.assertEquals(parent, child1.getParentController());
Assert.assertEquals(parent, child2.getParentController());
assertEquals(1, parent.getChildRouters().size());
assertEquals(childRouter, parent.getChildRouters().get(0));
assertEquals(2, childRouter.getBackstackSize());
assertEquals(child1, childRouter.getControllers().get(0));
assertEquals(child2, childRouter.getControllers().get(1));
assertEquals(parent, child1.getParentController());
assertEquals(parent, child2.getParentController());
childRouter.popController(child2);
Assert.assertEquals(1, parent.getChildRouters().size());
Assert.assertEquals(childRouter, parent.getChildRouters().get(0));
Assert.assertEquals(1, childRouter.getBackstackSize());
Assert.assertEquals(child1, childRouter.getControllers().get(0));
Assert.assertEquals(parent, child1.getParentController());
Assert.assertNull(child2.getParentController());
assertEquals(1, parent.getChildRouters().size());
assertEquals(childRouter, parent.getChildRouters().get(0));
assertEquals(1, childRouter.getBackstackSize());
assertEquals(child1, childRouter.getControllers().get(0));
assertEquals(parent, child1.getParentController());
assertNull(child2.getParentController());
childRouter.popController(child1);
Assert.assertEquals(1, parent.getChildRouters().size());
Assert.assertEquals(childRouter, parent.getChildRouters().get(0));
Assert.assertEquals(0, childRouter.getBackstackSize());
Assert.assertNull(child1.getParentController());
Assert.assertNull(child2.getParentController());
assertEquals(1, parent.getChildRouters().size());
assertEquals(childRouter, parent.getChildRouters().get(0));
assertEquals(0, childRouter.getBackstackSize());
assertNull(child1.getParentController());
assertNull(child2.getParentController());
}
@Test
@@ -315,47 +324,89 @@ public class ControllerTests {
router.pushController(RouterTransaction.with(parent));
Assert.assertEquals(0, parent.getChildRouters().size());
Assert.assertNull(child1.getParentController());
Assert.assertNull(child2.getParentController());
assertEquals(0, parent.getChildRouters().size());
assertNull(child1.getParentController());
assertNull(child2.getParentController());
Router childRouter1 = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1), null);
Router childRouter2 = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_2), null);
Router childRouter1 = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1));
Router childRouter2 = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_2));
childRouter1.setRoot(RouterTransaction.with(child1));
childRouter2.setRoot(RouterTransaction.with(child2));
Assert.assertEquals(2, parent.getChildRouters().size());
Assert.assertEquals(childRouter1, parent.getChildRouters().get(0));
Assert.assertEquals(childRouter2, parent.getChildRouters().get(1));
Assert.assertEquals(1, childRouter1.getBackstackSize());
Assert.assertEquals(1, childRouter2.getBackstackSize());
Assert.assertEquals(child1, childRouter1.getControllers().get(0));
Assert.assertEquals(child2, childRouter2.getControllers().get(0));
Assert.assertEquals(parent, child1.getParentController());
Assert.assertEquals(parent, child2.getParentController());
assertEquals(2, parent.getChildRouters().size());
assertEquals(childRouter1, parent.getChildRouters().get(0));
assertEquals(childRouter2, parent.getChildRouters().get(1));
assertEquals(1, childRouter1.getBackstackSize());
assertEquals(1, childRouter2.getBackstackSize());
assertEquals(child1, childRouter1.getControllers().get(0));
assertEquals(child2, childRouter2.getControllers().get(0));
assertEquals(parent, child1.getParentController());
assertEquals(parent, child2.getParentController());
parent.removeChildRouter(childRouter2);
Assert.assertEquals(1, parent.getChildRouters().size());
Assert.assertEquals(childRouter1, parent.getChildRouters().get(0));
Assert.assertEquals(1, childRouter1.getBackstackSize());
Assert.assertEquals(0, childRouter2.getBackstackSize());
Assert.assertEquals(child1, childRouter1.getControllers().get(0));
Assert.assertEquals(parent, child1.getParentController());
Assert.assertNull(child2.getParentController());
assertEquals(1, parent.getChildRouters().size());
assertEquals(childRouter1, parent.getChildRouters().get(0));
assertEquals(1, childRouter1.getBackstackSize());
assertEquals(0, childRouter2.getBackstackSize());
assertEquals(child1, childRouter1.getControllers().get(0));
assertEquals(parent, child1.getParentController());
assertNull(child2.getParentController());
parent.removeChildRouter(childRouter1);
Assert.assertEquals(0, parent.getChildRouters().size());
Assert.assertEquals(0, childRouter1.getBackstackSize());
Assert.assertEquals(0, childRouter2.getBackstackSize());
Assert.assertNull(child1.getParentController());
Assert.assertNull(child2.getParentController());
assertEquals(0, parent.getChildRouters().size());
assertEquals(0, childRouter1.getBackstackSize());
assertEquals(0, childRouter2.getBackstackSize());
assertNull(child1.getParentController());
assertNull(child2.getParentController());
}
@Test
public void testRestoredChildRouterBackstack() {
TestController parent = new TestController();
router.pushController(RouterTransaction.with(parent));
ViewUtils.reportAttached(parent.getView(), true);
RouterTransaction childTransaction1 = RouterTransaction.with(new TestController());
RouterTransaction childTransaction2 = RouterTransaction.with(new TestController());
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1));
childRouter.setPopsLastView(true);
childRouter.setRoot(childTransaction1);
childRouter.pushController(childTransaction2);
Bundle savedState = new Bundle();
childRouter.saveInstanceState(savedState);
parent.removeChildRouter(childRouter);
childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1));
assertEquals(0, childRouter.getBackstackSize());
childRouter.restoreInstanceState(savedState);
childRouter.rebindIfNeeded();
assertEquals(2, childRouter.getBackstackSize());
RouterTransaction restoredChildTransaction1 = childRouter.getBackstack().get(0);
RouterTransaction restoredChildTransaction2 = childRouter.getBackstack().get(1);
assertEquals(childTransaction1.transactionIndex, restoredChildTransaction1.transactionIndex);
assertEquals(childTransaction1.controller.getInstanceId(), restoredChildTransaction1.controller.getInstanceId());
assertEquals(childTransaction2.transactionIndex, restoredChildTransaction2.transactionIndex);
assertEquals(childTransaction2.controller.getInstanceId(), restoredChildTransaction2.controller.getInstanceId());
assertTrue(parent.handleBack());
assertEquals(1, childRouter.getBackstackSize());
assertEquals(restoredChildTransaction1, childRouter.getBackstack().get(0));
assertTrue(parent.handleBack());
assertEquals(0, childRouter.getBackstackSize());
}
private void assertCalls(CallState callState, TestController controller) {
Assert.assertEquals("Expected call counts and controller call counts do not match.", callState, controller.currentCallState);
assertEquals("Expected call counts and controller call counts do not match.", callState, controller.currentCallState);
}
}
@@ -4,11 +4,12 @@ import android.os.Bundle;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler;
import junit.framework.Assert;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class ControllerTransactionTests {
@Test
@@ -22,10 +23,10 @@ public class ControllerTransactionTests {
RouterTransaction restoredTransaction = new RouterTransaction(bundle);
Assert.assertEquals(transaction.controller.getClass(), restoredTransaction.controller.getClass());
Assert.assertEquals(transaction.pushChangeHandler().getClass(), restoredTransaction.pushChangeHandler().getClass());
Assert.assertEquals(transaction.popChangeHandler().getClass(), restoredTransaction.popChangeHandler().getClass());
Assert.assertEquals(transaction.tag(), restoredTransaction.tag());
assertEquals(transaction.controller.getClass(), restoredTransaction.controller.getClass());
assertEquals(transaction.pushChangeHandler().getClass(), restoredTransaction.pushChangeHandler().getClass());
assertEquals(transaction.popChangeHandler().getClass(), restoredTransaction.popChangeHandler().getClass());
assertEquals(transaction.tag(), restoredTransaction.tag());
}
}
@@ -1,85 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
public class MockChangeHandler extends ControllerChangeHandler {
private static final String KEY_REMOVES_FROM_VIEW_ON_PUSH = "MockChangeHandler.removesFromViewOnPush";
static class ChangeHandlerListener {
void willStartChange() { }
void didAttachOrDetach() { }
void didEndChange() { }
}
final ChangeHandlerListener listener;
boolean removesFromViewOnPush;
public MockChangeHandler() {
this(true, null);
}
public MockChangeHandler(boolean removesViewOnPush) {
this(removesViewOnPush, null);
}
public MockChangeHandler(@NonNull ChangeHandlerListener listener) {
this(true, listener);
}
public MockChangeHandler(boolean removesFromViewOnPush, ChangeHandlerListener listener) {
this.removesFromViewOnPush = removesFromViewOnPush;
if (listener == null) {
this.listener = new ChangeHandlerListener() { };
} else {
this.listener = listener;
}
}
@Override
public void performChange(@NonNull ViewGroup container, View from, View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
listener.willStartChange();
if (isPush) {
container.addView(to);
listener.didAttachOrDetach();
if (removesFromViewOnPush && from != null) {
container.removeView(from);
}
} else {
container.removeView(from);
listener.didAttachOrDetach();
if (to != null) {
container.addView(to);
}
}
changeListener.onChangeCompleted();
listener.didEndChange();
}
@Override
public boolean removesFromViewOnPush() {
return removesFromViewOnPush;
}
@Override
public void saveToBundle(@NonNull Bundle bundle) {
super.saveToBundle(bundle);
bundle.putBoolean(KEY_REMOVES_FROM_VIEW_ON_PUSH, removesFromViewOnPush);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
super.restoreFromBundle(bundle);
removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_VIEW_ON_PUSH);
}
}
@@ -3,13 +3,19 @@ package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.view.ViewGroup;
import org.junit.Assert;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ReattachCaseTests {
@@ -36,71 +42,71 @@ public class ReattachCaseTests {
final TestController controllerB = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Assert.assertTrue(controllerA.isAttached());
Assert.assertFalse(controllerB.isAttached());
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
sleepWakeDevice();
Assert.assertTrue(controllerA.isAttached());
Assert.assertFalse(controllerB.isAttached());
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
activityProxy.rotate();
router.rebindIfNeeded();
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
}
@Test
public void testChildNeedsAttachOnPauseAndOrientation() {
final TestController controllerA = new TestController();
final TestController childController = new TestController();
final TestController controllerB = new TestController();
final Controller controllerA = new TestController();
final Controller childController = new TestController();
final Controller controllerB = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID), null);
Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Assert.assertTrue(controllerA.isAttached());
Assert.assertTrue(childController.isAttached());
Assert.assertFalse(controllerB.isAttached());
assertTrue(controllerA.isAttached());
assertTrue(childController.isAttached());
assertFalse(controllerB.isAttached());
sleepWakeDevice();
Assert.assertTrue(controllerA.isAttached());
Assert.assertTrue(childController.isAttached());
Assert.assertFalse(controllerB.isAttached());
assertTrue(controllerA.isAttached());
assertTrue(childController.isAttached());
assertFalse(controllerB.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Assert.assertFalse(controllerA.isAttached());
Assert.assertFalse(childController.isAttached());
Assert.assertTrue(controllerB.isAttached());
assertFalse(controllerA.isAttached());
assertFalse(childController.isAttached());
assertTrue(controllerB.isAttached());
activityProxy.rotate();
router.rebindIfNeeded();
Assert.assertFalse(controllerA.isAttached());
Assert.assertFalse(childController.isAttached());
Assert.assertTrue(childController.getNeedsAttach());
Assert.assertTrue(controllerB.isAttached());
assertFalse(controllerA.isAttached());
assertFalse(childController.isAttached());
assertTrue(childController.getNeedsAttach());
assertTrue(controllerB.isAttached());
}
@Test
@@ -110,45 +116,45 @@ public class ReattachCaseTests {
final TestController childController = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Assert.assertTrue(controllerA.isAttached());
Assert.assertFalse(controllerB.isAttached());
Assert.assertFalse(childController.isAttached());
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
assertFalse(childController.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Router childRouter = controllerB.getChildRouter((ViewGroup)controllerB.getView().findViewById(TestController.VIEW_ID), null);
Router childRouter = controllerB.getChildRouter((ViewGroup)controllerB.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertTrue(childController.isAttached());
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
activityProxy.rotate();
router.rebindIfNeeded();
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertTrue(childController.isAttached());
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
router.handleBack();
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertFalse(childController.isAttached());
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertFalse(childController.isAttached());
router.handleBack();
Assert.assertTrue(controllerA.isAttached());
Assert.assertFalse(controllerB.isAttached());
Assert.assertFalse(childController.isAttached());
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
assertFalse(childController.isAttached());
}
// Attempt to test https://github.com/bluelinelabs/Conductor/issues/86#issuecomment-231381271
@@ -159,71 +165,96 @@ public class ReattachCaseTests {
TestController childController = new TestController();
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Assert.assertTrue(controllerA.isAttached());
Assert.assertFalse(controllerB.isAttached());
Assert.assertFalse(childController.isAttached());
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
assertFalse(childController.isAttached());
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Router childRouter = controllerB.getChildRouter((ViewGroup)controllerB.getView().findViewById(TestController.VIEW_ID), null);
Router childRouter = controllerB.getChildRouter((ViewGroup)controllerB.getView().findViewById(TestController.VIEW_ID));
childRouter.setPopsLastView(true);
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertTrue(childController.isAttached());
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
router.handleBack();
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertFalse(childController.isAttached());
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertFalse(childController.isAttached());
childController = new TestController();
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertTrue(childController.isAttached());
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
activityProxy.rotate();
router.rebindIfNeeded();
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertTrue(childController.isAttached());
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
router.handleBack();
childController = new TestController();
childRouter.pushController(RouterTransaction.with(childController)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertTrue(childController.isAttached());
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertTrue(childController.isAttached());
router.handleBack();
Assert.assertFalse(controllerA.isAttached());
Assert.assertTrue(controllerB.isAttached());
Assert.assertFalse(childController.isAttached());
assertFalse(controllerA.isAttached());
assertTrue(controllerB.isAttached());
assertFalse(childController.isAttached());
router.handleBack();
Assert.assertTrue(controllerA.isAttached());
Assert.assertFalse(controllerB.isAttached());
Assert.assertFalse(childController.isAttached());
assertTrue(controllerA.isAttached());
assertFalse(controllerB.isAttached());
assertFalse(childController.isAttached());
}
// Attempt to test https://github.com/bluelinelabs/Conductor/issues/367
@Test
public void testViewIsAttachedAfterStartedActivityIsRecreated() {
Controller controller1 = new TestController();
Controller controller2 = new TestController();
router.setRoot(RouterTransaction.with(controller1));
assertTrue(controller1.isAttached());
// Lock screen
Bundle bundle = new Bundle();
activityProxy.pause().saveInstanceState(bundle).stop(false);
// Push a 2nd controller, which will rotate the screen once it unlocked
router.pushController(RouterTransaction.with(controller2));
assertTrue(controller2.isAttached());
assertTrue(controller2.getNeedsAttach());
// Unlock screen and rotate
activityProxy.start();
activityProxy.rotate();
assertTrue(controller2.isAttached());
}
private void sleepWakeDevice() {
@@ -0,0 +1,354 @@
package com.bluelinelabs.conductor;
import android.view.View;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class RouterChangeHandlerTests {
private Router router;
@Before
public void setup() {
ActivityProxy activityProxy = new ActivityProxy().create(null).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), null);
}
@Test
public void testSetRootHandler() {
MockChangeHandler handler = MockChangeHandler.taggedHandler("root", true);
TestController rootController = new TestController();
router.setRoot(RouterTransaction.with(rootController).pushChangeHandler(handler));
assertTrue(rootController.changeHandlerHistory.isValidHistory);
assertNull(rootController.changeHandlerHistory.latestFromView());
assertNotNull(rootController.changeHandlerHistory.latestToView());
assertEquals(rootController.getView(), rootController.changeHandlerHistory.latestToView());
assertTrue(rootController.changeHandlerHistory.latestIsPush());
assertEquals(handler.tag, rootController.changeHandlerHistory.latestChangeHandler().tag);
}
@Test
public void testPushPopHandlers() {
TestController rootController = new TestController();
router.setRoot(RouterTransaction.with(rootController).pushChangeHandler(MockChangeHandler.defaultHandler()));
View rootView = rootController.getView();
MockChangeHandler pushHandler = MockChangeHandler.taggedHandler("push", true);
MockChangeHandler popHandler = MockChangeHandler.taggedHandler("pop", true);
TestController pushController = new TestController();
router.pushController(RouterTransaction.with(pushController).pushChangeHandler(pushHandler).popChangeHandler(popHandler));
assertTrue(rootController.changeHandlerHistory.isValidHistory);
assertTrue(pushController.changeHandlerHistory.isValidHistory);
assertNotNull(pushController.changeHandlerHistory.latestFromView());
assertNotNull(pushController.changeHandlerHistory.latestToView());
assertEquals(rootView, pushController.changeHandlerHistory.latestFromView());
assertEquals(pushController.getView(), pushController.changeHandlerHistory.latestToView());
assertTrue(pushController.changeHandlerHistory.latestIsPush());
assertEquals(pushHandler.tag, pushController.changeHandlerHistory.latestChangeHandler().tag);
View pushView = pushController.getView();
router.popController(pushController);
assertNotNull(pushController.changeHandlerHistory.latestFromView());
assertNotNull(pushController.changeHandlerHistory.latestToView());
assertEquals(pushView, pushController.changeHandlerHistory.fromViewAt(1));
assertEquals(rootController.getView(), pushController.changeHandlerHistory.latestToView());
assertFalse(pushController.changeHandlerHistory.latestIsPush());
assertEquals(popHandler.tag, pushController.changeHandlerHistory.latestChangeHandler().tag);
}
@Test
public void testResetRootHandlers() {
TestController initialController1 = new TestController();
MockChangeHandler initialPushHandler1 = MockChangeHandler.taggedHandler("initialPush1", true);
MockChangeHandler initialPopHandler1 = MockChangeHandler.taggedHandler("initialPop1", true);
router.setRoot(RouterTransaction.with(initialController1).pushChangeHandler(initialPushHandler1).popChangeHandler(initialPopHandler1));
TestController initialController2 = new TestController();
MockChangeHandler initialPushHandler2 = MockChangeHandler.taggedHandler("initialPush2", false);
MockChangeHandler initialPopHandler2 = MockChangeHandler.taggedHandler("initialPop2", false);
router.pushController(RouterTransaction.with(initialController2).pushChangeHandler(initialPushHandler2).popChangeHandler(initialPopHandler2));
View initialView1 = initialController1.getView();
View initialView2 = initialController2.getView();
TestController newRootController = new TestController();
MockChangeHandler newRootHandler = MockChangeHandler.taggedHandler("newRootHandler", true);
router.setRoot(RouterTransaction.with(newRootController).pushChangeHandler(newRootHandler));
assertTrue(initialController1.changeHandlerHistory.isValidHistory);
assertTrue(initialController2.changeHandlerHistory.isValidHistory);
assertTrue(newRootController.changeHandlerHistory.isValidHistory);
assertEquals(3, initialController1.changeHandlerHistory.size());
assertEquals(2, initialController2.changeHandlerHistory.size());
assertEquals(1, newRootController.changeHandlerHistory.size());
assertNotNull(initialController1.changeHandlerHistory.latestToView());
assertEquals(newRootController.getView(), initialController1.changeHandlerHistory.latestToView());
assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView());
assertEquals(newRootHandler.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController1.changeHandlerHistory.latestIsPush());
assertNull(initialController2.changeHandlerHistory.latestToView());
assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView());
assertEquals(newRootHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController2.changeHandlerHistory.latestIsPush());
assertNotNull(newRootController.changeHandlerHistory.latestToView());
assertEquals(newRootController.getView(), newRootController.changeHandlerHistory.latestToView());
assertEquals(initialView1, newRootController.changeHandlerHistory.latestFromView());
assertEquals(newRootHandler.tag, newRootController.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(newRootController.changeHandlerHistory.latestIsPush());
}
@Test
public void testSetBackstackHandlers() {
TestController initialController1 = new TestController();
MockChangeHandler initialPushHandler1 = MockChangeHandler.taggedHandler("initialPush1", true);
MockChangeHandler initialPopHandler1 = MockChangeHandler.taggedHandler("initialPop1", true);
router.setRoot(RouterTransaction.with(initialController1).pushChangeHandler(initialPushHandler1).popChangeHandler(initialPopHandler1));
TestController initialController2 = new TestController();
MockChangeHandler initialPushHandler2 = MockChangeHandler.taggedHandler("initialPush2", false);
MockChangeHandler initialPopHandler2 = MockChangeHandler.taggedHandler("initialPop2", false);
router.pushController(RouterTransaction.with(initialController2).pushChangeHandler(initialPushHandler2).popChangeHandler(initialPopHandler2));
View initialView1 = initialController1.getView();
View initialView2 = initialController2.getView();
TestController newController1 = new TestController();
TestController newController2 = new TestController();
MockChangeHandler setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true);
List<RouterTransaction> newBackstack = Arrays.asList(
RouterTransaction.with(newController1),
RouterTransaction.with(newController2)
);
router.setBackstack(newBackstack, setBackstackHandler);
assertTrue(initialController1.changeHandlerHistory.isValidHistory);
assertTrue(initialController2.changeHandlerHistory.isValidHistory);
assertTrue(newController1.changeHandlerHistory.isValidHistory);
assertEquals(3, initialController1.changeHandlerHistory.size());
assertEquals(2, initialController2.changeHandlerHistory.size());
assertEquals(0, newController1.changeHandlerHistory.size());
assertEquals(1, newController2.changeHandlerHistory.size());
assertNotNull(initialController1.changeHandlerHistory.latestToView());
assertEquals(newController2.getView(), initialController1.changeHandlerHistory.latestToView());
assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController1.changeHandlerHistory.latestIsPush());
assertNull(initialController2.changeHandlerHistory.latestToView());
assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController2.changeHandlerHistory.latestIsPush());
assertNotNull(newController2.changeHandlerHistory.latestToView());
assertEquals(newController2.getView(), newController2.changeHandlerHistory.latestToView());
assertEquals(initialView1, newController2.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, newController2.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(newController2.changeHandlerHistory.latestIsPush());
}
@Test
public void testSetBackstackWithTwoVisibleHandlers() {
TestController initialController1 = new TestController();
MockChangeHandler initialPushHandler1 = MockChangeHandler.taggedHandler("initialPush1", true);
MockChangeHandler initialPopHandler1 = MockChangeHandler.taggedHandler("initialPop1", true);
router.setRoot(RouterTransaction.with(initialController1).pushChangeHandler(initialPushHandler1).popChangeHandler(initialPopHandler1));
TestController initialController2 = new TestController();
MockChangeHandler initialPushHandler2 = MockChangeHandler.taggedHandler("initialPush2", false);
MockChangeHandler initialPopHandler2 = MockChangeHandler.taggedHandler("initialPop2", false);
router.pushController(RouterTransaction.with(initialController2).pushChangeHandler(initialPushHandler2).popChangeHandler(initialPopHandler2));
View initialView1 = initialController1.getView();
View initialView2 = initialController2.getView();
TestController newController1 = new TestController();
TestController newController2 = new TestController();
MockChangeHandler setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true);
MockChangeHandler pushController2Handler = MockChangeHandler.noRemoveViewOnPushHandler("pushController2");
List<RouterTransaction> newBackstack = Arrays.asList(
RouterTransaction.with(newController1),
RouterTransaction.with(newController2).pushChangeHandler(pushController2Handler)
);
router.setBackstack(newBackstack, setBackstackHandler);
assertTrue(initialController1.changeHandlerHistory.isValidHistory);
assertTrue(initialController2.changeHandlerHistory.isValidHistory);
assertTrue(newController1.changeHandlerHistory.isValidHistory);
assertEquals(3, initialController1.changeHandlerHistory.size());
assertEquals(2, initialController2.changeHandlerHistory.size());
assertEquals(2, newController1.changeHandlerHistory.size());
assertEquals(1, newController2.changeHandlerHistory.size());
assertNotNull(initialController1.changeHandlerHistory.latestToView());
assertEquals(newController1.getView(), initialController1.changeHandlerHistory.latestToView());
assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController1.changeHandlerHistory.latestIsPush());
assertNull(initialController2.changeHandlerHistory.latestToView());
assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController2.changeHandlerHistory.latestIsPush());
assertNotNull(newController1.changeHandlerHistory.latestToView());
assertEquals(newController1.getView(), newController1.changeHandlerHistory.toViewAt(0));
assertEquals(newController2.getView(), newController1.changeHandlerHistory.latestToView());
assertEquals(initialView1, newController1.changeHandlerHistory.fromViewAt(0));
assertEquals(newController1.getView(), newController1.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, newController1.changeHandlerHistory.changeHandlerAt(0).tag);
assertEquals(pushController2Handler.tag, newController1.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(newController1.changeHandlerHistory.latestIsPush());
assertNotNull(newController2.changeHandlerHistory.latestToView());
assertEquals(newController2.getView(), newController2.changeHandlerHistory.latestToView());
assertEquals(newController1.getView(), newController2.changeHandlerHistory.latestFromView());
assertEquals(pushController2Handler.tag, newController2.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(newController2.changeHandlerHistory.latestIsPush());
}
@Test
public void testSetBackstackForPushHandlers() {
TestController initialController = new TestController();
MockChangeHandler initialPushHandler = MockChangeHandler.taggedHandler("initialPush1", true);
MockChangeHandler initialPopHandler = MockChangeHandler.taggedHandler("initialPop1", true);
RouterTransaction initialTransaction = RouterTransaction.with(initialController).pushChangeHandler(initialPushHandler).popChangeHandler(initialPopHandler);
router.setRoot(initialTransaction);
View initialView = initialController.getView();
TestController newController = new TestController();
MockChangeHandler setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true);
List<RouterTransaction> newBackstack = Arrays.asList(
initialTransaction,
RouterTransaction.with(newController)
);
router.setBackstack(newBackstack, setBackstackHandler);
assertTrue(initialController.changeHandlerHistory.isValidHistory);
assertTrue(newController.changeHandlerHistory.isValidHistory);
assertEquals(2, initialController.changeHandlerHistory.size());
assertEquals(1, newController.changeHandlerHistory.size());
assertNotNull(initialController.changeHandlerHistory.latestToView());
assertEquals(newController.getView(), initialController.changeHandlerHistory.latestToView());
assertEquals(initialView, initialController.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController.changeHandlerHistory.latestIsPush());
assertTrue(newController.changeHandlerHistory.latestIsPush());
}
@Test
public void testSetBackstackForInvertHandlersWithRemovesView() {
TestController initialController1 = new TestController();
MockChangeHandler initialPushHandler1 = MockChangeHandler.taggedHandler("initialPush1", true);
MockChangeHandler initialPopHandler1 = MockChangeHandler.taggedHandler("initialPop1", true);
RouterTransaction initialTransaction1 = RouterTransaction.with(initialController1).pushChangeHandler(initialPushHandler1).popChangeHandler(initialPopHandler1);
router.setRoot(initialTransaction1);
TestController initialController2 = new TestController();
MockChangeHandler initialPushHandler2 = MockChangeHandler.taggedHandler("initialPush2", true);
MockChangeHandler initialPopHandler2 = MockChangeHandler.taggedHandler("initialPop2", true);
RouterTransaction initialTransaction2 = RouterTransaction.with(initialController2).pushChangeHandler(initialPushHandler2).popChangeHandler(initialPopHandler2);
router.pushController(initialTransaction2);
View initialView2 = initialController2.getView();
MockChangeHandler setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true);
List<RouterTransaction> newBackstack = Arrays.asList(
initialTransaction2,
initialTransaction1
);
router.setBackstack(newBackstack, setBackstackHandler);
assertTrue(initialController1.changeHandlerHistory.isValidHistory);
assertTrue(initialController2.changeHandlerHistory.isValidHistory);
assertEquals(3, initialController1.changeHandlerHistory.size());
assertEquals(2, initialController2.changeHandlerHistory.size());
assertNotNull(initialController1.changeHandlerHistory.latestToView());
assertEquals(initialView2, initialController1.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag);
assertFalse(initialController1.changeHandlerHistory.latestIsPush());
assertNotNull(initialController2.changeHandlerHistory.latestToView());
assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag);
assertFalse(initialController2.changeHandlerHistory.latestIsPush());
}
@Test
public void testSetBackstackForInvertHandlersWithoutRemovesView() {
TestController initialController1 = new TestController();
MockChangeHandler initialPushHandler1 = MockChangeHandler.taggedHandler("initialPush1", true);
MockChangeHandler initialPopHandler1 = MockChangeHandler.taggedHandler("initialPop1", true);
RouterTransaction initialTransaction1 = RouterTransaction.with(initialController1).pushChangeHandler(initialPushHandler1).popChangeHandler(initialPopHandler1);
router.setRoot(initialTransaction1);
TestController initialController2 = new TestController();
MockChangeHandler initialPushHandler2 = MockChangeHandler.taggedHandler("initialPush2", false);
MockChangeHandler initialPopHandler2 = MockChangeHandler.taggedHandler("initialPop2", false);
RouterTransaction initialTransaction2 = RouterTransaction.with(initialController2).pushChangeHandler(initialPushHandler2).popChangeHandler(initialPopHandler2);
router.pushController(initialTransaction2);
View initialView1 = initialController1.getView();
View initialView2 = initialController2.getView();
MockChangeHandler setBackstackHandler = MockChangeHandler.taggedHandler("setBackstackHandler", true);
List<RouterTransaction> newBackstack = Arrays.asList(
initialTransaction2,
initialTransaction1
);
router.setBackstack(newBackstack, setBackstackHandler);
assertTrue(initialController1.changeHandlerHistory.isValidHistory);
assertTrue(initialController2.changeHandlerHistory.isValidHistory);
assertEquals(2, initialController1.changeHandlerHistory.size());
assertEquals(2, initialController2.changeHandlerHistory.size());
assertNotNull(initialController1.changeHandlerHistory.latestToView());
assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView());
assertEquals(initialPushHandler2.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag);
assertTrue(initialController1.changeHandlerHistory.latestIsPush());
assertNull(initialController2.changeHandlerHistory.latestToView());
assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView());
assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag);
assertFalse(initialController2.changeHandlerHistory.latestIsPush());
}
}
@@ -1,15 +1,30 @@
package com.bluelinelabs.conductor;
import org.junit.Assert;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class RouterTests {
@@ -28,13 +43,13 @@ public class RouterTests {
Controller rootController = new TestController();
Assert.assertFalse(router.hasRootController());
assertFalse(router.hasRootController());
router.setRoot(RouterTransaction.with(rootController).tag(rootTag));
Assert.assertTrue(router.hasRootController());
assertTrue(router.hasRootController());
Assert.assertEquals(rootController, router.getControllerWithTag(rootTag));
assertEquals(rootController, router.getControllerWithTag(rootTag));
}
@Test
@@ -48,8 +63,8 @@ public class RouterTests {
router.setRoot(RouterTransaction.with(oldRootController).tag(oldRootTag));
router.setRoot(RouterTransaction.with(newRootController).tag(newRootTag));
Assert.assertNull(router.getControllerWithTag(oldRootTag));
Assert.assertEquals(newRootController, router.getControllerWithTag(newRootTag));
assertNull(router.getControllerWithTag(oldRootTag));
assertEquals(newRootController, router.getControllerWithTag(newRootTag));
}
@Test
@@ -58,8 +73,8 @@ public class RouterTests {
router.pushController(RouterTransaction.with(controller));
Assert.assertEquals(controller, router.getControllerWithInstanceId(controller.getInstanceId()));
Assert.assertNull(router.getControllerWithInstanceId("fake id"));
assertEquals(controller, router.getControllerWithInstanceId(controller.getInstanceId()));
assertNull(router.getControllerWithInstanceId("fake id"));
}
@Test
@@ -76,8 +91,8 @@ public class RouterTests {
router.pushController(RouterTransaction.with(controller2)
.tag(controller2Tag));
Assert.assertEquals(controller1, router.getControllerWithTag(controller1Tag));
Assert.assertEquals(controller2, router.getControllerWithTag(controller2Tag));
assertEquals(controller1, router.getControllerWithTag(controller1Tag));
assertEquals(controller2, router.getControllerWithTag(controller2Tag));
}
@Test
@@ -91,26 +106,51 @@ public class RouterTests {
router.pushController(RouterTransaction.with(controller1)
.tag(controller1Tag));
Assert.assertEquals(1, router.getBackstackSize());
assertEquals(1, router.getBackstackSize());
router.pushController(RouterTransaction.with(controller2)
.tag(controller2Tag));
Assert.assertEquals(2, router.getBackstackSize());
assertEquals(2, router.getBackstackSize());
router.popCurrentController();
Assert.assertEquals(1, router.getBackstackSize());
assertEquals(1, router.getBackstackSize());
Assert.assertEquals(controller1, router.getControllerWithTag(controller1Tag));
Assert.assertNull(router.getControllerWithTag(controller2Tag));
assertEquals(controller1, router.getControllerWithTag(controller1Tag));
assertNull(router.getControllerWithTag(controller2Tag));
router.popCurrentController();
Assert.assertEquals(0, router.getBackstackSize());
assertEquals(0, router.getBackstackSize());
Assert.assertNull(router.getControllerWithTag(controller1Tag));
Assert.assertNull(router.getControllerWithTag(controller2Tag));
assertNull(router.getControllerWithTag(controller1Tag));
assertNull(router.getControllerWithTag(controller2Tag));
}
@Test
public void testPopControllerConcurrentModificationException() {
int step = 1;
for (int i = 0; i < 10; i++, step++) {
router.pushController(RouterTransaction.with(new TestController()).tag("1"));
router.pushController(RouterTransaction.with(new TestController()).tag("2"));
router.pushController(RouterTransaction.with(new TestController()).tag("3"));
String tag;
if (step == 1) {
tag = "1";
} else if (step == 2) {
tag = "2";
} else {
tag = "3";
step = 0;
}
Controller controller = router.getControllerWithTag(tag);
if (controller != null) {
router.popController(controller);
}
router.popToRoot();
}
}
@Test
@@ -139,11 +179,11 @@ public class RouterTests {
router.popToTag(controller2Tag);
Assert.assertEquals(2, router.getBackstackSize());
Assert.assertEquals(controller1, router.getControllerWithTag(controller1Tag));
Assert.assertEquals(controller2, router.getControllerWithTag(controller2Tag));
Assert.assertNull(router.getControllerWithTag(controller3Tag));
Assert.assertNull(router.getControllerWithTag(controller4Tag));
assertEquals(2, router.getBackstackSize());
assertEquals(controller1, router.getControllerWithTag(controller1Tag));
assertEquals(controller2, router.getControllerWithTag(controller2Tag));
assertNull(router.getControllerWithTag(controller3Tag));
assertNull(router.getControllerWithTag(controller4Tag));
}
@Test
@@ -167,10 +207,10 @@ public class RouterTests {
router.popController(controller2);
Assert.assertEquals(2, router.getBackstackSize());
Assert.assertEquals(controller1, router.getControllerWithTag(controller1Tag));
Assert.assertNull(router.getControllerWithTag(controller2Tag));
Assert.assertEquals(controller3, router.getControllerWithTag(controller3Tag));
assertEquals(2, router.getBackstackSize());
assertEquals(controller1, router.getControllerWithTag(controller1Tag));
assertNull(router.getControllerWithTag(controller2Tag));
assertEquals(controller3, router.getControllerWithTag(controller3Tag));
}
@Test
@@ -179,81 +219,115 @@ public class RouterTests {
RouterTransaction middleTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = new ArrayList<>();
backstack.add(rootTransaction);
backstack.add(middleTransaction);
backstack.add(topTransaction);
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, middleTransaction, topTransaction);
router.setBackstack(backstack, null);
Assert.assertEquals(3, router.getBackstackSize());
assertEquals(3, router.getBackstackSize());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
Assert.assertEquals(rootTransaction, fetchedBackstack.get(0));
Assert.assertEquals(middleTransaction, fetchedBackstack.get(1));
Assert.assertEquals(topTransaction, fetchedBackstack.get(2));
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(middleTransaction, fetchedBackstack.get(1));
assertEquals(topTransaction, fetchedBackstack.get(2));
}
@Test
public void testNewSetBackstack() {
router.setRoot(RouterTransaction.with(new TestController()));
Assert.assertEquals(1, router.getBackstackSize());
assertEquals(1, router.getBackstackSize());
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction middleTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = new ArrayList<>();
backstack.add(rootTransaction);
backstack.add(middleTransaction);
backstack.add(topTransaction);
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, middleTransaction, topTransaction);
router.setBackstack(backstack, null);
Assert.assertEquals(3, router.getBackstackSize());
assertEquals(3, router.getBackstackSize());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
Assert.assertEquals(rootTransaction, fetchedBackstack.get(0));
Assert.assertEquals(middleTransaction, fetchedBackstack.get(1));
Assert.assertEquals(topTransaction, fetchedBackstack.get(2));
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(middleTransaction, fetchedBackstack.get(1));
assertEquals(topTransaction, fetchedBackstack.get(2));
assertEquals(router, rootTransaction.controller.getRouter());
assertEquals(router, middleTransaction.controller.getRouter());
assertEquals(router, topTransaction.controller.getRouter());
}
@Test
public void testNewSetBackstackWithNoRemoveViewOnPush() {
RouterTransaction oldRootTransaction = RouterTransaction.with(new TestController());
RouterTransaction oldTopTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false));
RouterTransaction oldTopTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
router.setRoot(oldRootTransaction);
router.pushController(oldTopTransaction);
Assert.assertEquals(2, router.getBackstackSize());
assertEquals(2, router.getBackstackSize());
Assert.assertTrue(oldRootTransaction.controller.isAttached());
Assert.assertTrue(oldTopTransaction.controller.isAttached());
assertTrue(oldRootTransaction.controller.isAttached());
assertTrue(oldTopTransaction.controller.isAttached());
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction middleTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false));
RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false));
List<RouterTransaction> backstack = new ArrayList<>();
backstack.add(rootTransaction);
backstack.add(middleTransaction);
backstack.add(topTransaction);
RouterTransaction middleTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, middleTransaction, topTransaction);
router.setBackstack(backstack, null);
Assert.assertEquals(3, router.getBackstackSize());
assertEquals(3, router.getBackstackSize());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
Assert.assertEquals(rootTransaction, fetchedBackstack.get(0));
Assert.assertEquals(middleTransaction, fetchedBackstack.get(1));
Assert.assertEquals(topTransaction, fetchedBackstack.get(2));
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(middleTransaction, fetchedBackstack.get(1));
assertEquals(topTransaction, fetchedBackstack.get(2));
Assert.assertFalse(oldRootTransaction.controller.isAttached());
Assert.assertFalse(oldTopTransaction.controller.isAttached());
Assert.assertTrue(rootTransaction.controller.isAttached());
Assert.assertTrue(middleTransaction.controller.isAttached());
Assert.assertTrue(topTransaction.controller.isAttached());
assertFalse(oldRootTransaction.controller.isAttached());
assertFalse(oldTopTransaction.controller.isAttached());
assertTrue(rootTransaction.controller.isAttached());
assertTrue(middleTransaction.controller.isAttached());
assertTrue(topTransaction.controller.isAttached());
}
@Test
public void testPopToRoot() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, transaction1, transaction2);
router.setBackstack(backstack, null);
assertEquals(3, router.getBackstackSize());
router.popToRoot();
assertEquals(1, router.getBackstackSize());
assertEquals(rootTransaction, router.getBackstack().get(0));
assertTrue(rootTransaction.controller.isAttached());
assertFalse(transaction1.controller.isAttached());
assertFalse(transaction2.controller.isAttached());
}
@Test
public void testPopToRootWithNoRemoveViewOnPush() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new HorizontalChangeHandler(false));
RouterTransaction transaction1 = RouterTransaction.with(new TestController()).pushChangeHandler(new HorizontalChangeHandler(false));
RouterTransaction transaction2 = RouterTransaction.with(new TestController()).pushChangeHandler(new HorizontalChangeHandler(false));
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, transaction1, transaction2);
router.setBackstack(backstack, null);
assertEquals(3, router.getBackstackSize());
router.popToRoot();
assertEquals(1, router.getBackstackSize());
assertEquals(rootTransaction, router.getBackstack().get(0));
assertTrue(rootTransaction.controller.isAttached());
assertFalse(transaction1.controller.isAttached());
assertFalse(transaction2.controller.isAttached());
}
@Test
@@ -261,61 +335,161 @@ public class RouterTests {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = new ArrayList<>();
backstack.add(rootTransaction);
backstack.add(topTransaction);
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, topTransaction);
router.setBackstack(backstack, null);
Assert.assertEquals(2, router.getBackstackSize());
assertEquals(2, router.getBackstackSize());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
Assert.assertEquals(rootTransaction, fetchedBackstack.get(0));
Assert.assertEquals(topTransaction, fetchedBackstack.get(1));
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(topTransaction, fetchedBackstack.get(1));
RouterTransaction newTopTransaction = RouterTransaction.with(new TestController());
router.replaceTopController(newTopTransaction);
Assert.assertEquals(2, router.getBackstackSize());
assertEquals(2, router.getBackstackSize());
fetchedBackstack = router.getBackstack();
Assert.assertEquals(rootTransaction, fetchedBackstack.get(0));
Assert.assertEquals(newTopTransaction, fetchedBackstack.get(1));
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(newTopTransaction, fetchedBackstack.get(1));
}
@Test
public void testReplaceTopControllerWithNoRemoveViewOnPush() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false));
List<RouterTransaction> backstack = new ArrayList<>();
backstack.add(rootTransaction);
backstack.add(topTransaction);
RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
List<RouterTransaction> backstack = Arrays.asList(rootTransaction, topTransaction);
router.setBackstack(backstack, null);
Assert.assertEquals(2, router.getBackstackSize());
assertEquals(2, router.getBackstackSize());
Assert.assertTrue(rootTransaction.controller.isAttached());
Assert.assertTrue(topTransaction.controller.isAttached());
assertTrue(rootTransaction.controller.isAttached());
assertTrue(topTransaction.controller.isAttached());
List<RouterTransaction> fetchedBackstack = router.getBackstack();
Assert.assertEquals(rootTransaction, fetchedBackstack.get(0));
Assert.assertEquals(topTransaction, fetchedBackstack.get(1));
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(topTransaction, fetchedBackstack.get(1));
RouterTransaction newTopTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(new MockChangeHandler(false));
RouterTransaction newTopTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
router.replaceTopController(newTopTransaction);
newTopTransaction.pushChangeHandler().completeImmediately();
Assert.assertEquals(2, router.getBackstackSize());
assertEquals(2, router.getBackstackSize());
fetchedBackstack = router.getBackstack();
Assert.assertEquals(rootTransaction, fetchedBackstack.get(0));
Assert.assertEquals(newTopTransaction, fetchedBackstack.get(1));
assertEquals(rootTransaction, fetchedBackstack.get(0));
assertEquals(newTopTransaction, fetchedBackstack.get(1));
Assert.assertTrue(rootTransaction.controller.isAttached());
Assert.assertFalse(topTransaction.controller.isAttached());
Assert.assertTrue(newTopTransaction.controller.isAttached());
assertTrue(rootTransaction.controller.isAttached());
assertFalse(topTransaction.controller.isAttached());
assertTrue(newTopTransaction.controller.isAttached());
}
@Test
public void testRearrangeTransactionBackstack() {
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = Arrays.asList(transaction1, transaction2);
router.setBackstack(backstack, null);
assertEquals(1, transaction1.transactionIndex);
assertEquals(2, transaction2.transactionIndex);
backstack = Arrays.asList(transaction2, transaction1);
router.setBackstack(backstack, null);
assertEquals(1, transaction2.transactionIndex);
assertEquals(2, transaction1.transactionIndex);
router.handleBack();
assertEquals(1, router.getBackstackSize());
assertEquals(transaction2, router.getBackstack().get(0));
router.handleBack();
assertEquals(0, router.getBackstackSize());
}
@Test
public void testChildRouterRearrangeTransactionBackstack() {
Controller parent = new TestController();
router.setRoot(RouterTransaction.with(parent));
Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.CHILD_VIEW_ID_1));
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
List<RouterTransaction> backstack = Arrays.asList(transaction1, transaction2);
childRouter.setBackstack(backstack, null);
assertEquals(2, transaction1.transactionIndex);
assertEquals(3, transaction2.transactionIndex);
backstack = Arrays.asList(transaction2, transaction1);
childRouter.setBackstack(backstack, null);
assertEquals(2, transaction2.transactionIndex);
assertEquals(3, transaction1.transactionIndex);
childRouter.handleBack();
assertEquals(1, childRouter.getBackstackSize());
assertEquals(transaction2, childRouter.getBackstack().get(0));
childRouter.handleBack();
assertEquals(0, childRouter.getBackstackSize());
}
@Test
public void testRemovesAllViewsOnDestroy() {
Controller controller1 = new TestController();
Controller controller2 = new TestController();
router.setRoot(RouterTransaction.with(controller1));
router.pushController(RouterTransaction.with(controller2)
.pushChangeHandler(new FadeChangeHandler(false)));
assertEquals(2, router.container.getChildCount());
router.destroy(true);
assertEquals(0, router.container.getChildCount());
}
@Test
public void testIsBeingDestroyed() {
final LifecycleListener lifecycleListener = new LifecycleListener() {
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
assertTrue(controller.isBeingDestroyed());
}
};
Controller controller1 = new TestController();
Controller controller2 = new TestController();
controller2.addLifecycleListener(lifecycleListener);
router.setRoot(RouterTransaction.with(controller1));
router.pushController(RouterTransaction.with(controller2));
assertFalse(controller1.isBeingDestroyed());
assertFalse(controller2.isBeingDestroyed());
router.popCurrentController();
assertFalse(controller1.isBeingDestroyed());
assertTrue(controller2.isBeingDestroyed());
Controller controller3 = new TestController();
controller3.addLifecycleListener(lifecycleListener);
router.pushController(RouterTransaction.with(controller3));
assertFalse(controller1.isBeingDestroyed());
assertFalse(controller3.isBeingDestroyed());
router.popToRoot();
assertFalse(controller1.isBeingDestroyed());
assertTrue(controller3.isBeingDestroyed());
}
}
@@ -3,13 +3,19 @@ package com.bluelinelabs.conductor;
import android.os.Bundle;
import android.view.ViewGroup;
import org.junit.Assert;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.MockChangeHandler;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class TargetControllerTests {
@@ -34,21 +40,21 @@ public class TargetControllerTests {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
Assert.assertNull(controllerA.getTargetController());
Assert.assertNull(controllerB.getTargetController());
assertNull(controllerA.getTargetController());
assertNull(controllerB.getTargetController());
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
controllerB.setTargetController(controllerA);
router.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Assert.assertNull(controllerA.getTargetController());
Assert.assertEquals(controllerA, controllerB.getTargetController());
assertNull(controllerA.getTargetController());
assertEquals(controllerA, controllerB.getTargetController());
}
@Test
@@ -56,22 +62,22 @@ public class TargetControllerTests {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
Assert.assertNull(controllerA.getTargetController());
Assert.assertNull(controllerB.getTargetController());
assertNull(controllerA.getTargetController());
assertNull(controllerB.getTargetController());
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
controllerB.setTargetController(controllerA);
Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID), null);
Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Assert.assertNull(controllerA.getTargetController());
Assert.assertEquals(controllerA, controllerB.getTargetController());
assertNull(controllerA.getTargetController());
assertEquals(controllerA, controllerB.getTargetController());
}
@Test
@@ -79,22 +85,22 @@ public class TargetControllerTests {
final TestController controllerA = new TestController();
final TestController controllerB = new TestController();
Assert.assertNull(controllerA.getTargetController());
Assert.assertNull(controllerB.getTargetController());
assertNull(controllerA.getTargetController());
assertNull(controllerB.getTargetController());
router.pushController(RouterTransaction.with(controllerA)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
controllerA.setTargetController(controllerB);
Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID), null);
Router childRouter = controllerA.getChildRouter((ViewGroup)controllerA.getView().findViewById(TestController.VIEW_ID));
childRouter.pushController(RouterTransaction.with(controllerB)
.pushChangeHandler(new MockChangeHandler())
.popChangeHandler(new MockChangeHandler()));
.pushChangeHandler(MockChangeHandler.defaultHandler())
.popChangeHandler(MockChangeHandler.defaultHandler()));
Assert.assertNull(controllerB.getTargetController());
Assert.assertEquals(controllerB, controllerA.getTargetController());
assertNull(controllerB.getTargetController());
assertEquals(controllerB, controllerA.getTargetController());
}
}
@@ -0,0 +1,236 @@
package com.bluelinelabs.conductor;
import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import com.bluelinelabs.conductor.internal.ViewAttachHandler;
import com.bluelinelabs.conductor.internal.ViewAttachHandler.ViewAttachListener;
import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.ViewUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ViewAttachHandlerTests {
private Activity activity;
private ViewAttachHandler viewAttachHandler;
private CountingViewAttachListener viewAttachListener;
@Before
public void setup() {
activity = new ActivityProxy().create(null).getActivity();
viewAttachListener = new CountingViewAttachListener();
viewAttachHandler = new ViewAttachHandler(viewAttachListener);
}
@Test
public void testSimpleViewAttachDetach() {
View view = new View(activity);
viewAttachHandler.listenForAttach(view);
assertEquals(0, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(2, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStopped();
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStarted();
assertEquals(3, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
}
@Test
public void testSimpleViewGroupAttachDetach() {
View view = new View(activity);
viewAttachHandler.listenForAttach(view);
assertEquals(0, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(2, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStopped();
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStarted();
assertEquals(3, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
}
@Test
public void testNestedViewGroupAttachDetach() {
ViewGroup view = new LinearLayout(activity);
View child = new LinearLayout(activity);
view.addView(child);
viewAttachHandler.listenForAttach(view);
assertEquals(0, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true, false);
assertEquals(0, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(child, true, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true, false);
ViewUtils.reportAttached(child, true, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(0, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true, false);
assertEquals(1, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(child, true, false);
assertEquals(2, viewAttachListener.attaches);
assertEquals(1, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStopped();
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(0, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, false, false);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
ViewUtils.reportAttached(view, true, false);
ViewUtils.reportAttached(child, true, false);
assertEquals(2, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
viewAttachHandler.onActivityStarted();
assertEquals(3, viewAttachListener.attaches);
assertEquals(2, viewAttachListener.detaches);
assertEquals(1, viewAttachListener.detachAfterStops);
}
private static class CountingViewAttachListener implements ViewAttachListener {
int attaches;
int detaches;
int detachAfterStops;
@Override
public void onAttached() {
attaches++;
}
@Override
public void onDetached(boolean fromActivityStop) {
detaches++;
}
@Override
public void onViewDetachAfterStop() {
detachAfterStops++;
}
}
}
@@ -0,0 +1,132 @@
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.util.ActivityProxy;
import com.bluelinelabs.conductor.util.TestController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ViewLeakTests {
private ActivityProxy activityProxy;
private Router router;
public void createActivityController(Bundle savedInstanceState) {
activityProxy = new ActivityProxy().create(savedInstanceState).start().resume();
router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new TestController()));
}
}
@Before
public void setup() {
createActivityController(null);
}
@Test
public void testPop() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller));
assertNotNull(controller.getView());
router.popCurrentController();
assertNull(controller.getView());
}
@Test
public void testPopWhenPushNeverAdded() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverAddChangeHandler()));
assertNotNull(controller.getView());
router.popCurrentController();
assertNull(controller.getView());
}
@Test
public void testPopWhenPushNeverCompleted() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverCompleteChangeHandler()));
assertNotNull(controller.getView());
router.popCurrentController();
assertNull(controller.getView());
}
@Test
public void testActivityStop() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller));
assertNotNull(controller.getView());
activityProxy.stop(true);
assertNull(controller.getView());
}
@Test
public void testActivityStopWhenPushNeverCompleted() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverCompleteChangeHandler()));
assertNotNull(controller.getView());
activityProxy.stop(true);
assertNull(controller.getView());
}
@Test
public void testActivityDestroyWhenPushNeverAdded() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverAddChangeHandler()));
assertNotNull(controller.getView());
activityProxy.stop(true).destroy();
assertNull(controller.getView());
}
public static class NeverAddChangeHandler extends ControllerChangeHandler {
@Override
public void performChange(@NonNull final ViewGroup container, @Nullable View from, @Nullable final View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
if (from != null) {
container.removeView(from);
}
}
}
public static class NeverCompleteChangeHandler extends ControllerChangeHandler {
@Override
public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
if (from != null) {
container.removeView(from);
}
container.addView(to);
}
}
}
@@ -1,10 +1,10 @@
package com.bluelinelabs.conductor;
package com.bluelinelabs.conductor.util;
import android.os.Bundle;
import android.support.annotation.IdRes;
import org.robolectric.Robolectric;
import org.robolectric.util.ActivityController;
import org.robolectric.android.controller.ActivityController;
public class ActivityProxy {
@@ -45,14 +45,19 @@ public class ActivityProxy {
return this;
}
public ActivityProxy stop() {
public ActivityProxy stop(boolean detachView) {
activityController.stop();
view.setAttached(false);
if (detachView) {
view.setAttached(false);
}
return this;
}
public ActivityProxy destroy() {
activityController.destroy();
view.setAttached(false);
return this;
}
@@ -1,4 +1,4 @@
package com.bluelinelabs.conductor;
package com.bluelinelabs.conductor.util;
import android.content.Context;
import android.os.IBinder;
@@ -1,4 +1,4 @@
package com.bluelinelabs.conductor;
package com.bluelinelabs.conductor.util;
import android.os.Parcel;
import android.os.Parcelable;
@@ -19,10 +19,8 @@ public class CallState implements Parcelable {
public int onActivityResultCalls;
public int onRequestPermissionsResultCalls;
public int createOptionsMenuCalls;
public CallState() {
this(false);
}
public int contextAvailableCalls;
public int contextUnavailableCalls;
public CallState(boolean setupForAddedController) {
if (setupForAddedController) {
@@ -30,6 +28,7 @@ public class CallState implements Parcelable {
changeEndCalls++;
createViewCalls++;
attachCalls++;
contextAvailableCalls++;
}
}
@@ -42,8 +41,14 @@ public class CallState implements Parcelable {
return false;
}
CallState callState = (CallState)o;
CallState callState = (CallState) o;
if (contextAvailableCalls != callState.contextAvailableCalls) {
return false;
}
if (contextUnavailableCalls != callState.contextUnavailableCalls) {
return false;
}
if (changeStartCalls != callState.changeStartCalls) {
return false;
}
@@ -102,6 +107,8 @@ public class CallState implements Parcelable {
result = 31 * result + onActivityResultCalls;
result = 31 * result + onRequestPermissionsResultCalls;
result = 31 * result + createOptionsMenuCalls;
result = 31 * result + contextAvailableCalls;
result = 31 * result + contextUnavailableCalls;
return result;
}
@@ -122,6 +129,8 @@ public class CallState implements Parcelable {
"\n onActivityResultCalls=" + onActivityResultCalls +
"\n onRequestPermissionsResultCalls=" + onRequestPermissionsResultCalls +
"\n createOptionsMenuCalls=" + createOptionsMenuCalls +
"\n contextAvailableCalls=" + contextAvailableCalls +
"\n contextUnavailableCalls=" + contextUnavailableCalls +
"}\n";
}
@@ -144,11 +153,13 @@ public class CallState implements Parcelable {
out.writeInt(onActivityResultCalls);
out.writeInt(onRequestPermissionsResultCalls);
out.writeInt(createOptionsMenuCalls);
out.writeInt(contextAvailableCalls);
out.writeInt(contextUnavailableCalls);
}
public static final Parcelable.Creator<CallState> CREATOR = new Parcelable.Creator<CallState>() {
public static final Parcelable.Creator<CallState> CREATOR = new Parcelable.Creator<CallState>() {
public CallState createFromParcel(Parcel in) {
CallState state = new CallState();
CallState state = new CallState(false);
state.changeStartCalls = in.readInt();
state.changeEndCalls = in.readInt();
@@ -164,6 +175,8 @@ public class CallState implements Parcelable {
state.onActivityResultCalls = in.readInt();
state.onRequestPermissionsResultCalls = in.readInt();
state.createOptionsMenuCalls = in.readInt();
state.contextAvailableCalls = in.readInt();
state.contextUnavailableCalls = in.readInt();
return state;
}
@@ -172,4 +185,4 @@ public class CallState implements Parcelable {
return new CallState[size];
}
};
}
}
@@ -0,0 +1,67 @@
package com.bluelinelabs.conductor.util;
import android.view.View;
import java.util.ArrayList;
import java.util.List;
public class ChangeHandlerHistory {
private List<Entry> entries = new ArrayList<>();
public boolean isValidHistory = true;
public void addEntry(View from, View to, boolean isPush, MockChangeHandler handler) {
entries.add(new Entry(from, to, isPush, handler));
}
public int size() {
return entries.size();
}
public View fromViewAt(int index) {
return entries.get(index).from;
}
public View toViewAt(int index) {
return entries.get(index).to;
}
public boolean isPushAt(int index) {
return entries.get(index).isPush;
}
public MockChangeHandler changeHandlerAt(int index) {
return entries.get(index).changeHandler;
}
public View latestFromView() {
return fromViewAt(size() - 1);
}
public View latestToView() {
return toViewAt(size() - 1);
}
public boolean latestIsPush() {
return isPushAt(size() - 1);
}
public MockChangeHandler latestChangeHandler() {
return changeHandlerAt(size() - 1);
}
private static class Entry {
final View from;
final View to;
final boolean isPush;
final MockChangeHandler changeHandler;
Entry(View from, View to, boolean isPush, MockChangeHandler changeHandler) {
this.from = from;
this.to = to;
this.isPush = isPush;
this.changeHandler = changeHandler;
}
}
}
@@ -0,0 +1,123 @@
package com.bluelinelabs.conductor.util;
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.ControllerChangeHandler;
public class MockChangeHandler extends ControllerChangeHandler {
private static final String KEY_REMOVES_FROM_VIEW_ON_PUSH = "MockChangeHandler.removesFromViewOnPush";
private static final String KEY_TAG = "MockChangeHandler.tag";
public static class ChangeHandlerListener {
public void willStartChange() { }
public void didAttachOrDetach() { }
public void didEndChange() { }
}
private final ChangeHandlerListener listener;
private boolean removesFromViewOnPush;
public View from;
public View to;
public String tag;
public static MockChangeHandler defaultHandler() {
return new MockChangeHandler(true, null, null);
}
public static MockChangeHandler noRemoveViewOnPushHandler() {
return new MockChangeHandler(false, null, null);
}
public static MockChangeHandler noRemoveViewOnPushHandler(String tag) {
return new MockChangeHandler(false, tag, null);
}
public static MockChangeHandler listeningChangeHandler(@NonNull ChangeHandlerListener listener) {
return new MockChangeHandler(true, null, listener);
}
public static MockChangeHandler taggedHandler(String tag, boolean removeViewOnPush) {
return new MockChangeHandler(removeViewOnPush, tag, null);
}
public MockChangeHandler() {
listener = null;
}
private MockChangeHandler(boolean removesFromViewOnPush, String tag, ChangeHandlerListener listener) {
this.removesFromViewOnPush = removesFromViewOnPush;
this.tag = tag;
if (listener == null) {
this.listener = new ChangeHandlerListener() { };
} else {
this.listener = listener;
}
}
@Override
public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
this.from = from;
this.to = to;
listener.willStartChange();
if (isPush) {
if (to != null) {
container.addView(to);
listener.didAttachOrDetach();
}
if (removesFromViewOnPush && from != null) {
container.removeView(from);
}
} else {
container.removeView(from);
listener.didAttachOrDetach();
if (to != null) {
container.addView(to);
}
}
changeListener.onChangeCompleted();
listener.didEndChange();
}
@Override
public boolean removesFromViewOnPush() {
return removesFromViewOnPush;
}
@Override
public void saveToBundle(@NonNull Bundle bundle) {
super.saveToBundle(bundle);
bundle.putBoolean(KEY_REMOVES_FROM_VIEW_ON_PUSH, removesFromViewOnPush);
bundle.putString(KEY_TAG, tag);
}
@Override
public void restoreFromBundle(@NonNull Bundle bundle) {
super.restoreFromBundle(bundle);
removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_VIEW_ON_PUSH);
tag = bundle.getString(KEY_TAG);
}
@NonNull
@Override
public ControllerChangeHandler copy() {
return new MockChangeHandler(removesFromViewOnPush, tag, listener);
}
@Override
public boolean isReusable() {
return true;
}
}
@@ -1,4 +1,4 @@
package com.bluelinelabs.conductor;
package com.bluelinelabs.conductor.util;
import android.app.Activity;
@@ -1,5 +1,6 @@
package com.bluelinelabs.conductor;
package com.bluelinelabs.conductor.util;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.IdRes;
@@ -11,6 +12,10 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.ControllerChangeType;
public class TestController extends Controller {
@IdRes public static final int VIEW_ID = 2342;
@@ -19,11 +24,8 @@ public class TestController extends Controller {
private static final String KEY_CALL_STATE = "TestController.currentCallState";
public CallState currentCallState;
public TestController() {
currentCallState = new CallState();
}
public CallState currentCallState = new CallState(false);
public ChangeHandlerHistory changeHandlerHistory = new ChangeHandlerHistory();
@NonNull
@Override
@@ -32,11 +34,11 @@ public class TestController extends Controller {
FrameLayout view = new AttachFakingFrameLayout(inflater.getContext());
view.setId(VIEW_ID);
FrameLayout childContainer1 = new FrameLayout(inflater.getContext());
FrameLayout childContainer1 = new AttachFakingFrameLayout(inflater.getContext());
childContainer1.setId(CHILD_VIEW_ID_1);
view.addView(childContainer1);
FrameLayout childContainer2 = new FrameLayout(inflater.getContext());
FrameLayout childContainer2 = new AttachFakingFrameLayout(inflater.getContext());
childContainer2.setId(CHILD_VIEW_ID_2);
view.addView(childContainer2);
@@ -53,6 +55,25 @@ public class TestController extends Controller {
protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
super.onChangeEnded(changeHandler, changeType);
currentCallState.changeEndCalls++;
if (changeHandler instanceof MockChangeHandler) {
MockChangeHandler mockHandler = (MockChangeHandler)changeHandler;
changeHandlerHistory.addEntry(mockHandler.from, mockHandler.to, changeType.isPush, mockHandler);
} else {
changeHandlerHistory.isValidHistory = false;
}
}
@Override
protected void onContextAvailable(@NonNull Context context) {
super.onContextAvailable(context);
currentCallState.contextAvailableCalls++;
}
@Override
protected void onContextUnavailable() {
super.onContextUnavailable();
currentCallState.contextUnavailableCalls++;
}
@Override
@@ -68,7 +89,7 @@ public class TestController extends Controller {
}
@Override
protected void onDestroyView(View view) {
protected void onDestroyView(@NonNull View view) {
super.onDestroyView(view);
currentCallState.destroyViewCalls++;
}
@@ -1,7 +1,8 @@
package com.bluelinelabs.conductor;
package com.bluelinelabs.conductor.util;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
import org.robolectric.util.ReflectionHelpers;
@@ -10,8 +11,12 @@ import java.util.List;
public class ViewUtils {
public static void reportAttached(View view, boolean attached) {
reportAttached(view, attached, true);
}
public static void reportAttached(View view, boolean attached, boolean propogateToChildren) {
if (view instanceof AttachFakingFrameLayout) {
((AttachFakingFrameLayout)view).setAttached(attached, false);
((AttachFakingFrameLayout) view).setAttached(attached, false);
}
List<OnAttachStateChangeListener> listeners = getAttachStateListeners(view);
@@ -38,6 +43,14 @@ public class ViewUtils {
}
}
if (propogateToChildren && view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
reportAttached(viewGroup.getChildAt(i), attached, true);
}
}
}
private static List<OnAttachStateChangeListener> getAttachStateListeners(View view) {
Executable → Regular
+24 -28
View File
@@ -1,22 +1,7 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
lintOptions {
abortOnError false
}
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
@@ -25,10 +10,11 @@ android {
defaultConfig {
applicationId "com.bluelinelabs.conductor.demo"
minSdkVersion 16
targetSdkVersion 23
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.0"
vectorDrawables.useSupportLibrary true
}
buildTypes {
@@ -37,20 +23,30 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
packagingOptions {
exclude 'META-INF/rxjava.properties'
}
}
dependencies {
compile rootProject.ext.supportV4
compile rootProject.ext.supportDesign
implementation rootProject.ext.supportV4
implementation rootProject.ext.supportDesign
apt rootProject.ext.butterknifeCompiler
compile rootProject.ext.butterknife
compile rootProject.ext.picasso
implementation rootProject.ext.archComponentsLiveDataCore // Fix duplicate classes
compile project(':conductor-support')
compile project(':conductor-rxlifecycle')
annotationProcessor rootProject.ext.butterknifeCompiler
implementation rootProject.ext.butterknife
implementation rootProject.ext.picasso
debugCompile rootProject.ext.leakCanary
releaseCompile rootProject.ext.leakCanaryNoOp
testCompile rootProject.ext.leakCanaryNoOp
implementation project(':conductor')
implementation project(':conductor-modules:support')
implementation project(':conductor-modules:rxlifecycle')
implementation project(':conductor-modules:rxlifecycle2')
implementation project(':conductor-modules:autodispose')
implementation project(':conductor-modules:arch-components-lifecycle')
debugImplementation rootProject.ext.leakCanary
releaseImplementation rootProject.ext.leakCanaryNoOp
testImplementation rootProject.ext.leakCanaryNoOp
}

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