/*
 * Copyright 2014 Niek Haarman
 *
 * 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.nhaarman.listviewanimations.itemmanipulation.expandablelistitem;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.FrameLayout;
import android.widget.LinearLayout;

import com.nhaarman.listviewanimations.ArrayAdapter;
import com.nhaarman.listviewanimations.util.AdapterViewUtil;
import com.nhaarman.listviewanimations.util.ListViewWrapper;
import com.nhaarman.listviewanimations.util.ListViewWrapperSetter;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorListenerAdapter;
import com.nineoldandroids.animation.ValueAnimator;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;

/**
 * An {@link ArrayAdapter} which allows items to be expanded using an animation.
 */
@SuppressWarnings("UnusedDeclaration")
public abstract class ExpandableListItemAdapter<T> extends ArrayAdapter<T> implements ListViewWrapperSetter {

    private static final int DEFAULTTITLEPARENTRESID = 10000;
    private static final int DEFAULTCONTENTPARENTRESID = 10001;

    @NonNull
    private final Context mContext;
    private final int mTitleParentResId;
    private final int mContentParentResId;

    @NonNull
    private final List<Long> mExpandedIds;
    private int mViewLayoutResId;
    private int mActionViewResId;
    private int mLimit;

    @Nullable
    private ListViewWrapper mListViewWrapper;

    @Nullable
    private ExpandCollapseListener mExpandCollapseListener;

    /**
     * Creates a new ExpandableListItemAdapter with an empty list.
     */
    protected ExpandableListItemAdapter(@NonNull final Context context) {
        this(context, null);
    }

    /**
     * Creates a new {@code ExpandableListItemAdapter} with the specified list,
     * or an empty list if items == null.
     */
    protected ExpandableListItemAdapter(@NonNull final Context context, @Nullable final List<T> items) {
        super(items);
        mContext = context;
        mTitleParentResId = DEFAULTTITLEPARENTRESID;
        mContentParentResId = DEFAULTCONTENTPARENTRESID;

        mExpandedIds = new ArrayList<>();
    }

    /**
     * Creates a new ExpandableListItemAdapter with an empty list. Uses given
     * layout resource for the view; titleParentResId and contentParentResId
     * should be identifiers for ViewGroups within that layout.
     */
    protected ExpandableListItemAdapter(@NonNull final Context context, final int layoutResId, final int titleParentResId, final int contentParentResId) {
        this(context, layoutResId, titleParentResId, contentParentResId, null);
    }

    /**
     * Creates a new ExpandableListItemAdapter with the specified list, or an
     * empty list if items == null. Uses given layout resource for the view;
     * titleParentResId and contentParentResId should be identifiers for
     * ViewGroups within that layout.
     */
    protected ExpandableListItemAdapter(@NonNull final Context context, final int layoutResId, final int titleParentResId, final int contentParentResId,
                                        @Nullable final List<T> items) {
        super(items);
        mContext = context;
        mViewLayoutResId = layoutResId;
        mTitleParentResId = titleParentResId;
        mContentParentResId = contentParentResId;

        mExpandedIds = new ArrayList<>();
    }

    @SuppressWarnings("NullableProblems")
    @Override
    public void setListViewWrapper(@NonNull final ListViewWrapper listViewWrapper) {
        mListViewWrapper = listViewWrapper;
    }

    /**
     * Set the resource id of the child {@link android.view.View} contained in the View
     * returned by {@link #getTitleView(int, android.view.View, android.view.ViewGroup)} that will be the
     * actuator of the expand / collapse animations.

     * If there is no View in the title View with given resId, a
     * {@link NullPointerException} is thrown.
 Default behavior: the whole
     * title View acts as the actuator.
     *
     * @param resId the resource id.
     */
    public void setActionViewResId(final int resId) {
        mActionViewResId = resId;
    }

    /**
     * Set the maximum number of items allowed to be expanded. When the
     * (limit+1)th item is expanded, the first expanded item will collapse.
     *
     * @param limit the maximum number of items allowed to be expanded. Use <= 0
     *              for no limit.
     */
    public void setLimit(final int limit) {
        mLimit = limit;
        mExpandedIds.clear();
        notifyDataSetChanged();
    }

    /**
     * Set the {@link ExpandCollapseListener} that should be notified of expand / collapse events.
     */
    public void setExpandCollapseListener(@Nullable final ExpandCollapseListener expandCollapseListener) {
        mExpandCollapseListener = expandCollapseListener;
    }

    @Override
    @NonNull
    public View getView(final int position, @Nullable final View convertView, @NonNull final ViewGroup parent) {
        ViewGroup view = (ViewGroup) convertView;
        ViewHolder viewHolder;

        if (view == null) {
            view = createView(parent);

            viewHolder = new ViewHolder();
            viewHolder.titleParent = (ViewGroup) view.findViewById(mTitleParentResId);
            viewHolder.contentParent = (ViewGroup) view.findViewById(mContentParentResId);

            view.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) view.getTag();
        }

        View titleView = getTitleView(position, viewHolder.titleView, viewHolder.titleParent);
        if (!titleView.equals(viewHolder.titleView)) {
            viewHolder.titleParent.removeAllViews();
            viewHolder.titleParent.addView(titleView);

            if (mActionViewResId == 0) {
                view.setOnClickListener(new TitleViewOnClickListener(viewHolder.contentParent));
            } else {
                view.findViewById(mActionViewResId).setOnClickListener(new TitleViewOnClickListener(viewHolder.contentParent));
            }
        }
        viewHolder.titleView = titleView;

        View contentView = getContentView(position, viewHolder.contentView, viewHolder.contentParent);
        if (!contentView.equals(viewHolder.contentView)) {
            viewHolder.contentParent.removeAllViews();
            viewHolder.contentParent.addView(contentView);
        }
        viewHolder.contentView = contentView;

        viewHolder.contentParent.setVisibility(mExpandedIds.contains(getItemId(position)) ? View.VISIBLE : View.GONE);
        viewHolder.contentParent.setTag(getItemId(position));

        LayoutParams layoutParams = viewHolder.contentParent.getLayoutParams();
        layoutParams.height = LayoutParams.WRAP_CONTENT;
        viewHolder.contentParent.setLayoutParams(layoutParams);

