How to implement a backstack when using the KitKat

2019-03-20 18:02发布

问题:

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.

回答1:

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;
    }
}


回答2:

I use an Otto event bus to communicate between my Activity and Fragments. 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);
            }
        }
    }
}