Compare commits

...

17 Commits

Author SHA1 Message Date
Eric Kuck ca84419e0c Version bump 2017-05-03 16:28:23 -05:00
Eric Kuck a04dec7ec1 Fixes views never attaching if the host activity is stopped before inflation. Fixes #273 2017-05-03 16:03:58 -05:00
Eric Kuck 81a499d121 Now handles sequences of immediate pushing and popping of controllers much better. Also guards against NPEs due to popping a controller during onAttach. Fixes #274 2017-05-03 14:42:17 -05:00
Eric Kuck ff8ab621bc From controller’s initial position now correctly based on translationX in HorizontalChangeHandler. Fixes #279 2017-05-02 12:31:26 -05:00
Eric Kuck cdb5e5a978 Ensures AnimatorChangeHandlers can’t animate more than once per change 2017-05-02 11:27:01 -05:00
Eric Kuck effa410eae Fixes #269 in the demo app 2017-04-24 07:43:35 -05:00
Paul Woitaschek df27bfaa3d Made removeLifecycleListener final too (#268) 2017-04-13 13:45:33 -05:00
Eric Kuck a865c210b6 addLifecycleListener method now final. Fixes #267 2017-04-13 13:39:03 -05:00
Leonardo Ferrari 7820748cce Update README.md (#261)
fix latest version value
2017-04-06 09:30:02 -05:00
Eric Kuck 2147b2aa5e Version bump 2017-04-05 17:09:26 -05:00
fergusonm 19418617dd Pass along the correct view state (#258) 2017-04-04 07:59:12 -05:00
Eric Kuck e1924bf8a7 Args and savedInstanceState bundles now have classLoaders properly set. Fixes #246. 2017-03-31 09:38:20 -05:00
Eric Kuck 6d3faaebe3 Now ensures view hierarchy-affecting calls are made on the main thread. Fixes #255 2017-03-31 09:24:18 -05:00
Eric Kuck 17639129b9 Fixed issue where child controllers added while the parent is in the process of transitioning off the screen would not be properly restored - Fixes #256. 2017-03-31 09:01:13 -05:00
Valery 1c809095ec Fix image display (#251)
Github doesn't understand spaces in file name
2017-03-21 10:16:00 -05:00
Valery 3bc563de38 Fixed readme table (#250) 2017-03-21 09:58:21 -05:00
Eric Kuck 7beb94f8cc Fixed readme table 2017-03-20 16:56:19 -05:00
17 changed files with 245 additions and 83 deletions
+12 -12
View File
@@ -4,8 +4,8 @@
A small, yet full-featured framework that allows building View-based Android applications. Conductor provides a light-weight wrapper around standard Android Views that does just about everything you'd want:
| Conductor
------|------------------------------
| | Conductor |
|-----------|-------------|
:tada: | Easy integration
:point_up: | Single Activity apps without using Fragments
:recycle: | Simple but powerful lifecycle management
@@ -20,26 +20,26 @@ Conductor is architecture-agnostic and does not try to force any design decision
## Installation
```gradle
compile 'com.bluelinelabs:conductor:2.1.1'
compile 'com.bluelinelabs:conductor:2.1.3'
// If you want the components that go along with
// Android's support libraries (currently just a PagerAdapter):
compile 'com.bluelinelabs:conductor-support:2.1.1'
compile 'com.bluelinelabs:conductor-support:2.1.3'
// If you want RxJava lifecycle support:
compile 'com.bluelinelabs:conductor-rxlifecycle:2.1.1'
compile 'com.bluelinelabs:conductor-rxlifecycle:2.1.3'
// If you want RxJava2 lifecycle support:
compile 'com.bluelinelabs:conductor-rxlifecycle2:2.1.1'
compile 'com.bluelinelabs:conductor-rxlifecycle2:2.1.3'
```
SNAPSHOT:
```gradle
compile 'com.bluelinelabs:conductor:2.1.2-SNAPSHOT'
compile 'com.bluelinelabs:conductor-support:2.1.2-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle:2.1.2-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle2:2.1.2-SNAPSHOT'
compile 'com.bluelinelabs:conductor:2.1.4-SNAPSHOT'
compile 'com.bluelinelabs:conductor-support:2.1.4-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle:2.1.4-SNAPSHOT'
compile 'com.bluelinelabs:conductor-rxlifecycle2:2.1.4-SNAPSHOT'
```
You also have to add the url to the snapshot repository:
@@ -55,7 +55,7 @@ allprojects {
## Components to Know
| Conductor Components
| | Conductor Components |
------|------------------------------
__Controller__ | The Controller is the View wrapper that will give you all of your lifecycle management features. Think of it as a lighter-weight and more predictable Fragment alternative with an easier to manage lifecycle.
__Router__ | A Router implements navigation and backstack handling for Controllers. Router objects are attached to Activity/containing ViewGroup pairs. Routers do not directly render or push Views to the container ViewGroup, but instead defer this responsibility to the ControllerChangeHandler specified in a given transaction.
@@ -118,7 +118,7 @@ public class HomeController extends Controller {
The lifecycle of a Controller is significantly simpler to understand than that of a Fragment. A lifecycle diagram is shown below:
![Controller Lifecycle](docs/Controller Lifecycle.jpg)
![Controller Lifecycle](docs/Controller%20Lifecycle.jpg)
## Advanced Topics
@@ -8,14 +8,15 @@ import android.support.annotation.UiThread;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.internal.LifecycleHandler;
import com.bluelinelabs.conductor.internal.ThreadUtils;
/**
* Point of initial interaction with Conductor. Used to attach a {@link Router} to your Activity.
*/
public final class Conductor {
private Conductor() {}
/**
* Conductor will create a {@link Router} that has been initialized for your Activity and containing ViewGroup.
* If an existing {@link Router} is already associated with this Activity/ViewGroup pair, either in memory
@@ -30,6 +31,8 @@ public final class Conductor {
*/
@NonNull @UiThread
public static Router attachRouter(@NonNull Activity activity, @NonNull ViewGroup container, @Nullable Bundle savedInstanceState) {
ThreadUtils.ensureMainThread();
LifecycleHandler lifecycleHandler = LifecycleHandler.install(activity);
Router router = lifecycleHandler.getRouter(container, savedInstanceState);
@@ -8,6 +8,7 @@ import android.content.IntentSender;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
@@ -55,7 +56,7 @@ public abstract class Controller {
private static final String KEY_OVERRIDDEN_PUSH_HANDLER = "Controller.overriddenPushHandler";
private static final String KEY_OVERRIDDEN_POP_HANDLER = "Controller.overriddenPopHandler";
private static final String KEY_VIEW_STATE_HIERARCHY = "Controller.viewState.hierarchy";
private static final String KEY_VIEW_STATE_BUNDLE = "Controller.viewState.bundle";
static final String KEY_VIEW_STATE_BUNDLE = "Controller.viewState.bundle";
private static final String KEY_RETAIN_VIEW_MODE = "Controller.retainViewMode";
private final Bundle args;
@@ -86,19 +87,26 @@ public abstract class Controller {
private final List<LifecycleListener> lifecycleListeners = new ArrayList<>();
private final ArrayList<String> requestedPermissions = new ArrayList<>();
private final ArrayList<RouterRequiringFunc> onRouterSetListeners = new ArrayList<>();
private final Handler handler = new Handler();
private WeakReference<View> destroyedView;
private boolean isPerformingExitTransition;
@NonNull
static Controller newInstance(@NonNull Bundle bundle) {
final String className = bundle.getString(KEY_CLASS_NAME);
//noinspection ConstantConditions
Constructor[] constructors = ClassUtils.classForName(className, false).getConstructors();
Class cls = ClassUtils.classForName(className, false);
Constructor[] constructors = cls.getConstructors();
Constructor bundleConstructor = getBundleConstructor(constructors);
Controller controller;
try {
if (bundleConstructor != null) {
controller = (Controller)bundleConstructor.newInstance(bundle.getBundle(KEY_ARGS));
Bundle args = bundle.getBundle(KEY_ARGS);
if (args != null) {
args.setClassLoader(cls.getClassLoader());
}
controller = (Controller)bundleConstructor.newInstance(args);
} else {
//noinspection ConstantConditions
controller = (Controller)getDefaultConstructor(constructors).newInstance();
@@ -124,7 +132,7 @@ public abstract class Controller {
* @param args Any arguments that need to be retained.
*/
protected Controller(@Nullable Bundle args) {
this.args = args != null ? args : new Bundle();
this.args = args != null ? args : new Bundle(getClass().getClassLoader());
instanceId = UUID.randomUUID().toString();
ensureRequiredConstructor();
}
@@ -211,6 +219,10 @@ public abstract class Controller {
childRouter = new ControllerHostedRouter(container.getId(), tag);
childRouter.setHost(this, container);
childRouters.add(childRouter);
if (isPerformingExitTransition) {
childRouter.setDetachFrozen(true);
}
}
} else if (!childRouter.hasHost()) {
childRouter.setHost(this, container);
@@ -593,7 +605,7 @@ public abstract class Controller {
*
* @param lifecycleListener The listener
*/
public void addLifecycleListener(@NonNull LifecycleListener lifecycleListener) {
public final void addLifecycleListener(@NonNull LifecycleListener lifecycleListener) {
if (!lifecycleListeners.contains(lifecycleListener)) {
lifecycleListeners.add(lifecycleListener);
}
@@ -604,7 +616,7 @@ public abstract class Controller {
*
* @param lifecycleListener The listener to be removed
*/
public void removeLifecycleListener(@NonNull LifecycleListener lifecycleListener) {
public final void removeLifecycleListener(@NonNull LifecycleListener lifecycleListener) {
lifecycleListeners.remove(lifecycleListener);
}
@@ -807,7 +819,7 @@ public abstract class Controller {
}
}
private void attach(@NonNull View view) {
private void attach(@NonNull final View view) {
attachedToUnownedParent = router == null || view.getParent() != router.container;
if (attachedToUnownedParent) {
return;
@@ -823,16 +835,23 @@ public abstract class Controller {
attached = true;
needsAttach = false;
onAttach(view);
// This must be posted in a handler to ensure the system can finish attaching views if needed. Otherwise
// we can get NPEs when developers decide to immediately pop this controller from onAttach.
handler.post(new Runnable() {
@Override
public void run() {
onAttach(view);
if (hasOptionsMenu && !optionsMenuHidden) {
router.invalidateOptionsMenu();
}
if (hasOptionsMenu && !optionsMenuHidden) {
router.invalidateOptionsMenu();
}
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postAttach(this, view);
}
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postAttach(Controller.this, view);
}
}
});
}
void detach(@NonNull View view, boolean forceViewRefRemoval, boolean blockViewRefRemoval) {
@@ -1026,7 +1045,7 @@ public abstract class Controller {
view.saveHierarchyState(hierarchyState);
viewState.putSparseParcelableArray(KEY_VIEW_STATE_HIERARCHY, hierarchyState);
Bundle stateBundle = new Bundle();
Bundle stateBundle = new Bundle(getClass().getClassLoader());
onSaveViewState(view, stateBundle);
viewState.putBundle(KEY_VIEW_STATE_BUNDLE, stateBundle);
@@ -1039,7 +1058,9 @@ public abstract class Controller {
private void restoreViewState(@NonNull View view) {
if (viewState != null) {
view.restoreHierarchyState(viewState.getSparseParcelableArray(KEY_VIEW_STATE_HIERARCHY));
onRestoreViewState(view, viewState.getBundle(KEY_VIEW_STATE_BUNDLE));
Bundle savedViewState = viewState.getBundle(KEY_VIEW_STATE_BUNDLE);
savedViewState.setClassLoader(getClass().getClassLoader());
onRestoreViewState(view, savedViewState);
restoreChildControllerHosts();
@@ -1080,7 +1101,7 @@ public abstract class Controller {
}
outState.putParcelableArrayList(KEY_CHILD_ROUTERS, childBundles);
Bundle savedState = new Bundle();
Bundle savedState = new Bundle(getClass().getClassLoader());
onSaveInstanceState(savedState);
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
@@ -1115,6 +1136,9 @@ public abstract class Controller {
}
this.savedInstanceState = savedInstanceState.getBundle(KEY_SAVED_STATE);
if (this.savedInstanceState != null) {
this.savedInstanceState.setClassLoader(getClass().getClassLoader());
}
performOnRestoreInstanceState();
}
@@ -1133,6 +1157,7 @@ public abstract class Controller {
final void changeStarted(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
if (!changeType.isEnter) {
isPerformingExitTransition = true;
for (ControllerHostedRouter router : childRouters) {
router.setDetachFrozen(true);
}
@@ -1148,6 +1173,7 @@ public abstract class Controller {
final void changeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) {
if (!changeType.isEnter) {
isPerformingExitTransition = false;
for (ControllerHostedRouter router : childRouters) {
router.setDetachFrozen(false);
}
@@ -25,6 +25,7 @@ class ControllerHostedRouter extends Router {
@IdRes private int hostId;
private String tag;
private boolean isDetachFrozen;
ControllerHostedRouter() { }
@@ -69,6 +70,7 @@ class ControllerHostedRouter extends Router {
}
final void setDetachFrozen(boolean frozen) {
isDetachFrozen = frozen;
for (RouterTransaction transaction : backstack) {
transaction.controller.setDetachFrozen(frozen);
}
@@ -80,6 +82,24 @@ class ControllerHostedRouter extends Router {
super.destroy(popViews);
}
@Override
protected void pushToBackstack(@NonNull RouterTransaction entry) {
if (isDetachFrozen) {
entry.controller.setDetachFrozen(true);
}
super.pushToBackstack(entry);
}
@Override
public void setBackstack(@NonNull List<RouterTransaction> newBackstack, @Nullable ControllerChangeHandler changeHandler) {
if (isDetachFrozen) {
for (RouterTransaction transaction : newBackstack) {
transaction.controller.setDetachFrozen(true);
}
}
super.setBackstack(newBackstack, changeHandler);
}
@Override @Nullable
public Activity getActivity() {
return hostController != null ? hostController.getActivity() : null;
@@ -32,7 +32,7 @@ abstract public class RestoreViewOnCreateController extends Controller {
@Override @NonNull
protected final View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return onCreateView(inflater, container, viewState);
return onCreateView(inflater, container, viewState == null ? null : viewState.getBundle(KEY_VIEW_STATE_BUNDLE));
}
/**
@@ -17,6 +17,7 @@ import com.bluelinelabs.conductor.Controller.LifecycleListener;
import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import com.bluelinelabs.conductor.internal.NoOpControllerChangeHandler;
import com.bluelinelabs.conductor.internal.ThreadUtils;
import com.bluelinelabs.conductor.internal.TransactionIndexer;
import java.util.ArrayList;
@@ -83,6 +84,8 @@ public abstract class Router {
*/
@UiThread
public boolean handleBack() {
ThreadUtils.ensureMainThread();
if (!backstack.isEmpty()) {
//noinspection ConstantConditions
if (backstack.peek().controller.handleBack()) {
@@ -103,6 +106,8 @@ public abstract class Router {
@SuppressWarnings("WeakerAccess")
@UiThread
public boolean popCurrentController() {
ThreadUtils.ensureMainThread();
RouterTransaction transaction = backstack.peek();
if (transaction == null) {
throw new IllegalStateException("Trying to pop the current controller when there are none on the backstack.");
@@ -118,26 +123,39 @@ public abstract class Router {
*/
@UiThread
public boolean popController(@NonNull Controller controller) {
RouterTransaction topController = backstack.peek();
boolean poppingTopController = topController != null && topController.controller == controller;
ThreadUtils.ensureMainThread();
RouterTransaction topTransaction = backstack.peek();
boolean poppingTopController = topTransaction != null && topTransaction.controller == controller;
if (poppingTopController) {
trackDestroyingController(backstack.pop());
performControllerChange(backstack.peek(), topTransaction, false);
} else {
RouterTransaction removedTransaction = null;
RouterTransaction nextTransaction = null;
for (RouterTransaction transaction : backstack) {
if (transaction.controller == controller) {
if (controller.isAttached()) {
trackDestroyingController(transaction);
}
backstack.remove(transaction);
removedTransaction = transaction;
} else if (removedTransaction != null) {
if (!transaction.controller.isAttached()) {
nextTransaction = transaction;
}
break;
}
}
}
if (poppingTopController) {
performControllerChange(backstack.peek(), topController, false);
if (removedTransaction != null) {
performControllerChange(nextTransaction, removedTransaction, false);
}
}
if (popsLastView) {
return topController != null;
return topTransaction != null;
} else {
return !backstack.isEmpty();
}
@@ -151,6 +169,8 @@ public abstract class Router {
*/
@UiThread
public void pushController(@NonNull RouterTransaction transaction) {
ThreadUtils.ensureMainThread();
RouterTransaction from = backstack.peek();
pushToBackstack(transaction);
performControllerChange(transaction, from, true);
@@ -165,6 +185,8 @@ public abstract class Router {
@SuppressWarnings("WeakerAccess")
@UiThread
public void replaceTopController(@NonNull RouterTransaction transaction) {
ThreadUtils.ensureMainThread();
RouterTransaction topTransaction = backstack.peek();
if (!backstack.isEmpty()) {
trackDestroyingController(backstack.pop());
@@ -235,6 +257,8 @@ public abstract class Router {
*/
@UiThread
public boolean popToRoot() {
ThreadUtils.ensureMainThread();
return popToRoot(null);
}
@@ -247,6 +271,8 @@ public abstract class Router {
@SuppressWarnings("WeakerAccess")
@UiThread
public boolean popToRoot(@Nullable ControllerChangeHandler changeHandler) {
ThreadUtils.ensureMainThread();
if (backstack.size() > 1) {
//noinspection ConstantConditions
popToTransaction(backstack.root(), changeHandler);
@@ -264,6 +290,8 @@ public abstract class Router {
*/
@UiThread
public boolean popToTag(@NonNull String tag) {
ThreadUtils.ensureMainThread();
return popToTag(tag, null);
}
@@ -277,6 +305,8 @@ public abstract class Router {
@SuppressWarnings("WeakerAccess")
@UiThread
public boolean popToTag(@NonNull String tag, @Nullable ControllerChangeHandler changeHandler) {
ThreadUtils.ensureMainThread();
for (RouterTransaction transaction : backstack) {
if (tag.equals(transaction.tag())) {
popToTransaction(transaction, changeHandler);
@@ -294,6 +324,8 @@ public abstract class Router {
*/
@UiThread
public void setRoot(@NonNull RouterTransaction transaction) {
ThreadUtils.ensureMainThread();
List<RouterTransaction> transactions = Collections.singletonList(transaction);
setBackstack(transactions, transaction.pushChangeHandler());
}
@@ -362,6 +394,8 @@ public abstract class Router {
@SuppressWarnings("WeakerAccess")
@UiThread
public void setBackstack(@NonNull List<RouterTransaction> newBackstack, @Nullable ControllerChangeHandler changeHandler) {
ThreadUtils.ensureMainThread();
List<RouterTransaction> oldVisibleTransactions = getVisibleTransactions(backstack.iterator());
boolean newRootRequiresPush = !(newBackstack.size() > 0 && backstack.contains(newBackstack.get(0)));
@@ -447,6 +481,8 @@ public abstract class Router {
*/
@UiThread
public void rebindIfNeeded() {
ThreadUtils.ensureMainThread();
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
RouterTransaction transaction = backstackIterator.next();
@@ -679,7 +715,7 @@ public abstract class Router {
ControllerChangeHandler.executeChange(toController, fromController, isPush, container, changeHandler, changeListeners);
}
private void pushToBackstack(@NonNull RouterTransaction entry) {
protected void pushToBackstack(@NonNull RouterTransaction entry) {
backstack.push(entry);
}
@@ -122,13 +122,20 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
if (to.getWidth() <= 0 && to.getHeight() <= 0) {
readyToAnimate = false;
to.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
boolean hasRun;
@Override
public boolean onPreDraw() {
final ViewTreeObserver observer = to.getViewTreeObserver();
if (observer.isAlive()) {
observer.removeOnPreDrawListener(this);
}
performAnimation(container, from, to, isPush, addingToView, changeListener);
// Apparently this gets called multiple times, even if removeOnPreDrawListener is called successfully.
if (!hasRun) {
hasRun = true;
performAnimation(container, from, to, isPush, addingToView, changeListener);
}
return true;
}
});
@@ -160,6 +167,16 @@ public abstract class AnimatorChangeHandler extends ControllerChangeHandler {
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);
@@ -46,7 +46,7 @@ public class HorizontalChangeHandler extends AnimatorChangeHandler {
}
if (to != null) {
// Allow this to have a nice transition when coming off an aborted push animation
float fromLeft = from != null ? from.getX() : 0;
float fromLeft = from != null ? from.getTranslationX() : 0;
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, fromLeft - to.getWidth(), 0));
}
}
@@ -20,10 +20,11 @@ import com.bluelinelabs.conductor.ControllerChangeHandler;
public abstract class TransitionChangeHandler extends ControllerChangeHandler {
public interface OnTransitionPreparedListener {
public void onPrepared();
void onPrepared();
}
private boolean canceled;
private boolean needsImmediateCompletion;
/**
* Should be overridden to return the Transition to use while replacing Views.
@@ -43,12 +44,24 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
canceled = true;
}
@Override
public void completeImmediately() {
super.completeImmediately();
needsImmediateCompletion = true;
}
@Override
public void performChange(@NonNull final ViewGroup container, @Nullable final View from, @Nullable final View to, final boolean isPush, @NonNull final ControllerChangeCompletedListener changeListener) {
if (canceled) {
changeListener.onChangeCompleted();
return;
}
if (needsImmediateCompletion) {
executePropertyChanges(container, from, to, null, isPush);
changeListener.onChangeCompleted();
return;
}
final Transition transition = getTransition(container, from, to, isPush);
transition.addListener(new TransitionListener() {
@@ -109,10 +122,10 @@ public abstract class TransitionChangeHandler extends ControllerChangeHandler {
* @param container The container these Views are hosted in
* @param from The previous View in the container or {@code null} if there was no Controller before this transition
* @param to The next View that should be put in the container or {@code null} if no Controller is being transitioned to
* @param transition The transition with which {@code TransitionManager.beginDelayedTransition} has been called
* @param transition The transition with which {@code TransitionManager.beginDelayedTransition} has been called. This will be null only if another ControllerChangeHandler immediately overrides this one.
* @param isPush True if this is a push transaction, false if it's a pop
*/
public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @NonNull Transition transition, boolean isPush) {
public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @Nullable Transition transition, boolean isPush) {
if (from != null && (removesFromViewOnPush() || !isPush) && from.getParent() == container) {
container.removeView(from);
}
@@ -7,6 +7,7 @@ import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.ControllerChangeHandler;
import com.bluelinelabs.conductor.internal.ClassUtils;
@@ -78,4 +79,19 @@ public class TransitionChangeHandlerCompat extends ControllerChangeHandler {
}
}
@Override
public void onAbortPush(@NonNull ControllerChangeHandler newHandler, @Nullable Controller newTop) {
changeHandler.onAbortPush(newHandler, newTop);
}
@Override
public void completeImmediately() {
changeHandler.completeImmediately();
}
@Override
public void setForceRemoveViewOnPush(boolean force) {
changeHandler.setForceRemoveViewOnPush(force);
}
}
@@ -0,0 +1,20 @@
package com.bluelinelabs.conductor.internal;
import android.os.Looper;
import android.util.AndroidRuntimeException;
public class ThreadUtils {
public static void ensureMainThread() {
if (Looper.getMainLooper().getThread() != Thread.currentThread()) {
throw new CalledFromWrongThreadException("Methods that affect the view hierarchy can can only be called from the main thread.");
}
}
private static final class CalledFromWrongThreadException extends AndroidRuntimeException {
CalledFromWrongThreadException(String msg) {
super(msg);
}
}
}
@@ -23,7 +23,7 @@ public class ViewAttachHandler implements OnAttachStateChangeListener {
}
private boolean rootAttached = false;
boolean childrenAttached = false;
private boolean childrenAttached = false;
private boolean activityStopped = false;
private ReportedState reportedState = ReportedState.VIEW_DETACHED;
private ViewAttachListener attachListener;
@@ -55,7 +55,7 @@ public class ViewAttachHandler implements OnAttachStateChangeListener {
rootAttached = false;
if (childrenAttached) {
childrenAttached = false;
reportDetached();
reportDetached(false);
}
}
@@ -78,34 +78,33 @@ public class ViewAttachHandler implements OnAttachStateChangeListener {
public void onActivityStopped() {
activityStopped = true;
reportDetached();
reportDetached(true);
}
void reportAttached() {
private void reportAttached() {
if (rootAttached && childrenAttached && !activityStopped && reportedState != ReportedState.ATTACHED) {
reportedState = ReportedState.ATTACHED;
attachListener.onAttached();
}
}
void reportDetached() {
private void reportDetached(boolean detachedForActivity) {
boolean wasDetachedForActivity = reportedState == ReportedState.ACTIVITY_STOPPED;
boolean isDetachedForActivity = rootAttached;
if (isDetachedForActivity) {
if (detachedForActivity) {
reportedState = ReportedState.ACTIVITY_STOPPED;
} else {
reportedState = ReportedState.VIEW_DETACHED;
}
if (wasDetachedForActivity && !isDetachedForActivity) {
if (wasDetachedForActivity && !detachedForActivity) {
attachListener.onViewDetachAfterStop();
} else {
attachListener.onDetached(isDetachedForActivity);
attachListener.onDetached(detachedForActivity);
}
}
void listenForDeepestChildAttach(final View view, final ChildAttachListener attachListener) {
private void listenForDeepestChildAttach(final View view, final ChildAttachListener attachListener) {
if (!(view instanceof ViewGroup)) {
attachListener.onAttached();
return;
@@ -86,18 +86,6 @@ public class ViewLeakTests {
assertNull(controller.getView());
}
@Test
public void testActivityStopWhenPushNeverAdded() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverAddChangeHandler()));
assertNotNull(controller.getView());
activityProxy.stop(true);
assertNull(controller.getView());
}
@Test
public void testActivityStopWhenPushNeverCompleted() {
Controller controller = new TestController();
@@ -110,9 +98,21 @@ public class ViewLeakTests {
assertNull(controller.getView());
}
@Test
public void testActivityDestroyWhenPushNeverAdded() {
Controller controller = new TestController();
router.pushController(RouterTransaction.with(controller).pushChangeHandler(new NeverAddChangeHandler()));
assertNotNull(controller.getView());
activityProxy.stop(true).destroy();
assertNull(controller.getView());
}
public static class NeverAddChangeHandler extends ControllerChangeHandler {
@Override
public void performChange(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
public void performChange(@NonNull final ViewGroup container, @Nullable View from, @Nullable final View to, boolean isPush, @NonNull ControllerChangeCompletedListener changeListener) {
if (from != null) {
container.removeView(from);
}
@@ -15,7 +15,6 @@ import com.bluelinelabs.conductor.changehandler.TransitionChangeHandler;
import com.bluelinelabs.conductor.demo.R;
import com.bluelinelabs.conductor.demo.changehandler.transitions.FabTransform;
import com.bluelinelabs.conductor.demo.util.AnimUtils;
import com.bluelinelabs.conductor.demo.util.AnimUtils.TransitionEndListener;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class FabToDialogTransitionChangeHandler extends TransitionChangeHandler {
@@ -64,7 +63,7 @@ public class FabToDialogTransitionChangeHandler extends TransitionChangeHandler
}
@Override
public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @NonNull Transition transition, boolean isPush) {
public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @Nullable Transition transition, boolean isPush) {
if (isPush) {
fabParent.removeView(fab);
container.addView(to);
@@ -74,7 +73,7 @@ public class FabToDialogTransitionChangeHandler extends TransitionChangeHandler
* Because otherwise we will be lost when trying to transition back.
* Set it to invisible because we don't want it to jump back after the transition
*/
transition.addListener(new AnimUtils.TransitionEndListener() {
AnimUtils.TransitionEndListener endListener = new AnimUtils.TransitionEndListener() {
@Override
public void onTransitionCompleted(Transition transition) {
fab.setVisibility(View.GONE);
@@ -82,19 +81,29 @@ public class FabToDialogTransitionChangeHandler extends TransitionChangeHandler
fab = null;
fabParent = null;
}
});
};
if (transition != null) {
transition.addListener(endListener);
} else {
endListener.onTransitionCompleted(null);
}
} else {
dialogBackground.setVisibility(View.INVISIBLE);
fabParent.addView(fab);
container.removeView(from);
transition.addListener(new TransitionEndListener() {
AnimUtils.TransitionEndListener endListener = new AnimUtils.TransitionEndListener() {
@Override
public void onTransitionCompleted(Transition transition) {
fabParent.removeView(dialogBackground);
dialogBackground = null;
}
});
};
if (transition != null) {
transition.addListener(endListener);
} else {
endListener.onTransitionCompleted(null);
}
}
}
@@ -95,7 +95,7 @@ public class SharedElementDelayingChangeHandler extends ArcFadeMoveChangeHandler
}
@Override
public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @NonNull Transition transition, boolean isPush) {
public void executePropertyChanges(@NonNull ViewGroup container, @Nullable View from, @Nullable View to, @Nullable Transition transition, boolean isPush) {
if (to != null) {
to.setVisibility(View.VISIBLE);
@@ -49,6 +49,12 @@ public class PagerController extends BaseController {
};
}
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_pager, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
@@ -58,16 +64,13 @@ public class PagerController extends BaseController {
@Override
protected void onDestroyView(@NonNull View view) {
viewPager.setAdapter(null);
if (!getActivity().isChangingConfigurations()) {
viewPager.setAdapter(null);
}
tabLayout.setupWithViewPager(null);
super.onDestroyView(view);
}
@NonNull
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_pager, container, false);
}
@Override
protected String getTitle() {
return "ViewPager Demo";
+1 -1
View File
@@ -1,4 +1,4 @@
VERSION_NAME=2.1.2-SNAPSHOT
VERSION_NAME=2.1.4-SNAPSHOT
VERSION_CODE=2
GROUP=com.bluelinelabs