        return view;
    }

    /**
     * Get a View that displays the title of the data at the specified
     * position in the data set. You can either create a View manually or
     * inflate it from an XML layout file. When the View is inflated, the parent
     * View (GridView, ListView...) will apply default layout parameters unless
     * you use
     * {@link android.view.LayoutInflater#inflate(int, android.view.ViewGroup, boolean)}
     * to specify a root view and to prevent attachment to the root.
     *
     * @param position    The position of the item within the adapter's data set of the
     *                    item whose view we want.
     * @param convertView The old view to reuse, if possible. Note: You should check
     *                    that this view is non-null and of an appropriate type before
     *                    using. If it is not possible to convert this view to display
     *                    the correct data, this method can create a new view.
     * @param parent      The parent that this view will eventually be attached to
     *
     * @return A View corresponding to the title of the data at the specified
     * position.
     */
    @NonNull
    public abstract View getTitleView(int position, @Nullable View convertView, @NonNull ViewGroup parent);

    /**
     * Get a View that displays the content of the data at the specified
     * position in the data set. You can either create a View manually or
     * inflate it from an XML layout file. When the View is inflated, the parent
     * View (GridView, ListView...) will apply default layout parameters unless
     * you use
     * {@link android.view.LayoutInflater#inflate(int, android.view.ViewGroup, boolean)}
     * to specify a root view and to prevent attachment to the root.
     *
     * @param position    The position of the item within the adapter's data set of the
     *                    item whose view we want.
     * @param convertView The old view to reuse, if possible. Note: You should check
     *                    that this view is non-null and of an appropriate type before
     *                    using. If it is not possible to convert this view to display
     *                    the correct data, this method can create a new view.
     * @param parent      The parent that this view will eventually be attached to
     *
     * @return A View corresponding to the content of the data at the specified
     * position.
     */
    @NonNull
    public abstract View getContentView(int position, @Nullable View convertView, @NonNull ViewGroup parent);

    /**
     * Indicates if the item at the specified position is expanded.
     *
     * @param position Index of the view whose state we want.
     *
     * @return true if the view is expanded, false otherwise.
     */
    public boolean isExpanded(final int position) {
        long itemId = getItemId(position);
        return mExpandedIds.contains(itemId);
    }

    /**
     * Return the title view at the specified position.
     *
     * @param position Index of the view we want.
     *
     * @return the view if it exist, null otherwise.
     */
    @Nullable
    public View getTitleView(final int position) {
        View titleView = null;

        View parentView = findViewForPosition(position);
        if (parentView != null) {
            Object tag = parentView.getTag();
            if (tag instanceof ViewHolder) {
                titleView = ((ViewHolder) tag).titleView;
            }
        }

        return titleView;
    }

    /**
     * Return the content view at the specified position.
     *
     * @param position Index of the view we want.
     *
     * @return the view if it exist, null otherwise.
     */
    @Nullable
    public View getContentView(final int position) {
        View contentView = null;

        View parentView = findViewForPosition(position);
        if (parentView != null) {
            Object tag = parentView.getTag();
            if (tag instanceof ViewHolder) {
                contentView = ((ViewHolder) tag).contentView;
            }
        }

        return contentView;
    }

    @Override
    public void notifyDataSetChanged() {
        super.notifyDataSetChanged();

        Collection<Long> removedIds = new HashSet<>(mExpandedIds);

        for (int i = 0; i < getCount(); ++i) {
            long id = getItemId(i);
            removedIds.remove(id);
        }

        mExpandedIds.removeAll(removedIds);
    }

    /**
     * Expand the view at given position. Will do nothing if the view is already expanded.
     *
     * @param position the position to expand.
     */
    public void expand(final int position) {
        long itemId = getItemId(position);
        if (mExpandedIds.contains(itemId)) {
            return;
        }

        toggle(position);
    }

    /**
     * Collapse the view at given position. Will do nothing if the view is already collapsed.
     *
     * @param position the position to collapse.
     */
    public void collapse(final int position) {
        long itemId = getItemId(position);
        if (!mExpandedIds.contains(itemId)) {
            return;
        }

        toggle(position);
    }

    /**
     * Toggle the {@link android.view.View} at given position, ignores header or footer Views.
     *
     * @param position the position of the view to toggle.
     */
    public void toggle(final int position) {
        long itemId = getItemId(position);
        boolean isExpanded = mExpandedIds.contains(itemId);

        View contentParent = getContentParent(position);
        if (contentParent != null) {
            toggle(contentParent);
        }

        if (contentParent == null && isExpanded) {
            mExpandedIds.remove(itemId);
        } else if (contentParent == null) {
            mExpandedIds.add(itemId);
        }
    }

    @NonNull
    private ViewGroup createView(@NonNull final ViewGroup parent) {
        ViewGroup view;

        if (mViewLayoutResId == 0) {
            view = new RootView(mContext);
        } else {
            view = (ViewGroup) LayoutInflater.from(mContext).inflate(mViewLayoutResId, parent, false);
        }

        return view;
    }

    /**
     * Return the content parent at the specified position.
     *
     * @param position Index of the view we want.
     *
     * @return the view if it exist, null otherwise.
     */
    @Nullable
    private View getContentParent(final int position) {
        View contentParent = null;

        View parentView = findViewForPosition(position);
        if (parentView != null) {
            Object tag = parentView.getTag();
            if (tag instanceof ViewHolder) {
                contentParent = ((ViewHolder) tag).contentParent;
            }
        }

        return contentParent;
    }

    @Nullable
    private View findViewForPosition(final int position) {
        if (mListViewWrapper == null) {
            throw new IllegalStateException("Call setAbsListView on this ExpanableListItemAdapter!");
        }

        View result = null;
        for (int i = 0; i < mListViewWrapper.getChildCount() && result == null; i++) {
            View childView = mListViewWrapper.getChildAt(i);
            if (childView != null && AdapterViewUtil.getPositionForView(mListViewWrapper, childView) == position) {
                result = childView;
            }
        }
        return result;
    }

    private int findPositionForId(final long id) {
        for (int i = 0; i < getCount(); i++) {
            if (getItemId(i) == id) {
                return i;
            }
        }
        return -1;
    }

    private void toggle(@NonNull final View contentParent) {
        if (mListViewWrapper == null) {
            throw new IllegalStateException("No ListView set!");
        }

        boolean isVisible = contentParent.getVisibility() == View.VISIBLE;
        boolean shouldCollapseOther = !isVisible && mLimit > 0 && mExpandedIds.size() >= mLimit;
        if (shouldCollapseOther) {
            Long firstId = mExpandedIds.get(0);

            int firstPosition = findPositionForId(firstId);
            View firstEV = getContentParent(firstPosition);
            if (firstEV != null) {
                ExpandCollapseHelper.animateCollapsing(firstEV);
            }
            mExpandedIds.remove(firstId);

            if (mExpandCollapseListener != null) {
                mExpandCollapseListener.onItemCollapsed(firstPosition);
            }
        }

        Long id = (Long) contentParent.getTag();
        int position = findPositionForId(id);
        if (isVisible) {
            ExpandCollapseHelper.animateCollapsing(contentParent);
            mExpandedIds.remove(id);

            if (mExpandCollapseListener != null) {
                mExpandCollapseListener.onItemCollapsed(position);
            }

        } else {
            ExpandCollapseHelper.animateExpanding(contentParent, mListViewWrapper);
            mExpandedIds.add(id);

            if (mExpandCollapseListener != null) {
                mExpandCollapseListener.onItemExpanded(position);
            }
        }
    }

    public interface ExpandCollapseListener {

        void onItemExpanded(int position);

        void onItemCollapsed(int position);
    }

    private static class RootView extends LinearLayout {

        private ViewGroup mTitleViewGroup;
        private ViewGroup mContentViewGroup;

        private RootView(@NonNull final Context context) {
            super(context);
            init();
        }

        private void init() {
            setOrientation(VERTICAL);

            mTitleViewGroup = new FrameLayout(getContext());
            mTitleViewGroup.setId(DEFAULTTITLEPARENTRESID);
            addView(mTitleViewGroup);

            mContentViewGroup = new FrameLayout(getContext());
            mContentViewGroup.setId(DEFAULTCONTENTPARENTRESID);
            addView(mContentViewGroup);
        }
    }

    private static class ViewHolder {
        ViewGroup titleParent;
        ViewGroup contentParent;
        View titleView;
        View contentView;
    }

    private static class ExpandCollapseHelper {

        public static void animateCollapsing(final View view) {
            int origHeight = view.getHeight();

            ValueAnimator animator = createHeightAnimator(view, origHeight, 0);
            animator.addListener(
                    new AnimatorListenerAdapter() {

                        @Override
                        public void onAnimationEnd(final Animator animation) {
                            view.setVisibility(View.GONE);
                        }
                    }
            );
            animator.start();
        }

        public static void animateExpanding(@NonNull final View view, @NonNull final ListViewWrapper listViewWrapper) {
            view.setVisibility(View.VISIBLE);

            View parent = (View) view.getParent();
            final int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getMeasuredWidth() - parent.getPaddingLeft() - parent.getPaddingRight(), View.MeasureSpec.AT_MOST);
            final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
            view.measure(widthSpec, heightSpec);

            ValueAnimator animator = createHeightAnimator(view, 0, view.getMeasuredHeight());
            animator.addUpdateListener(
                    new ValueAnimator.AnimatorUpdateListener() {
                        final int listViewHeight = listViewWrapper.getListView().getHeight();
                        final int listViewBottomPadding = listViewWrapper.getListView().getPaddingBottom();
                        final View v = findDirectChild(view, listViewWrapper.getListView());

                        @Override
                        public void onAnimationUpdate(final ValueAnimator animation) {
                            final int bottom = v.getBottom();
                            if (bottom > listViewHeight) {
                                final int top = v.getTop();
                                if (top > 0) {
                                    listViewWrapper.smoothScrollBy(Math.min(bottom - listViewHeight + listViewBottomPadding, top), 0);
                                }
                            }
                        }
                    }
            );
            animator.start();
        }

        public static ValueAnimator createHeightAnimator(final View view, final int start, final int end) {
            ValueAnimator animator = ValueAnimator.ofInt(start, end);
            animator.addUpdateListener(
                    new ValueAnimator.AnimatorUpdateListener() {

                        @Override
                        public void onAnimationUpdate(final ValueAnimator animation) {
                            int value = (Integer) animation.getAnimatedValue();

                            LayoutParams layoutParams = view.getLayoutParams();
                            layoutParams.height = value;
                            view.setLayoutParams(layoutParams);
                        }
                    }
            );
            return animator;
        }

        @NonNull
        private static View findDirectChild(@NonNull final View view, @NonNull final ViewGroup listView) {
            View result = view;
            View parent = (View) result.getParent();
            while (!parent.equals(listView)) {
                result = parent;
                parent = (View) result.getParent();
            }
            return result;
        }
    }

    private class TitleViewOnClickListener implements View.OnClickListener {

        private final View mContentParent;

        private TitleViewOnClickListener(final View contentParent) {
            mContentParent = contentParent;
        }

        @Override
        public void onClick(final View view) {
            toggle(mContentParent);
        }
    }
}


/*
 * Copyright 2014 Niek Haarman
 *
 * 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.nhaarman.listviewanimations.itemmanipulation.animateaddition;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Pair;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ListView;

import com.nhaarman.listviewanimations.BaseAdapterDecorator;
import com.nhaarman.listviewanimations.util.AbsListViewWrapper;
import com.nhaarman.listviewanimations.util.Insertable;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorListenerAdapter;
import com.nineoldandroids.animation.AnimatorSet;
import com.nineoldandroids.animation.ObjectAnimator;
import com.nineoldandroids.animation.ValueAnimator;
import com.nineoldandroids.view.ViewHelper;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

/**
 * An adapter for inserting rows into the {@link android.widget.ListView} with an animation. The root {@link android.widget.BaseAdapter} should implement {@link Insertable},
 * otherwise an {@link IllegalArgumentException} is thrown. This class only works with an instance of {@code ListView}!
 *

 * Usage:

 * - Wrap a new instance of this class around a {@link android.widget.BaseAdapter}.

 * - Set a {@code ListView} to this class using {@link #setListView(android.widget.ListView)}.

 * - Call {@link AnimateAdditionAdapter#insert(int, Object)} to animate the addition of an item.
 *

 * Extend this class and override {@link AnimateAdditionAdapter#getAdditionalAnimators(android.view.View,
 * android.view.ViewGroup)} to provide extra {@link com.nineoldandroids.animation.Animator}s.
 */
public class AnimateAdditionAdapter<T> extends BaseAdapterDecorator {

    private static final long DEFAULT_SCROLLDOWN_ANIMATION_MS = 300;

    private long mScrolldownAnimationDurationMs = DEFAULT_SCROLLDOWN_ANIMATION_MS;

    private static final long DEFAULT_INSERTION_ANIMATION_MS = 300;

    private long mInsertionAnimationDurationMs = DEFAULT_INSERTION_ANIMATION_MS;

    private static final String ALPHA = "alpha";

    @NonNull
    private final Insertable<T> mInsertable;

    @NonNull
    private final InsertQueue<T> mInsertQueue;

    /**
     * Describes whether the list should animate downwards when items are added above the first visible item.
     */
    private boolean mShouldAnimateDown = true;

    /**
     * Create a new {@code AnimateAdditionAdapter} with given {@link android.widget.BaseAdapter}.
     *
     * @param baseAdapter should implement {@link Insertable},
     *                    or be a {@link com.nhaarman.listviewanimations.BaseAdapterDecorator} whose BaseAdapter implements the interface.
     */
    public AnimateAdditionAdapter(@NonNull final BaseAdapter baseAdapter) {
        super(baseAdapter);

        BaseAdapter rootAdapter = getRootAdapter();
        if (!(rootAdapter instanceof Insertable)) {
            throw new IllegalArgumentException("BaseAdapter should implement Insertable!");
        }

        mInsertable = (Insertable<T>) rootAdapter;
        mInsertQueue = new InsertQueue<>(mInsertable);
    }

