Back to project page FreeFlow.
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.
/******************************************************************************* * 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); } } }