Compare commits
18 Commits
3.1.8
...
predictive-back
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a87a4140e | |||
| ad81c4819b | |||
| 49018ed84c | |||
| fbfe3ce2e9 | |||
| 5a1746b2d3 | |||
| 2ffafaee79 | |||
| eabfc005d0 | |||
| a90ca5180e | |||
| 4d4d8bfbd1 | |||
| f69fab6062 | |||
| 590debf975 | |||
| c3f7d128f5 | |||
| 5e1f072672 | |||
| b0d15d9f9e | |||
| 8ac2e04c62 | |||
| cdbdee5c42 | |||
| 8488242a26 | |||
| 1fe0187439 |
@@ -0,0 +1,25 @@
|
||||
root = true
|
||||
|
||||
[*.{java,kt,kts,xml,gradle}]
|
||||
charset=utf-8
|
||||
end_of_line=lf
|
||||
max_line_length=120
|
||||
insert_final_newline=true
|
||||
trim_trailing_whitespace=true
|
||||
indent_style=space
|
||||
|
||||
[*.{java,kt,kts,gradle}]
|
||||
spaces_around_operators=true
|
||||
indent_brace_style=K&R
|
||||
|
||||
[*.{java,gradle}]
|
||||
indent_size=4
|
||||
|
||||
[*.{kt,kts}]
|
||||
indent_size=2
|
||||
continuation_indent_size=2
|
||||
ij_kotlin_allow_trailing_comma=true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site=true
|
||||
|
||||
[*.xml]
|
||||
indent_size=2
|
||||
@@ -8,11 +8,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: set up JDK 11
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '11'
|
||||
java-version: '17'
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Test with Gradle
|
||||
@@ -24,15 +24,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: set up JDK 11
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '11'
|
||||
java-version: '17'
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Test with Gradle
|
||||
run: ./gradlew clean uploadArchives
|
||||
- name: Release with Gradle
|
||||
run: ./gradlew clean publishAllPublicationsToMavenCentral
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
|
||||
ORG_GRADLE_PROJECT_SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
|
||||
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
|
||||
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
*.ipr
|
||||
*.iws
|
||||
/.idea/*
|
||||
!/.idea/codeStyles/
|
||||
!/.idea/scopes/
|
||||
classes
|
||||
gen-external-apklibs
|
||||
|
||||
Generated
-137
@@ -1,137 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CALL_PARAMETERS_WRAP" value="5" />
|
||||
<option name="CALL_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
|
||||
<option name="CALL_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
|
||||
<option name="METHOD_PARAMETERS_WRAP" value="5" />
|
||||
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
|
||||
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
|
||||
<option name="EXTENDS_LIST_WRAP" value="1" />
|
||||
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
|
||||
<option name="ASSIGNMENT_WRAP" value="1" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
Generated
-6
@@ -1,6 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default (1)" />
|
||||
</state>
|
||||
</component>
|
||||
@@ -19,8 +19,12 @@ Conductor is architecture-agnostic and does not try to force any design decision
|
||||
|
||||
## Installation
|
||||
|
||||
Conductor 4.0 is coming soon. It is already being used in production with many, many millions of users. It is, however, not guaranteed to be API stable. As such, it is being released as a preview rather than a standard release. Preview in this context is _not_ a commentary on stability. It is considered to be up to the same quality standards as the current 3.x stable release.
|
||||
Changes in Conductor 4 are available in the [GitHub releases](https://github.com/bluelinelabs/Conductor/releases/). In preparation for the release of the next version, there are currently 3 installation options:
|
||||
|
||||
### Latest Stable 3.x
|
||||
```gradle
|
||||
def conductorVersion = '3.1.8'
|
||||
def conductorVersion = '3.2.0'
|
||||
|
||||
implementation "com.bluelinelabs:conductor:$conductorVersion"
|
||||
|
||||
@@ -32,17 +36,13 @@ implementation "com.bluelinelabs:conductor-viewpager:$conductorVersion"
|
||||
|
||||
// ViewPager2 Adapter:
|
||||
implementation "com.bluelinelabs:conductor-viewpager2:$conductorVersion"
|
||||
|
||||
// RxJava2 Autodispose support:
|
||||
implementation "com.bluelinelabs:conductor-autodispose:$conductorVersion"
|
||||
|
||||
// Lifecycle-aware Controllers (architecture components):
|
||||
implementation "com.bluelinelabs:conductor-archlifecycle:$conductorVersion"
|
||||
```
|
||||
|
||||
**SNAPSHOT**
|
||||
### 4.0 Preview
|
||||
Use `4.0.0-preview-4` as your version number in any of the dependencies above.
|
||||
|
||||
Just use `3.1.9-SNAPSHOT` as your version number in any of the dependencies above and add the url to the snapshot repository:
|
||||
### SNAPSHOT
|
||||
Use `4.0.0-SNAPSHOT` as your version number in any of the dependencies above and add the url to the snapshot repository:
|
||||
|
||||
```gradle
|
||||
allprojects {
|
||||
@@ -65,48 +65,43 @@ __RouterTransaction__ | Transactions are used to define data about adding Contro
|
||||
|
||||
### Minimal Activity implementation
|
||||
|
||||
```java
|
||||
public class MainActivity extends Activity {
|
||||
```kotlin
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private Router router;
|
||||
private lateinit var router: Router
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_main);
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
ViewGroup container = (ViewGroup) findViewById(R.id.controller_container);
|
||||
val container = findViewById<ViewGroup>(R.id.controller_container)
|
||||
|
||||
router = Conductor.attachRouter(this, container, savedInstanceState)
|
||||
.setPopRootControllerMode(PopRootControllerMode.NEVER);
|
||||
if (!router.hasRootController()) {
|
||||
router.setRoot(RouterTransaction.with(new HomeController()));
|
||||
}
|
||||
router = Conductor.attachRouter(this, binding.controllerContainer, savedInstanceState)
|
||||
.setPopRootControllerMode(PopRootControllerMode.NEVER)
|
||||
.setOnBackPressedDispatcherEnabled(true)
|
||||
|
||||
if (!router.hasRootController()) {
|
||||
router.setRoot(RouterTransaction.with(HomeController()))
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (!router.handleBack()) {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### Minimal Controller implementation
|
||||
|
||||
```java
|
||||
public class HomeController extends Controller {
|
||||
|
||||
@Override
|
||||
protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) {
|
||||
View view = inflater.inflate(R.layout.controller_home, container, false);
|
||||
((TextView) view.findViewById(R.id.tv_title)).setText("Hello World");
|
||||
return view;
|
||||
}
|
||||
```kotlin
|
||||
class HomeController : Controller() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup,
|
||||
savedViewState: Bundle?
|
||||
): View {
|
||||
val view = inflater.inflate(R.layout.controller_home, container, false)
|
||||
view.findViewById<TextView>(R.id.tv_title).text = "Hello World"
|
||||
return view
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+14
-4
@@ -6,14 +6,24 @@ buildscript {
|
||||
dependencies {
|
||||
classpath libs.agp
|
||||
classpath libs.kotlin.plugin
|
||||
classpath libs.mvnpublish
|
||||
classpath libs.dokka
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
if (project.hasProperty('maven_publish_url')) {
|
||||
pluginManager.withPlugin("com.vanniktech.maven.publish") {
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
url = maven_publish_url
|
||||
credentials {
|
||||
username = maven_publish_username
|
||||
password = maven_publish_password
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
apply plugin: 'com.android.library'
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
alias(libs.plugins.mvnpublish)
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion libs.versions.compilesdk.get() as Integer
|
||||
compileSdk libs.versions.compilesdk.get() as Integer
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion libs.versions.minsdk.get()
|
||||
@@ -9,6 +17,8 @@ android {
|
||||
versionCode Integer.parseInt(project.VERSION_CODE)
|
||||
versionName project.VERSION_NAME
|
||||
}
|
||||
|
||||
namespace "com.bluelinelabs.conductor.androidxtransition"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -19,5 +29,3 @@ dependencies {
|
||||
}
|
||||
|
||||
ext.artifactId = 'conductor-androidx-transition'
|
||||
|
||||
apply plugin: "com.vanniktech.maven.publish"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<manifest package="com.bluelinelabs.conductor.androidxtransition">
|
||||
<application />
|
||||
</manifest>
|
||||
+2
-2
@@ -113,7 +113,7 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removesFromViewOnPush() {
|
||||
public boolean getRemovesFromViewOnPush() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
|
||||
* @param isPush True if this is a push transaction, false if it's a pop
|
||||
*/
|
||||
public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @Nullable Transition transition, boolean isPush) {
|
||||
if (from != null && (removesFromViewOnPush() || !isPush) && from.getParent() == container) {
|
||||
if (from != null && (getRemovesFromViewOnPush() || !isPush) && from.getParent() == container) {
|
||||
container.removeView(from);
|
||||
}
|
||||
if (to != null && to.getParent() == null) {
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion libs.versions.compilesdk.get() as Integer
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion libs.versions.minsdk.get()
|
||||
targetSdkVersion libs.versions.targetsdk.get()
|
||||
versionCode Integer.parseInt(project.VERSION_CODE)
|
||||
versionName project.VERSION_NAME
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api libs.androidx.lifecycle.runtime
|
||||
|
||||
implementation project(':conductor')
|
||||
}
|
||||
|
||||
ext.artifactId = 'conductor-arch-components-lifecycle'
|
||||
|
||||
apply plugin: "com.vanniktech.maven.publish"
|
||||
@@ -1,3 +0,0 @@
|
||||
POM_NAME=Conductor Architecture Components Lifecycle Extensions
|
||||
POM_ARTIFACT_ID=conductor-archlifecycle
|
||||
POM_PACKAGING=aar
|
||||
@@ -1,3 +0,0 @@
|
||||
<manifest package="com.bluelinelabs.conductor.archlifecycle">
|
||||
<application />
|
||||
</manifest>
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
package com.bluelinelabs.conductor.archlifecycle;
|
||||
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.Lifecycle.Event;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LifecycleRegistry;
|
||||
import android.content.Context;
|
||||
import androidx.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) {
|
||||
// Only act on Controllers that have had at least the onContextAvailable call made on them.
|
||||
if (lifecycleRegistry.getCurrentState() != Lifecycle.State.INITIALIZED) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Event.ON_DESTROY); // --> State.DESTROYED;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Override @NonNull
|
||||
public Lifecycle getLifecycle() {
|
||||
return lifecycleRegistry;
|
||||
}
|
||||
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
package com.bluelinelabs.conductor.archlifecycle;
|
||||
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bluelinelabs.conductor.Controller;
|
||||
|
||||
public abstract class LifecycleController extends Controller implements LifecycleOwner {
|
||||
|
||||
private final ControllerLifecycleOwner lifecycleOwner = new ControllerLifecycleOwner(this);
|
||||
|
||||
public LifecycleController() {
|
||||
super();
|
||||
}
|
||||
|
||||
public LifecycleController(@Nullable Bundle args) {
|
||||
super(args);
|
||||
}
|
||||
|
||||
@Override @NonNull
|
||||
public Lifecycle getLifecycle() {
|
||||
return lifecycleOwner.getLifecycle();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: "com.vanniktech.maven.publish"
|
||||
|
||||
android {
|
||||
compileSdkVersion libs.versions.compilesdk.get() as Integer
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion libs.versions.minsdk.get()
|
||||
targetSdkVersion libs.versions.targetsdk.get()
|
||||
versionCode Integer.parseInt(project.VERSION_CODE)
|
||||
versionName project.VERSION_NAME
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api libs.rxjava2
|
||||
api libs.autodispose
|
||||
api libs.autodispose.lifecycle
|
||||
|
||||
implementation project(':conductor')
|
||||
}
|
||||
|
||||
ext.artifactId = 'conductor-autodispose'
|
||||
@@ -1,3 +0,0 @@
|
||||
POM_NAME=Conductor AutoDispose Extensions
|
||||
POM_ARTIFACT_ID=conductor-autodispose
|
||||
POM_PACKAGING=aar
|
||||
@@ -1,3 +0,0 @@
|
||||
<manifest package="com.bluelinelabs.conductor.autodispose">
|
||||
<application />
|
||||
</manifest>
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
package com.bluelinelabs.conductor.autodispose;
|
||||
|
||||
public enum ControllerEvent {
|
||||
|
||||
CREATE,
|
||||
CONTEXT_AVAILABLE,
|
||||
CREATE_VIEW,
|
||||
ATTACH,
|
||||
DETACH,
|
||||
DESTROY_VIEW,
|
||||
CONTEXT_UNAVAILABLE,
|
||||
DESTROY
|
||||
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
package com.bluelinelabs.conductor.autodispose;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.view.View;
|
||||
|
||||
import com.bluelinelabs.conductor.Controller;
|
||||
import com.uber.autodispose.OutsideScopeException;
|
||||
|
||||
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 OutsideScopeException("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;
|
||||
}
|
||||
}
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
package com.bluelinelabs.conductor.autodispose;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bluelinelabs.conductor.Controller;
|
||||
import com.uber.autodispose.OutsideScopeException;
|
||||
import com.uber.autodispose.lifecycle.LifecycleScopeProvider;
|
||||
import com.uber.autodispose.lifecycle.LifecycleScopes;
|
||||
import com.uber.autodispose.lifecycle.CorrespondingEventsFunction;
|
||||
|
||||
import io.reactivex.CompletableSource;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.subjects.BehaviorSubject;
|
||||
|
||||
public class ControllerScopeProvider implements LifecycleScopeProvider<ControllerEvent> {
|
||||
private static final CorrespondingEventsFunction<ControllerEvent> CORRESPONDING_EVENTS =
|
||||
new CorrespondingEventsFunction<ControllerEvent>() {
|
||||
@Override
|
||||
public ControllerEvent apply(ControllerEvent lastEvent) throws OutsideScopeException {
|
||||
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 OutsideScopeException("Cannot bind to Controller lifecycle when outside of it.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@NonNull private final BehaviorSubject<ControllerEvent> lifecycleSubject;
|
||||
@NonNull private final CorrespondingEventsFunction<ControllerEvent> correspondingEventsFunction;
|
||||
|
||||
public static ControllerScopeProvider from(@NonNull Controller controller) {
|
||||
return new ControllerScopeProvider(controller, CORRESPONDING_EVENTS);
|
||||
}
|
||||
|
||||
public static ControllerScopeProvider from(@NonNull Controller controller, @NonNull final ControllerEvent untilEvent) {
|
||||
return new ControllerScopeProvider(controller, new CorrespondingEventsFunction<ControllerEvent>() {
|
||||
@Override
|
||||
public ControllerEvent apply(ControllerEvent controllerEvent) {
|
||||
return untilEvent;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static ControllerScopeProvider from(@NonNull Controller controller, @NonNull final CorrespondingEventsFunction<ControllerEvent> correspondingEventsFunction) {
|
||||
return new ControllerScopeProvider(controller, correspondingEventsFunction);
|
||||
}
|
||||
|
||||
private ControllerScopeProvider(@NonNull Controller controller, @NonNull CorrespondingEventsFunction<ControllerEvent> correspondingEventsFunction) {
|
||||
lifecycleSubject = ControllerLifecycleSubjectHelper.create(controller);
|
||||
this.correspondingEventsFunction = correspondingEventsFunction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observable<ControllerEvent> lifecycle() {
|
||||
return lifecycleSubject.hide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CorrespondingEventsFunction<ControllerEvent> correspondingEvents() {
|
||||
return correspondingEventsFunction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ControllerEvent peekLifecycle() {
|
||||
return lifecycleSubject.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableSource requestScope() throws Exception {
|
||||
return LifecycleScopes.resolveScopeFromLifecycle(this);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,20 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
alias(libs.plugins.mvnpublish)
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion libs.versions.compilesdk.get() as Integer
|
||||
compileSdk libs.versions.compilesdk.get() as Integer
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion libs.versions.minsdk.get()
|
||||
@@ -10,6 +22,8 @@ android {
|
||||
versionCode Integer.parseInt(project.VERSION_CODE)
|
||||
versionName project.VERSION_NAME
|
||||
}
|
||||
|
||||
namespace "com.bluelinelabs.conductor.viewpager"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -21,4 +35,3 @@ dependencies {
|
||||
}
|
||||
|
||||
ext.artifactId = 'conductor-viewpager'
|
||||
apply plugin: "com.vanniktech.maven.publish"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<manifest package="com.bluelinelabs.conductor.viewpager">
|
||||
<application />
|
||||
</manifest>
|
||||
+2
-1
@@ -77,7 +77,8 @@ public abstract class RouterPagerAdapter extends PagerAdapter {
|
||||
savedPages.remove(position);
|
||||
}
|
||||
|
||||
Router router = host.getChildRouter(container, name);
|
||||
Router router = host.getChildRouter(container, name)
|
||||
.setPopRootControllerMode(Router.PopRootControllerMode.NEVER);
|
||||
if (!router.hasRootController()) {
|
||||
Bundle routerSavedState = savedPages.get(position);
|
||||
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
package com.bluelinelabs.conductor.viewpager.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import com.bluelinelabs.conductor.Conductor
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
@@ -15,7 +15,7 @@ import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.asTransaction
|
||||
import com.bluelinelabs.conductor.viewpager.RouterPagerAdapter
|
||||
|
||||
class TestActivity : Activity() {
|
||||
class TestActivity : FragmentActivity() {
|
||||
|
||||
private lateinit var router: Router
|
||||
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
id("kotlin-parcelize")
|
||||
alias(libs.plugins.mvnpublish)
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion libs.versions.compilesdk.get() as Integer
|
||||
compileSdk libs.versions.compilesdk.get() as Integer
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion libs.versions.minsdk.get()
|
||||
@@ -17,6 +29,8 @@ android {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
namespace "com.bluelinelabs.conductor.viewpager2"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -29,4 +43,3 @@ dependencies {
|
||||
}
|
||||
|
||||
ext.artifactId = 'conductor-viewpager2'
|
||||
apply plugin: "com.vanniktech.maven.publish"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<manifest package="com.bluelinelabs.conductor.viewpager2">
|
||||
<application />
|
||||
</manifest>
|
||||
+1
@@ -155,6 +155,7 @@ abstract class RouterStateAdapter(private val host: Controller) :
|
||||
private fun attachRouter(holder: RouterViewHolder, position: Int) {
|
||||
val itemId = getItemId(position)
|
||||
val router = host.getChildRouter(holder.container, "$itemId", true, false)!!
|
||||
.setPopRootControllerMode(Router.PopRootControllerMode.NEVER)
|
||||
|
||||
// This should have already been handled by onViewRecycled, but it seems like this wasn't
|
||||
// always reliably called
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package com.bluelinelabs.conductor.viewpager2
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Looper.getMainLooper
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.bluelinelabs.conductor.Conductor
|
||||
import com.bluelinelabs.conductor.Router
|
||||
@@ -26,7 +26,7 @@ class StateSaveTests {
|
||||
private val destroyedItems = mutableListOf<Int>()
|
||||
|
||||
init {
|
||||
val activityController = Robolectric.buildActivity(Activity::class.java).setup()
|
||||
val activityController = Robolectric.buildActivity(FragmentActivity::class.java).setup()
|
||||
val layout = FrameLayout(activityController.get())
|
||||
activityController.get().setContentView(layout)
|
||||
val router = Conductor.attachRouter(activityController.get(), FrameLayout(activityController.get()), null)
|
||||
|
||||
+22
-6
@@ -1,8 +1,21 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
id("kotlin-parcelize")
|
||||
alias(libs.plugins.mvnpublish)
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion libs.versions.compilesdk.get() as Integer
|
||||
compileSdk libs.versions.compilesdk.get() as Integer
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion libs.versions.minsdk.get()
|
||||
@@ -11,15 +24,19 @@ android {
|
||||
versionName project.VERSION_NAME
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
||||
namespace "com.bluelinelabs.conductor"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation libs.androidx.savedstate.ktx
|
||||
api libs.androidx.activity
|
||||
api libs.androidx.appcompat
|
||||
api libs.androidx.savedstate.ktx
|
||||
testImplementation libs.junit
|
||||
testImplementation libs.robolectric
|
||||
testImplementation libs.kotest
|
||||
|
||||
implementation libs.androidx.lifecycle.runtime
|
||||
api libs.androidx.lifecycle.runtime
|
||||
|
||||
api libs.androidx.annotation
|
||||
api libs.kotlin.stdlib
|
||||
@@ -28,4 +45,3 @@ dependencies {
|
||||
}
|
||||
|
||||
ext.artifactId = 'conductor'
|
||||
apply plugin: "com.vanniktech.maven.publish"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<manifest package="com.bluelinelabs.conductor">
|
||||
<application />
|
||||
</manifest>
|
||||
@@ -21,6 +21,10 @@ public class ActivityHostedRouter extends Router {
|
||||
private LifecycleHandler lifecycleHandler;
|
||||
private final TransactionIndexer transactionIndexer = new TransactionIndexer();
|
||||
|
||||
public ActivityHostedRouter() {
|
||||
popRootControllerMode = PopRootControllerMode.NEVER;
|
||||
}
|
||||
|
||||
public final void setHost(@NonNull LifecycleHandler lifecycleHandler, @NonNull ViewGroup container) {
|
||||
if (this.lifecycleHandler != lifecycleHandler || this.container != container) {
|
||||
if (this.container != null && this.container instanceof ControllerChangeListener) {
|
||||
@@ -68,8 +72,8 @@ public class ActivityHostedRouter extends Router {
|
||||
|
||||
@Override
|
||||
public final void invalidateOptionsMenu() {
|
||||
if (lifecycleHandler != null && lifecycleHandler.getFragmentManager() != null) {
|
||||
lifecycleHandler.getFragmentManager().invalidateOptionsMenu();
|
||||
if (lifecycleHandler != null && getActivity() != null) {
|
||||
getActivity().invalidateOptionsMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +89,7 @@ public class ActivityHostedRouter extends Router {
|
||||
|
||||
@Override
|
||||
void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) {
|
||||
lifecycleHandler.startActivityForResult(instanceId, intent, requestCode);
|
||||
lifecycleHandler.startActivityForResult(instanceId, intent, requestCode, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package com.bluelinelabs.conductor
|
||||
|
||||
import android.os.Bundle
|
||||
import java.util.*
|
||||
import java.util.ArrayDeque
|
||||
import java.util.Deque
|
||||
|
||||
internal class Backstack : Iterable<RouterTransaction> {
|
||||
|
||||
@@ -11,13 +12,15 @@ internal class Backstack : Iterable<RouterTransaction> {
|
||||
|
||||
val size: Int get() = backstack.size
|
||||
|
||||
var onBackstackUpdatedListener: OnBackstackUpdatedListener? = null
|
||||
|
||||
fun root(): RouterTransaction? = backstack.lastOrNull()
|
||||
|
||||
override fun iterator(): MutableIterator<RouterTransaction> {
|
||||
return backstack.iterator()
|
||||
}
|
||||
override fun iterator(): Iterator<RouterTransaction> = backstack.toTypedArray().iterator()
|
||||
|
||||
fun reverseIterator(): Iterator<RouterTransaction> = backstack.descendingIterator()
|
||||
fun reverseIterator(): Iterator<RouterTransaction> = backstack.reversed().iterator()
|
||||
|
||||
fun remove(transaction: RouterTransaction) = backstack.remove(transaction)
|
||||
|
||||
fun popTo(transaction: RouterTransaction): List<RouterTransaction> {
|
||||
if (transaction in backstack) {
|
||||
@@ -34,6 +37,7 @@ internal class Backstack : Iterable<RouterTransaction> {
|
||||
|
||||
fun pop(): RouterTransaction {
|
||||
return backstack.pop().also {
|
||||
onBackstackUpdatedListener?.onBackstackUpdated()
|
||||
it.controller.destroy()
|
||||
}
|
||||
}
|
||||
@@ -42,6 +46,7 @@ internal class Backstack : Iterable<RouterTransaction> {
|
||||
|
||||
fun push(transaction: RouterTransaction) {
|
||||
backstack.push(transaction)
|
||||
onBackstackUpdatedListener?.onBackstackUpdated()
|
||||
}
|
||||
|
||||
fun popAll(): List<RouterTransaction> {
|
||||
@@ -57,6 +62,8 @@ internal class Backstack : Iterable<RouterTransaction> {
|
||||
backstack.forEach { transaction ->
|
||||
this.backstack.push(transaction)
|
||||
}
|
||||
|
||||
onBackstackUpdatedListener?.onBackstackUpdated()
|
||||
}
|
||||
|
||||
operator fun contains(controller: Controller): Boolean {
|
||||
@@ -81,9 +88,15 @@ internal class Backstack : Iterable<RouterTransaction> {
|
||||
backstack.push(RouterTransaction(transactionBundle!!))
|
||||
}
|
||||
}
|
||||
|
||||
onBackstackUpdatedListener?.onBackstackUpdated()
|
||||
}
|
||||
|
||||
fun interface OnBackstackUpdatedListener {
|
||||
fun onBackstackUpdated()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_ENTRIES = "Backstack.entries"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,26 +7,35 @@ import androidx.annotation.UiThread
|
||||
import com.bluelinelabs.conductor.internal.LifecycleHandler
|
||||
import com.bluelinelabs.conductor.internal.ensureMainThread
|
||||
|
||||
/**
|
||||
* Conductor will create a [Router] that has been initialized for your Activity and containing ViewGroup.
|
||||
* If an existing [Router] is already associated with this Activity/ViewGroup pair, either in memory
|
||||
* or in the savedInstanceState, that router will be used and rebound instead of creating a new one with
|
||||
* an empty backstack.
|
||||
*
|
||||
* @param activity The Activity that will host the [Router] being attached.
|
||||
* @param container The ViewGroup in which the [Router]'s [Controller] views will be hosted
|
||||
* @param savedInstanceState The savedInstanceState passed into the hosting Activity's onCreate method. Used
|
||||
* for restoring the Router's state if possible.
|
||||
* @return A fully configured [Router] instance for use with this Activity/ViewGroup pair.
|
||||
*/
|
||||
@UiThread
|
||||
object Conductor {
|
||||
|
||||
|
||||
/**
|
||||
* Conductor will create a [Router] that has been initialized for your Activity and containing ViewGroup.
|
||||
* If an existing [Router] is already associated with this Activity/ViewGroup pair, either in memory
|
||||
* or in the savedInstanceState, that router will be used and rebound instead of creating a new one with
|
||||
* an empty backstack.
|
||||
*
|
||||
* @param activity The Activity that will host the [Router] being attached.
|
||||
* @param container The ViewGroup in which the [Router]'s [Controller] views will be hosted
|
||||
* @param savedInstanceState The savedInstanceState passed into the hosting Activity's onCreate method. Used
|
||||
* for restoring the Router's state if possible.
|
||||
* @param allowExperimentalAndroidXBacking Use AndroidX backing if true and if the activity parameter is a
|
||||
* FragmentActivity.
|
||||
* @return A fully configured [Router] instance for use with this Activity/ViewGroup pair.
|
||||
*/
|
||||
@UiThread
|
||||
@JvmStatic
|
||||
fun attachRouter(activity: Activity, container: ViewGroup, savedInstanceState: Bundle?): Router {
|
||||
@JvmOverloads
|
||||
fun attachRouter(
|
||||
activity: Activity,
|
||||
container: ViewGroup,
|
||||
savedInstanceState: Bundle?,
|
||||
allowExperimentalAndroidXBacking: Boolean = true,
|
||||
): Router {
|
||||
ensureMainThread()
|
||||
return LifecycleHandler.install(activity)
|
||||
return LifecycleHandler.install(activity, allowAndroidXBacking = allowExperimentalAndroidXBacking)
|
||||
.getRouter(container, savedInstanceState)
|
||||
.also { it.rebindIfNeeded() }
|
||||
.setPopRootControllerMode(Router.PopRootControllerMode.NEVER)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.bluelinelabs.conductor;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -9,6 +8,7 @@ import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -17,11 +17,19 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.activity.BackEventCompat;
|
||||
import androidx.activity.ComponentActivity;
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.activity.OnBackPressedDispatcher;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
import com.bluelinelabs.conductor.internal.BackGestureControllerView;
|
||||
import com.bluelinelabs.conductor.internal.BackGestureViewState;
|
||||
import com.bluelinelabs.conductor.internal.ClassUtils;
|
||||
import com.bluelinelabs.conductor.internal.ControllerLifecycleOwner;
|
||||
import com.bluelinelabs.conductor.internal.OwnViewTreeLifecycleAndRegistry;
|
||||
import com.bluelinelabs.conductor.internal.RouterRequiringFunc;
|
||||
import com.bluelinelabs.conductor.internal.ViewAttachHandler;
|
||||
@@ -32,7 +40,6 @@ import java.lang.reflect.Constructor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -80,6 +87,7 @@ public abstract class Controller {
|
||||
private boolean awaitingParentAttach;
|
||||
private boolean hasSavedViewState;
|
||||
boolean isDetachFrozen;
|
||||
boolean onBackPressedDispatcherEnabled;
|
||||
private ControllerChangeHandler overriddenPushHandler;
|
||||
private ControllerChangeHandler overriddenPopHandler;
|
||||
private RetainViewMode retainViewMode = RetainViewMode.RELEASE_DETACH;
|
||||
@@ -92,6 +100,138 @@ public abstract class Controller {
|
||||
private boolean isPerformingExitTransition;
|
||||
private boolean isContextAvailable;
|
||||
|
||||
final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
|
||||
ControllerChangeHandler.ChangeTransaction popTransaction = null;
|
||||
BackGestureViewState backGestureViewState = null;
|
||||
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
// Root-level routers should have PopRootControllerMode.NEVER, and so should never return false here.
|
||||
// This is meant to handle higher-level pops only, where the predictive back gesture doesn't come into play.
|
||||
if (!router.getRootRouter().handleBackDispatch()) {
|
||||
// Disable to ensure we don't have an infinite call loop.
|
||||
setEnabled(false);
|
||||
getOnBackPressedDispatcher().onBackPressed();
|
||||
|
||||
if (!isBeingDestroyed) {
|
||||
setEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleOnBackStarted(@NonNull BackEventCompat backEvent) {
|
||||
Log.d("KUCK", "it has begun");
|
||||
ControllerChangeHandler.ChangeTransaction transaction = getPopTransaction();
|
||||
if (transaction != null && transaction.changeHandler != null && transaction.changeHandler.getEnableOnBackGestureCallbacks()) {
|
||||
List<BackGestureControllerView> toViews;
|
||||
if (transaction.to != null && transaction.to.view == null) {
|
||||
toViews = toViewsForGesture(transaction, new ArrayList<>());
|
||||
} else {
|
||||
toViews = Collections.emptyList();
|
||||
}
|
||||
|
||||
for (int i = toViews.size() - 1; i > 0; i--) {
|
||||
transaction.container.addView(toViews.get(i).getView());
|
||||
}
|
||||
|
||||
backGestureViewState = new BackGestureViewState(transaction.from.view, toViews);
|
||||
|
||||
transaction.changeHandler.handleOnBackStarted(
|
||||
transaction.container,
|
||||
getToView(),
|
||||
backGestureViewState.getFromView(),
|
||||
backEvent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleOnBackProgressed(@NonNull BackEventCompat backEvent) {
|
||||
Log.d("KUCK", "it has progressed");
|
||||
|
||||
ControllerChangeHandler.ChangeTransaction transaction = getPopTransaction();
|
||||
if (transaction != null && transaction.changeHandler != null && transaction.changeHandler.getEnableOnBackGestureCallbacks()) {
|
||||
transaction.changeHandler.handleOnBackProgressed(
|
||||
transaction.container,
|
||||
getToView(),
|
||||
backGestureViewState.getFromView(),
|
||||
backEvent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleOnBackCancelled() {
|
||||
Log.d("KUCK", "it has canceled");
|
||||
|
||||
ControllerChangeHandler.ChangeTransaction transaction = getPopTransaction();
|
||||
if (transaction != null && transaction.changeHandler != null && transaction.changeHandler.getEnableOnBackGestureCallbacks()) {
|
||||
transaction.changeHandler.handleOnBackCancelled(
|
||||
transaction.container,
|
||||
getToView(),
|
||||
backGestureViewState.getFromView()
|
||||
);
|
||||
|
||||
for (int i = 0; i < backGestureViewState.getToViews().size(); i++) {
|
||||
BackGestureControllerView state = backGestureViewState.getToViews().get(i);
|
||||
ViewGroup parent = (ViewGroup) state.getView().getParent();
|
||||
if (parent != null) {
|
||||
parent.removeView(state.getView());
|
||||
}
|
||||
|
||||
if (state.getInflatedForGesture()) {
|
||||
state.getController().removeViewReference(state.getView().getContext());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
backGestureViewState = null;
|
||||
}
|
||||
|
||||
private ControllerChangeHandler.ChangeTransaction getPopTransaction() {
|
||||
if (popTransaction == null) {
|
||||
popTransaction = router.popTransaction(Controller.this);
|
||||
}
|
||||
return popTransaction;
|
||||
}
|
||||
|
||||
private List<BackGestureControllerView> toViewsForGesture(
|
||||
ControllerChangeHandler.ChangeTransaction transaction,
|
||||
List<BackGestureControllerView> aggregator
|
||||
) {
|
||||
if (transaction.to != null) {
|
||||
View view = transaction.to.view;
|
||||
boolean inflated = false;
|
||||
if (view == null) {
|
||||
view = transaction.to.inflate(transaction.container);
|
||||
inflated = true;
|
||||
} else if (view.getParent() != null) {
|
||||
return aggregator;
|
||||
}
|
||||
|
||||
aggregator.add(new BackGestureControllerView(transaction.to, view, inflated));
|
||||
|
||||
if (!transaction.changeHandler.getRemovesFromViewOnPush()) {
|
||||
toViewsForGesture(router.popTransaction(transaction.to), aggregator);
|
||||
}
|
||||
}
|
||||
|
||||
return aggregator;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private View getToView() {
|
||||
if (backGestureViewState.getToViews().size() == 0) {
|
||||
return null;
|
||||
} else {
|
||||
return backGestureViewState.getToViews().get(0).getView();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public final LifecycleOwner lifecycleOwner = new ControllerLifecycleOwner(this);
|
||||
|
||||
@NonNull
|
||||
static Controller newInstance(@NonNull Bundle bundle) {
|
||||
final String className = bundle.getString(KEY_CLASS_NAME);
|
||||
@@ -313,6 +453,17 @@ public abstract class Controller {
|
||||
return router != null ? router.getActivity() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OnBackPressedDispatcher for this Controller's {@link Router} or {@code null} if:
|
||||
* - This Router has not yet been attached to an Activity
|
||||
* - The attached Activity does not extend ComponentActivity
|
||||
* - The Activity has been destroyed
|
||||
*/
|
||||
@Nullable
|
||||
public final OnBackPressedDispatcher getOnBackPressedDispatcher() {
|
||||
return router != null ? router.getOnBackPressedDispatcher() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Resources from the host Activity or {@code null} if this Controller has not
|
||||
* yet been attached to an Activity or if the Activity has been destroyed.
|
||||
@@ -541,36 +692,21 @@ public abstract class Controller {
|
||||
* Calls startActivity(Intent) from this Controller's host Activity.
|
||||
*/
|
||||
public final void startActivity(@NonNull final Intent intent) {
|
||||
executeWithRouter(new RouterRequiringFunc() {
|
||||
@Override
|
||||
public void execute() {
|
||||
router.startActivity(intent);
|
||||
}
|
||||
});
|
||||
executeWithRouter(() -> router.startActivity(intent));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls startActivityForResult(Intent, int) from this Controller's host Activity.
|
||||
*/
|
||||
public final void startActivityForResult(@NonNull final Intent intent, final int requestCode) {
|
||||
executeWithRouter(new RouterRequiringFunc() {
|
||||
@Override
|
||||
public void execute() {
|
||||
router.startActivityForResult(instanceId, intent, requestCode);
|
||||
}
|
||||
});
|
||||
executeWithRouter(() -> router.startActivityForResult(instanceId, intent, requestCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls startActivityForResult(Intent, int, Bundle) from this Controller's host Activity.
|
||||
*/
|
||||
public final void startActivityForResult(@NonNull final Intent intent, final int requestCode, @Nullable final Bundle options) {
|
||||
executeWithRouter(new RouterRequiringFunc() {
|
||||
@Override
|
||||
public void execute() {
|
||||
router.startActivityForResult(instanceId, intent, requestCode, options);
|
||||
}
|
||||
});
|
||||
executeWithRouter(() -> router.startActivityForResult(instanceId, intent, requestCode, options));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -588,12 +724,7 @@ public abstract class Controller {
|
||||
* @param requestCode The request code being registered for.
|
||||
*/
|
||||
public final void registerForActivityResult(final int requestCode) {
|
||||
executeWithRouter(new RouterRequiringFunc() {
|
||||
@Override
|
||||
public void execute() {
|
||||
router.registerForActivityResult(instanceId, requestCode);
|
||||
}
|
||||
});
|
||||
executeWithRouter(() -> router.registerForActivityResult(instanceId, requestCode));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -612,16 +743,10 @@ public abstract class Controller {
|
||||
* including {@link #shouldShowRequestPermissionRationale(String)} and
|
||||
* {@link #onRequestPermissionsResult(int, String[], int[])} will be forwarded back to this Controller by the system.
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
public final void requestPermissions(@NonNull final String[] permissions, final int requestCode) {
|
||||
requestedPermissions.addAll(Arrays.asList(permissions));
|
||||
|
||||
executeWithRouter(new RouterRequiringFunc() {
|
||||
@Override
|
||||
public void execute() {
|
||||
router.requestPermissions(instanceId, permissions, requestCode);
|
||||
}
|
||||
});
|
||||
executeWithRouter(() -> router.requestPermissions(instanceId, permissions, requestCode));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -647,8 +772,11 @@ public abstract class Controller {
|
||||
/**
|
||||
* Should be overridden if this Controller needs to handle the back button being pressed.
|
||||
*
|
||||
* Note: This method has been deprecated and should be replaced with registering an OnBackPressedCallback.
|
||||
*
|
||||
* @return True if this Controller has consumed the back button press, otherwise false
|
||||
*/
|
||||
@Deprecated
|
||||
public boolean handleBack() {
|
||||
List<RouterTransaction> childTransactions = new ArrayList<>();
|
||||
|
||||
@@ -656,12 +784,7 @@ public abstract class Controller {
|
||||
childTransactions.addAll(childRouter.getBackstack());
|
||||
}
|
||||
|
||||
Collections.sort(childTransactions, new Comparator<RouterTransaction>() {
|
||||
@Override
|
||||
public int compare(RouterTransaction o1, RouterTransaction o2) {
|
||||
return o2.getTransactionIndex() - o1.getTransactionIndex();
|
||||
}
|
||||
});
|
||||
Collections.sort(childTransactions, (t1, t2) -> t2.getTransactionIndex() - t1.getTransactionIndex());
|
||||
|
||||
for (RouterTransaction transaction : childTransactions) {
|
||||
Controller childController = transaction.controller();
|
||||
@@ -858,6 +981,14 @@ public abstract class Controller {
|
||||
lifecycleListener.preContextAvailable(this);
|
||||
}
|
||||
|
||||
onBackPressedDispatcherEnabled = router.onBackPressedDispatcherEnabled;
|
||||
if (onBackPressedDispatcherEnabled) {
|
||||
if (!(context instanceof ComponentActivity)) {
|
||||
throw new IllegalStateException("Host activities must extend ComponentActivity when enabling OnBackPressedDispatcher support.");
|
||||
}
|
||||
getOnBackPressedDispatcher().addCallback(onBackPressedCallback);
|
||||
}
|
||||
|
||||
isContextAvailable = true;
|
||||
onContextAvailable(context);
|
||||
|
||||
@@ -886,6 +1017,10 @@ public abstract class Controller {
|
||||
isContextAvailable = false;
|
||||
onContextUnavailable();
|
||||
|
||||
if (onBackPressedDispatcherEnabled) {
|
||||
onBackPressedCallback.remove();
|
||||
}
|
||||
|
||||
listeners = new ArrayList<>(lifecycleListeners);
|
||||
for (LifecycleListener lifecycleListener : listeners) {
|
||||
lifecycleListener.postContextUnavailable(this);
|
||||
@@ -1369,11 +1504,17 @@ public abstract class Controller {
|
||||
if (isDetachFrozen != frozen) {
|
||||
isDetachFrozen = frozen;
|
||||
|
||||
boolean detach = !frozen && view != null && viewWasDetached;
|
||||
|
||||
for (ControllerHostedRouter router : childRouters) {
|
||||
if (detach) {
|
||||
router.prepareForHostDetach();
|
||||
}
|
||||
|
||||
router.setDetachFrozen(frozen);
|
||||
}
|
||||
|
||||
if (!frozen && view != null && viewWasDetached) {
|
||||
if (detach) {
|
||||
View aView = view;
|
||||
detach(view, false, false);
|
||||
if (view == null && aView.getParent() == router.container) {
|
||||
|
||||
@@ -1,322 +0,0 @@
|
||||
package com.bluelinelabs.conductor;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
|
||||
import com.bluelinelabs.conductor.internal.ClassUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* ControllerChangeHandlers are responsible for swapping the View for one Controller to the View
|
||||
* of another. They can be useful for performing animations and transitions between Controllers. Several
|
||||
* default ControllerChangeHandlers are included.
|
||||
*/
|
||||
public abstract class ControllerChangeHandler {
|
||||
|
||||
private static final String KEY_CLASS_NAME = "ControllerChangeHandler.className";
|
||||
private static final String KEY_SAVED_STATE = "ControllerChangeHandler.savedState";
|
||||
|
||||
static final Map<String, ChangeHandlerData> inProgressChangeHandlers = new HashMap<>();
|
||||
|
||||
boolean forceRemoveViewOnPush;
|
||||
private boolean hasBeenUsed;
|
||||
|
||||
/**
|
||||
* Responsible for swapping Views from one Controller to another.
|
||||
*
|
||||
* @param container The container these Views are hosted in.
|
||||
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
|
||||
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
|
||||
* @param isPush True if this is a push transaction, false if it's a pop.
|
||||
* @param changeListener This listener must be called when any transitions or animations are completed.
|
||||
*/
|
||||
public abstract void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener);
|
||||
|
||||
public ControllerChangeHandler() {
|
||||
ensureDefaultConstructor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves any data about this handler to a Bundle in case the application is killed.
|
||||
*
|
||||
* @param bundle The Bundle into which data should be stored.
|
||||
*/
|
||||
public void saveToBundle(@NonNull Bundle bundle) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores data that was saved in the {@link #saveToBundle(Bundle bundle)} method.
|
||||
*
|
||||
* @param bundle The bundle that has data to be restored
|
||||
*/
|
||||
public void restoreFromBundle(@NonNull Bundle bundle) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be called on change handlers that push a controller if the controller being pushed is
|
||||
* popped before it has completed.
|
||||
*
|
||||
* @param newHandler The change handler that has caused this push to be aborted
|
||||
* @param newTop The Controller that will now be at the top of the backstack or {@code null}
|
||||
* if there will be no new Controller at the top
|
||||
*/
|
||||
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be called on change handlers that push a controller if the controller being pushed is
|
||||
* needs to be attached immediately, without any animations or transitions.
|
||||
*/
|
||||
public void completeImmediately() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this ControllerChangeHandler. This method is internally used by the library, so
|
||||
* ensure it will return an exact copy of your handler if overriding. If not overriding, the handler
|
||||
* will be saved and restored from the Bundle format.
|
||||
*/
|
||||
@NonNull
|
||||
public ControllerChangeHandler copy() {
|
||||
return fromBundle(toBundle());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this is a reusable ControllerChangeHandler. Defaults to false and should
|
||||
* ONLY be overridden if there are absolutely no side effects to using this handler more than once.
|
||||
* In the case that a handler is not reusable, it will be copied using the {@link #copy()} method
|
||||
* prior to use.
|
||||
*/
|
||||
public boolean isReusable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
final Bundle toBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(KEY_CLASS_NAME, getClass().getName());
|
||||
|
||||
Bundle savedState = new Bundle();
|
||||
saveToBundle(savedState);
|
||||
bundle.putBundle(KEY_SAVED_STATE, savedState);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
private void ensureDefaultConstructor() {
|
||||
try {
|
||||
getClass().getConstructor();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(getClass() + " does not have a default constructor.");
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static ControllerChangeHandler fromBundle(@Nullable Bundle bundle) {
|
||||
if (bundle != null) {
|
||||
String className = bundle.getString(KEY_CLASS_NAME);
|
||||
ControllerChangeHandler changeHandler = ClassUtils.newInstance(className);
|
||||
//noinspection ConstantConditions
|
||||
changeHandler.restoreFromBundle(bundle.getBundle(KEY_SAVED_STATE));
|
||||
return changeHandler;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static boolean completeHandlerImmediately(@NonNull String controllerInstanceId) {
|
||||
ChangeHandlerData changeHandlerData = inProgressChangeHandlers.get(controllerInstanceId);
|
||||
if (changeHandlerData != null) {
|
||||
changeHandlerData.changeHandler.completeImmediately();
|
||||
inProgressChangeHandlers.remove(controllerInstanceId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void abortOrComplete(@NonNull Controller toAbort, @Nullable Controller newController, @NonNull ControllerChangeHandler newChangeHandler) {
|
||||
ChangeHandlerData changeHandlerData = inProgressChangeHandlers.get(toAbort.getInstanceId());
|
||||
if (changeHandlerData != null) {
|
||||
if (changeHandlerData.isPush) {
|
||||
changeHandlerData.changeHandler.onAbortPush(newChangeHandler, newController);
|
||||
} else {
|
||||
changeHandlerData.changeHandler.completeImmediately();
|
||||
}
|
||||
|
||||
inProgressChangeHandlers.remove(toAbort.getInstanceId());
|
||||
}
|
||||
}
|
||||
|
||||
static void executeChange(@NonNull final ChangeTransaction transaction) {
|
||||
executeChange(transaction.to, transaction.from, transaction.isPush, transaction.container, transaction.changeHandler, transaction.listeners);
|
||||
}
|
||||
|
||||
private static void executeChange(@Nullable final Controller to, @Nullable final Controller from, final boolean isPush, @Nullable final ViewGroup container, @Nullable final ControllerChangeHandler inHandler, @NonNull final List<ControllerChangeListener> listeners) {
|
||||
if (container != null) {
|
||||
final ControllerChangeHandler handler;
|
||||
if (inHandler == null) {
|
||||
handler = new SimpleSwapChangeHandler();
|
||||
} else if (inHandler.hasBeenUsed && !inHandler.isReusable()) {
|
||||
handler = inHandler.copy();
|
||||
} else {
|
||||
handler = inHandler;
|
||||
}
|
||||
handler.hasBeenUsed = true;
|
||||
|
||||
if (from != null) {
|
||||
if (isPush) {
|
||||
completeHandlerImmediately(from.getInstanceId());
|
||||
} else {
|
||||
abortOrComplete(from, to, handler);
|
||||
}
|
||||
}
|
||||
|
||||
if (to != null) {
|
||||
inProgressChangeHandlers.put(to.getInstanceId(), new ChangeHandlerData(handler, isPush));
|
||||
}
|
||||
|
||||
for (ControllerChangeListener listener : listeners) {
|
||||
listener.onChangeStarted(to, from, isPush, container, handler);
|
||||
}
|
||||
|
||||
final ControllerChangeType toChangeType = isPush ? ControllerChangeType.PUSH_ENTER : ControllerChangeType.POP_ENTER;
|
||||
final ControllerChangeType fromChangeType = isPush ? ControllerChangeType.PUSH_EXIT : ControllerChangeType.POP_EXIT;
|
||||
|
||||
final View toView;
|
||||
if (to != null) {
|
||||
toView = to.inflate(container);
|
||||
to.changeStarted(handler, toChangeType);
|
||||
} else {
|
||||
toView = null;
|
||||
}
|
||||
|
||||
final View fromView;
|
||||
if (from != null) {
|
||||
fromView = from.getView();
|
||||
from.changeStarted(handler, fromChangeType);
|
||||
} else {
|
||||
fromView = null;
|
||||
}
|
||||
|
||||
handler.performChange(container, fromView, toView, isPush, new ControllerChangeCompletedListener() {
|
||||
@Override
|
||||
public void onChangeCompleted() {
|
||||
if (from != null) {
|
||||
from.changeEnded(handler, fromChangeType);
|
||||
}
|
||||
|
||||
if (to != null) {
|
||||
inProgressChangeHandlers.remove(to.getInstanceId());
|
||||
to.changeEnded(handler, toChangeType);
|
||||
}
|
||||
|
||||
for (ControllerChangeListener listener : listeners) {
|
||||
listener.onChangeCompleted(to, from, isPush, container, handler);
|
||||
}
|
||||
|
||||
if (handler.forceRemoveViewOnPush && fromView != null) {
|
||||
ViewParent fromParent = fromView.getParent();
|
||||
if (fromParent != null && fromParent instanceof ViewGroup) {
|
||||
((ViewGroup) fromParent).removeView(fromView);
|
||||
}
|
||||
}
|
||||
|
||||
if (handler.removesFromViewOnPush() && from != null) {
|
||||
from.setNeedsAttach(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected void onEnd() {
|
||||
}
|
||||
|
||||
public boolean removesFromViewOnPush() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setForceRemoveViewOnPush(boolean force) {
|
||||
forceRemoveViewOnPush = force;
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener interface useful for allowing external classes to be notified of change events.
|
||||
*/
|
||||
public interface ControllerChangeListener {
|
||||
/**
|
||||
* Called when a {@link ControllerChangeHandler} has started changing {@link Controller}s
|
||||
*
|
||||
* @param to The new Controller or {@code null} if no Controller is being transitioned to
|
||||
* @param from The old Controller or {@code null} if there was no Controller before this transition
|
||||
* @param isPush True if this is a push operation, or false if it's a pop.
|
||||
* @param container The containing ViewGroup
|
||||
* @param handler The change handler being used.
|
||||
*/
|
||||
void onChangeStarted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler);
|
||||
|
||||
/**
|
||||
* Called when a {@link ControllerChangeHandler} has completed changing {@link Controller}s
|
||||
*
|
||||
* @param to The new Controller or {@code null} if no Controller is being transitioned to
|
||||
* @param from The old Controller or {@code null} if there was no Controller before this transition
|
||||
* @param isPush True if this was a push operation, or false if it's a pop
|
||||
* @param container The containing ViewGroup
|
||||
* @param handler The change handler that was used.
|
||||
*/
|
||||
void onChangeCompleted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler);
|
||||
}
|
||||
|
||||
static class ChangeTransaction {
|
||||
@Nullable
|
||||
final Controller to;
|
||||
@Nullable
|
||||
final Controller from;
|
||||
final boolean isPush;
|
||||
@Nullable
|
||||
final ViewGroup container;
|
||||
@Nullable
|
||||
final ControllerChangeHandler changeHandler;
|
||||
@NonNull
|
||||
final List<ControllerChangeListener> listeners;
|
||||
|
||||
public ChangeTransaction(@Nullable Controller to, @Nullable Controller from, boolean isPush, @Nullable ViewGroup container, @Nullable ControllerChangeHandler changeHandler, @NonNull List<ControllerChangeListener> listeners) {
|
||||
this.to = to;
|
||||
this.from = from;
|
||||
this.isPush = isPush;
|
||||
this.container = container;
|
||||
this.changeHandler = changeHandler;
|
||||
this.listeners = listeners;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simplified listener for being notified when the change is complete. This MUST be called by any custom
|
||||
* ControllerChangeHandlers in order to ensure that {@link Controller}s will be notified of this change.
|
||||
*/
|
||||
public interface ControllerChangeCompletedListener {
|
||||
/**
|
||||
* Called when the change is complete.
|
||||
*/
|
||||
void onChangeCompleted();
|
||||
}
|
||||
|
||||
private static class ChangeHandlerData {
|
||||
public final ControllerChangeHandler changeHandler;
|
||||
public final boolean isPush;
|
||||
|
||||
public ChangeHandlerData(ControllerChangeHandler changeHandler, boolean isPush) {
|
||||
this.changeHandler = changeHandler;
|
||||
this.isPush = isPush;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
package com.bluelinelabs.conductor
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.BackEventCompat
|
||||
import androidx.annotation.RestrictTo
|
||||
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler
|
||||
import com.bluelinelabs.conductor.internal.ClassUtils
|
||||
|
||||
/**
|
||||
* ControllerChangeHandlers are responsible for swapping the View for one Controller to the View
|
||||
* of another. They can be useful for performing animations and transitions between Controllers. Several
|
||||
* default ControllerChangeHandlers are included.
|
||||
*/
|
||||
abstract class ControllerChangeHandler {
|
||||
private var forceRemoveViewOnPush = false
|
||||
|
||||
/**
|
||||
* Returns whether or not this is a reusable ControllerChangeHandler. Defaults to false and should
|
||||
* ONLY be overridden if there are absolutely no side effects to using this handler more than once.
|
||||
* In the case that a handler is not reusable, it will be copied using the [.copy] method
|
||||
* prior to use.
|
||||
*/
|
||||
open val isReusable: Boolean = false
|
||||
|
||||
/**
|
||||
* Returns whether or not this handler removes the `from` view from the container when performing a push.
|
||||
*
|
||||
* If this is true:
|
||||
* - This handler's implementation of [performChange] should remove `from` from `container`
|
||||
* before calling `changeListener.onChangeCompleted()`
|
||||
* - When a controller is pushed, the previous controller will be detached and its view will be destroyed
|
||||
*
|
||||
* If this is false:
|
||||
* - This handler's implementation of [performChange] should only remove `from` from `container`
|
||||
* when `isPush` is false
|
||||
* - When a controller is pushed, the previous controller will stay attached and its view will remain created
|
||||
* - When a view is recreated (e.g. after a configuration change), any controllers underneath a transaction
|
||||
* using this handler will have their view recreated and attached, even though they're not the top-most
|
||||
* controller
|
||||
*
|
||||
* If a controller pushed onto the backstack will completely cover the previous controller,
|
||||
* using a change handler with [removesFromViewOnPush] true should result in no visual interruption
|
||||
* to the user, while allowing the previous controller's view to be destroyed to reclaim resources.
|
||||
* If instead, the previous controller should still be visible after the new controller is pushed,
|
||||
* using a change handler with [removesFromViewOnPush] false will keep the previous controller's
|
||||
* view in the view hierarchy, where it can still be seen (and even interacted with).
|
||||
*/
|
||||
open val removesFromViewOnPush: Boolean = true
|
||||
|
||||
private var hasBeenUsed = false
|
||||
|
||||
open val enableOnBackGestureCallbacks = false
|
||||
|
||||
init {
|
||||
try {
|
||||
javaClass.getConstructor()
|
||||
} catch (e: Throwable) {
|
||||
throw RuntimeException("$javaClass does not have a default constructor.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for swapping Views from one Controller to another.
|
||||
*
|
||||
* @param container The container these Views are hosted in.
|
||||
* @param from The previous View in the container or `null` if there was no Controller before this transition
|
||||
* @param to The next View that should be put in the container or `null` if no Controller is being transitioned to
|
||||
* @param isPush True if this is a push transaction, false if it's a pop.
|
||||
* @param changeListener This listener must be called when any transitions or animations are completed.
|
||||
*/
|
||||
abstract fun performChange(
|
||||
container: ViewGroup,
|
||||
from: View?,
|
||||
to: View?,
|
||||
isPush: Boolean,
|
||||
changeListener: ControllerChangeCompletedListener,
|
||||
)
|
||||
|
||||
/**
|
||||
* Saves any data about this handler to a Bundle in case the application is killed.
|
||||
*
|
||||
* @param bundle The Bundle into which data should be stored.
|
||||
*/
|
||||
open fun saveToBundle(bundle: Bundle) {}
|
||||
|
||||
/**
|
||||
* Restores data that was saved in the [.saveToBundle] method.
|
||||
*
|
||||
* @param bundle The bundle that has data to be restored
|
||||
*/
|
||||
open fun restoreFromBundle(bundle: Bundle) {}
|
||||
|
||||
/**
|
||||
* Will be called on change handlers that push a controller if the controller being pushed is
|
||||
* popped before it has completed.
|
||||
*
|
||||
* @param newHandler The change handler that has caused this push to be aborted
|
||||
* @param newTop The Controller that will now be at the top of the backstack or `null`
|
||||
* if there will be no new Controller at the top
|
||||
*/
|
||||
open fun onAbortPush(newHandler: ControllerChangeHandler, newTop: Controller?) {}
|
||||
|
||||
/**
|
||||
* Will be called on change handlers that push a controller if the controller being pushed is
|
||||
* needs to be attached immediately, without any animations or transitions.
|
||||
*/
|
||||
open fun completeImmediately() {}
|
||||
|
||||
/**
|
||||
* Returns a copy of this ControllerChangeHandler. This method is internally used by the library, so
|
||||
* ensure it will return an exact copy of your handler if overriding. If not overriding, the handler
|
||||
* will be saved and restored from the Bundle format.
|
||||
*/
|
||||
open fun copy(): ControllerChangeHandler = fromBundle(toBundle())!!
|
||||
|
||||
open fun handleOnBackStarted(container: ViewGroup, to: View?, from: View, event: BackEventCompat) {}
|
||||
|
||||
open fun handleOnBackProgressed(container: ViewGroup, to: View?, from: View, event: BackEventCompat) {}
|
||||
|
||||
open fun handleOnBackCancelled(container: ViewGroup, to: View?, from: View) {}
|
||||
|
||||
protected open fun onEnd() {}
|
||||
|
||||
fun toBundle(): Bundle {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_CLASS_NAME, javaClass.name)
|
||||
|
||||
val savedState = Bundle()
|
||||
saveToBundle(savedState)
|
||||
bundle.putBundle(KEY_SAVED_STATE, savedState)
|
||||
|
||||
return bundle
|
||||
}
|
||||
|
||||
// Internal modifier plays weirdly with Java, which is what Router is still written in.
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||
fun setForceRemoveViewOnPush(forceRemoveViewOnPush: Boolean) {
|
||||
this.forceRemoveViewOnPush = forceRemoveViewOnPush
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener interface useful for allowing external classes to be notified of change events.
|
||||
*/
|
||||
interface ControllerChangeListener {
|
||||
/**
|
||||
* Called when a [ControllerChangeHandler] has started changing [Controller]s
|
||||
*
|
||||
* @param to The new Controller or `null` if no Controller is being transitioned to
|
||||
* @param from The old Controller or `null` if there was no Controller before this transition
|
||||
* @param isPush True if this is a push operation, or false if it's a pop.
|
||||
* @param container The containing ViewGroup
|
||||
* @param handler The change handler being used.
|
||||
*/
|
||||
fun onChangeStarted(
|
||||
to: Controller?,
|
||||
from: Controller?,
|
||||
isPush: Boolean,
|
||||
container: ViewGroup,
|
||||
handler: ControllerChangeHandler,
|
||||
)
|
||||
|
||||
/**
|
||||
* Called when a [ControllerChangeHandler] has completed changing [Controller]s
|
||||
*
|
||||
* @param to The new Controller or `null` if no Controller is being transitioned to
|
||||
* @param from The old Controller or `null` if there was no Controller before this transition
|
||||
* @param isPush True if this was a push operation, or false if it's a pop
|
||||
* @param container The containing ViewGroup
|
||||
* @param handler The change handler that was used.
|
||||
*/
|
||||
fun onChangeCompleted(
|
||||
to: Controller?,
|
||||
from: Controller?,
|
||||
isPush: Boolean,
|
||||
container: ViewGroup,
|
||||
handler: ControllerChangeHandler,
|
||||
)
|
||||
}
|
||||
|
||||
class ChangeTransaction(
|
||||
@JvmField val to: Controller?,
|
||||
@JvmField val from: Controller?,
|
||||
@JvmField val isPush: Boolean,
|
||||
@JvmField val container: ViewGroup?,
|
||||
@JvmField val changeHandler: ControllerChangeHandler?,
|
||||
@JvmField val listeners: List<ControllerChangeListener>,
|
||||
)
|
||||
|
||||
/**
|
||||
* A simplified listener for being notified when the change is complete. This MUST be called by any custom
|
||||
* ControllerChangeHandlers in order to ensure that [Controller]s will be notified of this change.
|
||||
*/
|
||||
interface ControllerChangeCompletedListener {
|
||||
/**
|
||||
* Called when the change is complete.
|
||||
*/
|
||||
fun onChangeCompleted()
|
||||
}
|
||||
|
||||
class ChangeHandlerData(val changeHandler: ControllerChangeHandler, val isPush: Boolean)
|
||||
|
||||
companion object {
|
||||
private const val KEY_CLASS_NAME = "ControllerChangeHandler.className"
|
||||
private const val KEY_SAVED_STATE = "ControllerChangeHandler.savedState"
|
||||
val inProgressChangeHandlers: MutableMap<String, ChangeHandlerData> = HashMap()
|
||||
|
||||
@JvmStatic
|
||||
fun fromBundle(bundle: Bundle?): ControllerChangeHandler? {
|
||||
val bundle = bundle ?: return null
|
||||
val className = bundle.getString(KEY_CLASS_NAME) ?: return null
|
||||
val savedState = bundle.getBundle(KEY_SAVED_STATE) ?: return null
|
||||
|
||||
return ClassUtils.newInstance<ControllerChangeHandler>(className)?.also {
|
||||
it.restoreFromBundle(savedState)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun completeHandlerImmediately(controllerInstanceId: String): Boolean {
|
||||
inProgressChangeHandlers[controllerInstanceId]?.let { changeHandlerData ->
|
||||
changeHandlerData.changeHandler.completeImmediately()
|
||||
inProgressChangeHandlers.remove(controllerInstanceId)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun abortOrComplete(toAbort: Controller, newController: Controller?, newChangeHandler: ControllerChangeHandler) {
|
||||
inProgressChangeHandlers[toAbort.getInstanceId()]?.let { changeHandlerData ->
|
||||
if (changeHandlerData.isPush) {
|
||||
changeHandlerData.changeHandler.onAbortPush(newChangeHandler, newController)
|
||||
} else {
|
||||
changeHandlerData.changeHandler.completeImmediately()
|
||||
}
|
||||
inProgressChangeHandlers.remove(toAbort.getInstanceId())
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun executeChange(transaction: ChangeTransaction) {
|
||||
executeChange(
|
||||
to = transaction.to,
|
||||
from = transaction.from,
|
||||
isPush = transaction.isPush,
|
||||
container = transaction.container,
|
||||
inHandler = transaction.changeHandler,
|
||||
listeners = transaction.listeners,
|
||||
)
|
||||
}
|
||||
|
||||
private fun executeChange(
|
||||
to: Controller?,
|
||||
from: Controller?,
|
||||
isPush: Boolean,
|
||||
container: ViewGroup?,
|
||||
inHandler: ControllerChangeHandler?,
|
||||
listeners: List<ControllerChangeListener>,
|
||||
) {
|
||||
container ?: return
|
||||
|
||||
val handler: ControllerChangeHandler = if (inHandler == null) {
|
||||
SimpleSwapChangeHandler()
|
||||
} else if (inHandler.hasBeenUsed && !inHandler.isReusable) {
|
||||
inHandler.copy()
|
||||
} else {
|
||||
inHandler
|
||||
}
|
||||
|
||||
handler.hasBeenUsed = true
|
||||
|
||||
if (from != null) {
|
||||
if (isPush) {
|
||||
completeHandlerImmediately(from.getInstanceId())
|
||||
} else {
|
||||
abortOrComplete(from, to, handler)
|
||||
}
|
||||
}
|
||||
|
||||
if (to != null) {
|
||||
inProgressChangeHandlers[to.getInstanceId()] = ChangeHandlerData(handler, isPush)
|
||||
}
|
||||
|
||||
listeners.forEach { it.onChangeStarted(to, from, isPush, container, handler) }
|
||||
|
||||
val toChangeType = if (isPush) ControllerChangeType.PUSH_ENTER else ControllerChangeType.POP_ENTER
|
||||
val fromChangeType = if (isPush) ControllerChangeType.PUSH_EXIT else ControllerChangeType.POP_EXIT
|
||||
val toView = to?.let {
|
||||
it.inflate(container).also {
|
||||
to.changeStarted(handler, toChangeType)
|
||||
}
|
||||
}
|
||||
|
||||
val fromView = from?.let {
|
||||
from.getView().also {
|
||||
from.changeStarted(handler, fromChangeType)
|
||||
}
|
||||
}
|
||||
|
||||
handler.performChange(
|
||||
container = container,
|
||||
from = fromView,
|
||||
to = toView,
|
||||
isPush = isPush,
|
||||
changeListener = object : ControllerChangeCompletedListener {
|
||||
override fun onChangeCompleted() {
|
||||
from?.changeEnded(handler, fromChangeType)
|
||||
|
||||
to?.let {
|
||||
inProgressChangeHandlers.remove(it.getInstanceId())
|
||||
it.changeEnded(handler, toChangeType)
|
||||
}
|
||||
|
||||
listeners.forEach { it.onChangeCompleted(to, from, isPush, container, handler) }
|
||||
|
||||
if (handler.forceRemoveViewOnPush) {
|
||||
(fromView?.parent as? ViewGroup)?.let {
|
||||
it.removeView(fromView)
|
||||
}
|
||||
}
|
||||
|
||||
if (handler.removesFromViewOnPush) {
|
||||
from?.needsAttach = false
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,9 +32,12 @@ class ControllerHostedRouter extends Router {
|
||||
private boolean isDetachFrozen;
|
||||
private boolean boundToContainer;
|
||||
|
||||
ControllerHostedRouter() { }
|
||||
ControllerHostedRouter() {
|
||||
popRootControllerMode = PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW;
|
||||
}
|
||||
|
||||
ControllerHostedRouter(int hostId, @Nullable String tag, boolean boundToContainer) {
|
||||
this();
|
||||
if (!boundToContainer && tag == null) {
|
||||
throw new IllegalStateException("ControllerHostedRouter can't be created without a tag if not bounded to its container");
|
||||
}
|
||||
@@ -46,6 +49,7 @@ class ControllerHostedRouter extends Router {
|
||||
final void setHostController(@NonNull Controller controller) {
|
||||
if (hostController == null) {
|
||||
hostController = controller;
|
||||
setOnBackPressedDispatcherEnabled(controller.onBackPressedDispatcherEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +63,7 @@ class ControllerHostedRouter extends Router {
|
||||
|
||||
hostController = controller;
|
||||
this.container = container;
|
||||
setOnBackPressedDispatcherEnabled(controller.onBackPressedDispatcherEnabled);
|
||||
|
||||
for (RouterTransaction transaction : backstack) {
|
||||
transaction.controller().setParentController(controller);
|
||||
@@ -127,7 +132,7 @@ class ControllerHostedRouter extends Router {
|
||||
// If we're pushing a transaction that will detach controllers to an unattached child
|
||||
// router, we need mark all other controllers as NOT needing to be reattached.
|
||||
if (to != null && !hostController.isAttached()) {
|
||||
if (to.pushChangeHandler() == null || to.pushChangeHandler().removesFromViewOnPush()) {
|
||||
if (to.pushChangeHandler() == null || to.pushChangeHandler().getRemovesFromViewOnPush()) {
|
||||
for (RouterTransaction transaction : backstack) {
|
||||
transaction.controller().setNeedsAttach(false);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,15 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentSender;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.activity.ComponentActivity;
|
||||
import androidx.activity.OnBackPressedDispatcher;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
@@ -34,20 +37,39 @@ import java.util.List;
|
||||
*/
|
||||
public abstract class Router {
|
||||
|
||||
private static final String TAG = "Conductor";
|
||||
private static final String KEY_BACKSTACK = "Router.backstack";
|
||||
private static final String KEY_POP_ROOT_CONTROLLER_MODE = "Router.popRootControllerMode";
|
||||
private static final String KEY_ON_BACK_PRESSED_DISPATCHER_ENABLED = "Router.onBackPressedDispatcherEnabled";
|
||||
|
||||
final Backstack backstack = new Backstack();
|
||||
private final List<ControllerChangeListener> changeListeners = new ArrayList<>();
|
||||
private final List<ChangeTransaction> pendingControllerChanges = new ArrayList<>();
|
||||
final List<Controller> destroyingControllers = new ArrayList<>();
|
||||
|
||||
private PopRootControllerMode popRootControllerMode = PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW;
|
||||
PopRootControllerMode popRootControllerMode;
|
||||
boolean onBackPressedDispatcherEnabled;
|
||||
boolean containerFullyAttached = false;
|
||||
boolean isActivityStopped = false;
|
||||
|
||||
ViewGroup container;
|
||||
|
||||
Router() {
|
||||
backstack.setOnBackstackUpdatedListener(() -> {
|
||||
if (!onBackPressedDispatcherEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Iterator<RouterTransaction> iterator = getBackstack().iterator();
|
||||
int index = 0;
|
||||
while (iterator.hasNext()) {
|
||||
iterator.next().controller().onBackPressedCallback.setEnabled(
|
||||
index++ > 0 || popRootControllerMode != PopRootControllerMode.NEVER
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this Router's host Activity or {@code null} if it has either not yet been attached to
|
||||
* an Activity or if the Activity has been destroyed.
|
||||
@@ -65,6 +87,22 @@ public abstract class Router {
|
||||
*/
|
||||
public abstract void onActivityResult(int requestCode, int resultCode, @Nullable Intent data);
|
||||
|
||||
/**
|
||||
* Returns the OnBackPressedDispatcher for this Router's host Activity or {@code null} if:
|
||||
* - This Router has not yet been attached to an Activity
|
||||
* - The attached Activity does not extend ComponentActivity
|
||||
* - The Activity has been destroyed
|
||||
*/
|
||||
@Nullable
|
||||
public OnBackPressedDispatcher getOnBackPressedDispatcher() {
|
||||
Activity activity = getActivity();
|
||||
if (activity instanceof ComponentActivity) {
|
||||
return ((ComponentActivity) activity).getOnBackPressedDispatcher();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This should be called by the host Activity when its onRequestPermissionsResult method is called. The call will be forwarded
|
||||
* to the {@link Controller} with the instanceId passed in.
|
||||
@@ -85,12 +123,20 @@ public abstract class Router {
|
||||
* This should be called by the host Activity when its onBackPressed method is called. The call will be forwarded
|
||||
* to its top {@link Controller}. If that controller doesn't handle it, then it will be popped.
|
||||
*
|
||||
* Note: This method has been deprecated and should be replaced with registering OnBackPressedCallbacks with
|
||||
* Controller instances.
|
||||
*
|
||||
* @return Whether or not a back action was handled by the Router
|
||||
*/
|
||||
@UiThread
|
||||
@Deprecated
|
||||
public boolean handleBack() {
|
||||
ThreadUtils.ensureMainThread();
|
||||
|
||||
return handleBackDispatch();
|
||||
}
|
||||
|
||||
protected boolean handleBackDispatch() {
|
||||
if (!backstack.isEmpty()) {
|
||||
//noinspection ConstantConditions
|
||||
if (backstack.peek().controller().handleBack()) {
|
||||
@@ -141,13 +187,13 @@ public abstract class Router {
|
||||
RouterTransaction nextTransaction = null;
|
||||
Iterator<RouterTransaction> iterator = backstack.iterator();
|
||||
ControllerChangeHandler topPushHandler = topTransaction != null ? topTransaction.pushChangeHandler() : null;
|
||||
final boolean needsNextTransactionAttach = topPushHandler != null ? !topPushHandler.removesFromViewOnPush() : false;
|
||||
final boolean needsNextTransactionAttach = topPushHandler != null ? !topPushHandler.getRemovesFromViewOnPush() : false;
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
RouterTransaction transaction = iterator.next();
|
||||
if (transaction.controller() == controller) {
|
||||
trackDestroyingController(transaction);
|
||||
iterator.remove();
|
||||
backstack.remove(transaction);
|
||||
removedTransaction = transaction;
|
||||
} else if (removedTransaction != null) {
|
||||
if (needsNextTransactionAttach && !transaction.controller().isAttached()) {
|
||||
@@ -203,8 +249,8 @@ public abstract class Router {
|
||||
final ControllerChangeHandler handler = transaction.pushChangeHandler();
|
||||
if (topTransaction != null) {
|
||||
//noinspection ConstantConditions
|
||||
final boolean oldHandlerRemovedViews = topTransaction.pushChangeHandler() == null || topTransaction.pushChangeHandler().removesFromViewOnPush();
|
||||
final boolean newHandlerRemovesViews = handler == null || handler.removesFromViewOnPush();
|
||||
final boolean oldHandlerRemovedViews = topTransaction.pushChangeHandler() == null || topTransaction.pushChangeHandler().getRemovesFromViewOnPush();
|
||||
final boolean newHandlerRemovesViews = handler == null || handler.getRemovesFromViewOnPush();
|
||||
if (!oldHandlerRemovedViews && newHandlerRemovesViews) {
|
||||
for (RouterTransaction visibleTransaction : getVisibleTransactions(backstack.iterator(), true)) {
|
||||
performControllerChange(null, visibleTransaction, true, handler);
|
||||
@@ -225,8 +271,9 @@ public abstract class Router {
|
||||
final List<RouterTransaction> poppedControllers = backstack.popAll();
|
||||
trackDestroyingControllers(poppedControllers);
|
||||
|
||||
RouterTransaction topTransaction = null;
|
||||
if (popViews && poppedControllers.size() > 0) {
|
||||
RouterTransaction topTransaction = poppedControllers.get(0);
|
||||
topTransaction = poppedControllers.get(0);
|
||||
topTransaction.controller().addLifecycleListener(new Controller.LifecycleListener() {
|
||||
@Override
|
||||
public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
|
||||
@@ -241,6 +288,16 @@ public abstract class Router {
|
||||
|
||||
performControllerChange(null, topTransaction, false, topTransaction.popChangeHandler());
|
||||
}
|
||||
|
||||
if (poppedControllers.size() > 0) {
|
||||
NoOpControllerChangeHandler changeHandler = new NoOpControllerChangeHandler();
|
||||
for (RouterTransaction routerTransaction : poppedControllers) {
|
||||
if (routerTransaction != topTransaction) {
|
||||
routerTransaction.controller().changeStarted(changeHandler, ControllerChangeType.POP_EXIT);
|
||||
routerTransaction.controller().changeEnded(changeHandler, ControllerChangeType.POP_EXIT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getContainerId() {
|
||||
@@ -263,7 +320,7 @@ public abstract class Router {
|
||||
|
||||
/**
|
||||
* Sets the method this router will use to handle back presses when there is only one controller left in the backstack.
|
||||
* Defaults to POP_ROOT_CONTROLLER_LEAVING_VIEW so that the developer can either finish its containing Activity or
|
||||
* Defaults to POP_ROOT_CONTROLLER_BUT_NOT_VIEW so that the developer can either finish its containing Activity or
|
||||
* otherwise hide its parent view without any strange artifacting.
|
||||
*/
|
||||
@NonNull
|
||||
@@ -272,6 +329,15 @@ public abstract class Router {
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Router setOnBackPressedDispatcherEnabled(boolean enabled) {
|
||||
if (backstack.getSize() > 0 && enabled != onBackPressedDispatcherEnabled) {
|
||||
Log.e(TAG, "setOnBackPressedDispatcherEnabled call ignored, as controllers with a different setting have already been pushed.");
|
||||
}
|
||||
onBackPressedDispatcherEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops all {@link Controller}s until only the root is left
|
||||
*
|
||||
@@ -662,15 +728,17 @@ public abstract class Router {
|
||||
Bundle backstackState = new Bundle();
|
||||
backstack.saveInstanceState(backstackState);
|
||||
|
||||
outState.putParcelable(KEY_BACKSTACK, backstackState);
|
||||
outState.putInt(KEY_POP_ROOT_CONTROLLER_MODE, popRootControllerMode.ordinal());
|
||||
outState.putBoolean(KEY_ON_BACK_PRESSED_DISPATCHER_ENABLED, onBackPressedDispatcherEnabled);
|
||||
outState.putParcelable(KEY_BACKSTACK, backstackState);
|
||||
}
|
||||
|
||||
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
Bundle backstackBundle = savedInstanceState.getParcelable(KEY_BACKSTACK);
|
||||
//noinspection ConstantConditions
|
||||
backstack.restoreInstanceState(backstackBundle);
|
||||
popRootControllerMode = PopRootControllerMode.values()[savedInstanceState.getInt(KEY_POP_ROOT_CONTROLLER_MODE)];
|
||||
onBackPressedDispatcherEnabled = savedInstanceState.getBoolean(KEY_ON_BACK_PRESSED_DISPATCHER_ENABLED);
|
||||
backstack.restoreInstanceState(backstackBundle);
|
||||
|
||||
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
|
||||
while (backstackIterator.hasNext()) {
|
||||
@@ -736,6 +804,37 @@ public abstract class Router {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
ChangeTransaction popTransaction(@NonNull Controller controller) {
|
||||
RouterTransaction from = null;
|
||||
RouterTransaction to = null;
|
||||
|
||||
Iterator<RouterTransaction> iterator = backstack.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
RouterTransaction transaction = iterator.next();
|
||||
if (transaction.controller() == controller) {
|
||||
from = transaction;
|
||||
if (iterator.hasNext()) {
|
||||
to = iterator.next();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (from == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ChangeTransaction(
|
||||
to != null ? to.controller() : null,
|
||||
from.controller(),
|
||||
false,
|
||||
container,
|
||||
from.popChangeHandler(),
|
||||
Collections.emptyList()
|
||||
);
|
||||
}
|
||||
|
||||
void watchContainerAttach() {
|
||||
container.post(new Runnable() {
|
||||
@Override
|
||||
@@ -865,7 +964,7 @@ public abstract class Router {
|
||||
to.setNeedsAttach(true);
|
||||
}
|
||||
pendingControllerChanges.add(transaction);
|
||||
} else if (from != null && (changeHandler == null || changeHandler.removesFromViewOnPush()) && !containerFullyAttached) {
|
||||
} else if (from != null && (changeHandler == null || changeHandler.getRemovesFromViewOnPush()) && !containerFullyAttached) {
|
||||
// If the change handler will remove the from view, we have to make sure the container is fully attached first so we avoid NPEs
|
||||
// within ViewGroup (details on issue #287). Post this to the container to ensure the attach is complete before we try to remove
|
||||
// anything.
|
||||
@@ -874,12 +973,7 @@ public abstract class Router {
|
||||
}
|
||||
pendingControllerChanges.add(transaction);
|
||||
if (container != null) {
|
||||
container.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
performPendingControllerChanges();
|
||||
}
|
||||
});
|
||||
container.post(this::performPendingControllerChanges);
|
||||
}
|
||||
} else {
|
||||
ControllerChangeHandler.executeChange(transaction);
|
||||
@@ -995,7 +1089,7 @@ public abstract class Router {
|
||||
transactions.add(transaction);
|
||||
}
|
||||
|
||||
visible = transaction.pushChangeHandler() != null && !transaction.pushChangeHandler().removesFromViewOnPush();
|
||||
visible = transaction.pushChangeHandler() != null && !transaction.pushChangeHandler().getRemovesFromViewOnPush();
|
||||
|
||||
if (onlyTop && !visible) {
|
||||
break;
|
||||
@@ -1046,7 +1140,7 @@ public abstract class Router {
|
||||
public enum PopRootControllerMode {
|
||||
/**
|
||||
* The Router will not pop the final controller left on the backstack when the back button is pressed
|
||||
* or when pop events are called. This mode should generally be used for Activity-hosted routers.
|
||||
* or when pop events are called. This mode is the default for Activity-hosted routers.
|
||||
*/
|
||||
NEVER,
|
||||
/**
|
||||
|
||||
@@ -16,8 +16,7 @@ private const val KEY_ATTACHED_TO_ROUTER = "RouterTransaction.attachedToRouter"
|
||||
/**
|
||||
* Metadata used for adding [Controller]s to a [Router].
|
||||
*/
|
||||
class RouterTransaction
|
||||
private constructor(
|
||||
class RouterTransaction private constructor(
|
||||
@get:JvmName("controller")
|
||||
val controller: Controller,
|
||||
private var tag: String? = null,
|
||||
@@ -29,7 +28,6 @@ private constructor(
|
||||
var transactionIndex: Int = INVALID_INDEX
|
||||
) {
|
||||
|
||||
|
||||
@RestrictTo(LIBRARY)
|
||||
internal constructor(bundle: Bundle) : this(
|
||||
controller = Controller.newInstance(bundle.getBundle(KEY_VIEW_CONTROLLER_BUNDLE)!!),
|
||||
|
||||
-257
@@ -1,257 +0,0 @@
|
||||
package com.bluelinelabs.conductor.changehandler;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.Animator.AnimatorListener;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
import com.bluelinelabs.conductor.Controller;
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler;
|
||||
|
||||
/**
|
||||
* A base {@link ControllerChangeHandler} that facilitates using {@link android.animation.Animator}s to replace Controller Views
|
||||
*/
|
||||
public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
|
||||
|
||||
private static final String KEY_DURATION = "AnimatorChangeHandler.duration";
|
||||
private static final String KEY_REMOVES_FROM_ON_PUSH = "AnimatorChangeHandler.removesFromViewOnPush";
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public static final long DEFAULT_ANIMATION_DURATION = -1;
|
||||
|
||||
private long animationDuration;
|
||||
boolean removesFromViewOnPush;
|
||||
boolean canceled;
|
||||
boolean needsImmediateCompletion;
|
||||
private boolean completed;
|
||||
Animator animator;
|
||||
private OnAnimationReadyOrAbortedListener onAnimationReadyOrAbortedListener;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public AnimatorChangeHandler() {
|
||||
this(DEFAULT_ANIMATION_DURATION, true);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public AnimatorChangeHandler(boolean removesFromViewOnPush) {
|
||||
this(DEFAULT_ANIMATION_DURATION, removesFromViewOnPush);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public AnimatorChangeHandler(long duration) {
|
||||
this(duration, true);
|
||||
}
|
||||
|
||||
public AnimatorChangeHandler(long duration, boolean removesFromViewOnPush) {
|
||||
animationDuration = duration;
|
||||
this.removesFromViewOnPush = removesFromViewOnPush;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveToBundle(@NonNull Bundle bundle) {
|
||||
super.saveToBundle(bundle);
|
||||
bundle.putLong(KEY_DURATION, animationDuration);
|
||||
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, removesFromViewOnPush);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreFromBundle(@NonNull Bundle bundle) {
|
||||
super.restoreFromBundle(bundle);
|
||||
animationDuration = bundle.getLong(KEY_DURATION);
|
||||
removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
|
||||
super.onAbortPush(newHandler, newTop);
|
||||
|
||||
canceled = true;
|
||||
if (animator != null) {
|
||||
animator.cancel();
|
||||
} else if (onAnimationReadyOrAbortedListener != null) {
|
||||
onAnimationReadyOrAbortedListener.onReadyOrAborted();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void completeImmediately() {
|
||||
super.completeImmediately();
|
||||
|
||||
needsImmediateCompletion = true;
|
||||
if (animator != null) {
|
||||
animator.end();
|
||||
} else if (onAnimationReadyOrAbortedListener != null) {
|
||||
onAnimationReadyOrAbortedListener.onReadyOrAborted();
|
||||
}
|
||||
}
|
||||
|
||||
public long getAnimationDuration() {
|
||||
return animationDuration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removesFromViewOnPush() {
|
||||
return removesFromViewOnPush;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be overridden to return the Animator to use while replacing Views.
|
||||
*
|
||||
* @param container The container these Views are hosted in.
|
||||
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
|
||||
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
|
||||
* @param isPush True if this is a push transaction, false if it's a pop.
|
||||
* @param toAddedToContainer True if the "to" view was added to the container as a part of this ChangeHandler. False if it was already in the hierarchy.
|
||||
*/
|
||||
@NonNull
|
||||
protected abstract Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer);
|
||||
|
||||
/**
|
||||
* Will be called after the animation is complete to reset the View that was removed to its pre-animation state.
|
||||
*/
|
||||
protected abstract void resetFromView(@NonNull View from);
|
||||
|
||||
@Override
|
||||
public final void performChange(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
|
||||
boolean readyToAnimate = true;
|
||||
final boolean addingToView = to != null && to.getParent() == null;
|
||||
|
||||
if (addingToView) {
|
||||
if (isPush || from == null) {
|
||||
container.addView(to);
|
||||
} else if (to.getParent() == null) {
|
||||
container.addView(to, container.indexOfChild(from));
|
||||
}
|
||||
|
||||
if (to.getWidth() <= 0 && to.getHeight() <= 0) {
|
||||
readyToAnimate = false;
|
||||
onAnimationReadyOrAbortedListener = new OnAnimationReadyOrAbortedListener(container, from, to, isPush, true, changeListener);
|
||||
to.getViewTreeObserver().addOnPreDrawListener(onAnimationReadyOrAbortedListener);
|
||||
}
|
||||
}
|
||||
|
||||
if (readyToAnimate) {
|
||||
performAnimation(container, from, to, isPush, addingToView, changeListener);
|
||||
}
|
||||
}
|
||||
|
||||
void complete(@NonNull ControllerChangeCompletedListener changeListener, @Nullable AnimatorListener animatorListener) {
|
||||
if (!completed) {
|
||||
completed = true;
|
||||
changeListener.onChangeCompleted();
|
||||
}
|
||||
|
||||
if (animator != null) {
|
||||
if (animatorListener != null) {
|
||||
animator.removeListener(animatorListener);
|
||||
}
|
||||
animator.cancel();
|
||||
animator = null;
|
||||
}
|
||||
|
||||
onAnimationReadyOrAbortedListener = null;
|
||||
}
|
||||
|
||||
void performAnimation(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, final boolean toAddedToContainer, @NonNull final ControllerChangeCompletedListener changeListener) {
|
||||
if (canceled) {
|
||||
complete(changeListener, null);
|
||||
return;
|
||||
}
|
||||
if (needsImmediateCompletion) {
|
||||
if (from != null && (!isPush || removesFromViewOnPush)) {
|
||||
container.removeView(from);
|
||||
}
|
||||
complete(changeListener, null);
|
||||
if (isPush && from != null) {
|
||||
resetFromView(from);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
animator = getAnimator(container, from, to, isPush, toAddedToContainer);
|
||||
|
||||
if (animationDuration > 0) {
|
||||
animator.setDuration(animationDuration);
|
||||
}
|
||||
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
if (from != null) {
|
||||
resetFromView(from);
|
||||
}
|
||||
|
||||
if (to != null && to.getParent() == container) {
|
||||
container.removeView(to);
|
||||
}
|
||||
|
||||
complete(changeListener, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
if (!canceled && animator != null) {
|
||||
if (from != null && (!isPush || removesFromViewOnPush)) {
|
||||
container.removeView(from);
|
||||
}
|
||||
|
||||
complete(changeListener, this);
|
||||
|
||||
if (isPush && from != null) {
|
||||
resetFromView(from);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
animator.start();
|
||||
}
|
||||
|
||||
private class OnAnimationReadyOrAbortedListener implements ViewTreeObserver.OnPreDrawListener {
|
||||
@NonNull final ViewGroup container;
|
||||
@Nullable final View from;
|
||||
@Nullable final View to;
|
||||
final boolean isPush;
|
||||
final boolean addingToView;
|
||||
@NonNull final ControllerChangeCompletedListener changeListener;
|
||||
private boolean hasRun;
|
||||
|
||||
OnAnimationReadyOrAbortedListener(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean addingToView, @NonNull ControllerChangeCompletedListener changeListener) {
|
||||
this.container = container;
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.isPush = isPush;
|
||||
this.addingToView = addingToView;
|
||||
this.changeListener = changeListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreDraw() {
|
||||
onReadyOrAborted();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void onReadyOrAborted() {
|
||||
if (!hasRun) {
|
||||
hasRun = true;
|
||||
|
||||
if (to != null) {
|
||||
final ViewTreeObserver observer = to.getViewTreeObserver();
|
||||
if (observer.isAlive()) {
|
||||
observer.removeOnPreDrawListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
performAnimation(container, from, to, isPush, addingToView, changeListener);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
package com.bluelinelabs.conductor.changehandler
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
|
||||
/**
|
||||
* A base [ControllerChangeHandler] that facilitates using [android.animation.Animator]s to replace Controller Views
|
||||
*/
|
||||
abstract class AnimatorChangeHandler @JvmOverloads constructor(
|
||||
animationDuration: Long = DEFAULT_ANIMATION_DURATION,
|
||||
removesFromViewOnPush: Boolean = true,
|
||||
) : ControllerChangeHandler() {
|
||||
|
||||
var animationDuration: Long = animationDuration
|
||||
private set
|
||||
|
||||
private var canceled = false
|
||||
private var needsImmediateCompletion = false
|
||||
private var completed = false
|
||||
private var animator: Animator? = null
|
||||
private var onAnimationReadyOrAbortedListener: OnAnimationReadyOrAbortedListener? = null
|
||||
|
||||
private var _removesFromViewOnPush = removesFromViewOnPush
|
||||
override val removesFromViewOnPush: Boolean
|
||||
get() = _removesFromViewOnPush
|
||||
|
||||
constructor(removesFromViewOnPush: Boolean = true) : this(
|
||||
animationDuration = DEFAULT_ANIMATION_DURATION,
|
||||
removesFromViewOnPush = removesFromViewOnPush,
|
||||
)
|
||||
|
||||
override fun saveToBundle(bundle: Bundle) {
|
||||
super.saveToBundle(bundle)
|
||||
bundle.putLong(KEY_DURATION, animationDuration)
|
||||
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, removesFromViewOnPush)
|
||||
}
|
||||
|
||||
override fun restoreFromBundle(bundle: Bundle) {
|
||||
super.restoreFromBundle(bundle)
|
||||
animationDuration = bundle.getLong(KEY_DURATION)
|
||||
_removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH)
|
||||
}
|
||||
|
||||
override fun onAbortPush(newHandler: ControllerChangeHandler, newTop: Controller?) {
|
||||
super.onAbortPush(newHandler, newTop)
|
||||
|
||||
canceled = true
|
||||
if (animator != null) {
|
||||
animator!!.cancel()
|
||||
} else if (onAnimationReadyOrAbortedListener != null) {
|
||||
onAnimationReadyOrAbortedListener!!.onReadyOrAborted()
|
||||
}
|
||||
}
|
||||
|
||||
override fun completeImmediately() {
|
||||
super.completeImmediately()
|
||||
|
||||
needsImmediateCompletion = true
|
||||
if (animator != null) {
|
||||
animator!!.end()
|
||||
} else if (onAnimationReadyOrAbortedListener != null) {
|
||||
onAnimationReadyOrAbortedListener!!.onReadyOrAborted()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be overridden to return the Animator to use while replacing Views.
|
||||
*
|
||||
* @param container The container these Views are hosted in.
|
||||
* @param from The previous View in the container or `null` if there was no Controller before this transition
|
||||
* @param to The next View that should be put in the container or `null` if no Controller is being transitioned to
|
||||
* @param isPush True if this is a push transaction, false if it's a pop.
|
||||
* @param toAddedToContainer True if the "to" view was added to the container as a part of this ChangeHandler. False if it was already in the hierarchy.
|
||||
*/
|
||||
protected abstract fun getAnimator(
|
||||
container: ViewGroup,
|
||||
from: View?,
|
||||
to: View?,
|
||||
isPush: Boolean,
|
||||
toAddedToContainer: Boolean,
|
||||
): Animator
|
||||
|
||||
/**
|
||||
* Will be called after the animation is complete to reset the View that was removed to its pre-animation state.
|
||||
*/
|
||||
protected abstract fun resetFromView(from: View)
|
||||
|
||||
override fun performChange(
|
||||
container: ViewGroup,
|
||||
from: View?,
|
||||
to: View?,
|
||||
isPush: Boolean,
|
||||
changeListener: ControllerChangeCompletedListener,
|
||||
) {
|
||||
var readyToAnimate = true
|
||||
val addingToView = to != null && to.parent == null
|
||||
|
||||
if (addingToView) {
|
||||
if (isPush || from == null) {
|
||||
container.addView(to)
|
||||
} else if (to!!.parent == null) {
|
||||
container.addView(to, container.indexOfChild(from))
|
||||
}
|
||||
if (to!!.width <= 0 && to.height <= 0) {
|
||||
readyToAnimate = false
|
||||
onAnimationReadyOrAbortedListener = OnAnimationReadyOrAbortedListener(container, from, to, isPush, true, changeListener)
|
||||
to.viewTreeObserver.addOnPreDrawListener(onAnimationReadyOrAbortedListener)
|
||||
}
|
||||
}
|
||||
|
||||
if (readyToAnimate) {
|
||||
performAnimation(container, from, to, isPush, addingToView, changeListener)
|
||||
}
|
||||
}
|
||||
|
||||
fun complete(changeListener: ControllerChangeCompletedListener, animatorListener: Animator.AnimatorListener?) {
|
||||
if (!completed) {
|
||||
completed = true
|
||||
changeListener.onChangeCompleted()
|
||||
}
|
||||
|
||||
if (animator != null) {
|
||||
if (animatorListener != null) {
|
||||
animator!!.removeListener(animatorListener)
|
||||
}
|
||||
animator!!.cancel()
|
||||
animator = null
|
||||
}
|
||||
|
||||
onAnimationReadyOrAbortedListener = null
|
||||
}
|
||||
|
||||
fun performAnimation(
|
||||
container: ViewGroup,
|
||||
from: View?,
|
||||
to: View?,
|
||||
isPush: Boolean,
|
||||
toAddedToContainer: Boolean,
|
||||
changeListener: ControllerChangeCompletedListener,
|
||||
) {
|
||||
if (canceled) {
|
||||
complete(changeListener, null)
|
||||
return
|
||||
}
|
||||
|
||||
if (needsImmediateCompletion) {
|
||||
if (from != null && (!isPush || removesFromViewOnPush)) {
|
||||
container.removeView(from)
|
||||
}
|
||||
complete(changeListener, null)
|
||||
if (isPush && from != null) {
|
||||
resetFromView(from)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
animator = getAnimator(container, from, to, isPush, toAddedToContainer)
|
||||
if (animationDuration > 0) {
|
||||
animator!!.duration = animationDuration
|
||||
}
|
||||
|
||||
animator!!.addListener(
|
||||
object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationCancel(animation: Animator) {
|
||||
from?.let { resetFromView(it) }
|
||||
if (to != null && to.parent === container) {
|
||||
container.removeView(to)
|
||||
}
|
||||
complete(changeListener, this)
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
if (!canceled && animator != null) {
|
||||
if (from != null && (!isPush || removesFromViewOnPush)) {
|
||||
container.removeView(from)
|
||||
}
|
||||
complete(changeListener, this)
|
||||
if (isPush && from != null) {
|
||||
resetFromView(from)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
animator!!.start()
|
||||
}
|
||||
|
||||
private inner class OnAnimationReadyOrAbortedListener constructor(
|
||||
val container: ViewGroup,
|
||||
val from: View?,
|
||||
val to: View?,
|
||||
val isPush: Boolean,
|
||||
val addingToView: Boolean,
|
||||
val changeListener: ControllerChangeCompletedListener,
|
||||
) : ViewTreeObserver.OnPreDrawListener {
|
||||
|
||||
private var hasRun = false
|
||||
override fun onPreDraw(): Boolean {
|
||||
onReadyOrAborted()
|
||||
return true
|
||||
}
|
||||
|
||||
fun onReadyOrAborted() {
|
||||
if (!hasRun) {
|
||||
hasRun = true
|
||||
if (to != null) {
|
||||
val observer = to.viewTreeObserver
|
||||
if (observer.isAlive) {
|
||||
observer.removeOnPreDrawListener(this)
|
||||
}
|
||||
}
|
||||
performAnimation(container, from, to, isPush, addingToView, changeListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_DURATION = "AnimatorChangeHandler.duration"
|
||||
private const val KEY_REMOVES_FROM_ON_PUSH = "AnimatorChangeHandler.removesFromViewOnPush"
|
||||
const val DEFAULT_ANIMATION_DURATION: Long = -1
|
||||
}
|
||||
}
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
package com.bluelinelabs.conductor.changehandler;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler;
|
||||
|
||||
/**
|
||||
* An {@link AnimatorChangeHandler} that will cross fade two views
|
||||
*/
|
||||
public class FadeChangeHandler extends AnimatorChangeHandler {
|
||||
|
||||
public FadeChangeHandler() { }
|
||||
|
||||
public FadeChangeHandler(boolean removesFromViewOnPush) {
|
||||
super(removesFromViewOnPush);
|
||||
}
|
||||
|
||||
public FadeChangeHandler(long duration) {
|
||||
super(duration);
|
||||
}
|
||||
|
||||
public FadeChangeHandler(long duration, boolean removesFromViewOnPush) {
|
||||
super(duration, removesFromViewOnPush);
|
||||
}
|
||||
|
||||
@Override @NonNull
|
||||
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
|
||||
AnimatorSet animator = new AnimatorSet();
|
||||
if (to != null) {
|
||||
float start = toAddedToContainer ? 0 : to.getAlpha();
|
||||
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1));
|
||||
}
|
||||
|
||||
if (from != null && (!isPush || removesFromViewOnPush())) {
|
||||
animator.play(ObjectAnimator.ofFloat(from, View.ALPHA, 0));
|
||||
}
|
||||
|
||||
return animator;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetFromView(@NonNull View from) {
|
||||
from.setAlpha(1);
|
||||
}
|
||||
|
||||
@Override @NonNull
|
||||
public ControllerChangeHandler copy() {
|
||||
return new FadeChangeHandler(getAnimationDuration(), removesFromViewOnPush());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.bluelinelabs.conductor.changehandler
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
|
||||
/**
|
||||
* An [AnimatorChangeHandler] that will cross fade two views
|
||||
*/
|
||||
class FadeChangeHandler : AnimatorChangeHandler {
|
||||
constructor() : super()
|
||||
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
|
||||
constructor(duration: Long) : super(duration)
|
||||
constructor(duration: Long, removesFromViewOnPush: Boolean) : super(duration, removesFromViewOnPush)
|
||||
|
||||
override fun getAnimator(
|
||||
container: ViewGroup,
|
||||
from: View?,
|
||||
to: View?,
|
||||
isPush: Boolean,
|
||||
toAddedToContainer: Boolean,
|
||||
): Animator {
|
||||
val animator = AnimatorSet()
|
||||
if (to != null) {
|
||||
val start = if (toAddedToContainer) 0F else to.alpha
|
||||
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1f))
|
||||
}
|
||||
if (from != null && (!isPush || removesFromViewOnPush)) {
|
||||
animator.play(ObjectAnimator.ofFloat(from, View.ALPHA, 0f))
|
||||
}
|
||||
return animator
|
||||
}
|
||||
|
||||
override fun resetFromView(from: View) {
|
||||
from.alpha = 1f
|
||||
}
|
||||
|
||||
override fun copy(): ControllerChangeHandler = FadeChangeHandler(animationDuration, removesFromViewOnPush)
|
||||
}
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
package com.bluelinelabs.conductor.changehandler;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler;
|
||||
|
||||
/**
|
||||
* An {@link AnimatorChangeHandler} that will slide the views left or right, depending on if it's a push or pop.
|
||||
*/
|
||||
public class HorizontalChangeHandler extends AnimatorChangeHandler {
|
||||
|
||||
public HorizontalChangeHandler() { }
|
||||
|
||||
public HorizontalChangeHandler(boolean removesFromViewOnPush) {
|
||||
super(removesFromViewOnPush);
|
||||
}
|
||||
|
||||
public HorizontalChangeHandler(long duration) {
|
||||
super(duration);
|
||||
}
|
||||
|
||||
public HorizontalChangeHandler(long duration, boolean removesFromViewOnPush) {
|
||||
super(duration, removesFromViewOnPush);
|
||||
}
|
||||
|
||||
@Override @NonNull
|
||||
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
|
||||
AnimatorSet animatorSet = new AnimatorSet();
|
||||
|
||||
if (isPush) {
|
||||
if (from != null) {
|
||||
animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, -from.getWidth()));
|
||||
}
|
||||
if (to != null) {
|
||||
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, to.getWidth(), 0));
|
||||
}
|
||||
} else {
|
||||
if (from != null) {
|
||||
animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, from.getWidth()));
|
||||
}
|
||||
if (to != null) {
|
||||
// Allow this to have a nice transition when coming off an aborted push animation
|
||||
float fromLeft = from != null ? from.getTranslationX() : 0;
|
||||
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, fromLeft - to.getWidth(), 0));
|
||||
}
|
||||
}
|
||||
|
||||
return animatorSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetFromView(@NonNull View from) {
|
||||
from.setTranslationX(0);
|
||||
}
|
||||
|
||||
@Override @NonNull
|
||||
public ControllerChangeHandler copy() {
|
||||
return new HorizontalChangeHandler(getAnimationDuration(), removesFromViewOnPush());
|
||||
}
|
||||
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.bluelinelabs.conductor.changehandler
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
|
||||
/**
|
||||
* An [AnimatorChangeHandler] that will slide the views left or right, depending on if it's a push or pop.
|
||||
*/
|
||||
class HorizontalChangeHandler : AnimatorChangeHandler {
|
||||
constructor() : super()
|
||||
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
|
||||
constructor(duration: Long) : super(duration)
|
||||
constructor(duration: Long, removesFromViewOnPush: Boolean) : super(duration, removesFromViewOnPush)
|
||||
|
||||
override fun getAnimator(
|
||||
container: ViewGroup,
|
||||
from: View?,
|
||||
to: View?,
|
||||
isPush: Boolean,
|
||||
toAddedToContainer: Boolean,
|
||||
): Animator {
|
||||
val animatorSet = AnimatorSet()
|
||||
if (isPush) {
|
||||
if (from != null) {
|
||||
animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, -from.width.toFloat()))
|
||||
}
|
||||
if (to != null) {
|
||||
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, to.width.toFloat(), 0f))
|
||||
}
|
||||
} else {
|
||||
if (from != null) {
|
||||
animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, from.width.toFloat()))
|
||||
}
|
||||
if (to != null) {
|
||||
// Allow this to have a nice transition when coming off an aborted push animation
|
||||
val fromLeft = from?.translationX ?: 0F
|
||||
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, fromLeft - to.width, 0f))
|
||||
}
|
||||
}
|
||||
return animatorSet
|
||||
}
|
||||
|
||||
override fun resetFromView(from: View) {
|
||||
from.translationX = 0f
|
||||
}
|
||||
|
||||
override fun copy(): ControllerChangeHandler = HorizontalChangeHandler(animationDuration, removesFromViewOnPush)
|
||||
}
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
package com.bluelinelabs.conductor.changehandler;
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.View;
|
||||
import android.view.View.OnAttachStateChangeListener;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.bluelinelabs.conductor.Controller;
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler;
|
||||
|
||||
/**
|
||||
* A {@link ControllerChangeHandler} that will instantly swap Views with no animations or transitions.
|
||||
*/
|
||||
public class SimpleSwapChangeHandler extends ControllerChangeHandler implements OnAttachStateChangeListener {
|
||||
|
||||
private static final String KEY_REMOVES_FROM_ON_PUSH = "SimpleSwapChangeHandler.removesFromViewOnPush";
|
||||
|
||||
private boolean removesFromViewOnPush;
|
||||
|
||||
private boolean canceled;
|
||||
|
||||
private ViewGroup container;
|
||||
private ControllerChangeCompletedListener changeListener;
|
||||
|
||||
public SimpleSwapChangeHandler() {
|
||||
this(true);
|
||||
}
|
||||
|
||||
public SimpleSwapChangeHandler(boolean removesFromViewOnPush) {
|
||||
this.removesFromViewOnPush = removesFromViewOnPush;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveToBundle(@NonNull Bundle bundle) {
|
||||
super.saveToBundle(bundle);
|
||||
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, removesFromViewOnPush);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreFromBundle(@NonNull Bundle bundle) {
|
||||
super.restoreFromBundle(bundle);
|
||||
removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
|
||||
super.onAbortPush(newHandler, newTop);
|
||||
|
||||
canceled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void completeImmediately() {
|
||||
if (changeListener != null) {
|
||||
changeListener.onChangeCompleted();
|
||||
changeListener = null;
|
||||
|
||||
container.removeOnAttachStateChangeListener(this);
|
||||
container = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
|
||||
if (!canceled) {
|
||||
if (from != null && (!isPush || removesFromViewOnPush)) {
|
||||
container.removeView(from);
|
||||
}
|
||||
|
||||
if (to != null && to.getParent() == null) {
|
||||
container.addView(to);
|
||||
}
|
||||
}
|
||||
|
||||
if (container.getWindowToken() != null) {
|
||||
changeListener.onChangeCompleted();
|
||||
} else {
|
||||
this.changeListener = changeListener;
|
||||
this.container = container;
|
||||
container.addOnAttachStateChangeListener(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removesFromViewOnPush() {
|
||||
return removesFromViewOnPush;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAttachedToWindow(@NonNull View v) {
|
||||
v.removeOnAttachStateChangeListener(this);
|
||||
|
||||
if (changeListener != null) {
|
||||
changeListener.onChangeCompleted();
|
||||
changeListener = null;
|
||||
container = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(@NonNull View v) { }
|
||||
|
||||
@Override @NonNull
|
||||
public ControllerChangeHandler copy() {
|
||||
return new SimpleSwapChangeHandler(removesFromViewOnPush());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReusable() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package com.bluelinelabs.conductor.changehandler
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
|
||||
/**
|
||||
* A [ControllerChangeHandler] that will instantly swap Views with no animations or transitions.
|
||||
*/
|
||||
class SimpleSwapChangeHandler @JvmOverloads constructor(
|
||||
removesFromViewOnPush: Boolean = true,
|
||||
) : ControllerChangeHandler(), View.OnAttachStateChangeListener {
|
||||
|
||||
private var _removesFromViewOnPush = removesFromViewOnPush
|
||||
override val removesFromViewOnPush: Boolean
|
||||
get() = _removesFromViewOnPush
|
||||
|
||||
override val isReusable = true
|
||||
|
||||
private var canceled = false
|
||||
private var container: ViewGroup? = null
|
||||
private var changeListener: ControllerChangeCompletedListener? = null
|
||||
|
||||
override fun saveToBundle(bundle: Bundle) {
|
||||
super.saveToBundle(bundle)
|
||||
bundle.putBoolean(KEY_REMOVES_FROM_ON_PUSH, removesFromViewOnPush)
|
||||
}
|
||||
|
||||
override fun restoreFromBundle(bundle: Bundle) {
|
||||
super.restoreFromBundle(bundle)
|
||||
_removesFromViewOnPush = bundle.getBoolean(KEY_REMOVES_FROM_ON_PUSH)
|
||||
}
|
||||
|
||||
override fun onAbortPush(newHandler: ControllerChangeHandler, newTop: Controller?) {
|
||||
super.onAbortPush(newHandler, newTop)
|
||||
canceled = true
|
||||
}
|
||||
|
||||
override fun completeImmediately() {
|
||||
changeListener?.onChangeCompleted()
|
||||
changeListener = null
|
||||
|
||||
container?.removeOnAttachStateChangeListener(this)
|
||||
container = null
|
||||
}
|
||||
|
||||
override fun performChange(
|
||||
container: ViewGroup,
|
||||
from: View?,
|
||||
to: View?,
|
||||
isPush: Boolean,
|
||||
changeListener: ControllerChangeCompletedListener,
|
||||
) {
|
||||
if (canceled) return
|
||||
|
||||
if (from != null && (!isPush || removesFromViewOnPush)) {
|
||||
container.removeView(from)
|
||||
}
|
||||
if (to != null && to.parent == null) {
|
||||
container.addView(to)
|
||||
}
|
||||
|
||||
if (container.windowToken != null) {
|
||||
changeListener.onChangeCompleted()
|
||||
} else {
|
||||
this.changeListener = changeListener
|
||||
this.container = container
|
||||
container.addOnAttachStateChangeListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(v: View) {
|
||||
v.removeOnAttachStateChangeListener(this)
|
||||
|
||||
changeListener?.onChangeCompleted()
|
||||
changeListener = null
|
||||
container?.removeOnAttachStateChangeListener(this)
|
||||
container = null
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(v: View) = Unit
|
||||
|
||||
override fun copy(): ControllerChangeHandler = SimpleSwapChangeHandler(removesFromViewOnPush)
|
||||
}
|
||||
|
||||
private const val KEY_REMOVES_FROM_ON_PUSH = "SimpleSwapChangeHandler.removesFromViewOnPush"
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
package com.bluelinelabs.conductor.changehandler;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An {@link AnimatorChangeHandler} that will slide either slide a new View up or slide an old View down,
|
||||
* depending on whether a push or pop change is happening.
|
||||
*/
|
||||
public class VerticalChangeHandler extends AnimatorChangeHandler {
|
||||
|
||||
public VerticalChangeHandler() { }
|
||||
|
||||
public VerticalChangeHandler(boolean removesFromViewOnPush) {
|
||||
super(removesFromViewOnPush);
|
||||
}
|
||||
|
||||
public VerticalChangeHandler(long duration) {
|
||||
super(duration);
|
||||
}
|
||||
|
||||
public VerticalChangeHandler(long duration, boolean removesFromViewOnPush) {
|
||||
super(duration, removesFromViewOnPush);
|
||||
}
|
||||
|
||||
@Override @NonNull
|
||||
protected Animator getAnimator(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, boolean toAddedToContainer) {
|
||||
AnimatorSet animator = new AnimatorSet();
|
||||
List<Animator> viewAnimators = new ArrayList<>();
|
||||
|
||||
if (isPush && to != null) {
|
||||
viewAnimators.add(ObjectAnimator.ofFloat(to, View.TRANSLATION_Y, to.getHeight(), 0));
|
||||
} else if (!isPush && from != null) {
|
||||
viewAnimators.add(ObjectAnimator.ofFloat(from, View.TRANSLATION_Y, from.getHeight()));
|
||||
}
|
||||
|
||||
animator.playTogether(viewAnimators);
|
||||
return animator;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetFromView(@NonNull View from) { }
|
||||
|
||||
@Override @NonNull
|
||||
public ControllerChangeHandler copy() {
|
||||
return new VerticalChangeHandler(getAnimationDuration(), removesFromViewOnPush());
|
||||
}
|
||||
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.bluelinelabs.conductor.changehandler
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
|
||||
/**
|
||||
* An [AnimatorChangeHandler] that will slide either slide a new View up or slide an old View down,
|
||||
* depending on whether a push or pop change is happening.
|
||||
*/
|
||||
class VerticalChangeHandler : AnimatorChangeHandler {
|
||||
constructor() : super()
|
||||
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
|
||||
constructor(duration: Long) : super(duration)
|
||||
constructor(duration: Long, removesFromViewOnPush: Boolean) : super(duration, removesFromViewOnPush)
|
||||
|
||||
override fun getAnimator(
|
||||
container: ViewGroup,
|
||||
from: View?,
|
||||
to: View?,
|
||||
isPush: Boolean,
|
||||
toAddedToContainer: Boolean,
|
||||
): Animator {
|
||||
val animator = AnimatorSet()
|
||||
val viewAnimators: MutableList<Animator> = ArrayList()
|
||||
|
||||
if (isPush && to != null) {
|
||||
viewAnimators.add(ObjectAnimator.ofFloat(to, View.TRANSLATION_Y, to.height.toFloat(), 0f))
|
||||
} else if (!isPush && from != null) {
|
||||
viewAnimators.add(ObjectAnimator.ofFloat(from, View.TRANSLATION_Y, from.height.toFloat()))
|
||||
}
|
||||
|
||||
animator.playTogether(viewAnimators)
|
||||
return animator
|
||||
}
|
||||
|
||||
override fun resetFromView(from: View) = Unit
|
||||
|
||||
override fun copy(): ControllerChangeHandler = VerticalChangeHandler(animationDuration, removesFromViewOnPush)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.bluelinelabs.conductor.internal
|
||||
|
||||
import android.view.View
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
|
||||
class BackGestureViewState(
|
||||
val fromView: View,
|
||||
val toViews: List<BackGestureControllerView>,
|
||||
)
|
||||
|
||||
class BackGestureControllerView(
|
||||
val controller: Controller,
|
||||
val view: View,
|
||||
val inflatedForGesture: Boolean,
|
||||
)
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.bluelinelabs.conductor.internal
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.Controller.LifecycleListener
|
||||
|
||||
class ControllerLifecycleOwner(lifecycleController: Controller) : LifecycleOwner {
|
||||
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) // --> State.INITIALIZED
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = lifecycleRegistry
|
||||
|
||||
init {
|
||||
lifecycleController.addLifecycleListener(
|
||||
object : LifecycleListener() {
|
||||
override fun postContextAvailable(controller: Controller, context: Context) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) // --> State.CREATED;
|
||||
}
|
||||
|
||||
override fun postCreateView(controller: Controller, view: View) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) // --> State.STARTED;
|
||||
}
|
||||
|
||||
override fun postAttach(controller: Controller, view: View) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) // --> State.RESUMED;
|
||||
}
|
||||
|
||||
override fun preDetach(controller: Controller, view: View) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) // --> State.STARTED;
|
||||
}
|
||||
|
||||
override fun preDestroyView(controller: Controller, view: View) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) // --> State.CREATED;
|
||||
}
|
||||
|
||||
override fun preContextUnavailable(controller: Controller, context: Context) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
override fun preDestroy(controller: Controller) {
|
||||
// Only act on Controllers that have had at least the onContextAvailable call made on them.
|
||||
if (lifecycleRegistry.currentState != Lifecycle.State.INITIALIZED) {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) // --> State.DESTROYED;
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
package com.bluelinelabs.conductor.internal;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.app.Application.ActivityLifecycleCallbacks;
|
||||
import android.app.Fragment;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentSender;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.SparseArray;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bluelinelabs.conductor.ActivityHostedRouter;
|
||||
import com.bluelinelabs.conductor.Router;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class LifecycleHandler extends Fragment implements ActivityLifecycleCallbacks {
|
||||
|
||||
private static final String FRAGMENT_TAG = "LifecycleHandler";
|
||||
|
||||
private static final String KEY_PENDING_PERMISSION_REQUESTS = "LifecycleHandler.pendingPermissionRequests";
|
||||
private static final String KEY_PERMISSION_REQUEST_CODES = "LifecycleHandler.permissionRequests";
|
||||
private static final String KEY_ACTIVITY_REQUEST_CODES = "LifecycleHandler.activityRequests";
|
||||
private static final String KEY_ROUTER_STATE_PREFIX = "LifecycleHandler.routerState";
|
||||
|
||||
private Activity activity;
|
||||
private boolean hasRegisteredCallbacks;
|
||||
private boolean destroyed;
|
||||
private boolean attached;
|
||||
private boolean hasPreparedForHostDetach;
|
||||
|
||||
private static final Map<Activity, LifecycleHandler> activeLifecycleHandlers = new HashMap<>();
|
||||
private SparseArray<String> permissionRequestMap = new SparseArray<>();
|
||||
private SparseArray<String> activityRequestMap = new SparseArray<>();
|
||||
private ArrayList<PendingPermissionRequest> pendingPermissionRequests = new ArrayList<>();
|
||||
|
||||
private final Map<Integer, ActivityHostedRouter> routerMap = new HashMap<>();
|
||||
|
||||
public LifecycleHandler() {
|
||||
setRetainInstance(true);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static LifecycleHandler findInActivity(@NonNull Activity activity) {
|
||||
LifecycleHandler lifecycleHandler = activeLifecycleHandlers.get(activity);
|
||||
if (lifecycleHandler == null) {
|
||||
lifecycleHandler = (LifecycleHandler)activity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
|
||||
}
|
||||
if (lifecycleHandler != null) {
|
||||
lifecycleHandler.registerActivityListener(activity);
|
||||
}
|
||||
return lifecycleHandler;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static LifecycleHandler install(@NonNull Activity activity) {
|
||||
LifecycleHandler lifecycleHandler = findInActivity(activity);
|
||||
if (lifecycleHandler == null) {
|
||||
lifecycleHandler = new LifecycleHandler();
|
||||
activity.getFragmentManager().beginTransaction().add(lifecycleHandler, FRAGMENT_TAG).commit();
|
||||
}
|
||||
lifecycleHandler.registerActivityListener(activity);
|
||||
return lifecycleHandler;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Router getRouter(@NonNull ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
ActivityHostedRouter router = routerMap.get(getRouterHashKey(container));
|
||||
if (router == null) {
|
||||
router = new ActivityHostedRouter();
|
||||
router.setHost(this, container);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
Bundle routerSavedState = savedInstanceState.getBundle(KEY_ROUTER_STATE_PREFIX + router.getContainerId());
|
||||
if (routerSavedState != null) {
|
||||
router.restoreInstanceState(routerSavedState);
|
||||
}
|
||||
}
|
||||
routerMap.put(getRouterHashKey(container), router);
|
||||
} else {
|
||||
router.setHost(this, container);
|
||||
}
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<Router> getRouters() {
|
||||
return new ArrayList<Router>(routerMap.values());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Activity getLifecycleActivity() {
|
||||
return activity;
|
||||
}
|
||||
|
||||
private static int getRouterHashKey(@NonNull ViewGroup viewGroup) {
|
||||
return viewGroup.getId();
|
||||
}
|
||||
|
||||
private void registerActivityListener(@NonNull Activity activity) {
|
||||
this.activity = activity;
|
||||
|
||||
if (!hasRegisteredCallbacks) {
|
||||
hasRegisteredCallbacks = true;
|
||||
activity.getApplication().registerActivityLifecycleCallbacks(this);
|
||||
|
||||
// Since Fragment transactions are async, we have to keep an <Activity, LifecycleHandler> map in addition
|
||||
// to trying to find the LifecycleHandler fragment in the Activity to handle the case of the developer
|
||||
// trying to immediately get > 1 router in the same Activity. See issue #299.
|
||||
activeLifecycleHandlers.put(activity, this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
StringSparseArrayParceler permissionParcel = savedInstanceState.getParcelable(KEY_PERMISSION_REQUEST_CODES);
|
||||
permissionRequestMap = permissionParcel != null ? permissionParcel.getStringSparseArray() : new SparseArray<String>();
|
||||
|
||||
StringSparseArrayParceler activityParcel = savedInstanceState.getParcelable(KEY_ACTIVITY_REQUEST_CODES);
|
||||
activityRequestMap = activityParcel != null ? activityParcel.getStringSparseArray() : new SparseArray<String>();
|
||||
|
||||
ArrayList<PendingPermissionRequest> pendingRequests = savedInstanceState.getParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS);
|
||||
pendingPermissionRequests = pendingRequests != null ? pendingRequests : new ArrayList<PendingPermissionRequest>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
outState.putParcelable(KEY_PERMISSION_REQUEST_CODES, new StringSparseArrayParceler(permissionRequestMap));
|
||||
outState.putParcelable(KEY_ACTIVITY_REQUEST_CODES, new StringSparseArrayParceler(activityRequestMap));
|
||||
outState.putParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS, pendingPermissionRequests);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
if (activity != null) {
|
||||
activity.getApplication().unregisterActivityLifecycleCallbacks(this);
|
||||
activeLifecycleHandlers.remove(activity);
|
||||
destroyRouters(false);
|
||||
activity = null;
|
||||
}
|
||||
|
||||
routerMap.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
this.activity = activity;
|
||||
super.onAttach(activity);
|
||||
destroyed = false;
|
||||
setAttached();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
if (context instanceof Activity) {
|
||||
this.activity = (Activity) context;
|
||||
}
|
||||
|
||||
super.onAttach(context);
|
||||
destroyed = false;
|
||||
setAttached();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
|
||||
attached = false;
|
||||
|
||||
if (activity != null) {
|
||||
destroyRouters(activity.isChangingConfigurations());
|
||||
}
|
||||
}
|
||||
|
||||
private void setAttached() {
|
||||
if (!attached) {
|
||||
attached = true;
|
||||
|
||||
for (int i = pendingPermissionRequests.size() - 1; i >= 0; i--) {
|
||||
PendingPermissionRequest request = pendingPermissionRequests.remove(i);
|
||||
requestPermissions(request.instanceId, request.permissions, request.requestCode);
|
||||
}
|
||||
|
||||
for (ActivityHostedRouter router : new ArrayList<>(routerMap.values())) {
|
||||
router.onContextAvailable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void destroyRouters(boolean configurationChange) {
|
||||
if (!destroyed) {
|
||||
destroyed = true;
|
||||
|
||||
if (activity != null) {
|
||||
for (Router router : getRouters()) {
|
||||
router.onActivityDestroyed(activity, configurationChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
String instanceId = activityRequestMap.get(requestCode);
|
||||
if (instanceId != null) {
|
||||
for (Router router : getRouters()) {
|
||||
router.onActivityResult(instanceId, requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
String instanceId = permissionRequestMap.get(requestCode);
|
||||
if (instanceId != null) {
|
||||
for (Router router : getRouters()) {
|
||||
router.onRequestPermissionsResult(instanceId, requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldShowRequestPermissionRationale(@NonNull String permission) {
|
||||
for (Router router : getRouters()) {
|
||||
Boolean handled = router.handleRequestedPermission(permission);
|
||||
if (handled != null) {
|
||||
return handled;
|
||||
}
|
||||
}
|
||||
return super.shouldShowRequestPermissionRationale(permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
for (Router router : getRouters()) {
|
||||
router.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareOptionsMenu(Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
|
||||
for (Router router : getRouters()) {
|
||||
router.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
for (Router router : getRouters()) {
|
||||
if (router.onOptionsItemSelected(item)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public void registerForActivityResult(@NonNull String instanceId, int requestCode) {
|
||||
activityRequestMap.put(requestCode, instanceId);
|
||||
}
|
||||
|
||||
public void unregisterForActivityResults(@NonNull String instanceId) {
|
||||
for (int i = activityRequestMap.size() - 1; i >= 0; i--) {
|
||||
if (instanceId.equals(activityRequestMap.get(activityRequestMap.keyAt(i)))) {
|
||||
activityRequestMap.removeAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode) {
|
||||
registerForActivityResult(instanceId, requestCode);
|
||||
startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
public void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options) {
|
||||
registerForActivityResult(instanceId, requestCode);
|
||||
startActivityForResult(intent, requestCode, options);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
public void startIntentSenderForResult(@NonNull String instanceId, @NonNull IntentSender intent, int requestCode,
|
||||
@Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,
|
||||
@Nullable Bundle options) throws IntentSender.SendIntentException {
|
||||
registerForActivityResult(instanceId, requestCode);
|
||||
startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
public void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
|
||||
if (attached) {
|
||||
permissionRequestMap.put(requestCode, instanceId);
|
||||
requestPermissions(permissions, requestCode);
|
||||
} else {
|
||||
pendingPermissionRequests.add(new PendingPermissionRequest(instanceId, permissions, requestCode));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
|
||||
if (findInActivity(activity) == LifecycleHandler.this) {
|
||||
this.activity = activity;
|
||||
|
||||
for (ActivityHostedRouter router : new ArrayList<>(routerMap.values())) {
|
||||
router.onContextAvailable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(Activity activity) {
|
||||
if (this.activity == activity) {
|
||||
hasPreparedForHostDetach = false;
|
||||
|
||||
for (Router router : getRouters()) {
|
||||
router.onActivityStarted(activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResumed(Activity activity) {
|
||||
if (this.activity == activity) {
|
||||
for (Router router : getRouters()) {
|
||||
router.onActivityResumed(activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityPaused(Activity activity) {
|
||||
if (this.activity == activity) {
|
||||
for (Router router : getRouters()) {
|
||||
router.onActivityPaused(activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(Activity activity) {
|
||||
if (this.activity == activity) {
|
||||
prepareForHostDetachIfNeeded();
|
||||
|
||||
for (Router router : getRouters()) {
|
||||
router.onActivityStopped(activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
|
||||
if (this.activity == activity) {
|
||||
prepareForHostDetachIfNeeded();
|
||||
|
||||
for (Router router : getRouters()) {
|
||||
Bundle bundle = new Bundle();
|
||||
router.saveInstanceState(bundle);
|
||||
outState.putBundle(KEY_ROUTER_STATE_PREFIX + router.getContainerId(), bundle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(Activity activity) {
|
||||
activeLifecycleHandlers.remove(activity);
|
||||
}
|
||||
|
||||
private void prepareForHostDetachIfNeeded() {
|
||||
if (!hasPreparedForHostDetach) {
|
||||
hasPreparedForHostDetach = true;
|
||||
|
||||
for (Router router : getRouters()) {
|
||||
router.prepareForHostDetach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class PendingPermissionRequest implements Parcelable {
|
||||
final String instanceId;
|
||||
final String[] permissions;
|
||||
final int requestCode;
|
||||
|
||||
PendingPermissionRequest(@NonNull String instanceId, @NonNull String[] permissions, int requestCode) {
|
||||
this.instanceId = instanceId;
|
||||
this.permissions = permissions;
|
||||
this.requestCode = requestCode;
|
||||
}
|
||||
|
||||
PendingPermissionRequest(Parcel in) {
|
||||
instanceId = in.readString();
|
||||
permissions = in.createStringArray();
|
||||
requestCode = in.readInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeString(instanceId);
|
||||
out.writeStringArray(permissions);
|
||||
out.writeInt(requestCode);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<PendingPermissionRequest> CREATOR = new Parcelable.Creator<PendingPermissionRequest>() {
|
||||
@Override
|
||||
public PendingPermissionRequest createFromParcel(Parcel in) {
|
||||
return new PendingPermissionRequest(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingPermissionRequest[] newArray(int size) {
|
||||
return new PendingPermissionRequest[size];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
package com.bluelinelabs.conductor.internal
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application.ActivityLifecycleCallbacks
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.util.SparseArray
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.bluelinelabs.conductor.ActivityHostedRouter
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
internal interface LifecycleHandler {
|
||||
val routers: List<Router>
|
||||
val lifecycleActivity: Activity?
|
||||
fun getRouter(container: ViewGroup, savedInstanceState: Bundle?): Router
|
||||
fun registerActivityListener(activity: Activity)
|
||||
fun registerForActivityResult(instanceId: String, requestCode: Int)
|
||||
fun unregisterForActivityResults(instanceId: String)
|
||||
|
||||
fun startActivity(intent: Intent?)
|
||||
fun startActivityForResult(instanceId: String, intent: Intent, requestCode: Int, options: Bundle? = null)
|
||||
|
||||
@Throws(IntentSender.SendIntentException::class)
|
||||
fun startIntentSenderForResult(
|
||||
instanceId: String,
|
||||
intent: IntentSender,
|
||||
requestCode: Int,
|
||||
fillInIntent: Intent?,
|
||||
flagsMask: Int,
|
||||
flagsValues: Int,
|
||||
extraFlags: Int,
|
||||
options: Bundle?,
|
||||
)
|
||||
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
|
||||
fun requestPermissions(instanceId: String, permissions: Array<String>, requestCode: Int)
|
||||
|
||||
companion object {
|
||||
fun install(activity: Activity, allowAndroidXBacking: Boolean = true): LifecycleHandler {
|
||||
var lifecycleHandler = findInActivity(activity, allowAndroidXBacking)
|
||||
if (lifecycleHandler == null) {
|
||||
if (allowAndroidXBacking && activity is FragmentActivity) {
|
||||
lifecycleHandler = AndroidXLifecycleHandlerImpl()
|
||||
activity.supportFragmentManager.beginTransaction().add(lifecycleHandler, FRAGMENT_TAG).commit()
|
||||
} else {
|
||||
lifecycleHandler = PlatformLifecycleHandlerImpl()
|
||||
@Suppress("DEPRECATION")
|
||||
activity.fragmentManager.beginTransaction().add(lifecycleHandler, FRAGMENT_TAG).commit()
|
||||
}
|
||||
}
|
||||
lifecycleHandler.registerActivityListener(activity)
|
||||
return lifecycleHandler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class AndroidXLifecycleHandlerImpl : androidx.fragment.app.Fragment(), LifecycleHandler, LifecycleHandlerDelegate {
|
||||
|
||||
override val data: LifecycleHandlerData = LifecycleHandlerData(isAndroidXLifecycleHandler = true)
|
||||
|
||||
override val routers: List<Router>
|
||||
get() = data.routerMap.values.toList()
|
||||
|
||||
override val lifecycleActivity: Activity?
|
||||
get() = data.activity
|
||||
|
||||
init {
|
||||
@Suppress("DEPRECATION")
|
||||
retainInstance = true
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
handleOnCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
handleOnSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
handleOnAttach(context)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
handleOnActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
handleOnRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
|
||||
return handleShouldShowRequestPermissionRationale(permission) {
|
||||
super.shouldShowRequestPermissionRationale(permission)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
handleOnCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
handleOnPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return handleOnOptionsItemSelected(item) {
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRouter(container: ViewGroup, savedInstanceState: Bundle?): Router {
|
||||
return getRouter(container, savedInstanceState, this)
|
||||
}
|
||||
|
||||
override fun registerActivityListener(activity: Activity) {
|
||||
handleRegisterActivityListener(activity, this)
|
||||
}
|
||||
|
||||
override fun registerForActivityResult(instanceId: String, requestCode: Int) {
|
||||
handleRegisterForActivityResult(instanceId, requestCode)
|
||||
}
|
||||
|
||||
override fun unregisterForActivityResults(instanceId: String) {
|
||||
handleUnregisterForActivityResults(instanceId)
|
||||
}
|
||||
|
||||
override fun startActivityForResult(instanceId: String, intent: Intent, requestCode: Int, options: Bundle?) {
|
||||
handleStartActivityForResult(instanceId, intent, requestCode, options)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun startIntentSenderForResult(
|
||||
instanceId: String,
|
||||
intent: IntentSender,
|
||||
requestCode: Int,
|
||||
fillInIntent: Intent?,
|
||||
flagsMask: Int,
|
||||
flagsValues: Int,
|
||||
extraFlags: Int,
|
||||
options: Bundle?,
|
||||
) {
|
||||
handleStartIntentSenderForResult(
|
||||
instanceId = instanceId,
|
||||
intent = intent,
|
||||
requestCode = requestCode,
|
||||
fillInIntent = fillInIntent,
|
||||
flagsMask = flagsMask,
|
||||
flagsValues = flagsValues,
|
||||
extraFlags = extraFlags,
|
||||
options = options,
|
||||
) {
|
||||
startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options)
|
||||
}
|
||||
}
|
||||
|
||||
override fun requestPermissions(instanceId: String, permissions: Array<String>, requestCode: Int) {
|
||||
handleRequestPermissions(instanceId, permissions, requestCode)
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
handleOnDetach()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
handleOnDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
|
||||
internal class PlatformLifecycleHandlerImpl : android.app.Fragment(), LifecycleHandler, LifecycleHandlerDelegate {
|
||||
|
||||
override val data: LifecycleHandlerData = LifecycleHandlerData(isAndroidXLifecycleHandler = false)
|
||||
|
||||
override val routers: List<Router>
|
||||
get() = data.routerMap.values.toList()
|
||||
|
||||
override val lifecycleActivity: Activity?
|
||||
get() = data.activity
|
||||
|
||||
init {
|
||||
retainInstance = true
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
handleOnCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
handleOnSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onAttach(activity: Activity) {
|
||||
super.onAttach(activity)
|
||||
handleOnAttach(activity)
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
handleOnAttach(context)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
handleOnActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
handleOnRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
|
||||
return handleShouldShowRequestPermissionRationale(permission) {
|
||||
super.shouldShowRequestPermissionRationale(permission)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
handleOnCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
handleOnPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return handleOnOptionsItemSelected(item) {
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRouter(container: ViewGroup, savedInstanceState: Bundle?): Router {
|
||||
return getRouter(container, savedInstanceState, this)
|
||||
}
|
||||
|
||||
override fun registerActivityListener(activity: Activity) {
|
||||
handleRegisterActivityListener(activity, this)
|
||||
}
|
||||
|
||||
override fun registerForActivityResult(instanceId: String, requestCode: Int) {
|
||||
handleRegisterForActivityResult(instanceId, requestCode)
|
||||
}
|
||||
|
||||
override fun unregisterForActivityResults(instanceId: String) {
|
||||
handleUnregisterForActivityResults(instanceId)
|
||||
}
|
||||
|
||||
override fun startActivityForResult(instanceId: String, intent: Intent, requestCode: Int, options: Bundle?) {
|
||||
handleStartActivityForResult(instanceId, intent, requestCode, options)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun startIntentSenderForResult(
|
||||
instanceId: String,
|
||||
intent: IntentSender,
|
||||
requestCode: Int,
|
||||
fillInIntent: Intent?,
|
||||
flagsMask: Int,
|
||||
flagsValues: Int,
|
||||
extraFlags: Int,
|
||||
options: Bundle?,
|
||||
) {
|
||||
handleStartIntentSenderForResult(
|
||||
instanceId = instanceId,
|
||||
intent = intent,
|
||||
requestCode = requestCode,
|
||||
fillInIntent = fillInIntent,
|
||||
flagsMask = flagsMask,
|
||||
flagsValues = flagsValues,
|
||||
extraFlags = extraFlags,
|
||||
options = options,
|
||||
) {
|
||||
startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options)
|
||||
}
|
||||
}
|
||||
|
||||
override fun requestPermissions(instanceId: String, permissions: Array<String>, requestCode: Int) {
|
||||
handleRequestPermissions(instanceId, permissions, requestCode)
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
handleOnDetach()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
handleOnDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
private interface LifecycleHandlerDelegate : ActivityLifecycleCallbacks {
|
||||
val data: LifecycleHandlerData
|
||||
|
||||
private val routers: List<ActivityHostedRouter>
|
||||
get() = data.routerMap.values.toList()
|
||||
|
||||
fun handleOnCreate(savedInstanceState: Bundle?) {
|
||||
savedInstanceState ?: return
|
||||
data.permissionRequestMap = savedInstanceState.getParcelable<StringSparseArrayParceler>(KEY_PERMISSION_REQUEST_CODES)
|
||||
?.stringSparseArray
|
||||
?: SparseArray()
|
||||
data.activityRequestMap = savedInstanceState.getParcelable<StringSparseArrayParceler>(KEY_ACTIVITY_REQUEST_CODES)
|
||||
?.stringSparseArray
|
||||
?: SparseArray()
|
||||
data.pendingPermissionRequests = savedInstanceState.getParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS)
|
||||
?: ArrayList()
|
||||
}
|
||||
|
||||
fun handleOnSaveInstanceState(outState: Bundle) {
|
||||
outState.putParcelable(KEY_PERMISSION_REQUEST_CODES, StringSparseArrayParceler(data.permissionRequestMap))
|
||||
outState.putParcelable(KEY_ACTIVITY_REQUEST_CODES, StringSparseArrayParceler(data.activityRequestMap))
|
||||
outState.putParcelableArrayList(KEY_PENDING_PERMISSION_REQUESTS, data.pendingPermissionRequests)
|
||||
}
|
||||
|
||||
fun handleOnDestroy() {
|
||||
data.activity?.let { activity ->
|
||||
activity.application.unregisterActivityLifecycleCallbacks(this)
|
||||
activeLifecycleHandlers.remove(activity)
|
||||
destroyRouters(false)
|
||||
data.activity = null
|
||||
}
|
||||
|
||||
data.routerMap.clear()
|
||||
}
|
||||
|
||||
fun getRouter(container: ViewGroup, savedInstanceState: Bundle?, handler: LifecycleHandler): Router {
|
||||
data.routerMap[routerHashKey(container)]?.let {
|
||||
it.setHost(handler, container)
|
||||
return it
|
||||
}
|
||||
|
||||
val router = ActivityHostedRouter()
|
||||
router.setHost(handler, container)
|
||||
savedInstanceState?.getBundle("$KEY_ROUTER_STATE_PREFIX${router.containerId}")?.let {
|
||||
router.restoreInstanceState(it)
|
||||
}
|
||||
data.routerMap[routerHashKey(container)] = router
|
||||
return router
|
||||
}
|
||||
|
||||
fun handleRegisterActivityListener(activity: Activity, handler: LifecycleHandler) {
|
||||
data.activity = activity
|
||||
if (!data.hasRegisteredCallbacks) {
|
||||
data.hasRegisteredCallbacks = true
|
||||
activity.application.registerActivityLifecycleCallbacks(this)
|
||||
|
||||
// Since Fragment transactions are async, we have to keep an <Activity, LifecycleHandler> map in addition
|
||||
// to trying to find the LifecycleHandler fragment in the Activity to handle the case of the developer
|
||||
// trying to immediately get > 1 router in the same Activity. See issue #299.
|
||||
activeLifecycleHandlers[activity] = handler
|
||||
}
|
||||
}
|
||||
|
||||
fun requestPermissions(permissions: Array<String>, requestCode: Int)
|
||||
|
||||
fun handleOnAttach(context: Context) {
|
||||
if (context is Activity) {
|
||||
data.activity = context
|
||||
}
|
||||
|
||||
data.destroyed = false
|
||||
|
||||
if (!data.attached) {
|
||||
data.attached = true
|
||||
|
||||
for (i in data.pendingPermissionRequests.indices.reversed()) {
|
||||
val request = data.pendingPermissionRequests.removeAt(i)
|
||||
handleRequestPermissions(request.instanceId, request.permissions, request.requestCode)
|
||||
}
|
||||
|
||||
routers.forEach { it.onContextAvailable() }
|
||||
}
|
||||
}
|
||||
|
||||
fun handleOnDetach() {
|
||||
data.attached = false
|
||||
data.activity?.let { destroyRouters(it.isChangingConfigurations) }
|
||||
}
|
||||
|
||||
private fun destroyRouters(configurationChange: Boolean) {
|
||||
if (!data.destroyed) {
|
||||
data.destroyed = true
|
||||
data.activity?.let { activity ->
|
||||
routers.forEach { it.onActivityDestroyed(activity, configurationChange) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleOnActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
this.data.activityRequestMap[requestCode]?.let { instanceId ->
|
||||
routers.forEach { it.onActivityResult(instanceId, requestCode, resultCode, data) }
|
||||
}
|
||||
}
|
||||
|
||||
fun handleOnRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
data.permissionRequestMap[requestCode]?.let { instanceId ->
|
||||
routers.forEach { it.onRequestPermissionsResult(instanceId, requestCode, permissions, grantResults) }
|
||||
}
|
||||
}
|
||||
|
||||
fun handleShouldShowRequestPermissionRationale(permission: String, callSuper: () -> Boolean): Boolean {
|
||||
for (router in routers) {
|
||||
val handled = router.handleRequestedPermission(permission)
|
||||
if (handled != null) {
|
||||
return handled
|
||||
}
|
||||
}
|
||||
|
||||
return callSuper()
|
||||
}
|
||||
|
||||
fun handleOnCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
routers.forEach { it.onCreateOptionsMenu(menu, inflater) }
|
||||
}
|
||||
|
||||
fun handleOnPrepareOptionsMenu(menu: Menu) {
|
||||
routers.forEach { it.onPrepareOptionsMenu(menu) }
|
||||
}
|
||||
|
||||
fun handleOnOptionsItemSelected(item: MenuItem, callSuper: () -> Boolean): Boolean {
|
||||
return routers.any { it.onOptionsItemSelected(item) } || callSuper()
|
||||
}
|
||||
|
||||
fun handleRegisterForActivityResult(instanceId: String, requestCode: Int) {
|
||||
data.activityRequestMap.put(requestCode, instanceId)
|
||||
}
|
||||
|
||||
fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?)
|
||||
|
||||
fun handleStartActivityForResult(instanceId: String, intent: Intent, requestCode: Int, options: Bundle?) {
|
||||
handleRegisterForActivityResult(instanceId, requestCode)
|
||||
startActivityForResult(intent, requestCode, options)
|
||||
}
|
||||
|
||||
fun handleStartIntentSenderForResult(
|
||||
instanceId: String,
|
||||
intent: IntentSender,
|
||||
requestCode: Int,
|
||||
fillInIntent: Intent?,
|
||||
flagsMask: Int,
|
||||
flagsValues: Int,
|
||||
extraFlags: Int,
|
||||
options: Bundle?,
|
||||
startIntentSender: () -> Unit,
|
||||
) {
|
||||
handleRegisterForActivityResult(instanceId, requestCode)
|
||||
startIntentSender()
|
||||
}
|
||||
|
||||
fun handleUnregisterForActivityResults(instanceId: String) {
|
||||
for (i in data.activityRequestMap.size() - 1 downTo 0) {
|
||||
if (instanceId == data.activityRequestMap[data.activityRequestMap.keyAt(i)]) {
|
||||
data.activityRequestMap.removeAt(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleRequestPermissions(
|
||||
instanceId: String,
|
||||
permissions: Array<String>,
|
||||
requestCode: Int,
|
||||
) {
|
||||
if (data.attached) {
|
||||
data.permissionRequestMap.put(requestCode, instanceId)
|
||||
requestPermissions(permissions, requestCode)
|
||||
} else {
|
||||
data.pendingPermissionRequests.add(PendingPermissionRequest(instanceId, permissions, requestCode))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
if (findInActivity(activity, data.isAndroidXLifecycleHandler) === this) {
|
||||
data.activity = activity
|
||||
data.routerMap.values.toList().forEach { it.onContextAvailable() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
if (data.activity === activity) {
|
||||
data.hasPreparedForHostDetach = false
|
||||
routers.forEach { it.onActivityStarted(activity) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
if (data.activity === activity) {
|
||||
routers.forEach { it.onActivityResumed(activity) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
if (data.activity === activity) {
|
||||
routers.forEach { it.onActivityPaused(activity) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {
|
||||
if (data.activity === activity) {
|
||||
prepareForHostDetachIfNeeded()
|
||||
routers.forEach { it.onActivityStopped(activity) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
|
||||
if (data.activity === activity) {
|
||||
prepareForHostDetachIfNeeded()
|
||||
|
||||
routers.forEach {
|
||||
val bundle = Bundle()
|
||||
it.saveInstanceState(bundle)
|
||||
outState.putBundle("$KEY_ROUTER_STATE_PREFIX${it.containerId}", bundle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityPreDestroyed(activity: Activity) {
|
||||
if (data.activity === activity && !activity.isChangingConfigurations) {
|
||||
handleOnDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
activeLifecycleHandlers.remove(activity)
|
||||
}
|
||||
|
||||
private fun prepareForHostDetachIfNeeded() {
|
||||
if (!data.hasPreparedForHostDetach) {
|
||||
data.hasPreparedForHostDetach = true
|
||||
routers.forEach { it.prepareForHostDetach() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
internal class PendingPermissionRequest(
|
||||
val instanceId: String,
|
||||
val permissions: Array<String>,
|
||||
val requestCode: Int,
|
||||
) : Parcelable
|
||||
|
||||
internal class LifecycleHandlerData(
|
||||
val isAndroidXLifecycleHandler: Boolean,
|
||||
var activity: Activity? = null,
|
||||
var hasRegisteredCallbacks: Boolean = false,
|
||||
var destroyed: Boolean = false,
|
||||
var attached: Boolean = false,
|
||||
var hasPreparedForHostDetach: Boolean = false,
|
||||
var permissionRequestMap: SparseArray<String> = SparseArray(),
|
||||
var activityRequestMap: SparseArray<String> = SparseArray(),
|
||||
var pendingPermissionRequests: ArrayList<PendingPermissionRequest> = arrayListOf(),
|
||||
val routerMap: MutableMap<Int, ActivityHostedRouter> = mutableMapOf(),
|
||||
)
|
||||
|
||||
private fun findInActivity(activity: Activity, allowAndroidXBacking: Boolean): LifecycleHandler? {
|
||||
var lifecycleHandler = activeLifecycleHandlers[activity]
|
||||
if (lifecycleHandler == null) {
|
||||
lifecycleHandler = if (allowAndroidXBacking && activity is FragmentActivity) {
|
||||
activity.supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? LifecycleHandler
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
activity.fragmentManager.findFragmentByTag(FRAGMENT_TAG) as? LifecycleHandler
|
||||
}
|
||||
}
|
||||
lifecycleHandler?.registerActivityListener(activity)
|
||||
return lifecycleHandler
|
||||
}
|
||||
|
||||
private fun routerHashKey(viewGroup: ViewGroup) = viewGroup.id
|
||||
|
||||
private val activeLifecycleHandlers = mutableMapOf<Activity, LifecycleHandler>()
|
||||
|
||||
private const val FRAGMENT_TAG = "LifecycleHandler"
|
||||
private const val KEY_PENDING_PERMISSION_REQUESTS = "LifecycleHandler.pendingPermissionRequests"
|
||||
private const val KEY_PERMISSION_REQUEST_CODES = "LifecycleHandler.permissionRequests"
|
||||
private const val KEY_ACTIVITY_REQUEST_CODES = "LifecycleHandler.activityRequests"
|
||||
private const val KEY_ROUTER_STATE_PREFIX = "LifecycleHandler.routerState"
|
||||
+2
-2
@@ -6,6 +6,8 @@ import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
|
||||
class NoOpControllerChangeHandler : ControllerChangeHandler() {
|
||||
|
||||
override val isReusable = true
|
||||
|
||||
override fun performChange(
|
||||
container: ViewGroup,
|
||||
from: View?,
|
||||
@@ -17,6 +19,4 @@ class NoOpControllerChangeHandler : ControllerChangeHandler() {
|
||||
}
|
||||
|
||||
override fun copy(): ControllerChangeHandler = NoOpControllerChangeHandler()
|
||||
|
||||
override fun isReusable() = true
|
||||
}
|
||||
|
||||
+76
-51
@@ -6,14 +6,14 @@ import android.view.View
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import androidx.lifecycle.ViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.setViewTreeLifecycleOwner
|
||||
import androidx.savedstate.SavedStateRegistry
|
||||
import androidx.savedstate.SavedStateRegistryController
|
||||
import androidx.savedstate.SavedStateRegistryOwner
|
||||
import androidx.savedstate.ViewTreeSavedStateRegistryOwner
|
||||
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.bluelinelabs.conductor.R
|
||||
|
||||
/**
|
||||
* This class sets the [ViewTreeLifecycleOwner] and [ViewTreeSavedStateRegistryOwner] which is
|
||||
@@ -29,7 +29,10 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
|
||||
|
||||
private var hasSavedState = false
|
||||
private var savedRegistryState = Bundle.EMPTY
|
||||
private val parentChangeListeners = mutableMapOf<String, Controller.LifecycleListener>()
|
||||
override val lifecycle: LifecycleRegistry
|
||||
get() = lifecycleRegistry
|
||||
|
||||
override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry
|
||||
|
||||
init {
|
||||
controller.addLifecycleListener(object : Controller.LifecycleListener() {
|
||||
@@ -52,14 +55,11 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
|
||||
* it on purpose.
|
||||
*/
|
||||
if (
|
||||
view.getTag(R.id.view_tree_lifecycle_owner) == null &&
|
||||
view.getTag(R.id.view_tree_saved_state_registry_owner) == null
|
||||
view.getTag(androidx.lifecycle.runtime.R.id.view_tree_lifecycle_owner) == null &&
|
||||
view.getTag(androidx.savedstate.R.id.view_tree_saved_state_registry_owner) == null
|
||||
) {
|
||||
ViewTreeLifecycleOwner.set(view, this@OwnViewTreeLifecycleAndRegistry)
|
||||
ViewTreeSavedStateRegistryOwner.set(
|
||||
view,
|
||||
this@OwnViewTreeLifecycleAndRegistry
|
||||
)
|
||||
view.setViewTreeLifecycleOwner(this@OwnViewTreeLifecycleAndRegistry)
|
||||
view.setViewTreeSavedStateRegistryOwner(this@OwnViewTreeLifecycleAndRegistry)
|
||||
}
|
||||
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
@@ -78,7 +78,7 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
|
||||
if (
|
||||
controller === changeController &&
|
||||
changeType.isEnter &&
|
||||
changeHandler.removesFromViewOnPush() &&
|
||||
changeHandler.removesFromViewOnPush &&
|
||||
changeController.view?.windowToken != null &&
|
||||
lifecycleRegistry.currentState == Lifecycle.State.STARTED
|
||||
) {
|
||||
@@ -97,6 +97,8 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
|
||||
changeHandler = changeHandler,
|
||||
changeType = changeType,
|
||||
)
|
||||
|
||||
GlobalChangeStartListener.onChangeStart(changeController, changeHandler, changeType)
|
||||
}
|
||||
|
||||
override fun preDetach(controller: Controller, view: View) {
|
||||
@@ -126,8 +128,8 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
|
||||
if (controller.isBeingDestroyed && controller.router.backstackSize == 0) {
|
||||
val parent = view.parent as? View
|
||||
parent?.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(v: View?) = Unit
|
||||
override fun onViewDetachedFromWindow(v: View?) {
|
||||
override fun onViewAttachedToWindow(v: View) = Unit
|
||||
override fun onViewDetachedFromWindow(v: View) {
|
||||
parent.removeOnAttachStateChangeListener(this)
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
}
|
||||
@@ -138,53 +140,32 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
|
||||
}
|
||||
|
||||
override fun postContextAvailable(controller: Controller, context: Context) {
|
||||
listenForParentChangeStart(controller)
|
||||
listenForAncestorChangeStart(controller)
|
||||
}
|
||||
|
||||
override fun preContextUnavailable(controller: Controller, context: Context) {
|
||||
stopListeningForParentChangeStart(controller)
|
||||
stopListeningForAncestorChangeStart(controller)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getLifecycle() = lifecycleRegistry
|
||||
|
||||
override fun getSavedStateRegistry() = savedStateRegistryController.savedStateRegistry
|
||||
|
||||
private fun listenForParentChangeStart(controller: Controller) {
|
||||
controller.parentController?.let { parent ->
|
||||
val changeListener = object : Controller.LifecycleListener() {
|
||||
override fun onChangeStart(
|
||||
controller: Controller,
|
||||
changeHandler: ControllerChangeHandler,
|
||||
changeType: ControllerChangeType
|
||||
) {
|
||||
// No-op on the case where we (the child controller) hasn't yet created a View as our parent is being
|
||||
// changed out.
|
||||
if (::lifecycleRegistry.isInitialized) {
|
||||
pauseOnChangeStart(
|
||||
targetController = parent,
|
||||
changeController = controller,
|
||||
changeHandler = changeHandler,
|
||||
changeType = changeType,
|
||||
)
|
||||
}
|
||||
}
|
||||
private fun listenForAncestorChangeStart(controller: Controller) {
|
||||
GlobalChangeStartListener.subscribe(controller, controller.ancestors()) { ancestor, changeHandler, changeType ->
|
||||
// No-op on the case where we (the child controller) hasn't yet created a View as our parent is being
|
||||
// changed out.
|
||||
if (::lifecycleRegistry.isInitialized) {
|
||||
pauseOnChangeStart(
|
||||
targetController = ancestor,
|
||||
changeController = ancestor,
|
||||
changeHandler = changeHandler,
|
||||
changeType = changeType,
|
||||
)
|
||||
}
|
||||
|
||||
parent.addLifecycleListener(changeListener)
|
||||
parentChangeListeners[controller.instanceId] = changeListener
|
||||
|
||||
listenForParentChangeStart(parent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopListeningForParentChangeStart(controller: Controller) {
|
||||
controller.parentController?.let { parent ->
|
||||
parentChangeListeners.remove(parent.instanceId)?.let { listener ->
|
||||
parent.removeLifecycleListener(listener)
|
||||
}
|
||||
}
|
||||
private fun stopListeningForAncestorChangeStart(controller: Controller) {
|
||||
GlobalChangeStartListener.unsubscribe(controller)
|
||||
}
|
||||
|
||||
// AbstractComposeView adds its own OnAttachStateChangeListener by default. Since it
|
||||
@@ -200,7 +181,7 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
|
||||
if (
|
||||
targetController === changeController &&
|
||||
!changeType.isEnter &&
|
||||
changeHandler.removesFromViewOnPush() &&
|
||||
changeHandler.removesFromViewOnPush &&
|
||||
changeController.view != null &&
|
||||
lifecycleRegistry.currentState == Lifecycle.State.RESUMED
|
||||
) {
|
||||
@@ -213,6 +194,16 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun Controller.ancestors(): Collection<String> {
|
||||
return buildList {
|
||||
var ancestor = parentController
|
||||
while (ancestor != null) {
|
||||
add(ancestor.instanceId)
|
||||
ancestor = ancestor.parentController
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_SAVED_STATE = "Registry.savedState"
|
||||
|
||||
@@ -221,3 +212,37 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In order to prevent child controllers from having strong references to all of their ancestors, some of which may
|
||||
// break their connection before the child is made aware, this shared listener is used to call all interested parties
|
||||
// when a controller begins transitioning.
|
||||
private object GlobalChangeStartListener {
|
||||
private val listeners = mutableMapOf<String, Listener>()
|
||||
|
||||
fun subscribe(
|
||||
controller: Controller,
|
||||
targetControllers: Collection<String>,
|
||||
listener: (Controller, ControllerChangeHandler, ControllerChangeType) -> Unit,
|
||||
) {
|
||||
listeners[controller.instanceId] = Listener(targetControllers, listener)
|
||||
}
|
||||
|
||||
fun unsubscribe(controller: Controller) {
|
||||
listeners.remove(controller.instanceId)
|
||||
}
|
||||
|
||||
fun onChangeStart(controller: Controller, changeHandler: ControllerChangeHandler, changeType: ControllerChangeType) {
|
||||
listeners.values.forEach { it.call(controller, changeHandler, changeType) }
|
||||
}
|
||||
|
||||
private class Listener(
|
||||
private val targetControllers: Collection<String>,
|
||||
private val listener: (Controller, ControllerChangeHandler, ControllerChangeType) -> Unit,
|
||||
) {
|
||||
fun call(controller: Controller, changeHandler: ControllerChangeHandler, changeType: ControllerChangeType) {
|
||||
if (targetControllers.contains(controller.instanceId)) {
|
||||
listener(controller, changeHandler, changeType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,5 +1,7 @@
|
||||
package com.bluelinelabs.conductor;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler;
|
||||
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
|
||||
|
||||
@@ -7,8 +9,6 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class ControllerChangeHandlerTests {
|
||||
|
||||
@@ -32,10 +32,10 @@ public class ControllerChangeHandlerTests {
|
||||
FadeChangeHandler restoredFadeCast = (FadeChangeHandler) restoredFade;
|
||||
|
||||
assertEquals(horizontalChangeHandler.getAnimationDuration(), restoredHorizontalCast.getAnimationDuration());
|
||||
assertEquals(horizontalChangeHandler.removesFromViewOnPush(), restoredHorizontalCast.removesFromViewOnPush());
|
||||
assertEquals(horizontalChangeHandler.getRemovesFromViewOnPush(), restoredHorizontalCast.getRemovesFromViewOnPush());
|
||||
|
||||
assertEquals(fadeChangeHandler.getAnimationDuration(), restoredFadeCast.getAnimationDuration());
|
||||
assertEquals(fadeChangeHandler.removesFromViewOnPush(), restoredFadeCast.removesFromViewOnPush());
|
||||
assertEquals(fadeChangeHandler.getRemovesFromViewOnPush(), restoredFadeCast.getRemovesFromViewOnPush());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -155,7 +155,7 @@ class ControllerLifecycleActivityReferenceTests {
|
||||
)
|
||||
activity.router.popCurrentController()
|
||||
|
||||
Assert.assertEquals(listOf(true), listener.changeEndReferences)
|
||||
Assert.assertEquals(listOf(true, true), listener.changeEndReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postAttachReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDetachReferences)
|
||||
@@ -208,7 +208,7 @@ class ControllerLifecycleActivityReferenceTests {
|
||||
)
|
||||
activityController.pause().stop().destroy()
|
||||
|
||||
Assert.assertEquals(listOf(true), listener.changeEndReferences)
|
||||
Assert.assertEquals(listOf(true, true), listener.changeEndReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postCreateViewReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postAttachReferences)
|
||||
Assert.assertEquals(listOf(true), listener.postDetachReferences)
|
||||
|
||||
+2
@@ -465,6 +465,8 @@ class ControllerLifecycleCallbacksTests {
|
||||
assertCalls(expectedCallState, child)
|
||||
|
||||
activityController.get().router.popCurrentController()
|
||||
expectedCallState.changeStartCalls++
|
||||
expectedCallState.changeEndCalls++
|
||||
expectedCallState.detachCalls++
|
||||
expectedCallState.destroyViewCalls++
|
||||
expectedCallState.contextUnavailableCalls++
|
||||
|
||||
@@ -129,16 +129,8 @@ class ControllerTests {
|
||||
assertCalls(expectedCallState, controller)
|
||||
|
||||
// Ensure requesting the permission gets us the result back
|
||||
try {
|
||||
controller.requestPermissions(requestedPermissions, 1)
|
||||
} catch (ignored: NoSuchMethodError) { }
|
||||
controller.requestPermissions(requestedPermissions, 1)
|
||||
|
||||
router.onRequestPermissionsResult(
|
||||
controller.instanceId,
|
||||
1,
|
||||
requestedPermissions,
|
||||
intArrayOf(1)
|
||||
)
|
||||
expectedCallState.onRequestPermissionsResultCalls++
|
||||
assertCalls(expectedCallState, controller)
|
||||
}
|
||||
@@ -162,11 +154,8 @@ class ControllerTests {
|
||||
assertCalls(parentExpectedCallState, parent)
|
||||
|
||||
// Ensure requesting the permission gets us the result back
|
||||
try {
|
||||
child.requestPermissions(requestedPermissions, 1)
|
||||
} catch (ignored: NoSuchMethodError) { }
|
||||
child.requestPermissions(requestedPermissions, 1)
|
||||
|
||||
router.onRequestPermissionsResult(child.instanceId, 1, requestedPermissions, intArrayOf(1))
|
||||
childExpectedCallState.onRequestPermissionsResultCalls++
|
||||
assertCalls(childExpectedCallState, child)
|
||||
assertCalls(parentExpectedCallState, parent)
|
||||
|
||||
@@ -343,6 +343,8 @@ class RouterTests {
|
||||
|
||||
@Test
|
||||
fun testRearrangeTransactionBackstack() {
|
||||
router.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
|
||||
|
||||
val transaction1 = TestController().asTransaction()
|
||||
val transaction2 = TestController().asTransaction()
|
||||
var backstack = listOf(transaction1, transaction2)
|
||||
@@ -433,4 +435,4 @@ class RouterTests {
|
||||
Assert.assertFalse(controller1.isBeingDestroyed())
|
||||
Assert.assertTrue(controller3.isBeingDestroyed())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package com.bluelinelabs.conductor.util;
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler;
|
||||
|
||||
public class MockChangeHandler extends ControllerChangeHandler {
|
||||
@@ -92,7 +93,7 @@ public class MockChangeHandler extends ControllerChangeHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removesFromViewOnPush() {
|
||||
public boolean getRemovesFromViewOnPush() {
|
||||
return removesFromViewOnPush;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.bluelinelabs.conductor.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.bluelinelabs.conductor.Conductor
|
||||
import com.bluelinelabs.conductor.Router
|
||||
|
||||
class TestActivity : Activity() {
|
||||
class TestActivity : FragmentActivity() {
|
||||
|
||||
lateinit var router: Router
|
||||
|
||||
|
||||
+7
-9
@@ -27,20 +27,22 @@ android {
|
||||
compose = true
|
||||
}
|
||||
|
||||
compileSdkVersion 31
|
||||
compileSdk 34
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
jvmTarget = "11"
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion libs.versions.compose.get()
|
||||
kotlinCompilerExtensionVersion libs.versions.compose.compiler.get()
|
||||
}
|
||||
|
||||
namespace "com.bluelinelabs.conductor.demo"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -51,13 +53,9 @@ dependencies {
|
||||
|
||||
implementation libs.picasso
|
||||
|
||||
implementation libs.autodispose.ktx
|
||||
|
||||
implementation project(':conductor')
|
||||
implementation project(':conductor-modules:viewpager')
|
||||
implementation project(':conductor-modules:viewpager2')
|
||||
implementation project(':conductor-modules:autodispose')
|
||||
implementation project(':conductor-modules:arch-components-lifecycle')
|
||||
implementation project(':conductor-modules:androidx-transition')
|
||||
|
||||
implementation libs.compose.ui
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.bluelinelabs.conductor.demo">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
|
||||
<activity
|
||||
android:name="com.bluelinelabs.conductor.demo.MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.bluelinelabs.conductor.demo.MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -3,10 +3,10 @@ package com.bluelinelabs.conductor.demo
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import com.bluelinelabs.conductor.Conductor.attachRouter
|
||||
import com.bluelinelabs.conductor.Conductor
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import com.bluelinelabs.conductor.Router.PopRootControllerMode
|
||||
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.demo.controllers.HomeController
|
||||
import com.bluelinelabs.conductor.demo.databinding.ActivityMainBinding
|
||||
|
||||
@@ -23,17 +23,14 @@ class MainActivity : AppCompatActivity(), ToolbarProvider {
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
router = attachRouter(this, binding.controllerContainer, savedInstanceState)
|
||||
router = Conductor.attachRouter(this, binding.controllerContainer, savedInstanceState)
|
||||
.setPopRootControllerMode(PopRootControllerMode.NEVER)
|
||||
.setOnBackPressedDispatcherEnabled(true)
|
||||
|
||||
if (!router.hasRootController()) {
|
||||
router.setRoot(with(HomeController()))
|
||||
router.setRoot(RouterTransaction.with(HomeController()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (!router.handleBack()) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-5
@@ -1,20 +1,16 @@
|
||||
package com.bluelinelabs.conductor.demo.changehandler;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.view.View;
|
||||
import android.view.ViewAnimationUtils;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler;
|
||||
|
||||
/**
|
||||
* An {@link AnimatorChangeHandler} that will perform a circular reveal
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public class CircularRevealChangeHandler extends AnimatorChangeHandler {
|
||||
|
||||
private static final String KEY_CX = "CircularRevealChangeHandler.cx";
|
||||
|
||||
-110
@@ -1,110 +0,0 @@
|
||||
package com.bluelinelabs.conductor.demo.controllers
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.archlifecycle.LifecycleController
|
||||
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
|
||||
import com.bluelinelabs.conductor.demo.R
|
||||
import com.bluelinelabs.conductor.demo.ToolbarProvider
|
||||
import com.bluelinelabs.conductor.demo.controllers.base.watchForLeaks
|
||||
import com.bluelinelabs.conductor.demo.databinding.ControllerLifecycleBinding
|
||||
|
||||
class ArchLifecycleController : LifecycleController() {
|
||||
|
||||
|
||||
init {
|
||||
Log.i(TAG, "Conductor: Constructor called")
|
||||
watchForLeaks()
|
||||
|
||||
lifecycle.addObserver(object : LifecycleObserver {
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_ANY)
|
||||
fun onLifecycleEvent(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"Lifecycle: " + source.javaClass.simpleName + " emitted event " + event + " and is now in state " + source.lifecycle.currentState
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
Log.d(TAG, "Lifecycle: " + javaClass.simpleName + " is now in state " + lifecycle.currentState)
|
||||
}
|
||||
|
||||
override fun onContextAvailable(context: Context) {
|
||||
Log.i(TAG, "Conductor: onContextAvailable() called")
|
||||
super.onContextAvailable(context)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup,
|
||||
savedViewState: Bundle?
|
||||
): View {
|
||||
Log.i(TAG, "Conductor: onCreateView() called")
|
||||
|
||||
val binding = ControllerLifecycleBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.root.setBackgroundColor(ContextCompat.getColor(container.context, R.color.orange_300))
|
||||
binding.title.text = binding.root.resources.getString(R.string.rxlifecycle_title, TAG)
|
||||
|
||||
binding.nextReleaseView.setOnClickListener {
|
||||
retainViewMode = RetainViewMode.RELEASE_DETACH
|
||||
|
||||
router.pushController(
|
||||
RouterTransaction.with(TextController("Logcat should now report that the Controller's onDetach() and LifecycleObserver's onPause() methods were called, followed by the Controller's onDestroyView() and LifecycleObserver's onStop()."))
|
||||
.pushChangeHandler(HorizontalChangeHandler())
|
||||
.popChangeHandler(HorizontalChangeHandler())
|
||||
)
|
||||
}
|
||||
|
||||
binding.nextRetainView.setOnClickListener {
|
||||
retainViewMode = RetainViewMode.RETAIN_DETACH
|
||||
|
||||
router.pushController(
|
||||
RouterTransaction.with(TextController("Logcat should now report that the Controller's onDetach() and LifecycleObserver's onPause() methods were called."))
|
||||
.pushChangeHandler(HorizontalChangeHandler())
|
||||
.popChangeHandler(HorizontalChangeHandler())
|
||||
)
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onAttach(view: View) {
|
||||
Log.i(TAG, "Conductor: onAttach() called")
|
||||
super.onAttach(view)
|
||||
(activity as ToolbarProvider).toolbar.title = "Arch Components Lifecycle Demo"
|
||||
}
|
||||
|
||||
override fun onDetach(view: View) {
|
||||
Log.i(TAG, "Conductor: onDetach() called")
|
||||
super.onDetach(view)
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
Log.i(TAG, "Conductor: onDestroyView() called")
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
override fun onContextUnavailable() {
|
||||
Log.i(TAG, "Conductor: onContextUnavailable() called")
|
||||
super.onContextUnavailable()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.i(TAG, "Conductor: onDestroy() called")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ArchLifecycleController"
|
||||
}
|
||||
}
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
package com.bluelinelabs.conductor.demo.controllers
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
|
||||
import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider
|
||||
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
|
||||
import com.bluelinelabs.conductor.demo.R
|
||||
import com.bluelinelabs.conductor.demo.ToolbarProvider
|
||||
import com.bluelinelabs.conductor.demo.controllers.base.watchForLeaks
|
||||
import com.bluelinelabs.conductor.demo.databinding.ControllerLifecycleBinding
|
||||
import com.uber.autodispose.autoDisposable
|
||||
import io.reactivex.Observable
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
// Shamelessly borrowed from the official RxLifecycle demo by Trello and adapted for Conductor Controllers
|
||||
// instead of Activities or Fragments.
|
||||
class AutodisposeController : Controller() {
|
||||
|
||||
private val scopeProvider = ControllerScopeProvider.from(this)
|
||||
|
||||
init {
|
||||
watchForLeaks()
|
||||
Observable.interval(1, TimeUnit.SECONDS)
|
||||
.doOnDispose { Log.i(TAG, "Disposing from constructor") }
|
||||
.autoDisposable(scopeProvider)
|
||||
.subscribe { num: Long ->
|
||||
Log.i(TAG, "Started in constructor, running until onDestroy(): $num")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup,
|
||||
savedViewState: Bundle?
|
||||
): View {
|
||||
Log.i(TAG, "onCreateView() called")
|
||||
val binding = ControllerLifecycleBinding.inflate(inflater, container, false)
|
||||
binding.title.text = binding.root.resources.getString(R.string.rxlifecycle_title, TAG)
|
||||
|
||||
binding.nextReleaseView.setOnClickListener {
|
||||
retainViewMode = RetainViewMode.RELEASE_DETACH
|
||||
router.pushController(
|
||||
with(TextController("Logcat should now report that the observables from onAttach() and onViewBound() have been disposed of, while the constructor observable is still running."))
|
||||
.pushChangeHandler(HorizontalChangeHandler())
|
||||
.popChangeHandler(HorizontalChangeHandler())
|
||||
)
|
||||
}
|
||||
|
||||
binding.nextRetainView.setOnClickListener {
|
||||
retainViewMode = RetainViewMode.RETAIN_DETACH
|
||||
router.pushController(
|
||||
with(TextController("Logcat should now report that the observables from onAttach() has been disposed of, while the constructor and onViewBound() observables are still running."))
|
||||
.pushChangeHandler(HorizontalChangeHandler())
|
||||
.popChangeHandler(HorizontalChangeHandler())
|
||||
)
|
||||
}
|
||||
|
||||
Observable.interval(1, TimeUnit.SECONDS)
|
||||
.doOnDispose { Log.i(TAG, "Disposing from onCreateView()") }
|
||||
.autoDisposable(scopeProvider)
|
||||
.subscribe { num: Long ->
|
||||
Log.i(TAG, "Started in onCreateView(), running until onDestroyView(): $num")
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onAttach(view: View) {
|
||||
super.onAttach(view)
|
||||
Log.i(TAG, "onAttach() called")
|
||||
|
||||
(activity as ToolbarProvider).toolbar.title = "Autodispose Demo"
|
||||
Observable.interval(1, TimeUnit.SECONDS)
|
||||
.doOnDispose { Log.i(TAG, "Disposing from onAttach()") }
|
||||
.autoDisposable(scopeProvider)
|
||||
.subscribe { num: Long ->
|
||||
Log.i(TAG, "Started in onAttach(), running until onDetach(): $num")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
super.onDestroyView(view)
|
||||
Log.i(TAG, "onDestroyView() called")
|
||||
}
|
||||
|
||||
override fun onDetach(view: View) {
|
||||
super.onDetach(view)
|
||||
Log.i(TAG, "onDetach() called")
|
||||
}
|
||||
|
||||
public override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Log.i(TAG, "onDestroy() called")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AutodisposeController"
|
||||
}
|
||||
}
|
||||
-121
@@ -1,121 +0,0 @@
|
||||
package com.bluelinelabs.conductor.demo.controllers
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.bluelinelabs.conductor.demo.R
|
||||
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
|
||||
import com.bluelinelabs.conductor.demo.databinding.ControllerCityDetailBinding
|
||||
import com.bluelinelabs.conductor.demo.databinding.RowCityDetailBinding
|
||||
import com.bluelinelabs.conductor.demo.databinding.RowCityHeaderBinding
|
||||
import com.bluelinelabs.conductor.demo.util.viewBinding
|
||||
|
||||
class CityDetailController(args: Bundle) : BaseController(R.layout.controller_city_detail, args) {
|
||||
private val binding: ControllerCityDetailBinding by viewBinding(ControllerCityDetailBinding::bind)
|
||||
|
||||
override val title = args.getString(KEY_TITLE)!!
|
||||
|
||||
constructor(@DrawableRes image: Int, title: String) : this(
|
||||
bundleOf(
|
||||
KEY_TITLE to title,
|
||||
KEY_IMAGE to image
|
||||
)
|
||||
)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recyclerView.adapter = CityDetailAdapter(
|
||||
inflater = LayoutInflater.from(view.context),
|
||||
title = title,
|
||||
imageDrawableRes = args.getInt(KEY_IMAGE),
|
||||
details = LIST_ROWS,
|
||||
transitionNameBase = title
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_TITLE = "CityDetailController.title"
|
||||
private const val KEY_IMAGE = "CityDetailController.image"
|
||||
|
||||
private val LIST_ROWS = listOf(
|
||||
"• This is a city.",
|
||||
"• There's some cool stuff about it.",
|
||||
"• But really this is just a demo, not a city guide app.",
|
||||
"• This demo is meant to show some nice transitions.",
|
||||
"• You should have seen some sweet shared element transitions using the ImageView and the TextView in the \"header\" above.",
|
||||
"• This transition utilized some callbacks to ensure all the necessary rows in the RecyclerView were laid about before the transition occurred.",
|
||||
"• Just adding some more lines so it scrolls now...\n\n\n\n\n\n\nThe end."
|
||||
)
|
||||
}
|
||||
|
||||
class CityDetailAdapter(
|
||||
private val inflater: LayoutInflater,
|
||||
private val title: String,
|
||||
@DrawableRes private val imageDrawableRes: Int,
|
||||
private val details: List<String>,
|
||||
private val transitionNameBase: String
|
||||
) : RecyclerView.Adapter<ViewHolder<*>>() {
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (position == 0) VIEW_TYPE_HEADER else VIEW_TYPE_DETAIL
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<*> {
|
||||
return if (viewType == VIEW_TYPE_HEADER) {
|
||||
HeaderViewHolder(RowCityHeaderBinding.inflate(inflater, parent, false))
|
||||
} else {
|
||||
DetailViewHolder(RowCityDetailBinding.inflate(inflater, parent, false))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder<*>, position: Int) {
|
||||
when (holder) {
|
||||
is HeaderViewHolder -> {
|
||||
holder.bind(
|
||||
imageDrawableRes = imageDrawableRes,
|
||||
title = title,
|
||||
imageTransitionName = inflater.context.resources.getString(R.string.transition_tag_image_named, transitionNameBase),
|
||||
textViewTransitionName = inflater.context.resources.getString(R.string.transition_tag_title_named, transitionNameBase)
|
||||
)
|
||||
}
|
||||
is DetailViewHolder -> {
|
||||
holder.bind(details[position - 1])
|
||||
}
|
||||
else -> {
|
||||
throw IllegalStateException("Invalid view holder class ${holder.javaClass.canonicalName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = details.size + 1
|
||||
|
||||
companion object {
|
||||
private const val VIEW_TYPE_HEADER = 0
|
||||
private const val VIEW_TYPE_DETAIL = 1
|
||||
}
|
||||
}
|
||||
|
||||
open class ViewHolder<Binding : ViewBinding>(binding: Binding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
class HeaderViewHolder(private val binding: RowCityHeaderBinding) : ViewHolder<RowCityHeaderBinding>(binding) {
|
||||
fun bind(@DrawableRes imageDrawableRes: Int, title: String?, imageTransitionName: String?, textViewTransitionName: String?) {
|
||||
binding.imageView.setImageResource(imageDrawableRes)
|
||||
binding.textView.text = title
|
||||
binding.imageView.transitionName = imageTransitionName
|
||||
binding.textView.transitionName = textViewTransitionName
|
||||
}
|
||||
}
|
||||
|
||||
class DetailViewHolder(private val binding: RowCityDetailBinding) : ViewHolder<RowCityDetailBinding>(binding) {
|
||||
fun bind(detail: String) {
|
||||
binding.textView.text = detail
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package com.bluelinelabs.conductor.demo.controllers
|
||||
|
||||
import android.graphics.PorterDuff
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.demo.R
|
||||
import com.bluelinelabs.conductor.demo.changehandler.CityGridSharedElementTransitionChangeHandler
|
||||
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
|
||||
import com.bluelinelabs.conductor.demo.databinding.ControllerCityGridBinding
|
||||
import com.bluelinelabs.conductor.demo.databinding.RowCityGridBinding
|
||||
import com.bluelinelabs.conductor.demo.util.viewBinding
|
||||
|
||||
class CityGridController(args: Bundle) : BaseController(R.layout.controller_city_grid, args) {
|
||||
private val binding: ControllerCityGridBinding by viewBinding(ControllerCityGridBinding::bind)
|
||||
|
||||
override val title = "Shared Element Demos"
|
||||
|
||||
constructor(title: String?, dotColor: Int, fromPosition: Int) : this(
|
||||
bundleOf(
|
||||
KEY_TITLE to title,
|
||||
KEY_DOT_COLOR to dotColor,
|
||||
KEY_FROM_POSITION to fromPosition
|
||||
)
|
||||
)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
binding.title.text = args.getString(KEY_TITLE)!!
|
||||
binding.dot.drawable.setColorFilter(ContextCompat.getColor(view.context, args.getInt(KEY_DOT_COLOR)), PorterDuff.Mode.SRC_ATOP)
|
||||
|
||||
binding.title.transitionName = view.resources.getString(R.string.transition_tag_title_indexed, args.getInt(KEY_FROM_POSITION))
|
||||
binding.dot.transitionName = view.resources.getString(R.string.transition_tag_dot_indexed, args.getInt(KEY_FROM_POSITION))
|
||||
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.layoutManager = GridLayoutManager(view.context, 2)
|
||||
binding.recyclerView.adapter = CityGridAdapter(LayoutInflater.from(view.context), CITY_MODELS, ::onModelRowClick)
|
||||
}
|
||||
|
||||
private fun onModelRowClick(model: CityModel) {
|
||||
val names = listOf(
|
||||
resources!!.getString(R.string.transition_tag_title_named, model.title),
|
||||
resources!!.getString(R.string.transition_tag_image_named, model.title)
|
||||
)
|
||||
|
||||
router.pushController(
|
||||
RouterTransaction.with(CityDetailController(model.drawableRes, model.title))
|
||||
.pushChangeHandler(CityGridSharedElementTransitionChangeHandler(names))
|
||||
.popChangeHandler(CityGridSharedElementTransitionChangeHandler(names))
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_TITLE = "CityGridController.title"
|
||||
private const val KEY_DOT_COLOR = "CityGridController.dotColor"
|
||||
private const val KEY_FROM_POSITION = "CityGridController.position"
|
||||
|
||||
private val CITY_MODELS = arrayOf(
|
||||
CityModel(R.drawable.chicago, "Chicago"),
|
||||
CityModel(R.drawable.jakarta, "Jakarta"),
|
||||
CityModel(R.drawable.london, "London"),
|
||||
CityModel(R.drawable.sao_paulo, "Sao Paulo"),
|
||||
CityModel(R.drawable.tokyo, "Tokyo")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class CityModel(@DrawableRes val drawableRes: Int, val title: String)
|
||||
|
||||
private class CityGridAdapter(
|
||||
private val inflater: LayoutInflater,
|
||||
private val items: Array<CityModel>,
|
||||
private val modelClickListener: (CityModel) -> Unit
|
||||
) : RecyclerView.Adapter<CityGridAdapter.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(
|
||||
RowCityGridBinding.inflate(inflater, parent, false),
|
||||
modelClickListener
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
class ViewHolder(
|
||||
private val binding: RowCityGridBinding,
|
||||
private val modelClickListener: (CityModel) -> Unit
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: CityModel) {
|
||||
binding.image.setImageResource(item.drawableRes)
|
||||
binding.title.text = item.title
|
||||
|
||||
binding.image.transitionName = itemView.resources.getString(R.string.transition_tag_image_named, item.title)
|
||||
binding.image.transitionName = itemView.resources.getString(R.string.transition_tag_title_named, item.title)
|
||||
|
||||
itemView.setOnClickListener { modelClickListener(item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
package com.bluelinelabs.conductor.demo.controllers
|
||||
|
||||
import android.graphics.PorterDuff
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
|
||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||
import com.bluelinelabs.conductor.demo.R
|
||||
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
|
||||
import com.bluelinelabs.conductor.demo.databinding.ControllerAdditionalModulesBinding
|
||||
import com.bluelinelabs.conductor.demo.databinding.RowHomeBinding
|
||||
import com.bluelinelabs.conductor.demo.util.viewBinding
|
||||
|
||||
class ExternalModulesController : BaseController(R.layout.controller_additional_modules) {
|
||||
private val binding: ControllerAdditionalModulesBinding by viewBinding(
|
||||
ControllerAdditionalModulesBinding::bind
|
||||
)
|
||||
|
||||
override val title = "External Module Demos"
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recyclerView.adapter = AdditionalModulesAdapter(
|
||||
LayoutInflater.from(view.context),
|
||||
ModuleModel.values(),
|
||||
::onModelRowClick
|
||||
)
|
||||
}
|
||||
|
||||
private fun onModelRowClick(model: ModuleModel) {
|
||||
when (model) {
|
||||
ModuleModel.AUTODISPOSE -> router.pushController(
|
||||
with(AutodisposeController())
|
||||
.pushChangeHandler(FadeChangeHandler())
|
||||
.popChangeHandler(FadeChangeHandler())
|
||||
)
|
||||
ModuleModel.ARCH_LIFECYCLE -> router.pushController(
|
||||
with(ArchLifecycleController())
|
||||
.pushChangeHandler(FadeChangeHandler())
|
||||
.popChangeHandler(FadeChangeHandler())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class ModuleModel(val title: String, @ColorRes val color: Int) {
|
||||
AUTODISPOSE("Autodispose", R.color.purple_300),
|
||||
ARCH_LIFECYCLE("Arch Components Lifecycle", R.color.orange_300);
|
||||
}
|
||||
|
||||
private class AdditionalModulesAdapter(
|
||||
private val inflater: LayoutInflater,
|
||||
private val items: Array<ModuleModel>,
|
||||
private val modelClickListener: (ModuleModel) -> Unit
|
||||
) : RecyclerView.Adapter<AdditionalModulesAdapter.ViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(RowHomeBinding.inflate(inflater, parent, false), modelClickListener)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
|
||||
class ViewHolder(
|
||||
private val binding: RowHomeBinding,
|
||||
private val modelClickListener: (ModuleModel) -> Unit
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: ModuleModel) {
|
||||
binding.title.text = item.title
|
||||
binding.dot.drawable.setColorFilter(
|
||||
ContextCompat.getColor(itemView.context, item.color),
|
||||
PorterDuff.Mode.SRC_ATOP
|
||||
)
|
||||
itemView.setOnClickListener { modelClickListener(item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.bluelinelabs.conductor.demo.controllers
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.PorterDuff
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
@@ -11,18 +10,20 @@ import android.text.style.URLSpan
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.BackEventCompat
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.asTransaction
|
||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
|
||||
import com.bluelinelabs.conductor.demo.R
|
||||
import com.bluelinelabs.conductor.demo.changehandler.ArcFadeMoveChangeHandler
|
||||
import com.bluelinelabs.conductor.demo.changehandler.FabToDialogTransitionChangeHandler
|
||||
import com.bluelinelabs.conductor.demo.controllers.NavigationDemoController.DisplayUpMode
|
||||
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
|
||||
@@ -77,12 +78,13 @@ class HomeController : BaseController(R.layout.controller_home) {
|
||||
}
|
||||
|
||||
private fun onModelRowClick(model: DemoModel, position: Int) {
|
||||
println("onModelRowClick")
|
||||
when (model) {
|
||||
DemoModel.NAVIGATION -> {
|
||||
router.pushController(
|
||||
RouterTransaction.with(NavigationDemoController(0, DisplayUpMode.SHOW_FOR_CHILDREN_ONLY))
|
||||
.pushChangeHandler(FadeChangeHandler())
|
||||
.popChangeHandler(FadeChangeHandler())
|
||||
.pushChangeHandler(WhateverChangeHandler())
|
||||
.popChangeHandler(WhateverChangeHandler())
|
||||
.tag(NavigationDemoController.TAG_UP_TRANSACTION)
|
||||
)
|
||||
}
|
||||
@@ -112,30 +114,16 @@ class HomeController : BaseController(R.layout.controller_home) {
|
||||
}
|
||||
DemoModel.CHILD_CONTROLLERS -> {
|
||||
router.pushController(
|
||||
RouterTransaction.with(ParentController())
|
||||
RouterTransaction.with(MultipleChildRouterController())
|
||||
.pushChangeHandler(FadeChangeHandler())
|
||||
.popChangeHandler(FadeChangeHandler())
|
||||
)
|
||||
}
|
||||
DemoModel.SHARED_ELEMENT_TRANSITIONS -> {
|
||||
val titleSharedElementName =
|
||||
resources!!.getString(R.string.transition_tag_title_indexed, position)
|
||||
val dotSharedElementName =
|
||||
resources!!.getString(R.string.transition_tag_dot_indexed, position)
|
||||
DemoModel.ON_BACK_PRESSED_CALLBACK -> {
|
||||
router.pushController(
|
||||
RouterTransaction.with(CityGridController(model.title, model.color, position))
|
||||
.pushChangeHandler(
|
||||
ArcFadeMoveChangeHandler(
|
||||
titleSharedElementName,
|
||||
dotSharedElementName
|
||||
)
|
||||
)
|
||||
.popChangeHandler(
|
||||
ArcFadeMoveChangeHandler(
|
||||
titleSharedElementName,
|
||||
dotSharedElementName
|
||||
)
|
||||
)
|
||||
RouterTransaction.with(OnBackPressedCallbackController())
|
||||
.pushChangeHandler(FadeChangeHandler())
|
||||
.popChangeHandler(FadeChangeHandler())
|
||||
)
|
||||
}
|
||||
DemoModel.DRAG_DISMISS -> {
|
||||
@@ -145,20 +133,6 @@ class HomeController : BaseController(R.layout.controller_home) {
|
||||
.popChangeHandler(FadeChangeHandler())
|
||||
)
|
||||
}
|
||||
DemoModel.EXTERNAL_MODULES -> {
|
||||
router.pushController(
|
||||
RouterTransaction.with(ExternalModulesController())
|
||||
.pushChangeHandler(HorizontalChangeHandler())
|
||||
.popChangeHandler(HorizontalChangeHandler())
|
||||
)
|
||||
}
|
||||
DemoModel.MULTIPLE_CHILD_ROUTERS -> {
|
||||
router.pushController(
|
||||
RouterTransaction.with(MultipleChildRouterController())
|
||||
.pushChangeHandler(FadeChangeHandler())
|
||||
.popChangeHandler(FadeChangeHandler())
|
||||
)
|
||||
}
|
||||
DemoModel.MASTER_DETAIL -> {
|
||||
router.pushController(
|
||||
RouterTransaction.with(MasterDetailListController())
|
||||
@@ -214,15 +188,13 @@ private enum class DemoModel(val title: String, @ColorRes val color: Int) {
|
||||
COMPOSE("Jetpack Compose", R.color.amber_500),
|
||||
NAVIGATION("Navigation Demos", R.color.red_300),
|
||||
TRANSITIONS("Transition Demos", R.color.blue_grey_300),
|
||||
SHARED_ELEMENT_TRANSITIONS("Shared Element Demos", R.color.purple_300),
|
||||
ON_BACK_PRESSED_CALLBACK("Back Handling", R.color.purple_300),
|
||||
CHILD_CONTROLLERS("Child Controllers", R.color.orange_300),
|
||||
VIEW_PAGER("ViewPager", R.color.green_300),
|
||||
VIEW_PAGER_2("ViewPager2", R.color.pink_300),
|
||||
TARGET_CONTROLLER("Target Controller", R.color.deep_orange_300),
|
||||
MULTIPLE_CHILD_ROUTERS("Multiple Child Routers", R.color.grey_300),
|
||||
MASTER_DETAIL("Master Detail", R.color.lime_300),
|
||||
DRAG_DISMISS("Drag Dismiss", R.color.teal_300),
|
||||
EXTERNAL_MODULES("Bonus Modules", R.color.deep_purple_300)
|
||||
}
|
||||
|
||||
private class HomeAdapter(
|
||||
@@ -251,9 +223,9 @@ private class HomeAdapter(
|
||||
|
||||
fun bind(position: Int, item: DemoModel) {
|
||||
binding.title.text = item.title
|
||||
binding.dot.drawable.setColorFilter(
|
||||
binding.dot.drawable.colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
|
||||
ContextCompat.getColor(binding.root.context, item.color),
|
||||
PorterDuff.Mode.SRC_ATOP
|
||||
BlendModeCompat.SRC_ATOP,
|
||||
)
|
||||
binding.root.setOnClickListener { modelClickListener(item, position) }
|
||||
|
||||
@@ -263,4 +235,48 @@ private class HomeAdapter(
|
||||
binding.root.resources.getString(R.string.transition_tag_dot_indexed, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WhateverChangeHandler : ControllerChangeHandler() {
|
||||
override val enableOnBackGestureCallbacks = true
|
||||
|
||||
override fun handleOnBackStarted(container: ViewGroup, to: View?, from: View, event: BackEventCompat) {
|
||||
println("handleOnBackStarted")
|
||||
to?.let { container.addView(it) }
|
||||
}
|
||||
|
||||
override fun handleOnBackProgressed(container: ViewGroup, to: View?, from: View, event: BackEventCompat) {
|
||||
if (event.swipeEdge == BackEventCompat.EDGE_LEFT) {
|
||||
from.translationX = event.progress * container.width
|
||||
to?.translationX = -container.width + event.progress * container.width
|
||||
} else {
|
||||
from.translationX = -event.progress * container.width
|
||||
to?.translationX = container.width + -event.progress * container.width
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleOnBackCancelled(container: ViewGroup, to: View?, from: View) {
|
||||
to?.let { container.removeView(it) }
|
||||
}
|
||||
|
||||
override fun performChange(
|
||||
container: ViewGroup,
|
||||
from: View?,
|
||||
to: View?,
|
||||
isPush: Boolean,
|
||||
changeListener: ControllerChangeCompletedListener,
|
||||
) {
|
||||
from?.translationX = 0f
|
||||
to?.translationX = 0f
|
||||
println("perform change called")
|
||||
if (isPush) {
|
||||
to?.let { container.addView(it) }
|
||||
from?.let { container.removeView(it) }
|
||||
} else {
|
||||
to?.let { if (it.parent == null) container.addView(it) }
|
||||
from?.let { container.removeView(it) }
|
||||
}
|
||||
|
||||
changeListener.onChangeCompleted()
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -25,4 +25,4 @@ class MultipleChildRouterController : BaseController(R.layout.controller_multipl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package com.bluelinelabs.conductor.demo.controllers
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.RadioButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.demo.ToolbarProvider
|
||||
|
||||
class OnBackPressedCallbackController : Controller() {
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
Toast.makeText(activity!!, "Back handled at the Controller level", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContextAvailable(context: Context) {
|
||||
super.onContextAvailable(context)
|
||||
|
||||
onBackPressedDispatcher?.addCallback(lifecycleOwner, onBackPressedCallback)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup,
|
||||
savedViewState: Bundle?
|
||||
): View {
|
||||
return ComposeView(container.context).apply {
|
||||
setContent {
|
||||
OnBackPressedDemo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(view: View) {
|
||||
super.onAttach(view)
|
||||
|
||||
(activity as? ToolbarProvider)?.toolbar?.apply {
|
||||
title = "OnBackPressed Demo"
|
||||
menu.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OnBackPressedDemo(modifier: Modifier = Modifier) {
|
||||
val radioOptions = BackOption.values()
|
||||
val (selectedOption, onOptionSelected) = remember { mutableStateOf(radioOptions[0]) }
|
||||
|
||||
BackHandler(enabled = selectedOption == BackOption.Composable) {
|
||||
Toast.makeText(activity!!, "Back handled at the Composable level", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
MaterialTheme {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 16.dp),
|
||||
) {
|
||||
LaunchedEffect(selectedOption) {
|
||||
onBackPressedCallback.isEnabled = selectedOption == BackOption.Controller
|
||||
}
|
||||
|
||||
radioOptions.forEach { option ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = (option == selectedOption),
|
||||
onClick = {
|
||||
onOptionSelected(option)
|
||||
},
|
||||
)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RadioButton(
|
||||
selected = (option == selectedOption),
|
||||
onClick = { onOptionSelected(option) },
|
||||
)
|
||||
Text(
|
||||
text = option.title,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class BackOption(val title: String) {
|
||||
Controller("Handle back in Controller"),
|
||||
Composable("Handle back in Composable"),
|
||||
None("Disable back handlers"),
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package com.bluelinelabs.conductor.demo.controllers
|
||||
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.bluelinelabs.conductor.Router.PopRootControllerMode
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||
import com.bluelinelabs.conductor.demo.R
|
||||
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
|
||||
import com.bluelinelabs.conductor.demo.databinding.ControllerParentBinding
|
||||
import com.bluelinelabs.conductor.demo.util.getMaterialColor
|
||||
import com.bluelinelabs.conductor.demo.util.viewBinding
|
||||
|
||||
class ParentController : BaseController(R.layout.controller_parent) {
|
||||
private val binding: ControllerParentBinding by viewBinding(ControllerParentBinding::bind)
|
||||
|
||||
override val title = "Parent/Child Demo"
|
||||
|
||||
private var hasShownAll = false
|
||||
private var animatingOut = false
|
||||
|
||||
override fun onChangeEnded(
|
||||
changeHandler: ControllerChangeHandler,
|
||||
changeType: ControllerChangeType
|
||||
) {
|
||||
if (changeType == ControllerChangeType.PUSH_ENTER) {
|
||||
addChild(0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleBack(): Boolean {
|
||||
val childControllers = childRouters.filter { it.hasRootController() }.size
|
||||
|
||||
return if (childControllers != NUMBER_OF_CHILDREN || animatingOut) {
|
||||
true
|
||||
} else {
|
||||
animatingOut = true
|
||||
super.handleBack()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addChild(index: Int) {
|
||||
val container = when (index) {
|
||||
0 -> binding.childContent1
|
||||
1 -> binding.childContent2
|
||||
2 -> binding.childContent3
|
||||
3 -> binding.childContent4
|
||||
4 -> binding.childContent5
|
||||
else -> throw IllegalStateException("Invalid child index $index")
|
||||
}
|
||||
|
||||
val childRouter = getChildRouter(container).setPopRootControllerMode(PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
|
||||
if (!childRouter.hasRootController()) {
|
||||
val child = ChildController(
|
||||
title = "Child Controller #$index",
|
||||
backgroundColor = resources!!.getMaterialColor(index),
|
||||
colorIsResId = false
|
||||
)
|
||||
child.addLifecycleListener(object : LifecycleListener() {
|
||||
override fun onChangeEnd(
|
||||
controller: Controller,
|
||||
changeHandler: ControllerChangeHandler,
|
||||
changeType: ControllerChangeType
|
||||
) {
|
||||
if (!isBeingDestroyed) {
|
||||
if (changeType == ControllerChangeType.PUSH_ENTER && !hasShownAll) {
|
||||
if (index < NUMBER_OF_CHILDREN - 1) {
|
||||
addChild(index + 1)
|
||||
} else {
|
||||
hasShownAll = true
|
||||
}
|
||||
} else if (changeType == ControllerChangeType.POP_EXIT) {
|
||||
if (index > 0) {
|
||||
removeChild(index - 1)
|
||||
} else {
|
||||
router.popController(this@ParentController)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
childRouter.setRoot(
|
||||
RouterTransaction.with(child)
|
||||
.pushChangeHandler(FadeChangeHandler())
|
||||
.popChangeHandler(FadeChangeHandler())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeChild(index: Int) {
|
||||
val childRouters = childRouters
|
||||
if (index < childRouters.size) {
|
||||
removeChildRouter(childRouters[index])
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NUMBER_OF_CHILDREN = 5
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -81,4 +81,4 @@ abstract class BaseController(
|
||||
toolbar.menu.clear()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,9 @@ package com.bluelinelabs.conductor.demo.util;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.TimeInterpolator;
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.util.ArrayMap;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
@@ -82,7 +79,6 @@ public class AnimUtils {
|
||||
* Interrupting Activity transitions can yield an OperationNotSupportedException when the
|
||||
* transition tries to pause the animator. Yikes! We can fix this by wrapping the Animator:
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
public static class NoPauseAnimator extends Animator {
|
||||
private final Animator mAnimator;
|
||||
private final ArrayMap<AnimatorListener, AnimatorListener> mListeners = new ArrayMap<>();
|
||||
|
||||
+2
-4
@@ -16,14 +16,13 @@
|
||||
|
||||
package com.bluelinelabs.conductor.demo.widget;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import androidx.core.view.NestedScrollingParent;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.core.view.NestedScrollingParent;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -33,7 +32,6 @@ import java.util.List;
|
||||
* Applies an elasticity factor to reduce movement as you approach the given dismiss distance.
|
||||
* Optionally also scales down content during drag.
|
||||
*/
|
||||
@TargetApi(21)
|
||||
public class ElasticDragDismissFrameLayout extends FrameLayout implements NestedScrollingParent {
|
||||
|
||||
public static abstract class ElasticDragDismissCallback {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 487 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 434 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 300 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 338 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 439 KiB |
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#455A64"
|
||||
android:gravity="center"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="8dp"
|
||||
android:text="@string/additional_modules_explanation"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
/>
|
||||
@@ -1,32 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/city_grid_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingEnd="@dimen/activity_horizontal_margin">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/dot"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/circle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
style="@style/Base.TextAppearance.AppCompat.Large"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="40dp" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,56 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/orange_300"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:textSize="28sp"
|
||||
android:padding="16dp"
|
||||
android:text="@string/parent_controller"
|
||||
/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/child_content_1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/child_content_2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/child_content_3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/child_content_4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/child_content_5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -14,7 +14,6 @@
|
||||
android:elevation="6dp"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
tools:targetApi="lollipop"
|
||||
/>
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
android:elevation="6dp"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
tools:targetApi="lollipop"
|
||||
/>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
/>
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_weight="1"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
style="@style/Base.TextAppearance.AppCompat.Large"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="16dp" >
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="fitCenter"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
style="@style/Base.TextAppearance.AppCompat.Large"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -5,9 +5,6 @@
|
||||
<!-- Home Controller -->
|
||||
<string name="about">About</string>
|
||||
|
||||
<!-- Additional Modules Controller -->
|
||||
<string name="additional_modules_explanation">The following modules are provided as additional dependencies, and are not a part of the core library. If you\'ve made a module you want to see included, let us know!</string>
|
||||
|
||||
<!-- Navigation Demo -->
|
||||
<string name="pop_to_root">Pop to Root</string>
|
||||
<string name="go_up">Go Up</string>
|
||||
|
||||
+4
-2
@@ -1,5 +1,5 @@
|
||||
VERSION_CODE=3
|
||||
VERSION_NAME=3.1.9-SNAPSHOT
|
||||
VERSION_CODE=4
|
||||
VERSION_NAME=4.0.0-SNAPSHOT-PREDICTIVE
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
@@ -14,3 +14,5 @@ POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
|
||||
POM_LICENCE_DIST=repo
|
||||
POM_DEVELOPER_ID=erickuck
|
||||
POM_DEVELOPER_NAME=Eric Kuck
|
||||
SONATYPE_HOST=DEFAULT
|
||||
RELEASE_SIGNING_ENABLED=true
|
||||
+26
-30
@@ -1,51 +1,46 @@
|
||||
[versions]
|
||||
minsdk = "16"
|
||||
compilesdk = "28"
|
||||
minsdk = "19"
|
||||
compilesdk = "34"
|
||||
targetsdk = "28"
|
||||
|
||||
activity-compose = "1.3.0"
|
||||
agp = "7.2.1" # Note: update lint version whenever this is updated
|
||||
androidx-annotation = "1.1.0"
|
||||
androidx-appcompat = "1.3.0"
|
||||
androidx-collection = "1.1.0"
|
||||
androidx-core = "1.3.2"
|
||||
androidx-lifecycle = "2.3.1"
|
||||
androidx-savedstate = "1.1.0"
|
||||
androidx-transition = "1.3.1"
|
||||
agp = "8.1.4" # Note: update lint version whenever this is updated
|
||||
androidx-activity = "1.8.2"
|
||||
androidx-annotation = "1.7.1"
|
||||
androidx-appcompat = "1.6.1"
|
||||
androidx-collection = "1.3.0"
|
||||
androidx-core = "1.12.0"
|
||||
androidx-lifecycle = "2.7.0"
|
||||
androidx-savedstate = "1.2.1"
|
||||
androidx-transition = "1.4.1"
|
||||
androidx-viewpager2 = "1.0.0"
|
||||
autodispose = "1.0.0"
|
||||
compose = "1.1.0"
|
||||
dokka = "1.4.32"
|
||||
junit = "4.13"
|
||||
compose = "1.5.4"
|
||||
compose-compiler = "1.5.3"
|
||||
dokka = "1.9.0"
|
||||
junit = "4.13.2"
|
||||
kotest = "4.6.0"
|
||||
kotlin = "1.6.10"
|
||||
leakCanary = "2.7"
|
||||
lint = "30.2.1" # Should always be agp + 23
|
||||
material = "1.2.1"
|
||||
mvnpublish = "0.13.0"
|
||||
kotlin = "1.9.10"
|
||||
leakCanary = "2.12"
|
||||
lint = "31.1.4" # Should always be agp + 23
|
||||
material = "1.11.0"
|
||||
mvnpublish = "0.27.0"
|
||||
picasso = "2.5.2"
|
||||
robolectric = "4.5.1"
|
||||
rxjava2 = "2.1.14"
|
||||
robolectric = "4.11.1"
|
||||
|
||||
[libraries]
|
||||
activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
|
||||
activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
||||
agp = { module = "com.android.tools.build:gradle", version.ref = "agp" }
|
||||
androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" }
|
||||
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
|
||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
||||
androidx-collection = { module = "androidx.collection:collection", version.ref = "androidx-collection" }
|
||||
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
||||
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "androidx-lifecycle" }
|
||||
androidx-lifecycle-livedata-core = { module = "androidx.lifecycle:lifecycle-livedata-core", version.ref = "androidx-lifecycle" }
|
||||
androidx-savedstate-ktx = { module = "androidx.savedstate:savedstate-ktx", version.ref = "androidx-savedstate" }
|
||||
androidx-transition = { module = "androidx.transition:transition", version.ref = "androidx-transition" }
|
||||
androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "androidx-viewpager2" }
|
||||
autodispose = { module = "com.uber.autodispose:autodispose", version.ref = "autodispose" }
|
||||
autodispose-lifecycle = { module = "com.uber.autodispose:autodispose-lifecycle", version.ref = "autodispose" }
|
||||
autodispose-ktx = { module = "com.uber.autodispose:autodispose-ktx", version.ref = "autodispose" }
|
||||
compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" }
|
||||
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
|
||||
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
|
||||
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
|
||||
compose-material = { module = "androidx.compose.material:material", version.ref = "compose" }
|
||||
dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
@@ -58,7 +53,8 @@ lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "lint" }
|
||||
lint-checks = { module = "com.android.tools.lint:lint-checks", version.ref = "lint" }
|
||||
lint-tests = { module = "com.android.tools.lint:lint-tests", version.ref = "lint" }
|
||||
material = { module = "com.google.android.material:material", version.ref = "material" }
|
||||
mvnpublish = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "mvnpublish" }
|
||||
picasso = { module = "com.squareup.picasso:picasso", version.ref = "picasso" }
|
||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
||||
rxjava2 = { module = "io.reactivex.rxjava2:rxjava", version.ref = "rxjava2" }
|
||||
|
||||
[plugins]
|
||||
mvnpublish = { id = "com.vanniktech.maven.publish", version.ref = "mvnpublish" }
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
+15
-2
@@ -1,8 +1,21 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
include ':conductor'
|
||||
include ':conductor-lint'
|
||||
include ':conductor-modules:viewpager'
|
||||
include ':conductor-modules:viewpager2'
|
||||
include ':conductor-modules:autodispose'
|
||||
include ':conductor-modules:arch-components-lifecycle'
|
||||
include ':conductor-modules:androidx-transition'
|
||||
include ':demo'
|
||||
|
||||
Reference in New Issue
Block a user