    /**
     * @deprecated use {@link #setListView(android.widget.ListView)} instead.
     */
    @Override
    @Deprecated
    public void setAbsListView(@NonNull final AbsListView absListView) {
        if (absListView instanceof ListView) {
            setListView((ListView) absListView);
        } else {
            throw new IllegalArgumentException("AnimateAdditionAdapter requires a ListView!");
        }
    }

    /**
     * Sets the {@link android.widget.ListView} that is used for this {@code AnimateAdditionAdapter}.
     */
    public void setListView(@NonNull final ListView listView) {
        setListViewWrapper(new AbsListViewWrapper(listView));
    }

    /**
     * Sets whether the list should animate downwards when items are added above the first visible item.
     *
     * @param shouldAnimateDown defaults to {@code true}.
     */
    @SuppressWarnings("UnusedDeclaration")
    public void setShouldAnimateDown(final boolean shouldAnimateDown) {
        mShouldAnimateDown = shouldAnimateDown;
    }

    /**
     * Set the duration of the scrolldown animation per item for when items are inserted above the first visible item.
     *
     * @param scrolldownAnimationDurationMs the duration in ms.
     */
    @SuppressWarnings("UnusedDeclaration")
    public void setScrolldownAnimationDuration(final long scrolldownAnimationDurationMs) {
        mScrolldownAnimationDurationMs = scrolldownAnimationDurationMs;
    }

    /**
     * Set the duration of the insertion animation.
     *
     * @param insertionAnimationDurationMs the duration in ms.
     */
    @SuppressWarnings("UnusedDeclaration")
    public void setInsertionAnimationDuration(final long insertionAnimationDurationMs) {
        mInsertionAnimationDurationMs = insertionAnimationDurationMs;
    }

    /**
     * Inserts an item at given index. Will show an entrance animation for the new item if the newly added item is visible.
     * Will also call {@link Insertable#add(int, Object)} of the root {@link android.widget.BaseAdapter}.
     *
     * @param index the index the new item should be inserted at.
     * @param item  the item to insert.
     */
    public void insert(final int index, @NonNull final T item) {
        insert(new Pair<>(index, item));
    }

    /**
     * Inserts items, starting at given index. Will show an entrance animation for the new items if the newly added items are visible.
     * Will also call {@link Insertable#add(int, Object)} of the root {@link android.widget.BaseAdapter}.
     *
     * @param index the starting index the new items should be inserted at.
     * @param items the items to insert.
     */
    public void insert(final int index, @NonNull final T... items) {
        Pair<Integer, T>[] pairs = new Pair[items.length];
        for (int i = 0; i < items.length; i++) {
            pairs[i] = new Pair<>(index + i, items[i]);
        }
        insert(pairs);
    }

    /**
     * Inserts items at given indexes. Will show an entrance animation for the new items if the newly added item is visible.
     * Will also call {@link Insertable#add(int, Object)} of the root {@link android.widget.BaseAdapter}.
     *
     * @param indexItemPairs the index-item pairs to insert. The first argument of the {@code Pair} is the index, the second argument is the item.
     */
    public void insert(@NonNull final Pair<Integer, T>... indexItemPairs) {
        insert(Arrays.asList(indexItemPairs));
    }

    /**
     * Inserts items at given indexes. Will show an entrance animation for the new items if the newly added item is visible.
     * Will also call {@link Insertable#add(int, Object)} of the root {@link android.widget.BaseAdapter}.
     *
     * @param indexItemPairs the index-item pairs to insert. The first argument of the {@code Pair} is the index, the second argument is the item.
     */
    public void insert(@NonNull final Iterable<Pair<Integer, T>> indexItemPairs) {
        if (getListViewWrapper() == null) {
            throw new IllegalStateException("Call setListView on this AnimateAdditionAdapter!");
        }

        Collection<Pair<Integer, T>> visibleViews = new ArrayList<>();
        Collection<Integer> insertedPositions = new ArrayList<>();
        Collection<Integer> insertedBelowPositions = new ArrayList<>();

        int scrollDistance = 0;
        int numInsertedAbove = 0;

        for (Pair<Integer, T> pair : indexItemPairs) {
            if (getListViewWrapper().getFirstVisiblePosition() > pair.first) {
                /* Inserting an item above the first visible position */
                int index = pair.first;

                /* Correct the index for already inserted positions above the first visible view. */
                for (int insertedPosition : insertedPositions) {
                    if (index >= insertedPosition) {
                        index++;
                    }
                }

                mInsertable.add(index, pair.second);
                insertedPositions.add(index);
                numInsertedAbove++;

                if (mShouldAnimateDown) {
                    View view = getView(pair.first, null, getListViewWrapper().getListView());
                    view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
                    scrollDistance -= view.getMeasuredHeight();
                }
            } else if (getListViewWrapper().getLastVisiblePosition() >= pair.first || getListViewWrapper().getLastVisiblePosition() == AdapterView.INVALID_POSITION ||
                    !childrenFillAbsListView()) {
                /* Inserting an item that becomes visible on screen */
                int index = pair.first;

                /* Correct the index for already inserted positions above the first visible view */
                for (int insertedPosition : insertedPositions) {
                    if (index >= insertedPosition) {
                        index++;
                    }
                }
                Pair<Integer, T> newPair = new Pair<>(index, pair.second);
                visibleViews.add(newPair);
            } else {
                /* Inserting an item below the last visible item */
                int index = pair.first;

                /* Correct the index for already inserted positions above the first visible view */
                for (int insertedPosition : insertedPositions) {
                    if (index >= insertedPosition) {
                        index++;
                    }
                }

                /* Correct the index for already inserted positions below the last visible view */
                for (int queuedPosition : insertedBelowPositions) {
                    if (index >= queuedPosition) {
                        index++;
                    }
                }

                insertedBelowPositions.add(index);
                mInsertable.add(index, pair.second);
            }
        }

        if (mShouldAnimateDown) {
            ((AbsListView) getListViewWrapper().getListView()).smoothScrollBy(scrollDistance, (int) (mScrolldownAnimationDurationMs * numInsertedAbove));
        }

        mInsertQueue.insert(visibleViews);

        int firstVisiblePosition = getListViewWrapper().getFirstVisiblePosition();
        View firstChild = getListViewWrapper().getChildAt(0);
        int childTop = firstChild == null ? 0 : firstChild.getTop();
        ((ListView) getListViewWrapper().getListView()).setSelectionFromTop(firstVisiblePosition + numInsertedAbove, childTop);
    }

    /**
     * @return true if the children completely fill up the AbsListView.
     */
    private boolean childrenFillAbsListView() {
        if (getListViewWrapper() == null) {
            throw new IllegalStateException("Call setListView on this AnimateAdditionAdapter first!");
        }

        int childrenHeight = 0;
        for (int i = 0; i < getListViewWrapper().getCount(); i++) {
            View child = getListViewWrapper().getChildAt(i);
            if (child != null) {
                childrenHeight += child.getHeight();
            }
        }
        return getListViewWrapper().getListView().getHeight() <= childrenHeight;
    }

    @Override
    @NonNull
    public View getView(final int position, @Nullable final View convertView, @NonNull final ViewGroup parent) {
        final View view = super.getView(position, convertView, parent);

        if (mInsertQueue.getActiveIndexes().contains(position)) {
            int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(ViewGroup.LayoutParams.MATCH_PARENT, View.MeasureSpec.AT_MOST);
            int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(ViewGroup.LayoutParams.WRAP_CONTENT, View.MeasureSpec.UNSPECIFIED);
            view.measure(widthMeasureSpec, heightMeasureSpec);

            int originalHeight = view.getMeasuredHeight();

            ValueAnimator heightAnimator = ValueAnimator.ofInt(1, originalHeight);
            heightAnimator.addUpdateListener(new HeightUpdater(view));

            Animator[] customAnimators = getAdditionalAnimators(view, parent);
            Animator[] animators = new Animator[customAnimators.length + 1];
            animators[0] = heightAnimator;
            System.arraycopy(customAnimators, 0, animators, 1, customAnimators.length);

            AnimatorSet animatorSet = new AnimatorSet();
            animatorSet.playTogether(animators);

            ViewHelper.setAlpha(view, 0);
            ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(view, ALPHA, 0, 1);

            AnimatorSet allAnimatorsSet = new AnimatorSet();
            allAnimatorsSet.playSequentially(animatorSet, alphaAnimator);

            allAnimatorsSet.setDuration(mInsertionAnimationDurationMs);
            allAnimatorsSet.addListener(new ExpandAnimationListener(position));
            allAnimatorsSet.start();
        }

        return view;
    }

    /**
     * Override this method to provide additional animators on top of the default height and alpha animation.
     *
     * @param view   The {@link android.view.View} that will get animated.
     * @param parent The parent that this view will eventually be attached to.
     *
     * @return a non-null array of Animators.
     */
    @SuppressWarnings({"MethodMayBeStatic", "UnusedParameters"})
    @NonNull
    protected Animator[] getAdditionalAnimators(@NonNull final View view, @NonNull final ViewGroup parent) {
        return new Animator[]{};
    }

    /**
     * A class which applies the animated height value to a {@code View}.
     */
    private static class HeightUpdater implements ValueAnimator.AnimatorUpdateListener {

        private final View mView;

        HeightUpdater(final View view) {
            mView = view;
        }

        @Override
        public void onAnimationUpdate(final ValueAnimator animation) {
            ViewGroup.LayoutParams layoutParams = mView.getLayoutParams();
            layoutParams.height = (Integer) animation.getAnimatedValue();
            mView.setLayoutParams(layoutParams);
        }
    }

    /**
     * A class which removes the active index from the {@code InsertQueue} when the animation has finished.
     */
    private class ExpandAnimationListener extends AnimatorListenerAdapter {

        private final int mPosition;

        ExpandAnimationListener(final int position) {
            mPosition = position;
        }

        @Override
        public void onAnimationEnd(final Animator animation) {
            mInsertQueue.removeActiveIndex(mPosition);
        }
    }
}


