com.solera.defrag.ViewStack.java Source code

Java tutorial

Introduction

Here is the source code for com.solera.defrag.ViewStack.java

Source

/*
 * Copyright 2016 Tom Hall.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.solera.defrag;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.Pair;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Handles a stack of views, and animations between these views.
 */
public class ViewStack extends FrameLayout {
    //Explicitly create a new string - as we use this reference as a token
    public static final Bundle USE_EXISTING_SAVED_STATE = new Bundle();
    private static final String SINGLE_PARAMETER_KEY = "view_stack_single_param";
    final Deque<ViewStackEntry> viewStack = new ArrayDeque<>();
    private final Collection<ViewStackListener> viewStackListeners = new CopyOnWriteArrayList<>();
    @TraversingState
    private int traversingState = TraversingState.IDLE;
    private AnimationHandler animationHandler = new DefaultAnimationHandler();
    private Object result;

    public ViewStack(Context context) {
        super(context);
    }

    public ViewStack(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ViewStack(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public ViewStack(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @NonNull
    public AnimationHandler setAnimationHandler(@NonNull AnimationHandler handler) {
        final AnimationHandler oldHandler = this.animationHandler;
        animationHandler = handler;
        return oldHandler;
    }

    /**
     * It should be called in the {@link Activity#onBackPressed()} in order to handle the back press
     * events correctly.
     *
     * @return true if the back press event was handled by the ViewStack, false otherwise (and so the
     * activity should handle this event).
     */
    @Deprecated
    public boolean onBackPressed() {
        final View topView = getTopView();
        if (topView != null && topView instanceof HandlesBackPresses) {
            return ((HandlesBackPresses) topView).onBackPressed();
        }
        return pop();
    }

    @Nullable
    public View getTopView() {
        final ViewStackEntry peek = viewStack.peek();
        if (peek != null) {
            return peek.getView();
        }
        return null;
    }

    /**
     * Pops the top view from the stack.
     *
     * @return true if the operation succeeded, or false if there was no view.
     */
    public boolean pop() {
        return popWithResult(1, null);
    }

    public boolean popWithResult(int count, @Nullable Object result) {
        if (viewStack.size() <= count) {
            return false;
        }
        this.result = result;
        setTraversingState(TraversingState.POPPING);
        final View fromView = viewStack.pop().getView();
        while (--count > 0) {
            viewStack.pop();
        }
        final ViewStackEntry peek = viewStack.peek();
        final View toView = peek.getView();
        addView(toView);
        peek.restoreState(toView);
        ViewUtils.waitForMeasure(toView, new ViewUtils.OnMeasuredCallback() {
            @Override
            public void onMeasured(View view, int width, int height) {
                ViewStack.this.runAnimation(fromView, toView, TraversingOperation.POP);
            }
        });
        return true;
    }

    public void addTraversingListener(@NonNull ViewStackListener listener) {
        ViewUtils.verifyMainThread();
        viewStackListeners.add(listener);
    }

    public void removeTraversingListener(@NonNull ViewStackListener listener) {
        ViewUtils.verifyMainThread();
        viewStackListeners.remove(listener);
    }

    @TraversingState
    public int getTraversingState() {
        return traversingState;
    }

    void setTraversingState(@TraversingState int traversing) {
        if (traversing != TraversingState.IDLE && traversingState != TraversingState.IDLE) {
            throw new IllegalStateException("ViewStack is currently traversing");
        }

        traversingState = traversing;
        for (ViewStackListener listener : viewStackListeners) {
            listener.onTraversing(traversingState);
        }
    }

    /**
     * @return the top layout resource reference or 0 if the stack is empty.
     */
    @LayoutRes
    public int getTopLayout() {
        final ViewStackEntry peek = viewStack.peek();
        return peek != null ? peek.mLayout : 0;
    }

    public void replace(@LayoutRes int layout) {
        replaceWithParameters(layout, null);
    }

    public void replaceWithParameter(@LayoutRes int layout, @Nullable Serializable parameter) {
        replaceWithParameters(layout, createSimpleBundle(parameter));
    }

    public void replaceWithParameters(@LayoutRes int layout, @Nullable Bundle parameters) {
        // push layout instead of replacing it when view stack is empty
        if (viewStack.isEmpty()) {
            pushWithParameters(layout, parameters);
            return;
        }
        setTraversingState(TraversingState.REPLACING);
        final ViewStackEntry viewStackEntry = new ViewStackEntry(layout, parameters, null);
        final View view = viewStackEntry.getView();

        final ViewStackEntry topEntry = viewStack.peek();
        final View fromView = topEntry.getView();
        viewStack.push(viewStackEntry);
        addView(view);
        ViewUtils.waitForMeasure(view, new ViewUtils.OnMeasuredCallback() {
            @Override
            public void onMeasured(View view, int width, int height) {
                ViewStack.this.runAnimation(fromView, view, TraversingOperation.REPLACE);
                viewStack.remove(topEntry);
            }
        });
    }

    public int getViewCount() {
        return viewStack.size();
    }

    public void push(@LayoutRes int layout) {
        pushWithParameters(layout, null);
    }

    public void pushWithParameter(@LayoutRes int layout, @Nullable Serializable parameter) {
        pushWithParameters(layout, createSimpleBundle(parameter));
    }

    public void pushWithParameters(@LayoutRes int layout, @Nullable Bundle parameters) {
        final ViewStackEntry viewStackEntry = new ViewStackEntry(layout, parameters, null);
        final View view = viewStackEntry.getView();

        setTraversingState(TraversingState.PUSHING);
        if (viewStack.isEmpty()) {
            viewStack.push(viewStackEntry);
            addView(view);
            ViewUtils.waitForMeasure(view, new ViewUtils.OnMeasuredCallback() {
                @Override
                public void onMeasured(View view, int width, int height) {
                    setTraversingState(TraversingState.IDLE);
                }
            });
            return;
        }

        final ViewStackEntry peek = viewStack.peek();
        final View fromView = peek.getView();
        peek.saveState(fromView);
        viewStack.push(viewStackEntry);
        addView(view);

        ViewUtils.waitForMeasure(view, new ViewUtils.OnMeasuredCallback() {
            @Override
            public void onMeasured(View view, int width, int height) {
                ViewStack.this.runAnimation(fromView, view, TraversingOperation.PUSH);
            }
        });
    }

    /**
     * Replace the current stack with the given views,
     *
     * @param views the list of views to replace the stack with. The list consists of pairs of
     * Integer (layoutId) to Bundle (parameters) for the view. If the Bundle component
     * is the USE_EXISTING_SAVED_STATE tag, then we will use that saved state for that
     * view (if it exists, and is at the right location in the stack) otherwise this will be null.
     */
    public void replaceStack(@NonNull final List<Pair<Integer, Bundle>> views) {
        if (views.isEmpty()) {
            throw new IllegalArgumentException("Cannot replace stack with an empty views stack");
        }

        ViewStackEntry fromEntry = null;
        Iterator<ViewStackEntry> iterator = null;
        setTraversingState(TraversingState.REPLACING);
        if (!viewStack.isEmpty()) {
            fromEntry = viewStack.peek();

            //take a copy of the view stack:
            Deque<ViewStackEntry> copy = new ArrayDeque<>(viewStack);

            viewStack.clear();
            viewStack.push(fromEntry);

            iterator = copy.iterator();
        }

        for (Pair<Integer, Bundle> view : views) {
            Bundle savedParameter = view.second;
            SparseArray<Parcelable> viewState = null;
            if (view.second == USE_EXISTING_SAVED_STATE) {
                savedParameter = null;
                if (iterator != null && iterator.hasNext()) {
                    final ViewStackEntry next = iterator.next();
                    if (next.mLayout == view.first) {
                        savedParameter = next.mParameters;
                        viewState = next.mViewState;
                    } else {
                        iterator = null;
                    }
                }
            }
            viewStack.push(new ViewStackEntry(view.first, savedParameter, viewState));
        }

        final ViewStackEntry toEntry = viewStack.peek();

        final View toView = toEntry.getView();

        if (fromEntry == null || fromEntry.mLayout == toEntry.mLayout) {
            //if current topEntry layout is null or equal to the next proposed topEntry layout
            //we cannot do a transition animation
            viewStack.remove(fromEntry);
            removeAllViews();
            addView(toView);
            ViewUtils.waitForMeasure(toView, new ViewUtils.OnMeasuredCallback() {
                @Override
                public void onMeasured(View view, int width, int height) {
                    setTraversingState(TraversingState.IDLE);
                }
            });
        } else {
            final View fromView = fromEntry.getView();
            addView(toView);

            final ViewStackEntry finalFromEntry = fromEntry;
            ViewUtils.waitForMeasure(toView, new ViewUtils.OnMeasuredCallback() {
                @Override
                public void onMeasured(View view, int width, int height) {
                    ViewStack.this.runAnimation(fromView, toView, TraversingOperation.REPLACE);
                    viewStack.remove(finalFromEntry);
                }
            });
        }
    }

    /**
     * Replace the current stack with the given view and parameters.
     *
     * @param layout the layoutId for the view.
     * @param parameters the parameters for the view. If this is  USE_EXISTING_SAVED_STATE tag, then
     * we will use the saved state for that view (if it exists, and is at the right location in the
     * stack) otherwise this will be null.
     */
    public void replaceStack(@LayoutRes Integer layout, @Nullable Bundle parameters) {
        replaceStack(Collections.singletonList(Pair.create(layout, parameters)));
    }

    /**
     * @param <T> the type of the returned object.
     * @return the result (if any) of the last popped view, and clears this result.
     */
    @SuppressWarnings("unchecked")
    @Nullable
    public <T> T getResult() {
        final T result = (T) this.result;
        this.result = null;
        return result;
    }

    /**
     * @param view the view to retrieve the parameters for.
     * @param <T> the type of the returned parameter.
     * @return the parameters, or null if none found.
     */
    @SuppressWarnings("unchecked")
    @Nullable
    public <T extends Serializable> T getParameter(@NonNull Object view) {
        final Bundle parameters = getParameters(view);
        if (parameters == null) {
            return null;
        } else {
            return (T) parameters.getSerializable(SINGLE_PARAMETER_KEY);
        }
    }

    /**
     * @param view the view to set the parameter for.
     * @param parameter the parameter to set.
     */
    public void setParameter(@NonNull Object view, @Nullable Serializable parameter) {
        setParameters(view, createSimpleBundle(parameter));
    }

    /**
     * @param view the view to return the parameters from.
     * @return the start parameters of the view/presenter
     */
    @Nullable
    public Bundle getParameters(@NonNull Object view) {
        final Iterator<ViewStackEntry> viewStackEntryIterator = viewStack.descendingIterator();
        while (viewStackEntryIterator.hasNext()) {
            final ViewStackEntry viewStackEntry = viewStackEntryIterator.next();
            if (view == viewStackEntry.mViewReference.get()) {
                Bundle bundle = viewStackEntry.mParameters;
                bundle.setClassLoader(view.getClass().getClassLoader());
                return bundle;
            }
        }
        return null;
    }

    public void setParameters(@NonNull Object view, @Nullable Bundle parameters) {
        final Iterator<ViewStackEntry> viewStackEntryIterator = viewStack.descendingIterator();
        while (viewStackEntryIterator.hasNext()) {
            final ViewStackEntry viewStackEntry = viewStackEntryIterator.next();
            if (view == viewStackEntry.mViewReference.get()) {
                viewStackEntry.setParameters(parameters);
                return;
            }
        }
    }

    /**
     * Pop off the stack, with the given result.
     *
     * @param result the result.
     * @return true if the pop operation has been successful, false otherwise.
     */
    public boolean popWithResult(@Nullable Object result) {
        return popWithResult(1, result);
    }

    /**
     * Pop back to the given layout is on top.
     *
     * @param layout the layout to be on the top.
     * @param result the result to return to the (new) top view.
     * @return true if the pop operation has been successful, false otherwise.
     */
    public boolean popBackToWithResult(@LayoutRes int layout, @Nullable Object result) {
        final Iterator<ViewStackEntry> viewStackEntryIterator = viewStack.iterator();
        int popCount = 0;
        while (viewStackEntryIterator.hasNext()) {
            final ViewStackEntry next = viewStackEntryIterator.next();
            if (next.mLayout == layout) {
                return popWithResult(popCount, result);
            }
            popCount++;
        }
        return false;
    }

    public boolean pop(int count) {
        return popWithResult(count, null);
    }

    @Override
    protected int getChildDrawingOrder(int childCount, int index) {
        //if this method gets called - always reverse the order
        //There are at most 2 views in this ViewGroup
        return index == 0 ? 1 : 0;
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        final Parcelable parcelable = super.onSaveInstanceState();
        return SaveState.newInstance(this, parcelable);
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        final SaveState parcelable = (SaveState) state;
        for (SaveStateEntry entry : parcelable.stack()) {
            //we have to cast to SparseArray as we can't serialize a SparseArray<Parcelable>
            viewStack.add(new ViewStackEntry(entry.layout(), entry.parameters(), entry.viewState()));
        }
        if (!viewStack.isEmpty()) {
            addView(viewStack.peek().getView());
        }
        super.onRestoreInstanceState(parcelable.superState());
    }

    void runAnimation(@NonNull final View from, @NonNull final View to, @TraversingOperation int operation) {
        final TraversalAnimation traversalAnimation = createAnimation(from, to, operation);
        if (traversalAnimation == null) {
            removeView(from);
            setTraversingState(TraversingState.IDLE);
        } else {
            final Animator animator = traversalAnimation.animator();
            setChildrenDrawingOrderEnabled(traversalAnimation.drawOrder() == TraversalAnimation.BELOW);
            animator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    removeView(from);
                    setTraversingState(TraversingState.IDLE);
                    setChildrenDrawingOrderEnabled(false);
                }
            });
            animator.start();
        }
    }

