com.marshalchen.common.uimodule.freeflow.core.FreeFlowContainer.java Source code

Java tutorial

Introduction

Here is the source code for com.marshalchen.common.uimodule.freeflow.core.FreeFlowContainer.java

Source

/*******************************************************************************
 * Copyright 2013 Comcast Cable Communications Management, LLC
 *
 * 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.marshalchen.common.uimodule.freeflow.core;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Iterator;
import java.util.Map;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.support.v4.util.SimpleArrayMap;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
import android.view.ActionMode;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.HapticFeedbackConstants;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Checkable;
import android.widget.EdgeEffect;
import android.widget.OverScroller;

import com.marshalchen.common.commonUtils.logUtils.Logs;
import com.marshalchen.common.uimodule.freeflow.animations.DefaultLayoutAnimator;
import com.marshalchen.common.uimodule.freeflow.animations.FreeFlowLayoutAnimator;
import com.marshalchen.common.uimodule.freeflow.layouts.FreeFlowLayout;
import com.marshalchen.common.uimodule.freeflow.utils.ViewUtils;

public class FreeFlowContainer extends AbsLayoutContainer {

    private static final String TAG = "Container";

    // ViewPool class
    protected ViewPool viewpool;

    // Not used yet, but we'll probably need to
    // prevent layout in <code>layout()</code> method
    private boolean preventLayout = false;

    protected SectionedAdapter mAdapter;
    protected FreeFlowLayout mLayout;

    /**
     * The X position of the active ViewPort
     */
    protected int viewPortX = 0;

    /**
     * The Y position of the active ViewPort
     */
    protected int viewPortY = 0;

    /**
     * The scrollable width in pixels. This is usually computed as the
     * difference between the width of the container and the contentWidth as
     * computed by the layout.
     */
    protected int mScrollableWidth;

    /**
     * The scrollable height in pixels. This is usually computed as the
     * difference between the height of the container and the contentHeight as
     * computed by the layout.
     */
    protected int mScrollableHeight;

    private VelocityTracker mVelocityTracker = null;
    private float deltaX = -1f;
    private float deltaY = -1f;

    private int maxFlingVelocity;
    private int minFlingVelocity;
    private int overflingDistance;
    /*private int overscrollDistance;*/
    private int touchSlop;

    private Runnable mTouchModeReset;
    private Runnable mPerformClick;
    private Runnable mPendingCheckForTap;
    private Runnable mPendingCheckForLongPress;

    private OverScroller scroller;

    protected EdgeEffect mLeftEdge, mRightEdge, mTopEdge, mBottomEdge;

    private ArrayList<OnScrollListener> scrollListeners = new ArrayList<OnScrollListener>();

    // This flag controls whether onTap/onLongPress/onTouch trigger
    // the ActionMode
    // private boolean mDataChanged = false;

    /**
     * TODO: ContextMenu action on long press has not been implemented yet
     */
    protected ContextMenuInfo mContextMenuInfo = null;

    /**
     * Holds the checked items when the Container is in CHOICE_MODE_MULTIPLE
     */
    protected SimpleArrayMap<IndexPath, Boolean> mCheckStates = null;

    ActionMode mChoiceActionMode;

    /**
     * Wraps the callback for MultiChoiceMode
     */
    MultiChoiceModeWrapper mMultiChoiceModeCallback;

    /**
     * Normal list that does not indicate choices
     */
    public static final int CHOICE_MODE_NONE = 0;

    /**
     * The list allows up to one choice
     */
    public static final int CHOICE_MODE_SINGLE = 1;

    /**
     * The list allows multiple choices
     */
    public static final int CHOICE_MODE_MULTIPLE = 2;

    /**
     * The list allows multiple choices in a modal selection mode
     */
    public static final int CHOICE_MODE_MULTIPLE_MODAL = 3;

    /**
     * The value of the current ChoiceMode
     * 
     * @see <a href=
     *      "http://developer.android.com/reference/android/widget/AbsListView.html#attr_android:choiceMode"
     *      >List View's Choice Mode</a>
     */
    int mChoiceMode = CHOICE_MODE_NONE;

    private LayoutParams params = new LayoutParams(0, 0);

    private FreeFlowLayoutAnimator layoutAnimator = new DefaultLayoutAnimator();

    private FreeFlowItem beginTouchAt;

    private boolean markLayoutDirty = false;
    private boolean markAdapterDirty = false;

    /**
     * When Layout is computed, should scroll positions be recalculated? When a
     * new layout is set, the Container can try to make sure an item that was
     * visible in one layout is also visible in the new layout. However when
     * data is just invalidated and additional data is loaded, you don't want
     * the Viewport to be jumping around.
     */
    private boolean shouldRecalculateScrollWhenComputingLayout = true;

    private FreeFlowLayout oldLayout;

    private OnTouchModeChangedListener mOnTouchModeChangedListener;

    public void setOnTouchModeChangedListener(OnTouchModeChangedListener onTouchModeChangedListener) {
        mOnTouchModeChangedListener = onTouchModeChangedListener;
    }

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

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

    public FreeFlowContainer(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    protected void init(Context context) {

        viewpool = new ViewPool();
        frames = new LinkedHashMap<Object, FreeFlowItem>();

        ViewConfiguration configuration = ViewConfiguration.get(context);
        maxFlingVelocity = configuration.getScaledMaximumFlingVelocity();
        minFlingVelocity = configuration.getScaledMinimumFlingVelocity();
        overflingDistance = configuration.getScaledOverflingDistance();
        /*overscrollDistance = configuration.getScaledOverscrollDistance();*/

        touchSlop = configuration.getScaledTouchSlop();

        scroller = new OverScroller(context);

        setEdgeEffectsEnabled(true);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        logLifecycleEvent(" onMeasure ");

        int beforeWidth = getWidth();
        int beforeHeight = getHeight();

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int afterWidth = MeasureSpec.getSize(widthMeasureSpec);
        int afterHeight = MeasureSpec.getSize(heightMeasureSpec);

        // TODO: prepareLayout should at some point take sizeChanged as a param
        // to not
        // avoidable calculations
        boolean sizeChanged = (beforeHeight == afterHeight) && (beforeWidth == afterWidth);

        if (this.mLayout != null) {
            mLayout.setDimensions(afterWidth, afterHeight);
        }

        if (mLayout == null || mAdapter == null) {
            logLifecycleEvent("Nothing to do: returning");
            return;

        }

        if (markAdapterDirty || markLayoutDirty) {
            computeLayout(afterWidth, afterHeight);
        }

        if (dataSetChanged) {
            dataSetChanged = false;
            for (FreeFlowItem item : frames.values()) {
                if (item.itemIndex >= 0 && item.itemSection >= 0) {
                    mAdapter.getItemView(item.itemSection, item.itemIndex, item.view, this);
                }
            }
        }

    }

    protected boolean dataSetChanged = false;

    /**
     * Notifies the attached observers that the underlying data has been changed
     * and any View reflecting the data set should refresh itself.
     */
    public void notifyDataSetChanged() {
        dataSetChanged = true;
        requestLayout();
    }

    /**
     * @deprecated Use dataInvalidated(boolean shouldRecalculateScrollPositions)
     *             instead
     */
    public void dataInvalidated() {
        dataInvalidated(false);
    }

    /**
     * Called to inform the Container that the underlying data on the adapter
     * has changed (more items added/removed). Note that this won't update the
     * views if the adapter's data objects are the same but the values in those
     * objects have changed. To update those call {@code notifyDataSetChanged}
     * 
     * @param shouldRecalculateScrollPositions
     */
    public void dataInvalidated(boolean shouldRecalculateScrollPositions) {
        logLifecycleEvent("Data Invalidated");
        if (mLayout == null || mAdapter == null) {
            return;
        }
        shouldRecalculateScrollWhenComputingLayout = shouldRecalculateScrollPositions;
        markAdapterDirty = true;
        requestLayout();
    }

    /**
     * The heart of the system. Calls the layout to get the frames needed,
     * decides which view should be kept in focus if view transitions are going
     * to happen and then kicks off animation changes if things have changed
     * 
     * @param w
     *            Width of the viewport. Since right now we don't support
     *            margins and padding, this is width of the container.
     * @param h
     *            Height of the viewport. Since right now we don't support
     *            margins and padding, this is height of the container.
     */
    protected void computeLayout(int w, int h) {
        markLayoutDirty = false;
        markAdapterDirty = false;
        mLayout.prepareLayout();
        if (shouldRecalculateScrollWhenComputingLayout) {
            computeViewPort(mLayout);
        }
        Map<Object, FreeFlowItem> oldFrames = frames;

        frames = new LinkedHashMap<Object, FreeFlowItem>();
        copyFrames(mLayout.getItemProxies(viewPortX, viewPortY), frames);
        // Create a copy of the incoming values because the source
        // layout may change the map inside its own class

        dispatchLayoutComputed();

        animateChanges(getViewChanges(oldFrames, frames));

    }

    /**
     * Copies the frames from one LinkedHashMap into another. The items are
     * cloned cause we modify the rectangles of the items as they are moving
     */
    protected void copyFrames(Map<Object, FreeFlowItem> srcFrames, Map<Object, FreeFlowItem> destFrames) {
        Iterator<?> it = srcFrames.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<?, ?> pairs = (Map.Entry<?, ?>) it.next();
            FreeFlowItem pr = (FreeFlowItem) pairs.getValue();
            pr = FreeFlowItem.clone(pr);
            destFrames.put(pairs.getKey(), pr);
        }
    }

    /**
     * Adds a view based on the current viewport. If we can get a view from the
     * ViewPool, we dont need to construct a new instance, else we will based on
     * the View class returned by the <code>Adapter</code>
     * 
     * @param freeflowItem
     *            <code>FreeFlowItem</code> instance that determines the View
     *            being positioned
     */
    protected void addAndMeasureViewIfNeeded(FreeFlowItem freeflowItem) {
        View view;
        if (freeflowItem.view == null) {

            View convertView = viewpool.getViewFromPool(mAdapter.getViewType(freeflowItem));

            if (freeflowItem.isHeader) {
                view = mAdapter.getHeaderViewForSection(freeflowItem.itemSection, convertView, this);
            } else {
                view = mAdapter.getItemView(freeflowItem.itemSection, freeflowItem.itemIndex, convertView, this);
            }

            if (view instanceof FreeFlowContainer)
                throw new IllegalStateException("A container cannot be a direct child view to a container");

            freeflowItem.view = view;
            prepareViewForAddition(view, freeflowItem);
            addView(view, getChildCount(), params);
        }

        view = freeflowItem.view;

        int widthSpec = MeasureSpec.makeMeasureSpec(freeflowItem.frame.width(), MeasureSpec.EXACTLY);
        int heightSpec = MeasureSpec.makeMeasureSpec(freeflowItem.frame.height(), MeasureSpec.EXACTLY);
        view.measure(widthSpec, heightSpec);
    }

    /**
     * Does all the necessary work right before a view is about to be laid out.
     * 
     * @param view
     *            The View that will be added to the Container
     * @param freeflowItem
     *            The <code>FreeFlowItem</code> instance that represents the
     *            view that will be positioned
     */
    protected void prepareViewForAddition(View view, FreeFlowItem freeflowItem) {
        if (view instanceof Checkable) {
            ((Checkable) view).setChecked(isChecked(freeflowItem.itemSection, freeflowItem.itemIndex));
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        logLifecycleEvent("onLayout");
        dispatchLayoutComplete(isAnimatingChanges);
        // mDataChanged = false;

    }

    protected void doLayout(FreeFlowItem freeflowItem) {
        View view = freeflowItem.view;
        Rect frame = freeflowItem.frame;
        view.layout(frame.left - viewPortX, frame.top - viewPortY, frame.right - viewPortX,
                frame.bottom - viewPortY);
    }

    /**
     * Sets the layout on the Container. If a previous layout was already
     * applied, this causes the views to animate to the new layout positions.
     * Scroll positions will also be reset.
     * 
     * @see FreeFlowLayout
     * @param newLayout
     */
    public void setLayout(FreeFlowLayout newLayout) {
        if (newLayout == mLayout || newLayout == null) {
            return;
        }
        stopScrolling();
        oldLayout = mLayout;
        mLayout = newLayout;
        shouldRecalculateScrollWhenComputingLayout = true;
        if (mAdapter != null) {
            mLayout.setAdapter(mAdapter);
        }

        dispatchLayoutChanging(oldLayout, newLayout);

        markLayoutDirty = true;
        viewPortX = 0;
        viewPortY = 0;

        logLifecycleEvent("Setting layout");
        requestLayout();

    }

    /**
     * Stops the scrolling immediately
     */
    public void stopScrolling() {
        if (!scroller.isFinished()) {
            scroller.forceFinished(true);
        }
        removeCallbacks(flingRunnable);
        resetAllCallbacks();
        mTouchMode = TOUCH_MODE_REST;

        if (mOnTouchModeChangedListener != null) {
            mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
        }
    }

    /**
     * Resets all Runnables that are checking on various statuses
     */
    protected void resetAllCallbacks() {
        if (mPendingCheckForTap != null) {
            removeCallbacks(mPendingCheckForTap);
            mPendingCheckForTap = null;
        }

        if (mPendingCheckForLongPress != null) {
            removeCallbacks(mPendingCheckForLongPress);
            mPendingCheckForLongPress = null;
        }
        if (mTouchModeReset != null) {
            removeCallbacks(mTouchModeReset);
            mTouchModeReset = null;
        }
        if (mPerformClick != null) {
            removeCallbacks(mPerformClick);
            mPerformClick = null;
        }
    }

    /**
     * @return The layout currently applied to the Container
     */
    public FreeFlowLayout getLayout() {
        return mLayout;
    }

    /**
     * Computes the Rectangle that defines the ViewPort. The Container tries to
     * keep the view at the top left of the old layout visible in the new
     * layout.
     * 
     *
     */
    protected void computeViewPort(FreeFlowLayout newLayout) {
        if (mLayout == null || frames == null || frames.size() == 0) {
            viewPortX = 0;
            viewPortY = 0;
            return;
        }

        Object data = null;
        int lowestSection = Integer.MAX_VALUE;
        int lowestPosition = Integer.MAX_VALUE;

        // Find the frame of of the first item in the first section in the
        // current set of frames defining the viewport
        // Changing layout will then keep this item in the viewport of the new
        // layout
        // TODO: Need to make sure this item is actually being shown in the
        // viewport and not just in some offscreen buffer
        for (FreeFlowItem fd : frames.values()) {
            if (fd.itemSection < lowestSection
                    || (fd.itemSection == lowestSection && fd.itemIndex < lowestPosition)) {
                data = fd.data;
                lowestSection = fd.itemSection;
                lowestPosition = fd.itemIndex;
            }
        }

        FreeFlowItem freeflowItem = newLayout.getFreeFlowItemForItem(data);
        freeflowItem = FreeFlowItem.clone(freeflowItem);
        if (freeflowItem == null) {
            viewPortX = 0;
            viewPortY = 0;
            return;
        }

        Rect vpFrame = freeflowItem.frame;

        viewPortX = vpFrame.left;
        viewPortY = vpFrame.top;
        mScrollableWidth = mLayout.getContentWidth() - getWidth();
        mScrollableHeight = mLayout.getContentHeight() - getHeight();

        if (mScrollableWidth < 0) {
            mScrollableWidth = 0;
        }
        if (mScrollableHeight < 0) {
            mScrollableHeight = 0;
        }

        if (viewPortX > mScrollableWidth)
            viewPortX = mScrollableWidth;

        if (viewPortY > mScrollableHeight)
            viewPortY = mScrollableHeight;

    }

    /**
     * Returns the actual frame for a view as its on stage. The FreeFlowItem's
     * frame object always represents the position it wants to be in but actual
     * frame may be different based on animation etc.
     * 
     * @param freeflowItem
     *            The freeflowItem to get the <code>Frame</code> for
     * @return The Frame for the freeflowItem or null if that view doesn't exist
     */
    public Rect getActualFrame(final FreeFlowItem freeflowItem) {
        View v = freeflowItem.view;
        if (v == null) {
            return null;
        }

        Rect of = new Rect();
        of.left = (int) (v.getLeft() + v.getTranslationX());
        of.top = (int) (v.getTop() + v.getTranslationY());
        of.right = (int) (v.getRight() + v.getTranslationX());
        of.bottom = (int) (v.getBottom() + v.getTranslationY());

        return of;

    }

    /**
     * Returns the <code>FreeFlowItem</code> representing the data passed in IF
     * that item is being rendered in the Container.
     * 
     * @param dataItem
     *            The data object being rendered in a View managed by the
     *            Container, null otherwise
     * @return
     */
    public FreeFlowItem getFreeFlowItem(Object dataItem) {
        for (FreeFlowItem item : frames.values()) {
            if (item.data.equals(dataItem)) {
                return item;
            }
        }
        return null;
    }

    /**
     * TODO: This should be renamed to layoutInvalidated, since the layout isn't
     * changed
     */
    public void layoutChanged() {
        logLifecycleEvent("layoutChanged");
        markLayoutDirty = true;
        dispatchDataChanged();
        requestLayout();
    }

    protected boolean isAnimatingChanges = false;

    private void animateChanges(LayoutChangeset changeSet) {
        logLifecycleEvent("animating changes: " + changeSet.toString());
        if (changeSet.added.size() == 0 && changeSet.removed.size() == 0 && changeSet.moved.size() == 0) {
            return;
        }

        for (FreeFlowItem freeflowItem : changeSet.getAdded()) {
            addAndMeasureViewIfNeeded(freeflowItem);
            doLayout(freeflowItem);
        }

        if (isAnimatingChanges) {
            layoutAnimator.cancel();
        }
        isAnimatingChanges = true;

        dispatchAnimationsStarting();

        layoutAnimator.animateChanges(changeSet, this);

    }

    /**
     * This method is called by the <code>LayoutAnimator</code> instance once
     * all transition animations have been completed.
     * 
     * @param anim
     *            The LayoutAnimator instance that reported change complete.
     */
    public void onLayoutChangeAnimationsCompleted(FreeFlowLayoutAnimator anim) {
        // preventLayout = false;
        isAnimatingChanges = false;
        logLifecycleEvent("layout change animations complete");
        for (FreeFlowItem freeflowItem : anim.getChangeSet().getRemoved()) {
            View v = freeflowItem.view;
            removeView(v);
            returnItemToPoolIfNeeded(freeflowItem);
        }

        dispatchLayoutChangeAnimationsComplete();

        // changeSet = null;

    }

    public LayoutChangeset getViewChanges(Map<Object, FreeFlowItem> oldFrames,
            Map<Object, FreeFlowItem> newFrames) {
        return getViewChanges(oldFrames, newFrames, false);
    }

    public LayoutChangeset getViewChanges(Map<Object, FreeFlowItem> oldFrames, Map<Object, FreeFlowItem> newFrames,
            boolean moveEvenIfSame) {

        // cleanupViews();
        LayoutChangeset change = new LayoutChangeset();

        if (oldFrames == null) {
            markAdapterDirty = false;
            for (FreeFlowItem freeflowItem : newFrames.values()) {
                change.addToAdded(freeflowItem);
            }

            return change;
        }

        if (markAdapterDirty) {
            markAdapterDirty = false;
            for (FreeFlowItem freeflowItem : newFrames.values()) {
                change.addToAdded(freeflowItem);
            }

            for (FreeFlowItem freeflowItem : oldFrames.values()) {
                change.addToDeleted(freeflowItem);
            }

            return change;
        }

        Iterator<?> it = newFrames.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<?, ?> m = (Map.Entry<?, ?>) it.next();
            FreeFlowItem freeflowItem = (FreeFlowItem) m.getValue();

            if (oldFrames.get(m.getKey()) != null) {

                FreeFlowItem old = oldFrames.remove(m.getKey());
                freeflowItem.view = old.view;

                // if (moveEvenIfSame || !old.compareRect(((FreeFlowItem)
                // m.getValue()).frame)) {

                if (moveEvenIfSame || !old.frame.equals(((FreeFlowItem) m.getValue()).frame)) {

                    change.addToMoved(freeflowItem, getActualFrame(freeflowItem));
                }
            } else {
                change.addToAdded(freeflowItem);
            }

        }

        for (FreeFlowItem freeflowItem : oldFrames.values()) {
            change.addToDeleted(freeflowItem);
        }

        frames = newFrames;

        return change;
    }

    @Override
    public void requestLayout() {
        if (!preventLayout) {
            /**
             * Ends up with a call to <code>onMeasure</code> where all the logic
             * lives
             */
            super.requestLayout();
        }

    }

    /**
     * Sets the adapter for the this CollectionView.All view pools will be
     * cleared at this point and all views on the stage will be cleared
     * 
     * @param adapter
     *            The {@link SectionedAdapter} that will populate this
     *            Collection
     */
    public void setAdapter(SectionedAdapter adapter) {
        if (adapter == mAdapter) {
            return;
        }
        stopScrolling();
        logLifecycleEvent("setting adapter");
        markAdapterDirty = true;

        viewPortX = 0;
        viewPortY = 0;
        shouldRecalculateScrollWhenComputingLayout = true;
        this.mAdapter = adapter;
        if (adapter != null) {
            viewpool.initializeViewPool(adapter.getViewTypes());
        }
        if (mLayout != null) {
            mLayout.setAdapter(mAdapter);
        }
        requestLayout();
    }

    public FreeFlowLayout getLayoutController() {
        return mLayout;
    }

    /**
     * The Viewport defines the rectangular "window" that the container is
     * actually showing of the entire view.
     * 
     * @return The left (x) of the viewport within the entire container
     */
    public int getViewportLeft() {
        return viewPortX;
    }

    /**
     * The Viewport defines the rectangular "window" that the container is
     * actually showing of the entire view.
     * 
     * @return The top (y) of the viewport within the entire container
     * 
     */
    public int getViewportTop() {
        return viewPortY;
    }

    /**
     * Indicates that we are not in the middle of a touch gesture
     */
    public static final int TOUCH_MODE_REST = -1;

    /**
     * Indicates we just received the touch event and we are waiting to see if
     * the it is a tap or a scroll gesture.
     */
    public static final int TOUCH_MODE_DOWN = 0;

    /**
     * Indicates the touch has been recognized as a tap and we are now waiting
     * to see if the touch is a longpress
     */
    public static final int TOUCH_MODE_TAP = 1;

    /**
     * Indicates we have waited for everything we can wait for, but the user's
     * finger is still down
     */
    public static final int TOUCH_MODE_DONE_WAITING = 2;

    /**
     * Indicates the touch gesture is a scroll
     */
    public static final int TOUCH_MODE_SCROLL = 3;

    /**
     * Indicates the view is in the process of being flung
     */
    public static final int TOUCH_MODE_FLING = 4;

    /**
     * Indicates the touch gesture is an overscroll - a scroll beyond the
     * beginning or end.
     */
    public static final int TOUCH_MODE_OVERSCROLL = 5;

    /**
     * Indicates the view is being flung outside of normal content bounds and
     * will spring back.
     */
    public static final int TOUCH_MODE_OVERFLING = 6;

    /**
     * One of TOUCH_MODE_REST, TOUCH_MODE_DOWN, TOUCH_MODE_TAP,
     * TOUCH_MODE_SCROLL, or TOUCH_MODE_DONE_WAITING
     */
    int mTouchMode = TOUCH_MODE_REST;

    /**
     * The duration for which the scroller will wait before deciding whether the
     * user was actually trying to stop the scroll or swuipe again to increase
     * the velocity
     */
    protected final int FLYWHEEL_TIMEOUT = 40;

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        super.onTouchEvent(event);
        if (mLayout == null) {
            return false;
        }

        // flag to check if laid out items are wide or tall enough
        // to require scrolling
        boolean canScroll = false;

        if (mLayout.horizontalScrollEnabled() && this.mLayout.getContentWidth() > getWidth()) {
            canScroll = true;
        }
        if (mLayout.verticalScrollEnabled() && mLayout.getContentHeight() > getHeight()) {
            canScroll = true;
        }

        switch (event.getAction()) {
        case (MotionEvent.ACTION_DOWN):
            touchDown(event);
            break;
        case (MotionEvent.ACTION_MOVE):
            if (canScroll) {
                touchMove(event);
            }
            break;
        case (MotionEvent.ACTION_UP):
            touchUp(event);
            break;
        case (MotionEvent.ACTION_CANCEL):
            touchCancel(event);
            break;
        }

        if (!canScroll) {
            return true;
        }

        if (mVelocityTracker == null && canScroll) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(event);
        }

        return true;

    }

    protected void touchDown(MotionEvent event) {
        if (isAnimatingChanges) {
            layoutAnimator.onContainerTouchDown(event);
        }

        /*
         * Recompute this just to be safe. TODO: We should optimize this to be
         * only calculated when a data or layout change happens
         */
        mScrollableHeight = mLayout.getContentHeight() - getHeight();
        mScrollableWidth = mLayout.getContentWidth() - getWidth();

        if (mTouchMode == TOUCH_MODE_FLING) {
            // Wait for some time to see if the user is just trying
            // to speed up the scroll
            postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (mTouchMode == TOUCH_MODE_DOWN) {
                        if (mTouchMode == TOUCH_MODE_DOWN) {
                            scroller.forceFinished(true);
                        }
                    }
                }
            }, FLYWHEEL_TIMEOUT);
        }

        beginTouchAt = ViewUtils.getItemAt(frames, (int) (viewPortX + event.getX()),
                (int) (viewPortY + event.getY()));

        deltaX = event.getX();
        deltaY = event.getY();

        mTouchMode = TOUCH_MODE_DOWN;

        if (mOnTouchModeChangedListener != null) {
            mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
        }

        if (mPendingCheckForTap != null) {
            removeCallbacks(mPendingCheckForTap);
            mPendingCheckForLongPress = null;
        }

        if (beginTouchAt != null) {
            mPendingCheckForTap = new CheckForTap();
        }
        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

    }

    protected void touchMove(MotionEvent event) {
        float xDiff = event.getX() - deltaX;
        float yDiff = event.getY() - deltaY;

        double distance = Math.sqrt(xDiff * xDiff + yDiff * yDiff);

        if (mLayout.verticalScrollEnabled()) {
            if (yDiff > 0 && viewPortY == 0) {
                if (mEdgeEffectsEnabled) {
                    float str = (float) distance / getHeight();
                    mTopEdge.onPull(str);
                    invalidate();
                }
                return;
            }

            if (yDiff < 0 && viewPortY == mScrollableHeight) {
                if (mEdgeEffectsEnabled) {
                    float str = (float) distance / getHeight();
                    mBottomEdge.onPull(str);
                    invalidate();
                }
                return;
            }
        }

        if (mLayout.horizontalScrollEnabled()) {
            if (xDiff > 0 && viewPortX == 0) {
                if (mEdgeEffectsEnabled) {
                    float str = (float) distance / getWidth();
                    mLeftEdge.onPull(str);
                    invalidate();
                }
                return;
            }

            if (xDiff < 0 && viewPortY == mScrollableWidth) {
                if (mEdgeEffectsEnabled) {
                    float str = (float) distance / getWidth();
                    mRightEdge.onPull(str);
                    invalidate();
                }
                return;
            }
        }

        if ((mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_REST) && distance > touchSlop) {
            mTouchMode = TOUCH_MODE_SCROLL;

            if (mOnTouchModeChangedListener != null) {
                mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
            }

            if (mPendingCheckForTap != null) {
                removeCallbacks(mPendingCheckForTap);
                mPendingCheckForTap = null;
            }

        }

        if (mTouchMode == TOUCH_MODE_SCROLL) {
            moveViewportBy(event.getX() - deltaX, event.getY() - deltaY, false);
            invokeOnItemScrollListeners();
            deltaX = event.getX();
            deltaY = event.getY();
        }
    }

    protected void touchCancel(MotionEvent event) {
        mTouchMode = TOUCH_MODE_REST;

        if (mOnTouchModeChangedListener != null) {
            mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
        }

        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }

        // requestLayout();

    }

    protected void touchUp(MotionEvent event) {
        if ((mTouchMode == TOUCH_MODE_SCROLL || mTouchMode == TOUCH_MODE_OVERFLING) && mVelocityTracker != null) {

            mVelocityTracker.computeCurrentVelocity(1000, maxFlingVelocity);

            if (Math.abs(mVelocityTracker.getXVelocity()) > minFlingVelocity
                    || Math.abs(mVelocityTracker.getYVelocity()) > minFlingVelocity) {

                int maxX = mLayout.getContentWidth() - getWidth();
                int maxY = mLayout.getContentHeight() - getHeight();

                int allowedScrollOffset;
                if (mTouchMode == TOUCH_MODE_SCROLL) {
                    allowedScrollOffset = 0;
                } else {
                    allowedScrollOffset = overflingDistance;
                }

                scroller.fling(viewPortX, viewPortY, -(int) mVelocityTracker.getXVelocity(),
                        -(int) mVelocityTracker.getYVelocity(), 0, maxX, 0, maxY, allowedScrollOffset,
                        allowedScrollOffset);

                mTouchMode = TOUCH_MODE_FLING;

                if (mOnTouchModeChangedListener != null) {
                    mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
                }

                post(flingRunnable);

            } else {
                mTouchMode = TOUCH_MODE_REST;

                if (mOnTouchModeChangedListener != null) {
                    mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
                }
            }

        } else if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_DONE_WAITING) {
            if (mTouchModeReset != null) {
                removeCallbacks(mTouchModeReset);
            }

            FreeFlowItem endTouchAt = ViewUtils.getItemAt(frames, (int) (viewPortX + event.getX()),
                    (int) (viewPortY + event.getY()));

            if (beginTouchAt != null && beginTouchAt.view != null && beginTouchAt == endTouchAt) {

                beginTouchAt.view.setPressed(true);

                mTouchModeReset = new Runnable() {
                    @Override
                    public void run() {
                        mTouchModeReset = null;
                        mTouchMode = TOUCH_MODE_REST;

                        if (mOnTouchModeChangedListener != null) {
                            mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
                        }

                        if (beginTouchAt != null && beginTouchAt.view != null) {
                            beginTouchAt.view.setPressed(false);
                        }
                        if (mChoiceActionMode == null && mOnItemSelectedListener != null) {
                            mOnItemSelectedListener.onItemSelected(FreeFlowContainer.this, selectedFreeFlowItem);
                        }
                    }
                };
                selectedFreeFlowItem = beginTouchAt;
                postDelayed(mTouchModeReset, ViewConfiguration.getPressedStateDuration());

                mTouchMode = TOUCH_MODE_TAP;
                mPerformClick = new PerformClick();
                mPerformClick.run();

                if (mOnTouchModeChangedListener != null) {
                    mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
                }
            } else {
                mTouchMode = TOUCH_MODE_REST;

                if (mOnTouchModeChangedListener != null) {
                    mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
                }
            }

        }
    }

    public FreeFlowItem getSelectedFreeFlowItem() {
        return selectedFreeFlowItem;
    }

    private Runnable flingRunnable = new Runnable() {

        @Override
        public void run() {
            if (scroller.isFinished()) {
                mTouchMode = TOUCH_MODE_REST;

                if (mOnTouchModeChangedListener != null) {
                    mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
                }

                invokeOnItemScrollListeners();
                return;
            }
            boolean more = scroller.computeScrollOffset();
            if (mEdgeEffectsEnabled) {
                checkEdgeEffectDuringScroll();
            }
            if (mLayout.horizontalScrollEnabled()) {
                viewPortX = scroller.getCurrX();
            }
            if (mLayout.verticalScrollEnabled()) {
                viewPortY = scroller.getCurrY();
            }
            moveViewport(true);
            if (more) {
                post(flingRunnable);
            }
        }
    };

    protected void checkEdgeEffectDuringScroll() {
        if (mLeftEdge.isFinished() && viewPortX < 0 && mLayout.horizontalScrollEnabled()) {
            mLeftEdge.onAbsorb((int) scroller.getCurrVelocity());
        }

        if (mRightEdge.isFinished() && viewPortX > mLayout.getContentWidth() - getMeasuredWidth()
                && mLayout.horizontalScrollEnabled()) {
            mRightEdge.onAbsorb((int) scroller.getCurrVelocity());
        }

        if (mTopEdge.isFinished() && viewPortY < 0 && mLayout.verticalScrollEnabled()) {
            mTopEdge.onAbsorb((int) scroller.getCurrVelocity());
        }

        if (mBottomEdge.isFinished() && viewPortY > mLayout.getContentHeight() - getMeasuredHeight()
                && mLayout.verticalScrollEnabled()) {
            mBottomEdge.onAbsorb((int) scroller.getCurrVelocity());
        }

    }

    protected void moveViewportBy(float movementX, float movementY, boolean fling) {

        if (mLayout.horizontalScrollEnabled()) {
            viewPortX = (int) (viewPortX - movementX);
        }

        if (mLayout.verticalScrollEnabled()) {
            viewPortY = (int) (viewPortY - movementY);
        }
        moveViewport(fling);
    }

    protected void moveViewPort(int left, int top, boolean isInFlingMode) {
        viewPortX = left;
        viewPortY = top;
        moveViewport(isInFlingMode);
    }

    /**
     * Will move viewport to viewPortX and viewPortY values
     * 
     * @param isInFlingMode
     *            Setting this
     */
    protected void moveViewport(boolean isInFlingMode) {

        mScrollableWidth = mLayout.getContentWidth() - getWidth();
        if (mScrollableWidth < 0) {
            mScrollableWidth = 0;
        }
        mScrollableHeight = mLayout.getContentHeight() - getHeight();
        if (mScrollableHeight < 0) {
            mScrollableHeight = 0;
        }

        if (isInFlingMode) {
            if (viewPortX < 0 || viewPortX > mScrollableWidth || viewPortY < 0 || viewPortY > mScrollableHeight) {
                mTouchMode = TOUCH_MODE_OVERFLING;
            }
        } else {

            if (viewPortX < -overflingDistance) {
                viewPortX = -overflingDistance;
            } else if (viewPortX > mScrollableWidth + overflingDistance) {
                viewPortX = (mScrollableWidth + overflingDistance);
            }

            if (viewPortY < (int) (-overflingDistance)) {
                viewPortY = (int) -overflingDistance;
            } else if (viewPortY > mScrollableHeight + overflingDistance) {
                viewPortY = (int) (mScrollableHeight + overflingDistance);
            }

            if (mEdgeEffectsEnabled) {
                if (viewPortX <= 0) {
                    mLeftEdge.onPull(viewPortX / (-overflingDistance));
                } else if (viewPortX >= mScrollableWidth) {
                    mRightEdge.onPull((viewPortX - mScrollableWidth) / (-overflingDistance));
                }

                if (viewPortY <= 0) {
                    mTopEdge.onPull(viewPortY / (-overflingDistance));
                } else if (viewPortY >= mScrollableHeight) {
                    mBottomEdge.onPull((viewPortY - mScrollableHeight) / (-overflingDistance));
                }
            }
        }

        LinkedHashMap<Object, FreeFlowItem> oldFrames = new LinkedHashMap<Object, FreeFlowItem>();
        copyFrames(frames, oldFrames);
        frames = new LinkedHashMap<Object, FreeFlowItem>();
        copyFrames(mLayout.getItemProxies(viewPortX, viewPortY), frames);

        LayoutChangeset changeSet = getViewChanges(oldFrames, frames, true);

        for (FreeFlowItem freeflowItem : changeSet.added) {
            addAndMeasureViewIfNeeded(freeflowItem);
            doLayout(freeflowItem);
        }

        for (Pair<FreeFlowItem, Rect> freeflowItemPair : changeSet.moved) {
            doLayout(freeflowItemPair.first);
        }

        for (FreeFlowItem freeflowItem : changeSet.removed) {
            removeViewInLayout(freeflowItem.view);
            returnItemToPoolIfNeeded(freeflowItem);
        }

        invalidate();

    }

    protected boolean mEdgeEffectsEnabled = true;

    /**
     * Controls whether the edge glows are enabled or not
     */
    public void setEdgeEffectsEnabled(boolean val) {
        mEdgeEffectsEnabled = val;
        if (val) {
            Context context = getContext();
            setWillNotDraw(false);
            mLeftEdge = new EdgeEffect(context);
            mRightEdge = new EdgeEffect(context);
            mTopEdge = new EdgeEffect(context);
            mBottomEdge = new EdgeEffect(context);
        } else {
            setWillNotDraw(true);
            mLeftEdge = mRightEdge = mTopEdge = mBottomEdge = null;
        }
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);

        boolean needsInvalidate = false;

        final int height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
        final int width = getMeasuredWidth();

        if (!mLeftEdge.isFinished()) {
            final int restoreCount = canvas.save();

            canvas.rotate(270);
            canvas.translate(-height + getPaddingTop(), 0);// width);
            mLeftEdge.setSize(height, width);

            needsInvalidate = mLeftEdge.draw(canvas);
            canvas.restoreToCount(restoreCount);
        }

        if (!mTopEdge.isFinished()) {
            final int restoreCount = canvas.save();

            mTopEdge.setSize(width, height);

            needsInvalidate = mTopEdge.draw(canvas);
            canvas.restoreToCount(restoreCount);
        }

        if (!mRightEdge.isFinished()) {
            final int restoreCount = canvas.save();

            canvas.rotate(90);
            canvas.translate(0, -width);// width);
            mRightEdge.setSize(height, width);

            needsInvalidate = mRightEdge.draw(canvas);
            canvas.restoreToCount(restoreCount);
        }

        if (!mBottomEdge.isFinished()) {
            final int restoreCount = canvas.save();

            canvas.rotate(180);
            canvas.translate(-width + getPaddingTop(), -height);

            mBottomEdge.setSize(width, height);

            needsInvalidate = mBottomEdge.draw(canvas);
            canvas.restoreToCount(restoreCount);
        }

        if (needsInvalidate) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    protected void returnItemToPoolIfNeeded(FreeFlowItem freeflowItem) {
        View v = freeflowItem.view;
        v.setTranslationX(0);
        v.setTranslationY(0);
        v.setRotation(0);
        v.setScaleX(1f);
        v.setScaleY(1f);
        v.setAlpha(1);
        viewpool.returnViewToPool(v);
    }

    public SectionedAdapter getAdapter() {
        return mAdapter;
    }

    public void setLayoutAnimator(FreeFlowLayoutAnimator anim) {
        layoutAnimator = anim;
    }

    public FreeFlowLayoutAnimator getLayoutAnimator() {
        return layoutAnimator;
    }

    public Map<Object, FreeFlowItem> getFrames() {
        return frames;
    }

    public void clearFrames() {
        removeAllViews();
        frames = null;
    }

    @Override
    public boolean shouldDelayChildPressedState() {
        return true;
    }

    public int getCheckedItemCount() {
        return mCheckStates.size();
    }

    public ArrayList<IndexPath> getCheckedItemPositions() {
        ArrayList<IndexPath> checked = new ArrayList<IndexPath>();
        for (int i = 0; i < mCheckStates.size(); i++) {
            checked.add(mCheckStates.keyAt(i));
        }

        return checked;
    }

    public void clearChoices() {
        mCheckStates.clear();
    }

    /**
     * Defines the choice behavior for the Container allowing multi-select etc.
     * 
     * @see <a href=
     *      "http://developer.android.com/reference/android/widget/AbsListView.html#attr_android:choiceMode"
     *      >List View's Choice Mode</a>
     */
    public void setChoiceMode(int choiceMode) {
        mChoiceMode = choiceMode;
        if (mChoiceActionMode != null) {
            mChoiceActionMode.finish();
            mChoiceActionMode = null;
        }
        if (mChoiceMode != CHOICE_MODE_NONE) {
            if (mCheckStates == null) {
                mCheckStates = new SimpleArrayMap<IndexPath, Boolean>();
            }
            if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
                clearChoices();
                setLongClickable(true);
            }
        }
    }

    boolean isLongClickable = false;

    @Override
    public void setLongClickable(boolean b) {
        isLongClickable = b;
    }

    @Override
    public boolean isLongClickable() {
        return isLongClickable;
    }

    public void setMultiChoiceModeListener(MultiChoiceModeListener listener) {
        if (mMultiChoiceModeCallback == null) {
            mMultiChoiceModeCallback = new MultiChoiceModeWrapper();
        }
        mMultiChoiceModeCallback.setWrapped(listener);
    }

    final class CheckForTap implements Runnable {
        @Override
        public void run() {
            if (mTouchMode == TOUCH_MODE_DOWN) {
                mTouchMode = TOUCH_MODE_TAP;

                if (mOnTouchModeChangedListener != null) {
                    mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
                }

                if (beginTouchAt != null && beginTouchAt.view != null) {
                    beginTouchAt.view.setPressed(true);
                    // setPressed(true);
                }

                refreshDrawableState();
                final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
                final boolean longClickable = isLongClickable();

                if (longClickable) {
                    if (mPendingCheckForLongPress == null) {
                        mPendingCheckForLongPress = new CheckForLongPress();
                    }
                    postDelayed(mPendingCheckForLongPress, longPressTimeout);
                } else {
                    mTouchMode = TOUCH_MODE_DONE_WAITING;

                    if (mOnTouchModeChangedListener != null) {
                        mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
                    }
                }
            }
        }
    }

    private class CheckForLongPress implements Runnable {
        @Override
        public void run() {
            if (beginTouchAt == null) {
                // Assuming child that was being long pressed
                // is no longer valid
                return;
            }

            mCheckStates.clear();
            final View child = beginTouchAt.view;
            if (child != null) {
                boolean handled = false;
                // if (!mDataChanged) {
                handled = performLongPress();
                // }
                if (handled) {
                    mTouchMode = TOUCH_MODE_REST;

                    if (mOnTouchModeChangedListener != null) {
                        mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
                    }

                    // setPressed(false);
                    child.setPressed(false);
                } else {
                    mTouchMode = TOUCH_MODE_DONE_WAITING;

                    if (mOnTouchModeChangedListener != null) {
                        mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
                    }
                }
            }
        }
    }

    boolean performLongPress() {
        // CHOICE_MODE_MULTIPLE_MODAL takes over long press.
        if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
            if (mChoiceActionMode == null
                    && (mChoiceActionMode = startActionMode(mMultiChoiceModeCallback)) != null) {
                setItemChecked(beginTouchAt.itemSection, beginTouchAt.itemIndex, true);
                updateOnScreenCheckedViews();
                performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
            }
            return true;
        }

        boolean handled = false;
        final long longPressId = mAdapter.getItemId(beginTouchAt.itemSection, beginTouchAt.itemSection);
        if (mOnItemLongClickListener != null) {
            handled = mOnItemLongClickListener.onItemLongClick(this, beginTouchAt.view, beginTouchAt.itemSection,
                    beginTouchAt.itemIndex, longPressId);
        }
        if (!handled) {
            mContextMenuInfo = createContextMenuInfo(beginTouchAt.view, beginTouchAt.itemSection,
                    beginTouchAt.itemIndex, longPressId);
            handled = super.showContextMenuForChild(this);
        }
        if (handled) {
            updateOnScreenCheckedViews();
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        return handled;
    }

    ContextMenuInfo createContextMenuInfo(View view, int sectionIndex, int positionInSection, long id) {
        return new AbsLayoutContainerContextMenuInfo(view, sectionIndex, positionInSection, id);
    }

    class MultiChoiceModeWrapper implements MultiChoiceModeListener {

        private MultiChoiceModeListener mWrapped;

        public void setWrapped(MultiChoiceModeListener wrapped) {
            mWrapped = wrapped;
        }

        public boolean hasWrappedCallback() {
            return mWrapped != null;
        }

        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            if (mWrapped.onCreateActionMode(mode, menu)) {
                // Initialize checked graphic state?
                setLongClickable(false);
                return true;
            }
            return false;
        }

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            return mWrapped.onPrepareActionMode(mode, menu);
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            return mWrapped.onActionItemClicked(mode, item);
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {
            mWrapped.onDestroyActionMode(mode);
            mChoiceActionMode = null;

            // Ending selection mode means deselecting everything.
            clearChoices();
            updateOnScreenCheckedViews();

            // rememberSyncState();
            requestLayout();

            setLongClickable(true);
        }

        @Override
        public void onItemCheckedStateChanged(ActionMode mode, int section, int position, long id,
                boolean checked) {
            mWrapped.onItemCheckedStateChanged(mode, section, position, id, checked);

            // If there are no items selected we no longer need the selection
            // mode.
            if (getCheckedItemCount() == 0) {
                mode.finish();
            }
        }
    }

    public interface OnTouchModeChangedListener {
        void onTouchModeChanged(int touchMode);
    }

    public interface MultiChoiceModeListener extends ActionMode.Callback {
        /**
         * Called when an item is checked or unchecked during selection mode.
         * 
         * @param mode
         *            The {@link android.view.ActionMode} providing the selection mode
         * @param section
         *            The Section of the item that was checked
         * @param position
         *            Adapter position of the item in the section that was
         *            checked or unchecked
         * @param id
         *            Adapter ID of the item that was checked or unchecked
         * @param checked
         *            <code>true</code> if the item is now checked,
         *            <code>false</code> if the item is now unchecked.
         */
        public void onItemCheckedStateChanged(ActionMode mode, int section, int position, long id, boolean checked);
    }

    public void setItemChecked(int sectionIndex, int positionInSection, boolean value) {
        if (mChoiceMode == CHOICE_MODE_NONE) {
            return;
        }

        // Start selection mode if needed. We don't need to if we're unchecking
        // something.
        if (value && mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) {
            if (mMultiChoiceModeCallback == null || !mMultiChoiceModeCallback.hasWrappedCallback()) {
                throw new IllegalStateException("Container: attempted to start selection mode "
                        + "for CHOICE_MODE_MULTIPLE_MODAL but no choice mode callback was "
                        + "supplied. Call setMultiChoiceModeListener to set a callback.");
            }
            mChoiceActionMode = startActionMode(mMultiChoiceModeCallback);
        }

        if (mChoiceMode == CHOICE_MODE_MULTIPLE || mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {

            setCheckedValue(sectionIndex, positionInSection, value);
            if (mChoiceActionMode != null) {
                final long id = mAdapter.getItemId(sectionIndex, positionInSection);
                mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode, sectionIndex,
                        positionInSection, id, value);
            }
        } else {
            setCheckedValue(sectionIndex, positionInSection, value);
        }

        // if (!mInLayout && !mBlockLayoutRequests) {
        // mDataChanged = true;
        // rememberSyncState();
        requestLayout();
        // }
    }

    @Override
    public boolean performItemClick(View view, int section, int position, long id) {
        boolean handled = false;
        boolean dispatchItemClick = true;
        if (mChoiceMode != CHOICE_MODE_NONE) {
            handled = true;
            boolean checkedStateChanged = false;

            if (mChoiceMode == CHOICE_MODE_MULTIPLE
                    || (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode != null)) {
                boolean checked = isChecked(section, position);
                checked = !checked;
                setCheckedValue(section, position, checked);

                if (mChoiceActionMode != null) {
                    mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode, section, position, id,
                            checked);
                    dispatchItemClick = false;
                }
                checkedStateChanged = true;
            } else if (mChoiceMode == CHOICE_MODE_SINGLE) {
                boolean checked = !isChecked(section, position);
                if (checked) {
                    setCheckedValue(section, position, checked);
                }
                checkedStateChanged = true;
            }

            if (checkedStateChanged) {
                updateOnScreenCheckedViews();
            }
        }

        if (dispatchItemClick) {

            handled |= super.performItemClick(view, section, position, id);
        }

        return handled;
    }

    private class PerformClick implements Runnable {
        @Override
        public void run() {
            // if (mDataChanged) return;
            View view = beginTouchAt.view;
            if (view != null) {
                performItemClick(view, beginTouchAt.itemSection, beginTouchAt.itemIndex,
                        mAdapter.getItemId(beginTouchAt.itemSection, beginTouchAt.itemIndex));
            }
            // }
        }
    }

    /**
     * Perform a quick, in-place update of the checked or activated state on all
     * visible item views. This should only be called when a valid choice mode
     * is active.
     */
    private void updateOnScreenCheckedViews() {
        Iterator<?> it = frames.entrySet().iterator();
        View child = null;
        while (it.hasNext()) {
            Map.Entry<?, FreeFlowItem> pairs = (Map.Entry<?, FreeFlowItem>) it.next();
            child = pairs.getValue().view;
            boolean isChecked = isChecked(pairs.getValue().itemSection, pairs.getValue().itemIndex);
            if (child instanceof Checkable) {
                ((Checkable) child).setChecked(isChecked);
            } else {
                child.setActivated(isChecked);
            }
        }
    }

    public boolean isChecked(int sectionIndex, int positionInSection) {
        for (int i = 0; i < mCheckStates.size(); i++) {
            IndexPath p = mCheckStates.keyAt(i);
            if (p.section == sectionIndex && p.positionInSection == positionInSection) {
                return true;
            }
        }
        return false;
    }

    /**
     * Updates the internal ArrayMap keeping track of checked states. Will not
     * update the check UI.
     */
    protected void setCheckedValue(int sectionIndex, int positionInSection, boolean val) {
        int foundAtIndex = -1;
        for (int i = 0; i < mCheckStates.size(); i++) {
            IndexPath p = mCheckStates.keyAt(i);
            if (p.section == sectionIndex && p.positionInSection == positionInSection) {
                foundAtIndex = i;
                break;
            }
        }
        if (foundAtIndex > -1 && val == false) {
            mCheckStates.removeAt(foundAtIndex);
        } else if (foundAtIndex == -1 && val == true) {
            IndexPath pos = new IndexPath(sectionIndex, positionInSection);
            mCheckStates.put(pos, true);
        }

    }

    public void addScrollListener(OnScrollListener listener) {
        if (!scrollListeners.contains(listener))
            scrollListeners.add(listener);
    }

    public void removeScrollListener(OnScrollListener listener) {
        scrollListeners.remove(listener);
    }

    public void scrollToItem(int sectionIndex, int itemIndex, boolean animate) {
        Section section;

        if (sectionIndex > mAdapter.getNumberOfSections() || sectionIndex < 0
                || (section = mAdapter.getSection(sectionIndex)) == null) {
            return;
        }

        if (itemIndex < 0 || itemIndex > section.getDataCount()) {
            return;
        }

        FreeFlowItem freeflowItem = mLayout.getFreeFlowItemForItem(section.getDataAtIndex(itemIndex));
        freeflowItem = FreeFlowItem.clone(freeflowItem);

        int newVPX = freeflowItem.frame.left;
        int newVPY = freeflowItem.frame.top;

        if (newVPX > mLayout.getContentWidth() - getMeasuredWidth())
            newVPX = mLayout.getContentWidth() - getMeasuredWidth();

        if (newVPY > mLayout.getContentHeight() - getMeasuredHeight())
            newVPY = mLayout.getContentHeight() - getMeasuredHeight();

        if (animate) {
            scroller.startScroll(viewPortX, viewPortY, (newVPX - viewPortX), (newVPY - viewPortY), 1500);
            post(flingRunnable);
        } else {
            moveViewportBy((viewPortX - newVPX), (viewPortY - newVPY), false);
            invokeOnItemScrollListeners();
        }
    }

    /**
     * Returns the percentage of width scrolled. The values range from 0 to 1
     * 
     * @return
     */
    public float getScrollPercentX() {
        if (mLayout == null || mAdapter == null)
            return 0;
        float w = mLayout.getContentWidth();
        float scrollableWidth = w - getWidth();
        if (scrollableWidth == 0)
            return 0;
        return viewPortX / scrollableWidth;
    }

    /**
     * Returns the percentage of height scrolled. The values range from 0 to 1
     * 
     * @return
     */
    public float getScrollPercentY() {
        if (mLayout == null || mAdapter == null)
            return 0;
        float ht = mLayout.getContentHeight();
        float scrollableHeight = ht - getHeight();
        if (scrollableHeight == 0)
            return 0;
        return viewPortY / scrollableHeight;
    }

    protected void invokeOnItemScrollListeners() {
        for (OnScrollListener l : scrollListeners) {
            l.onScroll(this);
        }
    }

    protected void reportScrollStateChange(int state) {
        // TODO:
    }

    public interface OnScrollListener {
        public int SCROLL_STATE_IDLE = 0;
        public int SCROLL_STATE_TOUCH_SCROLL = 1;
        public int SCROLL_STATE_FLING = 2;

        public void onScroll(FreeFlowContainer container);
    }

    /******** DEBUGGING HELPERS *******/

    /**
     * A flag for conditionally printing Container lifecycle events to LogCat
     * for debugging
     */
    public boolean logDebugEvents = false;

    /**
     * A utility method for debugging lifecycle events and putting them in the
     * log messages
     * 
     * @param msg
     */
    private void logLifecycleEvent(String msg) {
        Logs.d("ContainerLifecycleEvent", msg);
    }

}