/*
 * Copyright 2014 Niek Haarman
 *
 * 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.nhaarman.listviewanimations.itemmanipulation;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;

import com.nhaarman.listviewanimations.BaseAdapterDecorator;
import com.nhaarman.listviewanimations.itemmanipulation.animateaddition.AnimateAdditionAdapter;
import com.nhaarman.listviewanimations.itemmanipulation.dragdrop.DragAndDropHandler;
import com.nhaarman.listviewanimations.itemmanipulation.dragdrop.DraggableManager;
import com.nhaarman.listviewanimations.itemmanipulation.dragdrop.DynamicListViewWrapper;
import com.nhaarman.listviewanimations.itemmanipulation.dragdrop.OnItemMovedListener;
import com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.DismissableManager;
import com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.OnDismissCallback;
import com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.SwipeDismissTouchListener;
import com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.SwipeTouchListener;
import com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.undo.SwipeUndoAdapter;
import com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.undo.SwipeUndoTouchListener;
import com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.undo.UndoCallback;
import com.nhaarman.listviewanimations.util.Insertable;

import java.util.Collection;
import java.util.HashSet;

/**
 * A {@link android.widget.ListView} implementation which provides the following functionality:
 *

 *
Drag and drop

 *
Swipe to dismiss

 *
Swipe to dismiss with contextual undo

 *
Animate addition

 *

 */
public class DynamicListView extends ListView {

    @NonNull
    private final MyOnScrollListener mMyOnScrollListener;

    /**
     * The {@link com.nhaarman.listviewanimations.itemmanipulation.dragdrop.DragAndDropHandler}
     * that will handle drag and drop functionality, if set.
     */
    @Nullable
    private DragAndDropHandler mDragAndDropHandler;

    /**
     * The {@link com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.SwipeTouchListener}
     * that will handle swipe movement functionality, if set.
     */
    @Nullable
    private SwipeTouchListener mSwipeTouchListener;

    /**
     * The {@link com.nhaarman.listviewanimations.itemmanipulation.TouchEventHandler}
     * that is currently actively consuming {@code MotionEvent}s.
     */
    @Nullable
    private TouchEventHandler mCurrentHandlingTouchEventHandler;

    /**
     * The {@link com.nhaarman.listviewanimations.itemmanipulation.animateaddition.AnimateAdditionAdapter}
     * that is possibly set to animate insertions.
     */
    @Nullable
    private AnimateAdditionAdapter<Object> mAnimateAdditionAdapter;

    @Nullable
    private SwipeUndoAdapter mSwipeUndoAdapter;

    public DynamicListView(@NonNull final Context context) {
        this(context, null);
    }

    public DynamicListView(@NonNull final Context context, @Nullable final AttributeSet attrs) {
        //noinspection HardCodedStringLiteral
        this(context, attrs, Resources.getSystem().getIdentifier("listViewStyle", "attr", "android"));
    }

    public DynamicListView(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
        super(context, attrs, defStyle);

        mMyOnScrollListener = new MyOnScrollListener();
        super.setOnScrollListener(mMyOnScrollListener);
    }

    @Override
    public void setOnTouchListener(final OnTouchListener onTouchListener) {
        if (onTouchListener instanceof SwipeTouchListener) {
            return;
        }

        super.setOnTouchListener(onTouchListener);
    }

    @Override
    public void setOnScrollListener(final OnScrollListener onScrollListener) {
        mMyOnScrollListener.addOnScrollListener(onScrollListener);
    }