    @Nullable
    private TraversalAnimation createAnimation(@NonNull View from, @NonNull View to,
            @TraversingOperation int operation) {
        TraversalAnimation animation = null;
        if (to instanceof HasTraversalAnimation) {
            animation = ((HasTraversalAnimation) to).createAnimation(from);
        }

        if (animation == null) {
            return animationHandler.createAnimation(from, to, operation);
        } else {
            return animation;
        }
    }

    @Nullable
    private Bundle createSimpleBundle(@Nullable Serializable parameter) {
        final Bundle parameterBundle;
        if (parameter == null) {
            parameterBundle = null;
        } else {
            parameterBundle = new Bundle(1);
            parameterBundle.putSerializable(SINGLE_PARAMETER_KEY, parameter);
        }

        return parameterBundle;
    }

    static class SaveState implements Parcelable {
        private List<SaveStateEntry> mStack;
        private Parcelable mSuperState;

        static SaveState newInstance(@NonNull ViewStack viewstack, @NonNull Parcelable superState) {
            List<SaveStateEntry> stack = new ArrayList<>(viewstack.getViewCount());
            for (ViewStackEntry entry : viewstack.viewStack) {
                stack.add(SaveStateEntry.newInstance(entry.mLayout, entry.mParameters, entry.mViewState));
            }
            return new SaveState(stack, superState);
        }

