Android Open Source - FreeFlow Free Flow Container






From Project

Back to project page FreeFlow.

License

The source code is released under:

Apache License

If you think the Android project FreeFlow listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*******************************************************************************
 * Copyright 2013 Comcast Cable Communications Management, LLC
 */*  w ww.j a  v a 2  s .  c  o m*/
 * 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.comcast.freeflow.core;

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

import org.freeflow.BuildConfig;

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.comcast.freeflow.animations.DefaultLayoutAnimator;
import com.comcast.freeflow.animations.FreeFlowLayoutAnimator;
import com.comcast.freeflow.layouts.FreeFlowLayout;
import com.comcast.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<FreeFlowContainer.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 widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int afterWidth = 0;
    int afterHeight = 0;
    
//    int beforeWidth = getWidth();
//    int beforeHeight = getHeight();

    afterWidth = MeasureSpec.getSize(widthMeasureSpec);
    afterHeight = MeasureSpec.getSize(heightMeasureSpec);

    
    // TODO: prepareLayout should at some point take sizeChanged as a param
    // to not
    // avoidable calculations

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

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

    }
    
    if (widthMode != MeasureSpec.UNSPECIFIED && heightMode != MeasureSpec.UNSPECIFIED) {
      markAdapterDirty = false;
      markLayoutDirty = false;
      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);
        }
      }
    }

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  }

  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) {
    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.
   * 
   * @see getViewportTop
   * @see getViewportLeft
   * 
   */
  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 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);
  }

  @Override
  public void setOnItemLongClickListener(OnItemLongClickListener listener) {
    super.setOnItemLongClickListener(listener);
    if (mCheckStates==null) {
      mCheckStates = new SimpleArrayMap<IndexPath, Boolean>(); 
    }
  }
  
  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) {
    if (logDebugEvents && BuildConfig.DEBUG) {
      Log.d("ContainerLifecycleEvent", msg);
    }
  }

}




Java Source Code List

com.comcast.freeflow.animations.DefaultLayoutAnimator.java
com.comcast.freeflow.animations.FreeFlowLayoutAnimator.java
com.comcast.freeflow.animations.NoAnimationLayoutAnimator.java
com.comcast.freeflow.animations.interpolators.EaseInOutQuintInterpolator.java
com.comcast.freeflow.core.AbsLayoutContainer.java
com.comcast.freeflow.core.FreeFlowContainerTest.java
com.comcast.freeflow.core.FreeFlowContainer.java
com.comcast.freeflow.core.FreeFlowEventListener.java
com.comcast.freeflow.core.FreeFlowItem.java
com.comcast.freeflow.core.IndexPath.java
com.comcast.freeflow.core.LayoutChangeset.java
com.comcast.freeflow.core.Section.java
com.comcast.freeflow.core.SectionedAdapter.java
com.comcast.freeflow.core.ViewPool.java
com.comcast.freeflow.debug.BaseFreeFlowEventListener.java
com.comcast.freeflow.debug.TouchDebugUtils.java
com.comcast.freeflow.examples.artbook.AboutActivity.java
com.comcast.freeflow.examples.artbook.ArtbookActivity.java
com.comcast.freeflow.examples.artbook.data.DribbbleDataAdapter.java
com.comcast.freeflow.examples.artbook.layouts.ArtbookLayout.java
com.comcast.freeflow.examples.artbook.models.DribbbleFeed.java
com.comcast.freeflow.examples.artbook.models.DribbbleFetch.java
com.comcast.freeflow.examples.artbook.models.Player.java
com.comcast.freeflow.examples.artbook.models.Shot.java
com.comcast.freeflow.examples.freeflowphotogrid.MainActivity.java
com.comcast.freeflow.helpers.DefaultSectionAdapter.java
com.comcast.freeflow.layouts.FreeFlowLayoutBase.java
com.comcast.freeflow.layouts.FreeFlowLayout.java
com.comcast.freeflow.layouts.HGridLayout.java
com.comcast.freeflow.layouts.HLayout.java
com.comcast.freeflow.layouts.VGridLayoutTest.java
com.comcast.freeflow.layouts.VGridLayout.java
com.comcast.freeflow.layouts.VLayout.java
com.comcast.freeflow.teststub.MainActivity.java
com.comcast.freeflow.utils.MathUtils.java
com.comcast.freeflow.utils.ViewUtils.java