    /**
     * Enables the drag and drop functionality for this {@code DynamicListView}.
     *

     * NOTE: This method can only be called on devices running ICS (14) and above, otherwise an exception will be thrown.
     *
     * @throws java.lang.UnsupportedOperationException if the device uses an older API than 14.
     */
    public void enableDragAndDrop() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            throw new UnsupportedOperationException("Drag and drop is only supported API levels 14 and up!");
        }

        mDragAndDropHandler = new DragAndDropHandler(this);
    }

    /**
     * Disables the drag and drop functionality.
     */
    public void disableDragAndDrop() {
        mDragAndDropHandler = null;
    }

    /**
     * Enables swipe to dismiss functionality for this {@code DynamicListView}.
     *
     * @param onDismissCallback the {@link com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.OnDismissCallback}
     *                          that is notified of dismissals.
     */
    public void enableSwipeToDismiss(@NonNull final OnDismissCallback onDismissCallback) {
        mSwipeTouchListener = new SwipeDismissTouchListener(new DynamicListViewWrapper(this), onDismissCallback);
    }

    /**
     * Enables swipe to dismiss with contextual undo for this {@code DynamicListView}.
     *
     * @param undoCallback the {@link com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.undo.UndoCallback}
     *                     that is used.
     */
    public void enableSwipeUndo(@NonNull final UndoCallback undoCallback) {
        mSwipeTouchListener = new SwipeUndoTouchListener(new DynamicListViewWrapper(this), undoCallback);
    }

    /**
     * Enables swipe to dismiss with contextual undo for this {@code DynamicListView}.
     * This method requires that a {@link com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.undo.SwipeUndoAdapter} has been set.
     * It is allowed to have the {@code SwipeUndoAdapter} wrapped in a {@link com.nhaarman.listviewanimations.BaseAdapterDecorator}.
     *
     * @throws java.lang.IllegalStateException if the adapter that was set does not extend {@code SwipeUndoAdapter}.
     */
    public void enableSimpleSwipeUndo() {
        if (mSwipeUndoAdapter == null) {
            throw new IllegalStateException("enableSimpleSwipeUndo requires a SwipeUndoAdapter to be set as an adapter");
        }

        mSwipeTouchListener = new SwipeUndoTouchListener(new DynamicListViewWrapper(this), mSwipeUndoAdapter.getUndoCallback());
        mSwipeUndoAdapter.setSwipeUndoTouchListener((SwipeUndoTouchListener) mSwipeTouchListener);
    }

    /**
     * Disables any swipe to dismiss functionality set by either {@link #enableSwipeToDismiss(com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.OnDismissCallback)},
     * {@link #enableSwipeUndo(com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.undo.UndoCallback)} or {@link #enableSimpleSwipeUndo()}.
     */
    public void disableSwipeToDismiss() {
        mSwipeTouchListener = null;
    }

    /**
     * Sets the {@link ListAdapter} for this {@code DynamicListView}.
     * If the drag and drop functionality is or will be enabled, the adapter should have stable ids,
     * and should implement {@link com.nhaarman.listviewanimations.util.Swappable}.
     *
     * @param adapter the adapter.
     *
     * @throws java.lang.IllegalStateException    if the drag and drop functionality is enabled
     *                                            and the adapter does not have stable ids.
     * @throws java.lang.IllegalArgumentException if the drag and drop functionality is enabled
     *                                            and the adapter does not implement {@link com.nhaarman.listviewanimations.util.Swappable}.
     */
    @Override
    public void setAdapter(final ListAdapter adapter) {
        ListAdapter wrappedAdapter = adapter;
        mSwipeUndoAdapter = null;

        if (adapter instanceof BaseAdapter) {
            BaseAdapter rootAdapter = (BaseAdapter) wrappedAdapter;
            while (rootAdapter instanceof BaseAdapterDecorator) {
                if (rootAdapter instanceof SwipeUndoAdapter) {
                    mSwipeUndoAdapter = (SwipeUndoAdapter) rootAdapter;
                }
                rootAdapter = ((BaseAdapterDecorator) rootAdapter).getDecoratedBaseAdapter();
            }

            if (rootAdapter instanceof Insertable) {
                mAnimateAdditionAdapter = new AnimateAdditionAdapter((BaseAdapter) wrappedAdapter);
                mAnimateAdditionAdapter.setListView(this);
                wrappedAdapter = mAnimateAdditionAdapter;
            }
        }

        super.setAdapter(wrappedAdapter);

        if (mDragAndDropHandler != null) {
            mDragAndDropHandler.setAdapter(adapter);
        }
    }

    @Override
    public boolean dispatchTouchEvent(@NonNull final MotionEvent ev) {
        if (mCurrentHandlingTouchEventHandler == null) {
            /* None of the TouchEventHandlers are actively consuming events yet. */
            boolean firstTimeInteracting = false;

            /* We don't support dragging items when there are items in the undo state. */
            if (!(mSwipeTouchListener instanceof SwipeUndoTouchListener) || !((SwipeUndoTouchListener) mSwipeTouchListener).hasPendingItems()) {
                /* Offer the event to the DragAndDropHandler */
                if (mDragAndDropHandler != null) {
                    mDragAndDropHandler.onTouchEvent(ev);
                    firstTimeInteracting = mDragAndDropHandler.isInteracting();
                    if (firstTimeInteracting) {
                        mCurrentHandlingTouchEventHandler = mDragAndDropHandler;
                        sendCancelEvent(mSwipeTouchListener, ev);
                    }
                }
            }

            /* If not handled, offer the event to the SwipeDismissTouchListener */
            if (mCurrentHandlingTouchEventHandler == null && mSwipeTouchListener != null) {
                mSwipeTouchListener.onTouchEvent(ev);
                firstTimeInteracting = mSwipeTouchListener.isInteracting();
                if (firstTimeInteracting) {
                    mCurrentHandlingTouchEventHandler = mSwipeTouchListener;
                    sendCancelEvent(mDragAndDropHandler, ev);
                }
            }

            if (firstTimeInteracting) {
                /* One of the TouchEventHandlers is now taking over control.
                   Cancel touch event handling on this DynamicListView */
                MotionEvent cancelEvent = MotionEvent.obtain(ev);
                cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
                super.onTouchEvent(cancelEvent);
            }

            return firstTimeInteracting || super.dispatchTouchEvent(ev);
        } else {
            return onTouchEvent(ev);
        }
    }

    @Override
    public boolean onTouchEvent(@NonNull final MotionEvent ev) {
        if (mCurrentHandlingTouchEventHandler != null) {
            mCurrentHandlingTouchEventHandler.onTouchEvent(ev);
        }

        if (ev.getActionMasked() == MotionEvent.ACTION_UP || ev.getActionMasked() == MotionEvent.ACTION_CANCEL) {
            /* Gesture is finished, reset the active TouchEventHandler */
            mCurrentHandlingTouchEventHandler = null;
        }

        return mCurrentHandlingTouchEventHandler != null || super.onTouchEvent(ev);
    }

    /**
     * Sends a cancel event to given {@link com.nhaarman.listviewanimations.itemmanipulation.TouchEventHandler}.
     *
     * @param touchEventHandler the {@code TouchEventHandler} to send the event to.
     * @param motionEvent       the {@link MotionEvent} to base the cancel event on.
     */
    private void sendCancelEvent(@Nullable final TouchEventHandler touchEventHandler, @NonNull final MotionEvent motionEvent) {
        if (touchEventHandler != null) {
            MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
            cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
            touchEventHandler.onTouchEvent(cancelEvent);
        }
    }

    @Override
    protected void dispatchDraw(@NonNull final Canvas canvas) {
        super.dispatchDraw(canvas);
        if (mDragAndDropHandler != null) {
            mDragAndDropHandler.dispatchDraw(canvas);
        }
    }

    @Override
    public int computeVerticalScrollOffset() {
        return super.computeVerticalScrollOffset();
    }

    @Override
    public int computeVerticalScrollExtent() {
        return super.computeVerticalScrollExtent();
    }

    @Override
    public int computeVerticalScrollRange() {
        return super.computeVerticalScrollRange();
    }

    /* Proxy methods below */

    /**
     * Inserts an item at given index. Will show an entrance animation for the new item if the newly added item is visible.
     * Will also call {@link Insertable#add(int, Object)} of the root {@link android.widget.BaseAdapter}.
     *
     * @param index the index the new item should be inserted at.
     * @param item  the item to insert.
     *
     * @throws java.lang.IllegalStateException if the adapter that was set does not implement {@link com.nhaarman.listviewanimations.util.Insertable}.
     */
    public void insert(final int index, final Object item) {
        if (mAnimateAdditionAdapter == null) {
            throw new IllegalStateException("Adapter should implement Insertable!");
        }
        mAnimateAdditionAdapter.insert(index, item);
    }

    /**
     * Inserts items, starting at given index. Will show an entrance animation for the new items if the newly added items are visible.
     * Will also call {@link Insertable#add(int, Object)} of the root {@link android.widget.BaseAdapter}.
     *
     * @param index the starting index the new items should be inserted at.
     * @param items the items to insert.
     *
     * @throws java.lang.IllegalStateException if the adapter that was set does not implement {@link com.nhaarman.listviewanimations.util.Insertable}.
     */
    public void insert(final int index, final Object... items) {
        if (mAnimateAdditionAdapter == null) {
            throw new IllegalStateException("Adapter should implement Insertable!");
        }
        mAnimateAdditionAdapter.insert(index, items);
    }

    /**
     * Inserts items at given indexes. Will show an entrance animation for the new items if the newly added item is visible.
     * Will also call {@link Insertable#add(int, Object)} of the root {@link android.widget.BaseAdapter}.
     *
     * @param indexItemPairs the index-item pairs to insert. The first argument of the {@code Pair} is the index, the second argument is the item.
     *
     * @throws java.lang.IllegalStateException if the adapter that was set does not implement {@link com.nhaarman.listviewanimations.util.Insertable}.
     */
    public <T> void insert(@NonNull final Pair<Integer, T>... indexItemPairs) {
        if (mAnimateAdditionAdapter == null) {
            throw new IllegalStateException("Adapter should implement Insertable!");
        }
        ((AnimateAdditionAdapter<T>) mAnimateAdditionAdapter).insert(indexItemPairs);
    }

    /**
     * Insert items at given indexes. Will show an entrance animation for the new items if the newly added item is visible.
     * Will also call {@link Insertable#add(int, Object)} of the root {@link android.widget.BaseAdapter}.
     *
     * @param indexItemPairs the index-item pairs to insert. The first argument of the {@code Pair} is the index, the second argument is the item.
     *
     * @throws java.lang.IllegalStateException if the adapter that was set does not implement {@link com.nhaarman.listviewanimations.util.Insertable}.
     */
    public <T> void insert(@NonNull final Iterable<Pair<Integer, T>> indexItemPairs) {
        if (mAnimateAdditionAdapter == null) {
            throw new IllegalStateException("Adapter should implement Insertable!");
        }
        ((AnimateAdditionAdapter<T>) mAnimateAdditionAdapter).insert(indexItemPairs);
    }

    /**
     * Sets the {@link com.nhaarman.listviewanimations.itemmanipulation.dragdrop.DraggableManager} to be used
     * for determining whether an item should be dragged when the user issues a down {@code MotionEvent}.
     *

     * This method does nothing if the drag and drop functionality is not enabled.
     */
    public void setDraggableManager(@NonNull final DraggableManager draggableManager) {
        if (mDragAndDropHandler != null) {
            mDragAndDropHandler.setDraggableManager(draggableManager);
        }
    }

    /**
     * Sets the {@link com.nhaarman.listviewanimations.itemmanipulation.dragdrop.OnItemMovedListener}
     * that is notified when user has dropped a dragging item.
     *

     * This method does nothing if the drag and drop functionality is not enabled.
     */
    public void setOnItemMovedListener(@Nullable final OnItemMovedListener onItemMovedListener) {
        if (mDragAndDropHandler != null) {
            mDragAndDropHandler.setOnItemMovedListener(onItemMovedListener);
        }
    }

    /**
     * Starts dragging the item at given position. User must be touching this {@code DynamicListView}.
     *

     * This method does nothing if the drag and drop functionality is not enabled.
     *
     * @param position the position of the item in the adapter to start dragging. Be sure to subtract any header views.
     *
     * @throws java.lang.IllegalStateException if the user is not touching this {@code DynamicListView},
     *                                         or if there is no adapter set.
     */
    public void startDragging(final int position) {
        /* We don't support dragging items when items are in the undo state. */
        if (mSwipeTouchListener instanceof SwipeUndoTouchListener && ((SwipeUndoTouchListener) mSwipeTouchListener).hasPendingItems()) {
            return;
        }

        if (mDragAndDropHandler != null) {
            mDragAndDropHandler.startDragging(position);
        }
    }

    /**
     * Sets the scroll speed when dragging an item. Defaults to {@code 1.0f}.
     *

     * This method does nothing if the drag and drop functionality is not enabled.
     *
     * @param speed {@code <1.0f} to slow down scrolling, {@code >1.0f} to speed up scrolling.
     */
    public void setScrollSpeed(final float speed) {
        if (mDragAndDropHandler != null) {
            mDragAndDropHandler.setScrollSpeed(speed);
        }
    }

    /**
     * Sets the {@link com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.DismissableManager} to specify which views can or cannot be swiped.
     *

     * This method does nothing if no swipe functionality is enabled.
     *
     * @param dismissableManager {@code null} for no restrictions.
     */
    public void setDismissableManager(@Nullable final DismissableManager dismissableManager) {
        if (mSwipeTouchListener != null) {
            mSwipeTouchListener.setDismissableManager(dismissableManager);
        }
    }

    /**
     * Flings the {@link android.view.View} corresponding to given position out of sight.
     * Calling this method has the same effect as manually swiping an item off the screen.
     *

     * This method does nothing if no swipe functionality is enabled.
     *
     * @param position the position of the item in the {@link android.widget.ListAdapter}. Must be visible.
     */
    public void fling(final int position) {
        if (mSwipeTouchListener != null) {
            mSwipeTouchListener.fling(position);
        }
    }

    /**
     * Sets the resource id of a child view that should be touched to engage swipe.
     * When the user touches a region outside of that view, no swiping will occur.
     *

     * This method does nothing if no swipe functionality is enabled.
     *
     * @param childResId The resource id of the list items' child that the user should touch to be able to swipe the list items.
     */
    public void setSwipeTouchChild(final int childResId) {
        if (mSwipeTouchListener != null) {
            mSwipeTouchListener.setTouchChild(childResId);
        }
    }

    /**
     * Sets the minimum value of the alpha property swiping Views should have.
     *

     * This method does nothing if no swipe functionality is enabled.
     *
     * @param minimumAlpha the alpha value between 0.0f and 1.0f.
     */
    public void setMinimumAlpha(final float minimumAlpha) {
        if (mSwipeTouchListener != null) {
            mSwipeTouchListener.setMinimumAlpha(minimumAlpha);
        }
    }

    /**
     * Dismisses the {@link android.view.View} corresponding to given position.
     * Calling this method has the same effect as manually swiping an item off the screen.
     *

     * This method does nothing if no swipe functionality is enabled.
     * It will however throw an exception if an incompatible swipe functionality is enabled.
     *
     * @param position the position of the item in the {@link android.widget.ListAdapter}. Must be visible.
     *
     * @throws java.lang.IllegalStateException if the {@link com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.SwipeTouchListener}
     *                                         that is enabled does not extend {@link com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.SwipeDismissTouchListener}.
     */
    public void dismiss(final int position) {
        if (mSwipeTouchListener != null) {
            if (mSwipeTouchListener instanceof SwipeDismissTouchListener) {
                ((SwipeDismissTouchListener) mSwipeTouchListener).dismiss(position);
            } else {
                throw new IllegalStateException("Enabled swipe functionality does not support dismiss");
            }
        }
    }

    /**
     * Performs the undo animation and restores the original state for given {@link android.view.View}.
     *

     * This method does nothing if no swipe functionality is enabled.
     * It will however throw an exception if an incompatible swipe functionality is enabled.
     *
     * @param view the parent {@code View} which contains both primary and undo {@code View}s.
     *
     * @throws java.lang.IllegalStateException if the {@link com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.SwipeTouchListener}
     *                                         that is enabled doe snot extend {@link com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.undo.SwipeUndoTouchListener}.
     */
    public void undo(@NonNull final View view) {
        if (mSwipeTouchListener != null) {
            if (mSwipeTouchListener instanceof SwipeUndoTouchListener) {
                ((SwipeUndoTouchListener) mSwipeTouchListener).undo(view);
            } else {
                throw new IllegalStateException("Enabled swipe functionality does not support undo");
            }
        }
    }

    private class MyOnScrollListener implements OnScrollListener {

        private final Collection<OnScrollListener> mOnScrollListeners = new HashSet<>();

        @Override
        public void onScrollStateChanged(final AbsListView view, final int scrollState) {
            for (OnScrollListener onScrollListener : mOnScrollListeners) {
                onScrollListener.onScrollStateChanged(view, scrollState);
            }

            if (scrollState == SCROLL_STATE_TOUCH_SCROLL) {
                if (mSwipeTouchListener instanceof SwipeUndoTouchListener) {
                    ((SwipeUndoTouchListener) mSwipeTouchListener).dimissPending();
                }
            }
        }

        @Override
        public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount, final int totalItemCount) {
            for (OnScrollListener onScrollListener : mOnScrollListeners) {
                onScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
            }
        }

        public void addOnScrollListener(final OnScrollListener onScrollListener) {
            mOnScrollListeners.add(onScrollListener);
        }
    }
}