        SaveState(@NonNull List<SaveStateEntry> stack, @NonNull Parcelable superState) {
            mStack = stack;
            mSuperState = superState;
        }

        SaveState(Parcel in) {
            if (in.readByte() == 0x01) {
                mStack = new ArrayList<SaveStateEntry>();
                in.readList(mStack, SaveStateEntry.class.getClassLoader());
            } else {
                mStack = null;
            }
            mSuperState = in.readParcelable(Parcelable.class.getClassLoader());
        }

        @NonNull
        List<SaveStateEntry> stack() {
            return mStack;
        }

        @NonNull
        Parcelable superState() {
            return mSuperState;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            if (mStack == null) {
                dest.writeByte((byte) (0x00));
            } else {
                dest.writeByte((byte) (0x01));
                dest.writeList(mStack);
            }
            dest.writeParcelable(mSuperState, PARCELABLE_WRITE_RETURN_VALUE);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            SaveState saveState = (SaveState) o;

            if (mStack != null ? !mStack.equals(saveState.mStack) : saveState.mStack != null)
                return false;
            return mSuperState != null ? mSuperState.equals(saveState.mSuperState) : saveState.mSuperState == null;
        }

        @Override
        public int hashCode() {
            int result = mStack != null ? mStack.hashCode() : 0;
            result = 31 * result + (mSuperState != null ? mSuperState.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "SaveState{" + "mStack=" + mStack + ", mSuperState=" + mSuperState + '}';
        }

        @SuppressWarnings("unused")
        public static final Parcelable.Creator<SaveState> CREATOR = new Parcelable.Creator<SaveState>() {
            @Override
            public SaveState createFromParcel(Parcel in) {
                return new SaveState(in);
            }

            @Override
            public SaveState[] newArray(int size) {
                return new SaveState[size];
            }
        };
    }

