I am using the new KitKat Transitions API on Android. I have created two Scene
objects using two layouts. I animate from Scene 1
to Scene 2
inside a Fragment
. I want to automatically move back to the previous Scene
when the user presses the back button.
Is there some kind of built-in backstack mechanism when using Transitions
, or do I have to roll my own?
It is easy enough to call TransitionManager.go(scene1)
, but I really do not want to implement an onBackPressed()
listener in all my fragments that have Scene
animations.
I ended up rolling my own solution.
Have your Activity
implement this
public interface SceneBackstackHandler {
public void addBackstackListener(BackstackListener listener);
public void removeBackstackListener(BackstackListener listener);
public void removeAllBackstackListeners();
public interface BackstackListener {
public boolean onBackPressed();
}
}
Activity
private final Object mBackstackListenerLock = new Object();
private List<BackstackListener> mBackstackListeners = new ArrayList<>();
@Override
public void onBackPressed() {
synchronized (mBackstackListenerLock) {
for (BackstackListener mBackstackListener : mBackstackListeners) {
if (mBackstackListener.onBackPressed()) {
// handled by fragment
return;
}
}
super.onBackPressed();
}
}
@Override
protected void onPause() {
super.onPause();
removeAllBackstackListeners();
}
@Override
public void addBackstackListener(BackstackListener listener) {
synchronized (mBackstackListenerLock) {
mBackstackListeners.add(listener);
}
}
@Override
public void removeBackstackListener(BackstackListener listener) {
synchronized (mBackstackListenerLock) {
mBackstackListeners.remove(listener);
}
}
@Override
public void removeAllBackstackListeners() {
synchronized (mBackstackListenerLock) {
mBackstackListeners.clear();
}
}
Child Fragment:
public class MySceneFragment extends Fragment
implements SceneBackstackHandler.BackstackListener {
private Scene mCurrentScene;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mBackstackHandler = (SceneBackstackHandler) activity;
mBackstackHandler.addBackstackListener(this);
}
@Override
public void onDetach() {
super.onDetach();
mBackstackHandler.removeBackstackListener(this);
}
@Override
public boolean onBackPressed() {
if (mCurrentScene != null && mCurrentScene.equals(mMyScene)) {
removeMyScene();
return true;
}
return false;
}
private void changeScene(Scene scene) {
TransitionManager.go(scene);
mCurrentScene = scene;
}
}
I use an Otto event bus to communicate between my Activity
and Fragment
s. The controlling Activity
maintains its own Stack
of custom back events which each contain a back action Runnable
, i.e. what action should be taken when the back button is pressed.
The advantage to this approach is a slightly more decoupled design and should scale with more fragments. For readability, I have defined the Otto Events inside my Fragment
, here, but these could be easily moved elsewhere in your project.
Here's some sample code to give you an idea of how it's done.
Fragment(s)
The Fragment signals its intent to take hold of the next back press by posting a BackStackRequestEvent
to the Otto event bus and supplying a Runnable
action to be executed when the event is popped off the Activity
's custom stack. When the Fragment is detached, it sends a ClearBackStackEvent
to the bus to remove any of the Fragment
's back actions from the activity's custom stack.
public class MyFragment extends Fragment {
private final String BACK_STACK_ID = "MY_FRAGMENT";
...
public class BackStackRequestEvent {
private Runnable action;
private String id;
public BackStackRequestEvent(Runnable action, String id) {
this.action = action;
this.id = id;
}
public void goBack() {
action.run();
}
public String getId() {
return id;
}
}
public class ClearBackStackEvent {
private String id;
public ClearBackStackEvent(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
...
@Override
public void onDetach() {
super.onDetach();
// Get your Otto singleton and notify Activity that this
// Fragment's back actions are no longer needed
// The Fragment lifecycle stage in which you do this might vary
// based on your needs
EventBus.getInstance().post(new ClearBackStackEvent(BACK_STACK_ID));
}
...
public void someChangeInFragment() {
// Notify the Activity that we want to intercept the next onBackPressed
EventBus.getInstance().post(new BackStackRequestEvent(new Runnable()
{
@Override
public void run() {
// Reverse what we did
doBackAction();
}
}, BACK_STACK_ID)); // constant used later to remove items from Stack
}
}
Activity
The activity registers / unregisters its interest in the events we defined above in onStart()
and onStop()
. When it receives a new BackStackRequestEvent
it adds it to its custom back stack. Once onBackPressed()
is called, it pops the back stack and invokes the back action using BackStackRequestEvent.goBack()
which in turn runs the Fragment's Runnable
. If there is nothing on the Stack, the normal back behaviour is followed.
When the Fragment is detached, the Activity receives a ClearBackStackEvent
and it removes all items of the supplied id
from the Stack.
public class MyActivity extends Activity {
private Stack<MyFragment.BackStackRequestEvent> customBackStack = new Stack<>();
...
@Override
protected void onStart() {
super.onStart();
EventBus.getInstance().register(this);
}
@Override
protected void onStop() {
super.onStop();
EventBus.getInstance().unregister(this);
}
@Subscribe // Annotation indicating that we want to intercept this Otto event
public void backStackRequested(MyFragment.BackStackRequestEvent request) {
customBackStack.push(request);
}
@Override
public void onBackPressed() {
if (customBackStack.empty()) {
// No custom actions so default behaviour followed
super.onBackPressed();
}
else {
// Pop the custom action and call its goBack() action
MyFragment.BackStackRequestEvent back = customBackStack.pop();
back.goBack();
}
}
@Subscribe
public void clearBackStackRequested(MyFragment.ClearBackStackEvent request) {
String id = request.getId();
for (MyFragment.BackStackRequestEvent backItem : customBackStack) {
if (backItem.getId().contentEquals(id)) {
customBackStack.remove(backItem);
}
}
}
}