/*
 * Copyright 2014 Niek Haarman
 *
 * 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.nhaarman.listviewanimations.itemmanipulation.dragdrop;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.support.annotation.NonNull;
import android.view.View;

class BitmapUtils {

    private BitmapUtils() {
    }

    /**
     * Returns a bitmap showing a screenshot of the view passed in.
     */
    @NonNull
    static Bitmap getBitmapFromView(@NonNull final View v) {
        Bitmap bitmap = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        v.draw(canvas);
        return bitmap;
    }

}


/*
 * Copyright 2014 Niek Haarman
 *
 * 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.nhaarman.listviewanimations.itemmanipulation.swipedismiss.undo;

import android.support.annotation.NonNull;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

class Util {

    private Util() {
    }

    @NonNull
    static Collection<Integer> processDeletions(@NonNull final Collection<Integer> positions, @NonNull final int[] dismissedPositions) {
        List<Integer> dismissedList = new ArrayList<>();
        for (int position : dismissedPositions) {
            dismissedList.add(position);
        }
        return processDeletions(positions, dismissedList);
    }

    /**
     * Removes positions in {@code dismissedPositions} from {@code positions}, and shifts the remaining positions accordingly.
     *
     * @param positions          the list of positions to remove from.
     * @param dismissedPositions the list of positions to remove.
     *
     * @return a new {@link java.util.Collection} instance, containing the resulting positions.
     */
    @NonNull
    static Collection<Integer> processDeletions(@NonNull final Collection<Integer> positions, @NonNull final List<Integer> dismissedPositions) {
        Collection<Integer> result = new ArrayList<>(positions);
        Collections.sort(dismissedPositions, Collections.reverseOrder());
        Collection<Integer> newUndoPositions = new ArrayList<>();
        for (int position : dismissedPositions) {
            for (Iterator<Integer> iterator = result.iterator(); iterator.hasNext(); ) {
                int undoPosition = iterator.next();
                if (undoPosition > position) {
                    iterator.remove();
                    newUndoPositions.add(undoPosition - 1);
                } else if (undoPosition == position) {
                    iterator.remove();
                } else {
                    newUndoPositions.add(undoPosition);
                }
            }
            result.clear();
            result.addAll(newUndoPositions);
            newUndoPositions.clear();
        }

        return result;
    }
}


/*
 * Copyright 2014 Niek Haarman
 *
 * 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.
 */
/* Originally based on Roman Nurik's SwipeDismissListViewTouchListener (https://gist.github.com/romannurik/2980593). */
package com.nhaarman.listviewanimations.itemmanipulation.swipedismiss;

import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.AdapterView;

import com.nhaarman.listviewanimations.itemmanipulation.TouchEventHandler;
import com.nhaarman.listviewanimations.util.AdapterViewUtil;
import com.nhaarman.listviewanimations.util.ListViewWrapper;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorListenerAdapter;
import com.nineoldandroids.animation.AnimatorSet;
import com.nineoldandroids.animation.ObjectAnimator;
import com.nineoldandroids.view.ViewHelper;

/**
 * An {@link android.view.View.OnTouchListener} that makes the list items in a {@link android.widget.AbsListView} swipeable.
 * Implementations of this class should implement {@link #afterViewFling(android.view.View, int)} to specify what to do after an item has been swiped.
 */
public abstract class SwipeTouchListener implements View.OnTouchListener, TouchEventHandler {

    /**
     * TranslationX View property.
     */
    private static final String TRANSLATION_X = "translationX";

    /**
     * Alpha View property.
     */
    private static final String ALPHA = "alpha";

    private static final int MIN_FLING_VELOCITY_FACTOR = 16;

    /**
     * The minimum distance in pixels that should be moved before starting horizontal item movement.
     */
    private final int mSlop;

    /**
     * The minimum velocity to initiate a fling, as measured in pixels per second.
     */
    private final int mMinFlingVelocity;

    /**
     * The maximum velocity to initiate a fling, as measured in pixels per second.
     */
    private final int mMaxFlingVelocity;

    /**
     * The duration of the fling animation.
     */
    private final long mAnimationTime;

    @NonNull
    private final ListViewWrapper mListViewWrapper;

    /**
     * The minimum alpha value of swiped Views.
     */
    private float mMinimumAlpha;

    /**
     * The width of the {@link android.widget.AbsListView} in pixels.
     */
    private int mViewWidth = 1;

    /**
     * The raw X coordinate of the down event.
     */
    private float mDownX;

    /**
     * The raw Y coordinate of the down event.
     */
    private float mDownY;

    /**
     * Indicates whether the user is swiping an item.
     */
    private boolean mSwiping;

    /**
     * Indicates whether the user can dismiss the current item.
     */
    private boolean mCanDismissCurrent;

    /**
     * The {@code VelocityTracker} used in the swipe movement.
     */
    @Nullable
    private VelocityTracker mVelocityTracker;

    /**
     * The parent {@link android.view.View} being swiped.
     */
    @Nullable
    private View mCurrentView;

    /**
     * The {@link android.view.View} that is actually being swiped.
     */
    @Nullable
    private View mSwipingView;

    /**
     * The current position being swiped.
     */
    private int mCurrentPosition = AdapterView.INVALID_POSITION;

    /**
     * The number of items in the {@code AbsListView}, minus the pending dismissed items.
     */
    private int mVirtualListCount = -1;

    /**
     * Indicates whether the {@link android.widget.AbsListView} is in a horizontal scroll container.
     * If so, this class will prevent the horizontal scroller from receiving any touch events.
     */
    private boolean mParentIsHorizontalScrollContainer;

    /**
     * The resource id of the {@link android.view.View} that may steal touch events from their parents. Useful for example
     * when the {@link android.widget.AbsListView} is in a horizontal scroll container, but not the whole {@code AbsListView} should
     * steal the touch events.
     */
    private int mTouchChildResId;

    /**
     * The {@link com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.DismissableManager} which decides
     * whether or not a list item can be swiped.
     */
    @Nullable
    private DismissableManager mDismissableManager;

    /**
     * The number of active swipe animations.
     */
    private int mActiveSwipeCount;

    /**
     * Indicates whether swipe is enabled.
     */
    private boolean mSwipeEnabled = true;

    /**
     * Constructs a new {@code SwipeTouchListener} for the given {@link android.widget.AbsListView}.
     */
    @SuppressWarnings("UnnecessaryFullyQualifiedName")
    protected SwipeTouchListener(@NonNull final ListViewWrapper listViewWrapper) {
        ViewConfiguration vc = ViewConfiguration.get(listViewWrapper.getListView().getContext());
        mSlop = vc.getScaledTouchSlop();
        mMinFlingVelocity = vc.getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_FACTOR;
        mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
        mAnimationTime = listViewWrapper.getListView().getContext().getResources().getInteger(android.R.integer.config_shortAnimTime);
        mListViewWrapper = listViewWrapper;
    }

    /**
     * Sets the {@link com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.DismissableManager} to specify which views can or cannot be swiped.
     *
     * @param dismissableManager {@code null} for no restrictions.
     */
    public void setDismissableManager(@Nullable final DismissableManager dismissableManager) {
        mDismissableManager = dismissableManager;
    }

    /**
     * Set the minimum value of the alpha property swiping Views should have.
     *
     * @param minimumAlpha the alpha value between 0.0f and 1.0f.
     */
    public void setMinimumAlpha(final float minimumAlpha) {
        mMinimumAlpha = minimumAlpha;
    }

    /**
     * If the {@link android.widget.AbsListView} is hosted inside a parent(/grand-parent/etc) that can scroll horizontally, horizontal swipes won't
     * work, because the parent will prevent touch-events from reaching the {@code AbsListView}.
     * <p/>
     * Call this method to fix this behavior.
     * Note that this will prevent the parent from scrolling horizontally when the user touches anywhere in a list item.
     */
    public void setParentIsHorizontalScrollContainer() {
        mParentIsHorizontalScrollContainer = true;
        mTouchChildResId = 0;
    }

    /**
     * Sets the resource id of a child view that should be touched to engage swipe.
     * When the user touches a region outside of that view, no swiping will occur.
     *
     * @param childResId The resource id of the list items' child that the user should touch to be able to swipe the list items.
     */
    public void setTouchChild(final int childResId) {
        mTouchChildResId = childResId;
        mParentIsHorizontalScrollContainer = false;
    }

    /**
     * Notifies this {@code SwipeTouchListener} that the adapter contents have changed.
     */
    public void notifyDataSetChanged() {
        if (mListViewWrapper.getAdapter() != null) {
            mVirtualListCount = mListViewWrapper.getCount() - mListViewWrapper.getHeaderViewsCount();
        }
    }

    /**
     * Returns whether the user is currently swiping an item.
     *
     * @return {@code true} if the user is swiping an item.
     */
    public boolean isSwiping() {
        return mSwiping;
    }

    @NonNull
    public ListViewWrapper getListViewWrapper() {
        return mListViewWrapper;
    }

    /**
     * Enables the swipe behavior.
     */
    public void enableSwipe() {
        mSwipeEnabled = true;
    }

    /**
     * Disables the swipe behavior.
     */
    public void disableSwipe() {
        mSwipeEnabled = false;
    }