    static class SaveStateEntry implements Parcelable {
        @LayoutRes
        private final int mLayout;
        private final Bundle mParameters;
        private final SparseArray<Parcelable> mViewState;

        static SaveStateEntry newInstance(@LayoutRes int layout, @Nullable Bundle parameters,
                @Nullable SparseArray<Parcelable> viewState) {
            return new SaveStateEntry(layout, parameters, viewState);
        }

        SaveStateEntry(@LayoutRes int layout, @Nullable Bundle parameters,
                @Nullable SparseArray<Parcelable> viewState) {
            mLayout = layout;
            mParameters = parameters;
            mViewState = viewState;
        }

        SaveStateEntry(Parcel in) {
            mLayout = in.readInt();
            mParameters = in.readBundle();
            mViewState = (SparseArray) in.readValue(SparseArray.class.getClassLoader());
        }

        @LayoutRes
        int layout() {
            return mLayout;
        }

        @Nullable
        Bundle parameters() {
            return mParameters;
        }

        @Nullable
        SparseArray<Parcelable> viewState() {
            return mViewState;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            SaveStateEntry that = (SaveStateEntry) o;

            if (mLayout != that.mLayout)
                return false;
            if (mParameters != null ? !mParameters.equals(that.mParameters) : that.mParameters != null) {
                return false;
            }
            return mViewState != null ? mViewState.equals(that.mViewState) : that.mViewState == null;
        }

