me.lizheng.deckview.views.DeckView.java Source code

Java tutorial

Introduction

Here is the source code for me.lizheng.deckview.views.DeckView.java

Source

/*
 * Copyright (C) 2016 Zheng Li <https://lizheng.me>
 * Copyright (C) 2014 The Android Open Source Project
 *
 * 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 me.lizheng.deckview.views;

import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;

import me.lizheng.deckview.R;
import me.lizheng.deckview.helpers.DeckChildViewTransform;
import me.lizheng.deckview.helpers.DeckViewConfig;
import me.lizheng.deckview.utilities.DVUtils;
import me.lizheng.deckview.utilities.DozeTrigger;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;

/* The visual representation of a task stack view */
public class DeckView<T> extends FrameLayout implements /*TaskStack.TaskStackCallbacks,*/
        DeckChildView.DeckChildViewCallbacks<T>, DeckViewScroller.DeckViewScrollerCallbacks,
        ViewPool.ViewPoolConsumer<DeckChildView<T>, T> {

    DeckViewConfig mConfig;

    DeckViewLayoutAlgorithm<T> mLayoutAlgorithm;
    DeckViewScroller mStackScroller;
    DeckViewTouchHandler mTouchHandler;
    ViewPool<DeckChildView<T>, T> mViewPool;
    ArrayList<DeckChildViewTransform> mCurrentTaskTransforms = new ArrayList<>();
    DozeTrigger mUIDozeTrigger;
    Rect mTaskStackBounds = new Rect();
    int mFocusedTaskIndex = -1;
    //    int mPrevAccessibilityFocusedIndex = -1;

    // Optimizations
    int mStackViewsAnimationDuration;
    boolean mStackViewsDirty = true;
    boolean mStackViewsClipDirty = true;
    boolean mAwaitingFirstLayout = true;
    boolean mStartEnterAnimationRequestedAfterLayout;
    boolean mStartEnterAnimationCompleted;
    ViewAnimation.TaskViewEnterContext mStartEnterAnimationContext;
    int[] mTmpVisibleRange = new int[2];
    float[] mTmpCoord = new float[2];
    Matrix mTmpMatrix = new Matrix();
    Rect mTmpRect = new Rect();
    DeckChildViewTransform mTmpTransform = new DeckChildViewTransform();
    HashMap<T, DeckChildView> mTmpTaskViewMap = new HashMap<>();
    LayoutInflater mInflater;

    // A convenience update listener to request updating clipping of tasks
    ValueAnimator.AnimatorUpdateListener mRequestUpdateClippingListener = new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            requestUpdateStackViewsClip();
        }
    };

    public DeckView(Context context) {
        this(context, null);
    }

    public DeckView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DeckView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        DeckViewConfig.reinitialize(getContext());
        mConfig = DeckViewConfig.getInstance();
    }

    public void initialize(Callback<T> callback) {
        mCallback = callback;
        requestLayout();

        mViewPool = new ViewPool<>(getContext(), this);
        mInflater = LayoutInflater.from(getContext());
        mLayoutAlgorithm = new DeckViewLayoutAlgorithm<>(mConfig);
        mStackScroller = new DeckViewScroller(getContext(), mConfig, mLayoutAlgorithm);
        mStackScroller.setCallbacks(this);
        mTouchHandler = new DeckViewTouchHandler(getContext(), this, mConfig, mStackScroller);

        mUIDozeTrigger = new DozeTrigger(mConfig.taskBarDismissDozeDelaySeconds, new Runnable() {
            @Override
            public void run() {
                // Show the task bar dismiss buttons
                int childCount = getChildCount();
                for (int i = 0; i < childCount; i++) {
                    DeckChildView tv = (DeckChildView) getChildAt(i);
                    tv.startNoUserInteractionAnimation();
                }
            }
        });
    }

    /**
     * Resets this TaskStackView for reuse.
     */
    //    void reset() {
    //        // Reset the focused task
    //        resetFocusedTask();
    //
    //        // Return all the views to the pool
    //        int childCount = getChildCount();
    //        for (int i = childCount - 1; i >= 0; i--) {
    //            @SuppressWarnings("unchecked") DeckChildView<T> tv = (DeckChildView) getChildAt(i);
    //            mViewPool.returnViewToPool(tv);
    //        }
    //
    //        // Mark each task view for relayout
    //        if (mViewPool != null) {
    //            Iterator<DeckChildView<T>> iter = mViewPool.poolViewIterator();
    //            if (iter != null) {
    //                while (iter.hasNext()) {
    //                    DeckChildView tv = iter.next();
    //                    tv.reset();
    //                }
    //            }
    //        }
    //
    //        // Reset the stack state
    //        mStackViewsDirty = true;
    //        mStackViewsClipDirty = true;
    //        mAwaitingFirstLayout = true;
    //        mPrevAccessibilityFocusedIndex = -1;
    //        if (mUIDozeTrigger != null) {
    //            mUIDozeTrigger.stopDozing();
    //            mUIDozeTrigger.resetTrigger();
    //        }
    //        mStackScroller.reset();
    //    }

    /**
     * Requests that the views be synchronized with the model
     */
    void requestSynchronizeStackViewsWithModel() {
        requestSynchronizeStackViewsWithModel(0);
    }

    void requestSynchronizeStackViewsWithModel(int duration) {
        if (!mStackViewsDirty) {
            invalidate();
            mStackViewsDirty = true;
        }
        if (mAwaitingFirstLayout) {
            // Skip the animation if we are awaiting first layout
            mStackViewsAnimationDuration = 0;
        } else {
            mStackViewsAnimationDuration = Math.max(mStackViewsAnimationDuration, duration);
        }
    }

    /**
     * Requests that the views clipping be updated.
     */
    void requestUpdateStackViewsClip() {
        if (!mStackViewsClipDirty) {
            invalidate();
            mStackViewsClipDirty = true;
        }
    }

    /**
     * Finds the child view given a specific task.
     */
    public DeckChildView getChildViewForTask(T key) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            DeckChildView tv = (DeckChildView) getChildAt(i);
            if (tv.getAttachedKey().equals(key)) {
                return tv;
            }
        }
        return null;
    }

    /**
     * Returns the stack algorithm for this task stack.
     */
    public DeckViewLayoutAlgorithm getStackAlgorithm() {
        return mLayoutAlgorithm;
    }

    /**
     * Gets the stack transforms of a list of tasks, and returns the visible range of tasks.
     */
    private boolean updateStackTransforms(ArrayList<DeckChildViewTransform> taskTransforms, ArrayList<T> data,
            float stackScroll, int[] visibleRangeOut, boolean boundTranslationsToRect) {
        int taskTransformCount = taskTransforms.size();
        int taskCount = data.size();
        int frontMostVisibleIndex = -1;
        int backMostVisibleIndex = -1;

        // We can reuse the task transforms where possible to reduce object allocation
        if (taskTransformCount < taskCount) {
            // If there are less transforms than tasks, then add as many transforms as necessary
            for (int i = taskTransformCount; i < taskCount; i++) {
                taskTransforms.add(new DeckChildViewTransform());
            }
        } else if (taskTransformCount > taskCount) {
            // If there are more transforms than tasks, then just subset the transform list
            taskTransforms.subList(0, taskCount);
        }

        // Update the stack transforms
        DeckChildViewTransform prevTransform = null;
        for (int i = taskCount - 1; i >= 0; i--) {
            DeckChildViewTransform transform = mLayoutAlgorithm.getStackTransform(data.get(i), stackScroll,
                    taskTransforms.get(i), prevTransform);
            if (transform.visible) {
                if (frontMostVisibleIndex < 0) {
                    frontMostVisibleIndex = i;
                }
                backMostVisibleIndex = i;
            } else {
                if (backMostVisibleIndex != -1) {
                    // We've reached the end of the visible range, so going down the rest of the
                    // stack, we can just reset the transforms accordingly
                    while (i >= 0) {
                        taskTransforms.get(i).reset();
                        i--;
                    }
                    break;
                }
            }

            if (boundTranslationsToRect) {
                transform.translationY = Math.min(transform.translationY, mLayoutAlgorithm.mViewRect.bottom);
            }
            prevTransform = transform;
        }
        if (visibleRangeOut != null) {
            visibleRangeOut[0] = frontMostVisibleIndex;
            visibleRangeOut[1] = backMostVisibleIndex;
        }
        return frontMostVisibleIndex != -1 && backMostVisibleIndex != -1;
    }

    /**
     * Synchronizes the views with the model
     */
    boolean synchronizeStackViewsWithModel() {
        if (mStackViewsDirty) {
            // Get all the task transforms
            ArrayList<T> data = mCallback.getData();
            float stackScroll = mStackScroller.getStackScroll();
            int[] visibleRange = mTmpVisibleRange;
            boolean isValidVisibleRange = updateStackTransforms(mCurrentTaskTransforms, data, stackScroll,
                    visibleRange, false);

            // Return all the invisible children to the pool
            mTmpTaskViewMap.clear();
            int childCount = getChildCount();
            for (int i = childCount - 1; i >= 0; i--) {
                @SuppressWarnings("unchecked")
                DeckChildView<T> tv = (DeckChildView) getChildAt(i);
                T key = tv.getAttachedKey();
                int taskIndex = data.indexOf(key);

                if (visibleRange[1] <= taskIndex && taskIndex <= visibleRange[0]) {
                    mTmpTaskViewMap.put(key, tv);
                } else {
                    mViewPool.returnViewToPool(tv);
                }
            }

            for (int i = visibleRange[0]; isValidVisibleRange && i >= visibleRange[1]; i--) {
                T key = data.get(i);
                DeckChildViewTransform transform = mCurrentTaskTransforms.get(i);
                DeckChildView tv = mTmpTaskViewMap.get(key);

                if (tv == null) {
                    // TODO Check
                    tv = mViewPool.pickUpViewFromPool(key, key);

                    if (mStackViewsAnimationDuration > 0) {
                        // For items in the list, put them in start animating them from the
                        // approriate ends of the list where they are expected to appear
                        if (Float.compare(transform.p, 0f) <= 0) {
                            mLayoutAlgorithm.getStackTransform(0f, 0f, mTmpTransform, null);
                        } else {
                            mLayoutAlgorithm.getStackTransform(1f, 0f, mTmpTransform, null);
                        }
                        tv.updateViewPropertiesToTaskTransform(mTmpTransform, 0);
                    }
                }

                // Animate the task into place
                tv.updateViewPropertiesToTaskTransform(mCurrentTaskTransforms.get(i), mStackViewsAnimationDuration,
                        mRequestUpdateClippingListener);
            }

            // Reset the request-synchronize params
            mStackViewsAnimationDuration = 0;
            mStackViewsDirty = false;
            mStackViewsClipDirty = true;
            return true;
        }
        return false;
    }

    /**
     * Updates the clip for each of the task views.
     */
    void clipTaskViews() {
        // Update the clip on each task child

        int childCount = getChildCount();
        for (int i = 0; i < childCount - 1; i++) {
            DeckChildView tv = (DeckChildView) getChildAt(i);
            DeckChildView nextTv = null;
            DeckChildView tmpTv;
            //int clipBottom = 0;
            if (tv.shouldClipViewInStack()) {
                // Find the next view to clip against
                int nextIndex = i;
                while (nextIndex < getChildCount()) {
                    tmpTv = (DeckChildView) getChildAt(++nextIndex);
                    if (tmpTv != null && tmpTv.shouldClipViewInStack()) {
                        nextTv = tmpTv;
                        break;
                    }
                }

                // Clip against the next view, this is just an approximation since we are
                // stacked and we can make assumptions about the visibility of the this
                // task relative to the ones in front of it.
                if (nextTv != null) {
                    // Map the top edge of next task view into the local space of the current
                    // task view to find the clip amount in local space
                    mTmpCoord[0] = mTmpCoord[1] = 0;
                    DVUtils.mapCoordInDescendentToSelf(nextTv, this, mTmpCoord, false);
                    DVUtils.mapCoordInSelfToDescendent(tv, this, mTmpCoord, mTmpMatrix);
                    //                        clipBottom = (int) Math.floor(tv.getMeasuredHeight() - mTmpCoord[1]
                    //                                - nextTv.getPaddingTop() - 1);
                }
            }
        }

        //            if (getChildCount() > 0) {
        //                // The front most task should never be clipped
        //            }

        mStackViewsClipDirty = false;
    }

    /**
     * The stack insets to apply to the stack contents
     */
    public void setStackInsetRect(Rect r) {
        mTaskStackBounds.set(r);
    }

    /**
     * Updates the min and max virtual scroll bounds
     */
    void updateMinMaxScroll(boolean boundScrollToNewMinMax, boolean launchedWithAltTab, boolean launchedFromHome) {
        // Compute the min and max scroll values
        mLayoutAlgorithm.computeMinMaxScroll(mCallback.getData(), launchedWithAltTab, launchedFromHome);

        // Debug logging
        if (boundScrollToNewMinMax) {
            mStackScroller.boundScroll();
        }
    }

    /**
     * Returns the scroller.
     */
    //    public DeckViewScroller getScroller() {
    //        return mStackScroller;
    //    }

    /**
     * Focuses the task at the specified index in the stack
     */
    void focusTask(int childIndex, boolean scrollToNewPosition, final boolean animateFocusedState) {
        // Return early if the task is already focused
        if (childIndex == mFocusedTaskIndex)
            return;

        ArrayList<T> data = mCallback.getData();

        if (0 <= childIndex && childIndex < data.size()) {
            mFocusedTaskIndex = childIndex;

            // Focus the view if possible, otherwise, focus the view after we scroll into position
            T key = data.get(childIndex);
            DeckChildView tv = getChildViewForTask(key);
            Runnable postScrollRunnable = null;
            if (tv != null) {
                tv.setFocusedTask(animateFocusedState);
            } else {
                postScrollRunnable = new Runnable() {
                    @Override
                    public void run() {
                        DeckChildView tv = getChildViewForTask(mCallback.getData().get(mFocusedTaskIndex));
                        if (tv != null) {
                            tv.setFocusedTask(animateFocusedState);
                        }
                    }
                };
            }

            // Scroll the view into position (just center it in the curve)
            if (scrollToNewPosition) {
                float newScroll = mLayoutAlgorithm.getStackScrollForTask(key) - 0.5f;
                newScroll = mStackScroller.getBoundedStackScroll(newScroll);
                mStackScroller.animateScroll(mStackScroller.getStackScroll(), newScroll, postScrollRunnable);
            } else {
                if (postScrollRunnable != null) {
                    postScrollRunnable.run();
                }
            }
        }
    }

    /**
     * Ensures that there is a task focused, if nothing is focused, then we will use the task
     * at the center of the visible stack.
     */
    public boolean ensureFocusedTask() {
        if (mFocusedTaskIndex < 0) {
            // If there is no task focused, then find the task that is closes to the center
            // of the screen and use that as the currently focused task
            int x = mLayoutAlgorithm.mStackVisibleRect.centerX();
            int y = mLayoutAlgorithm.mStackVisibleRect.centerY();
            int childCount = getChildCount();
            for (int i = childCount - 1; i >= 0; i--) {
                DeckChildView tv = (DeckChildView) getChildAt(i);
                tv.getHitRect(mTmpRect);
                if (mTmpRect.contains(x, y)) {
                    mFocusedTaskIndex = i;
                    break;
                }
            }
            // If we can't find the center task, then use the front most index
            if (mFocusedTaskIndex < 0 && childCount > 0) {
                mFocusedTaskIndex = childCount - 1;
            }
        }
        return mFocusedTaskIndex >= 0;
    }

    /**
     * Focuses the next task in the stack.
     *
     * @param animateFocusedState determines whether to actually draw the highlight along with
     *                            the change in focus, as well as whether to scroll to fit the
     *                            task into view.
     */
    public void focusNextTask(boolean forward, boolean animateFocusedState) {
        // Find the next index to focus
        int numTasks = mCallback.getData().size();
        if (numTasks == 0)
            return;

        int direction = (forward ? -1 : 1);
        int newIndex = mFocusedTaskIndex + direction;
        if (newIndex >= 0 && newIndex <= (numTasks - 1)) {
            newIndex = Math.max(0, Math.min(numTasks - 1, newIndex));
            focusTask(newIndex, true, animateFocusedState);
        }
    }

    /**
     * Dismisses the focused task.
     */
    //    public void dismissFocusedTask() {
    //        // Return early if the focused task index is invalid
    //        if (mFocusedTaskIndex < 0 || mFocusedTaskIndex >= mCallback.getData().size()) {
    //            mFocusedTaskIndex = -1;
    //            return;
    //        }
    //
    //        Long id = mAdapter.getItemId(mFocusedTaskIndex);
    //        T key = mCallback.getData().get(mFocusedTaskIndex);
    //        DeckChildView tv = getChildViewForTask(key);
    //        tv.dismissTask();
    //    }

    /**
     * Resets the focused task.
     */
    //    void resetFocusedTask() {
    //        if ((0 <= mFocusedTaskIndex) && (mFocusedTaskIndex < mCallback.getData().size())) {
    //            DeckChildView tv = getChildViewForTask(mCallback.getData().get(mFocusedTaskIndex));
    //            if (tv != null) {
    //                tv.unsetFocusedTask();
    //            }
    //        }
    //        mFocusedTaskIndex = -1;
    //    }

    @Override
    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
        super.onInitializeAccessibilityEvent(event);
        int childCount = getChildCount();
        if (childCount > 0) {
            @SuppressWarnings("unchecked")
            DeckChildView<T> backMostTask = (DeckChildView) getChildAt(0);
            @SuppressWarnings("unchecked")
            DeckChildView<T> frontMostTask = (DeckChildView) getChildAt(childCount - 1);
            event.setFromIndex(mCallback.getData().indexOf(backMostTask.getAttachedKey()));
            event.setToIndex(mCallback.getData().indexOf(frontMostTask.getAttachedKey()));
        }
        event.setItemCount(mCallback.getData().size());
        event.setScrollY(mStackScroller.mScroller.getCurrY());
        //        event.setMaxScrollY(mStackScroller.progressToScrollRange(mLayoutAlgorithm.mMaxScrollP));
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mTouchHandler.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return mTouchHandler.onTouchEvent(ev);
    }

    @Override
    public boolean onGenericMotionEvent(MotionEvent ev) {
        return mTouchHandler.onGenericMotionEvent(ev);
    }

    @Override
    public void computeScroll() {
        mStackScroller.computeScroll();

        // Synchronize the views
        synchronizeStackViewsWithModel();
        clipTaskViews();

        // Notify accessibility
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED);
    }

    /**
     * Computes the stack and task rects
     */
    public void computeRects(int windowWidth, int windowHeight, Rect taskStackBounds, boolean launchedWithAltTab,
            boolean launchedFromHome) {
        // Compute the rects in the stack algorithm
        mLayoutAlgorithm.computeRects(windowWidth, windowHeight, taskStackBounds);

        // Update the scroll bounds
        updateMinMaxScroll(false, launchedWithAltTab, launchedFromHome);
    }

    public int getCurrentChildIndex() {
        if (getChildCount() == 0)
            return -1;

        @SuppressWarnings("unchecked")
        DeckChildView<T> frontMostChild = (DeckChildView) getChildAt(getChildCount() / 2);

        if (frontMostChild != null) {
            return mCallback.getData().indexOf(frontMostChild.getAttachedKey());
        }

        return -1;
    }

    /**
     * Focuses the task at the specified index in the stack
     */
    public void scrollToChild(int childIndex) {
        if (getCurrentChildIndex() == childIndex)
            return;

        if (0 <= childIndex && childIndex < mCallback.getData().size()) {
            // Scroll the view into position (just center it in the curve)
            float newScroll = mLayoutAlgorithm.getStackScrollForTask(mCallback.getData().get(childIndex)) - 0.5f;
            newScroll = mStackScroller.getBoundedStackScroll(newScroll);
            mStackScroller.setStackScroll(newScroll);
            //Alternate (animated) way
            //mStackScroller.animateScroll(mStackScroller.getStackScroll(), newScroll, null);
        }
    }

    /**
     * Computes the maximum number of visible tasks and thumbnails.  Requires that
     * updateMinMaxScrollForStack() is called first.
     */
    //    public DeckViewLayoutAlgorithm.VisibilityReport computeStackVisibilityReport() {
    //        return mLayoutAlgorithm.computeStackVisibilityReport(mCallback.getData());
    //    }

    /**
     * This is called with the full window width and height to allow stack view children to
     * perform the full screen transition down.
     */
    @SuppressLint("DrawAllocation")
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        Rect _taskStackBounds = new Rect();
        mConfig.getTaskStackBounds(width, height, mConfig.systemInsets.top, mConfig.systemInsets.right,
                _taskStackBounds);

        setStackInsetRect(_taskStackBounds);

        // Compute our stack/task rects
        Rect taskStackBounds = new Rect(mTaskStackBounds);
        taskStackBounds.bottom -= mConfig.systemInsets.bottom;
        computeRects(width, height, taskStackBounds, mConfig.launchedWithAltTab, mConfig.launchedFromHome);

        // If this is the first layout, then scroll to the front of the stack and synchronize the
        // stack views immediately to load all the views
        if (mAwaitingFirstLayout) {
            mStackScroller.setStackScrollToInitialState();
            requestSynchronizeStackViewsWithModel();
            synchronizeStackViewsWithModel();
        }

        // Measure each of the TaskViews
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            DeckChildView tv = (DeckChildView) getChildAt(i);
            if (tv.getBackground() != null) {
                tv.getBackground().getPadding(mTmpRect);
            } else {
                mTmpRect.setEmpty();
            }
            tv.measure(
                    MeasureSpec.makeMeasureSpec(mLayoutAlgorithm.mTaskRect.width() + mTmpRect.left + mTmpRect.right,
                            MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(
                            mLayoutAlgorithm.mTaskRect.height() + mTmpRect.top + mTmpRect.bottom,
                            MeasureSpec.EXACTLY));
        }

        setMeasuredDimension(width, height);
    }

    /**
     * This is called with the size of the space not including the top or right insets, or the
     * search bar height in portrait (but including the search bar width in landscape, since we want
     * to draw under it.
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        // Layout each of the children
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            DeckChildView tv = (DeckChildView) getChildAt(i);
            if (tv.getBackground() != null) {
                tv.getBackground().getPadding(mTmpRect);
            } else {
                mTmpRect.setEmpty();
            }
            tv.layout(mLayoutAlgorithm.mTaskRect.left - mTmpRect.left,
                    mLayoutAlgorithm.mTaskRect.top - mTmpRect.top,
                    mLayoutAlgorithm.mTaskRect.right + mTmpRect.right,
                    mLayoutAlgorithm.mTaskRect.bottom + mTmpRect.bottom);
        }

        if (mAwaitingFirstLayout) {
            mAwaitingFirstLayout = false;
            onFirstLayout();
        }
    }

    /**
     * Handler for the first layout.
     */
    void onFirstLayout() {
        int offscreenY = mLayoutAlgorithm.mViewRect.bottom
                - (mLayoutAlgorithm.mTaskRect.top - mLayoutAlgorithm.mViewRect.top);

        int childCount = getChildCount();

        // Prepare the first view for its enter animation
        for (int i = childCount - 1; i >= 0; i--) {
            DeckChildView tv = (DeckChildView) getChildAt(i);
            // TODO: The false needs to go!
            tv.prepareEnterRecentsAnimation(i == childCount - 1, false, offscreenY);
        }

        // If the enter animation started already and we haven't completed a layout yet, do the
        // enter animation now
        if (mStartEnterAnimationRequestedAfterLayout) {
            startEnterRecentsAnimation(mStartEnterAnimationContext);
            mStartEnterAnimationRequestedAfterLayout = false;
            mStartEnterAnimationContext = null;
        }

        // When Alt-Tabbing, focus the previous task (but leave the animation until we finish the
        // enter animation).
        if (mConfig.launchedWithAltTab) {
            if (mConfig.launchedFromAppWithThumbnail) {
                focusTask(Math.max(0, mCallback.getData().size() - 2), false,
                        mConfig.launchedHasConfigurationChanged);
            } else {
                focusTask(Math.max(0, mCallback.getData().size() - 1), false,
                        mConfig.launchedHasConfigurationChanged);
            }
        }

        // Start dozing
        mUIDozeTrigger.startDozing();
    }

    //    void showDeck(Context context) {
    //        // Try and start the enter animation (or restart it on configuration changed)
    //        ReferenceCountedTrigger t = new ReferenceCountedTrigger(context, null, null, null);
    //        ViewAnimation.TaskViewEnterContext ctx = new ViewAnimation.TaskViewEnterContext(t);
    //
    //        // We have to increment/decrement the post animation trigger in case there are no children
    //        // to ensure that it runs
    //        ctx.postAnimationTrigger.increment();
    //        startEnterRecentsAnimation(ctx);
    //        ctx.postAnimationTrigger.decrement();
    //    }

    /**
     * Requests this task stacks to start it's enter-recents animation
     */
    public void startEnterRecentsAnimation(ViewAnimation.TaskViewEnterContext ctx) {
        // If we are still waiting to layout, then just defer until then
        if (mAwaitingFirstLayout) {
            mStartEnterAnimationRequestedAfterLayout = true;
            mStartEnterAnimationContext = ctx;
            return;
        }

        if (mCallback.getData().size() > 0) {
            int childCount = getChildCount();

            // Animate all the task views into view
            for (int i = childCount - 1; i >= 0; i--) {
                @SuppressWarnings("unchecked")
                DeckChildView<T> tv = (DeckChildView) getChildAt(i);
                T key = tv.getAttachedKey();
                ctx.currentTaskTransform = new DeckChildViewTransform();
                ctx.currentStackViewIndex = i;
                ctx.currentStackViewCount = childCount;
                ctx.currentTaskRect = mLayoutAlgorithm.mTaskRect;
                // TODO: this needs to go
                ctx.currentTaskOccludesLaunchTarget = false;
                ctx.updateListener = mRequestUpdateClippingListener;
                mLayoutAlgorithm.getStackTransform(key, mStackScroller.getStackScroll(), ctx.currentTaskTransform,
                        null);
                tv.startEnterRecentsAnimation(ctx);
            }

            // Add a runnable to the post animation ref counter to clear all the views
            ctx.postAnimationTrigger.addLastDecrementRunnable(new Runnable() {
                @Override
                public void run() {
                    mStartEnterAnimationCompleted = true;
                    // Poke the dozer to restart the trigger after the animation completes
                    mUIDozeTrigger.poke();
                }
            });
        }
    }

    //    @Override
    //    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
    //        // Update the configuration with the latest system insets and trigger a relayout
    //        // mConfig.updateSystemInsets(insets.getSystemWindowInsets());
    //        mConfig.updateSystemInsets(new Rect(insets.getSystemWindowInsetLeft(),
    //                insets.getSystemWindowInsetTop(),
    //                insets.getSystemWindowInsetRight(),
    //                insets.getSystemWindowInsetBottom()));
    //        requestLayout();
    //        return insets.consumeSystemWindowInsets();
    //    }

    //    void hideDeck(Context context, Runnable finishRunnable) {
    //        ReferenceCountedTrigger exitTrigger = new ReferenceCountedTrigger(context,
    //                null, finishRunnable, null);
    //        ViewAnimation.TaskViewExitContext exitCtx =
    //                new ViewAnimation.TaskViewExitContext(exitTrigger);
    //
    //        exitCtx.postAnimationTrigger.increment();
    //        startExitToHomeAnimation(
    //                new ViewAnimation.TaskViewExitContext(exitTrigger));
    //        exitCtx.postAnimationTrigger.decrement();
    //    }

    /**
     * Requests this task stacks to start it's exit-recents animation.
     */
    //    public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) {
    //        // Stop any scrolling
    //        mStackScroller.stopScroller();
    //        mStackScroller.stopBoundScrollAnimation();
    //        // Animate all the task views out of view
    //        ctx.offscreenTranslationY = mLayoutAlgorithm.mViewRect.bottom -
    //                (mLayoutAlgorithm.mTaskRect.top - mLayoutAlgorithm.mViewRect.top);
    //        int childCount = getChildCount();
    //        for (int i = 0; i < childCount; i++) {
    //            DeckChildView tv = (DeckChildView) getChildAt(i);
    //            tv.startExitToHomeAnimation(ctx);
    //        }
    //    }

    /**
     * Animates a task view in this stack as it launches.
     */
    //    public void startLaunchTaskAnimation(DeckChildView tv, Runnable r, boolean lockToTask) {
    //        int childCount = getChildCount();
    //        for (int i = 0; i < childCount; i++) {
    //            DeckChildView t = (DeckChildView) getChildAt(i);
    //            if (t == tv) {
    //                t.setClipViewInStack(false);
    //                t.startLaunchTaskAnimation(r, true, true, lockToTask);
    //            } else {
    //                // TODO: the false needs to go
    //                t.startLaunchTaskAnimation(null, false, false, lockToTask);
    //            }
    //        }
    //    }

    /**
     * Final callback after Recents is finally hidden.
     */
    //    void onRecentsHidden() {
    //        reset();
    //    }

    public boolean isTransformedTouchPointInView(float x, float y, View child) {
        // TODO: confirm if this is the right approach
        if (child == null)
            return false;

        final Rect frame = new Rect();
        child.getHitRect(frame);

        return frame.contains((int) x, (int) y);
    }

    /**
     * Pokes the dozer on user interaction.
     */
    //    void onUserInteraction() {
    //        // Poke the doze trigger if it is dozing
    //        mUIDozeTrigger.poke();
    //    }

    /**
     * * ViewPoolConsumer Implementation ***
     */

    @SuppressWarnings("unchecked")
    @Override
    public DeckChildView createView(Context context) {
        return (DeckChildView) mInflater.inflate(R.layout.deck_child_view, this, false);
    }

    @Override
    public void prepareViewToEnterPool(DeckChildView<T> tv) {
        T key = tv.getAttachedKey();

        mCallback.unloadViewData(key);
        tv.onTaskUnbound();
        tv.onDataUnloaded();

        // Detach the view from the hierarchy
        detachViewFromParent(tv);

        // Reset the view properties
        tv.resetViewProperties();

        // Reset the clip state of the task view
        tv.setClipViewInStack(false);
    }

    @Override
    public void prepareViewToLeavePool(DeckChildView<T> dcv, T key, boolean isNewView) {
        // It is possible for a view to be returned to the view pool before it is laid out,
        // which means that we will need to relayout the view when it is first used next.
        boolean requiresRelayout = dcv.getWidth() <= 0 && !isNewView;

        // Rebind the task and request that this task's data be filled into the TaskView
        dcv.onTaskBound(key);

        // Load the task data
        mCallback.loadViewData(new WeakReference<>(dcv), key);

        // If the doze trigger has already fired, then update the state for this task view
        if (mUIDozeTrigger.hasTriggered()) {
            dcv.setNoUserInteractionState();
        }

        // If we've finished the start animation, then ensure we always enable the focus animations
        if (mStartEnterAnimationCompleted) {
            dcv.enableFocusAnimations();
        }

        // Find the index where this task should be placed in the stack
        int insertIndex = -1;
        int position = mCallback.getData().indexOf(key);
        if (position != -1) {
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                @SuppressWarnings("unchecked")
                T otherKey = ((DeckChildView<T>) getChildAt(i)).getAttachedKey();
                int pos = mCallback.getData().indexOf(otherKey);
                if (position < pos) {
                    insertIndex = i;
                    break;
                }
            }
        }

        // Add/attach the view to the hierarchy
        if (isNewView) {
            addView(dcv, insertIndex);
        } else {
            attachViewToParent(dcv, insertIndex, dcv.getLayoutParams());
            if (requiresRelayout) {
                dcv.requestLayout();
            }
        }

        // Set the new state for this view, including the callbacks and view clipping
        dcv.setCallbacks(this);
        dcv.setTouchEnabled(true);
        dcv.setClipViewInStack(true);
    }

    @Override
    public boolean hasPreferredData(DeckChildView<T> tv, T preferredData) {
        return (tv.getAttachedKey() != null && tv.getAttachedKey().equals(preferredData));
    }

    /**
     * * DeckChildCallbacks Implementation ***
     */

    @Override
    public void onDeckChildViewAppInfoClicked(DeckChildView tv) {
    }

    @Override
    public void onDeckChildViewClicked(DeckChildView<T> dcv, T key) {
        // Cancel any doze triggers
        mUIDozeTrigger.stopDozing();
        mCallback.onItemClick(key);
    }

    @Override
    public void onDeckChildViewDismissed(DeckChildView<T> dcv) {
        boolean taskWasFocused = dcv.isFocusedTask();

        T key = dcv.getAttachedKey();
        int taskIndex = mCallback.getData().indexOf(key);

        onStackTaskRemoved(dcv);

        // If the dismissed task was focused, then we should focus the new task in the same index
        if (taskIndex != -1 && taskWasFocused) {
            int nextTaskIndex = Math.min(mCallback.getData().size() - 1, taskIndex - 1);
            if (nextTaskIndex >= 0) {
                DeckChildView nextTv = getChildViewForTask(mCallback.getData().get(nextTaskIndex));
                if (nextTv != null) {
                    // Focus the next task, and only animate the visible state if we are launched
                    // from Alt-Tab
                    nextTv.setFocusedTask(mConfig.launchedWithAltTab);
                }
            }
        }
    }

    public void onStackTaskRemoved(DeckChildView<T> removedView) {
        // Remove the view associated with this task, we can't rely on updateTransforms
        // to work here because the task is no longer in the list
        if (removedView != null) {
            T key = removedView.getAttachedKey();
            //int removedPosition = mCallback.getData().indexOf(key);
            mViewPool.returnViewToPool(removedView);

            // Notify the callback that we've removed the task and it can clean up after it
            mCallback.onViewDismissed(key);
        }

        /*
        // Get the stack scroll of the task to anchor to (since we are removing something, the front
        // most task will be our anchor task)
        T anchorTask = null;
        float prevAnchorTaskScroll = 0;
        boolean pullStackForward = mCallback.getData().size() > 0;
        if (pullStackForward) {
        anchorTask = mCallback.getData().get(mCallback.getData().size() - 1);
        prevAnchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask);
        }
            
        // Update the min/max scroll and animate other task views into their new positions
        updateMinMaxScroll(true, mConfig.launchedWithAltTab, mConfig.launchedFromHome);
            
        // Offset the stack by as much as the anchor task would otherwise move back
        if (pullStackForward) {
        float anchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask);
        mStackScroller.setStackScroll(mStackScroller.getStackScroll() + (anchorTaskScroll
                - prevAnchorTaskScroll));
        mStackScroller.boundScroll();
        }
            
        // Animate all the tasks into place
        requestSynchronizeStackViewsWithModel(200);
            
        T newFrontMostTask = mCallback.getData().get(mCallback.getData().size() - 1);
        // Update the new front most task
        if (newFrontMostTask != null) {
        DeckChildView<T> frontTv = getChildViewForTask(newFrontMostTask);
        if (frontTv != null) {
            frontTv.onTaskBound(newFrontMostTask);
        }
        }
            
        // If there are no remaining tasks
        if (mCallback.getData().size() == 0) {
        mCallback.onNoViewsToDeck();
        }
        */
    }

    public void notifyDataSetChanged() {
        // Get the stack scroll of the task to anchor to (since we are removing something, the front
        // most task will be our anchor task)
        T anchorTask = null;
        float prevAnchorTaskScroll = 0;
        boolean pullStackForward = mCallback.getData().size() > 0;
        if (pullStackForward) {
            anchorTask = mCallback.getData().get(mCallback.getData().size() - 1);
            prevAnchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask);
        }

        // Update the min/max scroll and animate other task views into their new positions
        updateMinMaxScroll(true, mConfig.launchedWithAltTab, mConfig.launchedFromHome);

        // Offset the stack by as much as the anchor task would otherwise move back
        if (pullStackForward) {
            float anchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask);
            mStackScroller
                    .setStackScroll(mStackScroller.getStackScroll() + (anchorTaskScroll - prevAnchorTaskScroll));
            mStackScroller.boundScroll();
        }

        // Animate all the tasks into place
        requestSynchronizeStackViewsWithModel(100);

        T newFrontMostTask = mCallback.getData().size() > 0
                ? mCallback.getData().get(mCallback.getData().size() - 1)
                : null;
        // Update the new front most task
        if (newFrontMostTask != null) {
            @SuppressWarnings("unchecked")
            DeckChildView<T> frontTv = getChildViewForTask(newFrontMostTask);
            if (frontTv != null) {
                frontTv.onTaskBound(newFrontMostTask);
            }
        }

        // If there are no remaining tasks
        if (mCallback.getData().size() == 0) {
            mCallback.onNoViewsToDeck();
        }
    }

    @Override
    public void onDeckChildViewClipStateChanged(DeckChildView tv) {
        if (!mStackViewsDirty) {
            invalidate();
        }
    }

    @Override
    public void onDeckChildViewFocusChanged(DeckChildView<T> tv, boolean focused) {
        if (focused) {
            mFocusedTaskIndex = mCallback.getData().indexOf(tv.getAttachedKey());
        }
    }

    /**
     * * TaskStackViewScroller.TaskStackViewScrollerCallbacks ***
     */

    @Override
    public void onScrollChanged(float p) {
        mUIDozeTrigger.poke();
        requestSynchronizeStackViewsWithModel();
        ViewCompat.postInvalidateOnAnimation(this);
    }

    //    public void notifyDataSetChangedOld() {
    //        ArrayList<T> data = mCallback.getData();
    //
    //        // Get the stack scroll of the task to anchor to (since we are removing something, the front
    //        // most task will be our anchor task)
    //        T anchorTask = null;
    //        float prevAnchorTaskScroll = 0;
    //        boolean pullStackForward = data.size() > 0;
    //        if (pullStackForward) {
    //            anchorTask = data.get(data.size() - 1);
    //            prevAnchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask);
    //        }
    //
    //        // Update the min/max scroll and animate other task views into their new positions
    //        updateMinMaxScroll(true, mConfig.launchedWithAltTab, mConfig.launchedFromHome);
    //
    //        // Offset the stack by as much as the anchor task would otherwise move back
    //        if (pullStackForward) {
    //            float anchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask);
    //            mStackScroller.setStackScroll(mStackScroller.getStackScroll() + (anchorTaskScroll
    //                    - prevAnchorTaskScroll));
    //            mStackScroller.boundScroll();
    //        }
    //
    //        // Animate all the tasks into place
    //        requestSynchronizeStackViewsWithModel(100);
    //
    //        T newFrontMostTask = data.get(data.size() - 1);
    //        // Update the new front most task
    //        if (newFrontMostTask != null) {
    //            DeckChildView<T> frontTv = getChildViewForTask(newFrontMostTask);
    //            if (frontTv != null) {
    //                frontTv.onTaskBound(newFrontMostTask);
    //            }
    //        }
    //
    //        // If there are no remaining tasks
    //        if (mCallback.getData().size() == 0)
    //            mCallback.onNoViewsToDeck();
    //    }

    Callback<T> mCallback;

    public interface Callback<T> {
        ArrayList<T> getData();

        void loadViewData(WeakReference<DeckChildView<T>> dcv, T item);

        void unloadViewData(T item);

        void onViewDismissed(T item);

        void onItemClick(T item);

        void onNoViewsToDeck();
    }
}