    /**
     * Flings the {@link android.view.View} corresponding to given position out of sight.
     * Calling this method has the same effect as manually swiping an item off the screen.
     *
     * @param position the position of the item in the {@link android.widget.ListAdapter}. Must be visible.
     */
    public void fling(final int position) {
        int firstVisiblePosition = mListViewWrapper.getFirstVisiblePosition();
        int lastVisiblePosition = mListViewWrapper.getLastVisiblePosition();
        if (position < firstVisiblePosition || position > lastVisiblePosition) {
            throw new IllegalArgumentException("View for position " + position + " not visible!");
        }

        View downView = AdapterViewUtil.getViewForPosition(mListViewWrapper, position);
        if (downView == null) {
            throw new IllegalStateException("No view found for position " + position);
        }
        flingView(downView, position, true);

        mActiveSwipeCount++;
        mVirtualListCount--;
    }

    @Override
    public boolean isInteracting() {
        return mSwiping;
    }

    @Override
    public boolean onTouchEvent(@NonNull final MotionEvent event) {
        return onTouch(null, event);
    }

    @Override
    public boolean onTouch(@Nullable final View view, @NonNull final MotionEvent event) {
        if (mListViewWrapper.getAdapter() == null) {
            return false;
        }

        if (mVirtualListCount == -1 || mActiveSwipeCount == 0) {
            mVirtualListCount = mListViewWrapper.getCount() - mListViewWrapper.getHeaderViewsCount();
        }

        if (mViewWidth < 2) {
            mViewWidth = mListViewWrapper.getListView().getWidth();
        }

        boolean result;
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                result = handleDownEvent(view, event);
                break;
            case MotionEvent.ACTION_MOVE:
                result = handleMoveEvent(view, event);
                break;
            case MotionEvent.ACTION_CANCEL:
                result = handleCancelEvent();
                break;
            case MotionEvent.ACTION_UP:
                result = handleUpEvent(event);
                break;
            default:
                result = false;
        }

        return result;
    }

    private boolean handleDownEvent(@Nullable final View view, @NonNull final MotionEvent motionEvent) {
        if (!mSwipeEnabled) {
            return false;
        }

        View downView = findDownView(motionEvent);
        if (downView == null) {
            return false;
        }

        int downPosition = AdapterViewUtil.getPositionForView(mListViewWrapper, downView);
        mCanDismissCurrent = isDismissable(downPosition);

        /* Check if we are processing the item at this position */
        if (mCurrentPosition == downPosition || downPosition >= mVirtualListCount) {
            return false;
        }

        if (view != null) {
            view.onTouchEvent(motionEvent);
        }

        disableHorizontalScrollContainerIfNecessary(motionEvent, downView);

        mDownX = motionEvent.getX();
        mDownY = motionEvent.getY();

        mCurrentView = downView;
        mSwipingView = getSwipeView(downView);
        mCurrentPosition = downPosition;

        mVelocityTracker = VelocityTracker.obtain();
        mVelocityTracker.addMovement(motionEvent);
        return true;
    }

    /**
     * Returns the child {@link android.view.View} that was touched, by performing a hit test.
     *
     * @param motionEvent the {@link android.view.MotionEvent} to find the {@code View} for.
     *
     * @return the touched {@code View}, or {@code null} if none found.
     */
    @Nullable
    private View findDownView(@NonNull final MotionEvent motionEvent) {
        Rect rect = new Rect();
        int childCount = mListViewWrapper.getChildCount();
        int x = (int) motionEvent.getX();
        int y = (int) motionEvent.getY();
        View downView = null;
        for (int i = 0; i < childCount && downView == null; i++) {
            View child = mListViewWrapper.getChildAt(i);
            if (child != null) {
                child.getHitRect(rect);
                if (rect.contains(x, y)) {
                    downView = child;
                }
            }
        }
        return downView;
    }

    /**
     * Finds out whether the item represented by given position is dismissable.
     *
     * @param position the position of the item.
     *
     * @return {@code true} if the item is dismissable, false otherwise.
     */
    private boolean isDismissable(final int position) {
        if (mListViewWrapper.getAdapter() == null) {
            return false;
        }

        if (mDismissableManager != null) {
            long downId = mListViewWrapper.getAdapter().getItemId(position);
            return mDismissableManager.isDismissable(downId, position);
        }
        return true;
    }

    private void disableHorizontalScrollContainerIfNecessary(@NonNull final MotionEvent motionEvent, @NonNull final View view) {
        if (mParentIsHorizontalScrollContainer) {
            mListViewWrapper.getListView().requestDisallowInterceptTouchEvent(true);
        } else if (mTouchChildResId != 0) {
            mParentIsHorizontalScrollContainer = false;

            final View childView = view.findViewById(mTouchChildResId);
            if (childView != null) {
                final Rect childRect = getChildViewRect(mListViewWrapper.getListView(), childView);
                if (childRect.contains((int) motionEvent.getX(), (int) motionEvent.getY())) {
                    mListViewWrapper.getListView().requestDisallowInterceptTouchEvent(true);
                }
            }
        }
    }

    private boolean handleMoveEvent(@Nullable final View view, @NonNull final MotionEvent motionEvent) {
        if (mVelocityTracker == null || mCurrentView == null) {
            return false;
        }

        mVelocityTracker.addMovement(motionEvent);

        float deltaX = motionEvent.getX() - mDownX;
        float deltaY = motionEvent.getY() - mDownY;

        if (Math.abs(deltaX) > mSlop && Math.abs(deltaX) > Math.abs(deltaY)) {
            if (!mSwiping) {
                mActiveSwipeCount++;
                onStartSwipe(mCurrentView, mCurrentPosition);
            }
            mSwiping = true;
            mListViewWrapper.getListView().requestDisallowInterceptTouchEvent(true);

            /* Cancel ListView's touch (un-highlighting the item) */
            if (view != null) {
                MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
                cancelEvent.setAction(MotionEvent.ACTION_CANCEL | motionEvent.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
                view.onTouchEvent(cancelEvent);
                cancelEvent.recycle();
            }
        }

        if (mSwiping) {
            if (mCanDismissCurrent) {
                ViewHelper.setTranslationX(mSwipingView, deltaX);
                ViewHelper.setAlpha(mSwipingView, Math.max(mMinimumAlpha, Math.min(1, 1 - 2 * Math.abs(deltaX) / mViewWidth)));
            } else {
                ViewHelper.setTranslationX(mSwipingView, deltaX * 0.1f);
            }
            return true;
        }
        return false;
    }

    private boolean handleCancelEvent() {
        if (mVelocityTracker == null || mCurrentView == null) {
            return false;
        }

        if (mCurrentPosition != AdapterView.INVALID_POSITION && mSwiping) {
            onCancelSwipe(mCurrentView, mCurrentPosition);
            restoreCurrentViewTranslation();
        }

        reset();
        return false;
    }

    private boolean handleUpEvent(@NonNull final MotionEvent motionEvent) {
        if (mVelocityTracker == null || mCurrentView == null) {
            return false;
        }

        if (mSwiping) {
            boolean shouldDismiss = false;
            boolean dismissToRight = false;

            if (mCanDismissCurrent) {
                float deltaX = motionEvent.getX() - mDownX;

                mVelocityTracker.addMovement(motionEvent);
                mVelocityTracker.computeCurrentVelocity(1000);

                float velocityX = Math.abs(mVelocityTracker.getXVelocity());
                float velocityY = Math.abs(mVelocityTracker.getYVelocity());

                if (Math.abs(deltaX) > mViewWidth / 2) {
                    shouldDismiss = true;
                    dismissToRight = deltaX > 0;
                } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity && velocityY < velocityX) {
                    shouldDismiss = true;
                    dismissToRight = mVelocityTracker.getXVelocity() > 0;
                }
            }

            if (shouldDismiss) {
                beforeViewFling(mCurrentView, mCurrentPosition);
                if (willLeaveDataSetOnFling(mCurrentView, mCurrentPosition)) {
                    mVirtualListCount--;
                }
                flingCurrentView(dismissToRight);
            } else {
                onCancelSwipe(mCurrentView, mCurrentPosition);
                restoreCurrentViewTranslation();
            }
        }

        reset();
        return false;
    }

    /**
     * Flings the pending {@link android.view.View} out of sight.
     *
     * @param flingToRight {@code true} if the {@code View} should be flinged to the right, {@code false} if it should be flinged to the left.
     */
    private void flingCurrentView(final boolean flingToRight) {
        if (mCurrentView != null) {
            flingView(mCurrentView, mCurrentPosition, flingToRight);
        }
    }

    /**
     * Flings given {@link android.view.View} out of sight.
     *
     * @param view         the parent {@link android.view.View}.
     * @param position     the position of the item in the {@link android.widget.ListAdapter} corresponding to the {@code View}.
     * @param flingToRight {@code true} if the {@code View} should be flinged to the right, {@code false} if it should be flinged to the left.
     */
    private void flingView(@NonNull final View view, final int position, final boolean flingToRight) {
        if (mViewWidth < 2) {
            mViewWidth = mListViewWrapper.getListView().getWidth();
        }

        View swipeView = getSwipeView(view);
        ObjectAnimator xAnimator = ObjectAnimator.ofFloat(swipeView, TRANSLATION_X, flingToRight ? mViewWidth : -mViewWidth);
        ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(swipeView, ALPHA, 0);

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playTogether(xAnimator, alphaAnimator);
        animatorSet.setDuration(mAnimationTime);
        animatorSet.addListener(new FlingAnimatorListener(view, position));
        animatorSet.start();
    }

    /**
     * Animates the pending {@link android.view.View} back to its original position.
     */
    private void restoreCurrentViewTranslation() {
        if (mCurrentView == null) {
            return;
        }

        ObjectAnimator xAnimator = ObjectAnimator.ofFloat(mSwipingView, TRANSLATION_X, 0);
        ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(mSwipingView, ALPHA, 1);

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playTogether(xAnimator, alphaAnimator);
        animatorSet.setDuration(mAnimationTime);
        animatorSet.addListener(new RestoreAnimatorListener(mCurrentView, mCurrentPosition));
        animatorSet.start();
    }

    /**
     * Resets the fields to the initial values, ready to start over.
     */
    private void reset() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
        }

        mVelocityTracker = null;
        mDownX = 0;
        mDownY = 0;
        mCurrentView = null;
        mSwipingView = null;
        mCurrentPosition = AdapterView.INVALID_POSITION;
        mSwiping = false;
        mCanDismissCurrent = false;
    }

    /**
     * Called when the user starts swiping a {@link android.view.View}.
     *
     * @param view     the {@code View} that is being swiped.
     * @param position the position of the item in the {@link android.widget.ListAdapter} corresponding to the {@code View}.
     */
    protected void onStartSwipe(@NonNull final View view, final int position) {
    }

    /**
     * Called when the swipe movement is canceled. A restore animation starts at this point.
     *
     * @param view     the {@code View} that was swiped.
     * @param position the position of the item in the {@link android.widget.ListAdapter} corresponding to the {@code View}.
     */
    protected void onCancelSwipe(@NonNull final View view, final int position) {
    }

    /**
     * Called after the restore animation of a canceled swipe movement ends.
     *
     * @param view     the {@code View} that is being swiped.
     * @param position the position of the item in the {@link android.widget.ListAdapter} corresponding to the {@code View}.
     */
    protected void afterCancelSwipe(@NonNull final View view, final int position) {
    }

    /**
     * Called when the user lifted their finger off the screen, and the {@link android.view.View} should be swiped away. A fling animation starts at this point.
     *
     * @param view     the {@code View} that is being flinged.
     * @param position the position of the item in the {@link android.widget.ListAdapter} corresponding to the {@code View}.
     */
    protected void beforeViewFling(@NonNull final View view, final int position) {
    }

    /**
     * Returns whether flinging the item at given position in the current state
     * would cause it to be removed from the data set.
     *
     * @param view the {@code View} that would be flinged.
     * @param position the position of the item in the {@link android.widget.ListAdapter} corresponding to the {@code View}.
     *
     * @return {@code true} if the item would leave the data set, {@code false} otherwise.
     */
    protected abstract boolean willLeaveDataSetOnFling(@NonNull View view, int position);

    /**
     * Called after the fling animation of a succesful swipe ends.
     * Users of this class should implement any finalizing behavior at this point, such as notifying the adapter.
     *
     * @param view     the {@code View} that is being swiped.
     * @param position the position of the item in the {@link android.widget.ListAdapter} corresponding to the {@code View}.
     */
    protected abstract void afterViewFling(@NonNull View view, int position);

    /**
     * Restores the {@link android.view.View}'s {@code alpha} and {@code translationX} values.
     * Users of this class should call this method when recycling {@code View}s.
     *
     * @param view the {@code View} whose presentation should be restored.
     */
    protected void restoreViewPresentation(@NonNull final View view) {
        View swipedView = getSwipeView(view);
        ViewHelper.setAlpha(swipedView, 1);
        ViewHelper.setTranslationX(swipedView, 0);
    }

    /**
     * Returns the number of active swipe animations.
     */
    protected int getActiveSwipeCount() {
        return mActiveSwipeCount;
    }

    /**
     * Returns the {@link android.view.View} that should be swiped away. Must be a child of given {@code View}, or the {@code View} itself.
     *
     * @param view the parent {@link android.view.View}.
     */
    @NonNull
    protected View getSwipeView(@NonNull final View view) {
        return view;
    }

    private static Rect getChildViewRect(final View parentView, final View childView) {
        Rect childRect = new Rect(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom());
        if (!parentView.equals(childView)) {
            View workingChildView = childView;
            ViewGroup parent;
            while (!(parent = (ViewGroup) workingChildView.getParent()).equals(parentView)) {
                childRect.offset(parent.getLeft(), parent.getTop());
                workingChildView = parent;
            }
        }
        return childRect;
    }

    /**
     * An {@link com.nineoldandroids.animation.Animator.AnimatorListener} that notifies when the fling animation has ended.
     */
    private class FlingAnimatorListener extends AnimatorListenerAdapter {

        @NonNull
        private final View mView;

        private final int mPosition;

        private FlingAnimatorListener(@NonNull final View view, final int position) {
            mView = view;
            mPosition = position;
        }

        @Override
        public void onAnimationEnd(@NonNull final Animator animation) {
            mActiveSwipeCount--;
            afterViewFling(mView, mPosition);
        }
    }

    /**
     * An {@link com.nineoldandroids.animation.Animator.AnimatorListener} that performs the dismissal animation when the current animation has ended.
     */
    private class RestoreAnimatorListener extends AnimatorListenerAdapter {

        @NonNull
        private final View mView;

        private final int mPosition;

        private RestoreAnimatorListener(@NonNull final View view, final int position) {
            mView = view;
            mPosition = position;
        }

        @Override
        public void onAnimationEnd(@NonNull final Animator animation) {
            mActiveSwipeCount--;
            afterCancelSwipe(mView, mPosition);
        }
    }
}