        @Override
        public int hashCode() {
            int result = mLayout;
            result = 31 * result + (mParameters != null ? mParameters.hashCode() : 0);
            result = 31 * result + (mViewState != null ? mViewState.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "SaveStateEntry{" + "mLayout=" + mLayout + ", mParameters=" + mParameters + ", mViewState="
                    + mViewState + '}';
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(mLayout);
            dest.writeBundle(mParameters);
            dest.writeValue(mViewState);
        }

        @SuppressWarnings("unused")
        public static final Parcelable.Creator<SaveStateEntry> CREATOR = new Parcelable.Creator<SaveStateEntry>() {
            @Override
            public SaveStateEntry createFromParcel(Parcel in) {
                return new SaveStateEntry(in);
            }

            @Override
            public SaveStateEntry[] newArray(int size) {
                return new SaveStateEntry[size];
            }
        };
    }

    private class ViewStackEntry {
        @LayoutRes
        final int mLayout;
        @Nullable
        Bundle mParameters;
        @Nullable
        SparseArray<Parcelable> mViewState;
        WeakReference<View> mViewReference = new WeakReference<>(null);

        ViewStackEntry(@LayoutRes int layout, @Nullable Bundle parameters,
                @Nullable SparseArray<Parcelable> viewState) {
            mLayout = layout;
            mParameters = parameters;
            mViewState = viewState;
        }

        void setParameters(@Nullable Bundle parameters) {
            mParameters = parameters;
        }

        void saveState(@NonNull View view) {
            final SparseArray<Parcelable> parcelableSparseArray = new SparseArray<Parcelable>();
            view.saveHierarchyState(parcelableSparseArray);
            mViewState = parcelableSparseArray;
        }

        void restoreState(@NonNull View view) {
            if (mViewState != null) {
                view.restoreHierarchyState(mViewState);
            }
        }

        @NonNull
        View getView() {
            View view = mViewReference.get();
            if (view == null) {
                view = LayoutInflater.from(getContext()).inflate(mLayout, ViewStack.this, false);
                mViewReference = new WeakReference<>(view);
            }
            return view;
        }
    }
}