/*
 * Copyright 2014 Niek Haarman
 *
 * 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.nhaarman.listviewanimations.itemmanipulation.dragdrop;

import android.graphics.drawable.BitmapDrawable;
import android.support.annotation.NonNull;
import android.view.MotionEvent;
import android.view.View;

/**
 * A Drawable which represents a dragging {@link View}.
 */
class HoverDrawable extends BitmapDrawable {

    /**
     * The original y coordinate of the top of given {@code View}.
     */
    private float mOriginalY;

    /**
     * The original y coordinate of the position that was touched.
     */
    private float mDownY;

    /**
     * The distance the {@code ListView} has been scrolling while this {@code HoverDrawable} is alive.
     */
    private float mScrollDistance;

    /**
     * Creates a new {@code HoverDrawable} for given {@link View}, using given {@link MotionEvent}.
     *
     * @param view the {@code View} to represent.
     * @param ev   the {@code MotionEvent} to use as down position.
     */
    HoverDrawable(@NonNull final View view, @NonNull final MotionEvent ev) {
        this(view, ev.getY());
    }

    /**
     * Creates a new {@code HoverDrawable} for given {@link View}, using given {@link MotionEvent}.
     *
     * @param view  the {@code View} to represent.
     * @param downY the y coordinate of the down event.
     */
    HoverDrawable(@NonNull final View view, final float downY) {
        super(view.getResources(), BitmapUtils.getBitmapFromView(view));
        mOriginalY = view.getTop();
        mDownY = downY;

        setBounds(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
    }

    /**
     * Calculates the new position for this {@code HoverDrawable} using given {@link MotionEvent}.
     *
     * @param ev the {@code MotionEvent}.
     *           {@code ev.getActionMasked()} should typically equal {@link MotionEvent#ACTION_MOVE}.
     */
    void handleMoveEvent(@NonNull final MotionEvent ev) {
        int top = (int) (mOriginalY - mDownY + ev.getY() + mScrollDistance);
        setTop(top);
    }

    /**
     * Updates the original y position of the view, and calculates the scroll distance.
     *
     * @param mobileViewTopY the top y coordinate of the mobile view this {@code HoverDrawable} represents.
     */
    void onScroll(final float mobileViewTopY) {
        mScrollDistance += mOriginalY - mobileViewTopY;
        mOriginalY = mobileViewTopY;
    }

    /**
     * Returns whether the user is currently dragging this {@code HoverDrawable} upwards.
     *
     * @return true if dragging upwards.
     */
    boolean isMovingUpwards() {
        return mOriginalY > getBounds().top;
    }

    /**
     * Returns the number of pixels between the original y coordinate of the view, and the current y coordinate.
     * A negative value means this {@code HoverDrawable} is moving upwards.
     *
     * @return the number of pixels.
     */
    int getDeltaY() {
        return (int) (getBounds().top - mOriginalY);
    }

    /**
     * Returns the top coordinate of this {@code HoverDrawable}.
     */
    int getTop() {
        return getBounds().top;
    }

    /**
     * Sets the top coordinate of this {@code HoverDrawable}.
     */
    void setTop(final int top) {
        setBounds(getBounds().left, top, getBounds().left + getIntrinsicWidth(), top + getIntrinsicHeight());
    }

    /**
     * Shifts the original y coordinates of this {@code HoverDrawable} {code height} pixels upwards or downwards,
     * depending on the move direction.
     *
     * @param height the number of pixels this {@code HoverDrawable} should be moved. Should be positive.
     */
    void shift(final int height) {
        int shiftSize = isMovingUpwards() ? -height : height;
        mOriginalY += shiftSize;
        mDownY += shiftSize;
    }
}


/*
 * Copyright 2014 Niek Haarman
 *
 * 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.nhaarman.listviewanimations.itemmanipulation.swipedismiss.undo;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.FrameLayout;

/**
 * A convenience class which holds a primary and a undo {@link android.view.View}.
 */
class SwipeUndoView extends FrameLayout {

    /**
     * The primary {@link android.view.View}.
     */
    @Nullable
    private View mPrimaryView;

    /**
     * The undo {@link android.view.View}.
     */
    @Nullable
    private View mUndoView;

    /**
     * Creates a new {@code SwipeUndoView}.
     */
    SwipeUndoView(final Context context) {
        super(context);
    }

    /**
     * Sets the primary {@link android.view.View}. Removes any existing primary {@code View} if present.
     */
    void setPrimaryView(@NonNull final View primaryView) {
        if (mPrimaryView != null) {
            removeView(mPrimaryView);
        }
        mPrimaryView = primaryView;
        addView(mPrimaryView);
    }

    /**
     * Sets the undo {@link android.view.View}. Removes any existing primary {@code View} if present, and sets the visibility of the {@code undoView} to {@link #GONE}.
     */
    void setUndoView(@NonNull final View undoView) {
        if (mUndoView != null) {
            removeView(mUndoView);
        }
        mUndoView = undoView;
        mUndoView.setVisibility(GONE);
        addView(mUndoView);
    }

    /**
     * Returns the primary {@link android.view.View}.
     */
    @Nullable
    View getPrimaryView() {
        return mPrimaryView;
    }

    /**
     * Returns the undo {@link android.view.View}.
     */
    @Nullable
    View getUndoView() {
        return mUndoView;
    }
}