/*******************************************************************************
* Copyright 2013 Comcast Cable Communications Management, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.marshalchen.common.uimodule.freeflow.core;
import 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.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.view.ViewGroup;
import android.widget.Checkable;
import android.widget.EdgeEffect;
import android.widget.OverScroller;
import com.marshalchen.common.commonUtils.logUtils.Logs;
import com.marshalchen.common.uimodule.freeflow.animations.DefaultLayoutAnimator;
import com.marshalchen.common.uimodule.freeflow.animations.FreeFlowLayoutAnimator;
import com.marshalchen.common.uimodule.freeflow.layouts.FreeFlowLayout;
import com.marshalchen.common.uimodule.freeflow.utils.ViewUtils;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
public class FreeFlowContainer extends AbsLayoutContainer {
private static final String TAG = "Container";
// ViewPool class
protected ViewPool viewpool;
// Not used yet, but we'll probably need to
// prevent layout in <code>layout()</code> method
private boolean preventLayout = false;
protected SectionedAdapter mAdapter;
protected FreeFlowLayout mLayout;
/**
* The X position of the active ViewPort
*/
protected int viewPortX = 0;
/**
* The Y position of the active ViewPort
*/
protected int viewPortY = 0;
/**
* The scrollable width in pixels. This is usually computed as the
* difference between the width of the container and the contentWidth as
* computed by the layout.
*/
protected int mScrollableWidth;
/**
* The scrollable height in pixels. This is usually computed as the
* difference between the height of the container and the contentHeight as
* computed by the layout.
*/
protected int mScrollableHeight;
private VelocityTracker mVelocityTracker = null;
private float deltaX = -1f;
private float deltaY = -1f;
private int maxFlingVelocity;
private int minFlingVelocity;
private int overflingDistance;
/*private int overscrollDistance;*/
private int touchSlop;
private Runnable mTouchModeReset;
private Runnable mPerformClick;
private Runnable mPendingCheckForTap;
private Runnable mPendingCheckForLongPress;
private OverScroller scroller;
protected EdgeEffect mLeftEdge, mRightEdge, mTopEdge, mBottomEdge;
private ArrayList<OnScrollListener> scrollListeners = new ArrayList<OnScrollListener>();
// This flag controls whether onTap/onLongPress/onTouch trigger
// the ActionMode
// private boolean mDataChanged = false;
/**
* TODO: ContextMenu action on long press has not been implemented yet
*/
protected ContextMenuInfo mContextMenuInfo = null;
/**
* Holds the checked items when the Container is in CHOICE_MODE_MULTIPLE
*/
protected SimpleArrayMap<IndexPath, Boolean> mCheckStates = null;
ActionMode mChoiceActionMode;
/**
* Wraps the callback for MultiChoiceMode
*/
MultiChoiceModeWrapper mMultiChoiceModeCallback;
/**
* Normal list that does not indicate choices
*/
public static final int CHOICE_MODE_NONE = 0;
/**
* The list allows up to one choice
*/
public static final int CHOICE_MODE_SINGLE = 1;
/**
* The list allows multiple choices
*/
public static final int CHOICE_MODE_MULTIPLE = 2;
/**
* The list allows multiple choices in a modal selection mode
*/
public static final int CHOICE_MODE_MULTIPLE_MODAL = 3;
/**
* The value of the current ChoiceMode
*
* @see <a href=
* "http://developer.android.com/reference/android/widget/AbsListView.html#attr_android:choiceMode"
* >List View's Choice Mode</a>
*/
int mChoiceMode = CHOICE_MODE_NONE;
private ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(0, 0);
private FreeFlowLayoutAnimator layoutAnimator = new DefaultLayoutAnimator();
private FreeFlowItem beginTouchAt;
private boolean markLayoutDirty = false;
private boolean markAdapterDirty = false;
/**
* When Layout is computed, should scroll positions be recalculated? When a
* new layout is set, the Container can try to make sure an item that was
* visible in one layout is also visible in the new layout. However when
* data is just invalidated and additional data is loaded, you don't want
* the Viewport to be jumping around.
*/
private boolean shouldRecalculateScrollWhenComputingLayout = true;
private FreeFlowLayout oldLayout;
private OnTouchModeChangedListener mOnTouchModeChangedListener;
public void setOnTouchModeChangedListener(
OnTouchModeChangedListener onTouchModeChangedListener) {
mOnTouchModeChangedListener = onTouchModeChangedListener;
}
public FreeFlowContainer(Context context) {
super(context);
}
public FreeFlowContainer(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FreeFlowContainer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void init(Context context) {
viewpool = new ViewPool();
frames = new LinkedHashMap<Object, FreeFlowItem>();
ViewConfiguration configuration = ViewConfiguration.get(context);
maxFlingVelocity = configuration.getScaledMaximumFlingVelocity();
minFlingVelocity = configuration.getScaledMinimumFlingVelocity();
overflingDistance = configuration.getScaledOverflingDistance();
/*overscrollDistance = configuration.getScaledOverscrollDistance();*/
touchSlop = configuration.getScaledTouchSlop();
scroller = new OverScroller(context);
setEdgeEffectsEnabled(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
logLifecycleEvent(" onMeasure ");
int beforeWidth = getWidth();
int beforeHeight = getHeight();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int afterWidth = View.MeasureSpec.getSize(widthMeasureSpec);
int afterHeight = View.MeasureSpec.getSize(heightMeasureSpec);
// TODO: prepareLayout should at some point take sizeChanged as a param
// to not
// avoidable calculations
boolean sizeChanged = (beforeHeight == afterHeight)
&& (beforeWidth == afterWidth);
if (this.mLayout != null) {
mLayout.setDimensions(afterWidth, afterHeight);
}
if (mLayout == null || mAdapter == null) {
logLifecycleEvent("Nothing to do: returning");
return;
}
if (markAdapterDirty || markLayoutDirty) {
computeLayout(afterWidth, afterHeight);
}
if (dataSetChanged) {
dataSetChanged = false;
for (FreeFlowItem item : frames.values()) {
if (item.itemIndex >= 0 && item.itemSection >= 0) {
mAdapter.getItemView(item.itemSection, item.itemIndex,
item.view, this);
}
}
}
}
protected boolean dataSetChanged = false;
/**
* Notifies the attached observers that the underlying data has been changed
* and any View reflecting the data set should refresh itself.
*/
public void notifyDataSetChanged() {
dataSetChanged = true;
requestLayout();
}
/**
* @deprecated Use dataInvalidated(boolean shouldRecalculateScrollPositions)
* instead
*/
public void dataInvalidated() {
dataInvalidated(false);
}
/**
* Called to inform the Container that the underlying data on the adapter
* has changed (more items added/removed). Note that this won't update the
* views if the adapter's data objects are the same but the values in those
* objects have changed. To update those call {@code notifyDataSetChanged}
*
* @param shouldRecalculateScrollPositions
*/
public void dataInvalidated(boolean shouldRecalculateScrollPositions) {
logLifecycleEvent("Data Invalidated");
if (mLayout == null || mAdapter == null) {
return;
}
shouldRecalculateScrollWhenComputingLayout = shouldRecalculateScrollPositions;
markAdapterDirty = true;
requestLayout();
}
/**
* The heart of the system. Calls the layout to get the frames needed,
* decides which view should be kept in focus if view transitions are going
* to happen and then kicks off animation changes if things have changed
*
* @param w
* Width of the viewport. Since right now we don't support
* margins and padding, this is width of the container.
* @param h
* Height of the viewport. Since right now we don't support
* margins and padding, this is height of the container.
*/
protected void computeLayout(int w, int h) {
markLayoutDirty = false;
markAdapterDirty = false;
mLayout.prepareLayout();
if (shouldRecalculateScrollWhenComputingLayout) {
computeViewPort(mLayout);
}
Map<Object, FreeFlowItem> oldFrames = frames;
frames = new LinkedHashMap<Object, FreeFlowItem>();
copyFrames(mLayout.getItemProxies(viewPortX, viewPortY), frames);
// Create a copy of the incoming values because the source
// layout may change the map inside its own class
dispatchLayoutComputed();
animateChanges(getViewChanges(oldFrames, frames));
}
/**
* Copies the frames from one LinkedHashMap into another. The items are
* cloned cause we modify the rectangles of the items as they are moving
*/
protected void copyFrames(Map<Object, FreeFlowItem> srcFrames,
Map<Object, FreeFlowItem> destFrames) {
Iterator<?> it = srcFrames.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<?, ?> pairs = (Map.Entry<?, ?>) it.next();
FreeFlowItem pr = (FreeFlowItem) pairs.getValue();
pr = FreeFlowItem.clone(pr);
destFrames.put(pairs.getKey(), pr);
}
}
/**
* Adds a view based on the current viewport. If we can get a view from the
* ViewPool, we dont need to construct a new instance, else we will based on
* the View class returned by the <code>Adapter</code>
*
* @param freeflowItem
* <code>FreeFlowItem</code> instance that determines the View
* being positioned
*/
protected void addAndMeasureViewIfNeeded(FreeFlowItem freeflowItem) {
View view;
if (freeflowItem.view == null) {
View convertView = viewpool.getViewFromPool(mAdapter
.getViewType(freeflowItem));
if (freeflowItem.isHeader) {
view = mAdapter.getHeaderViewForSection(
freeflowItem.itemSection, convertView, this);
} else {
view = mAdapter.getItemView(freeflowItem.itemSection,
freeflowItem.itemIndex, convertView, this);
}
if (view instanceof FreeFlowContainer)
throw new IllegalStateException(
"A container cannot be a direct child view to a container");
freeflowItem.view = view;
prepareViewForAddition(view, freeflowItem);
addView(view, getChildCount(), params);
}
view = freeflowItem.view;
int widthSpec = View.MeasureSpec.makeMeasureSpec(freeflowItem.frame.width(),
View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(
freeflowItem.frame.height(), View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);
}
/**
* Does all the necessary work right before a view is about to be laid out.
*
* @param view
* The View that will be added to the Container
* @param freeflowItem
* The <code>FreeFlowItem</code> instance that represents the
* view that will be positioned
*/
protected void prepareViewForAddition(View view, FreeFlowItem freeflowItem) {
if (view instanceof Checkable) {
((Checkable) view).setChecked(isChecked(freeflowItem.itemSection,
freeflowItem.itemIndex));
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
logLifecycleEvent("onLayout");
dispatchLayoutComplete(isAnimatingChanges);
// mDataChanged = false;
}
protected void doLayout(FreeFlowItem freeflowItem) {
View view = freeflowItem.view;
Rect frame = freeflowItem.frame;
view.layout(frame.left - viewPortX, frame.top - viewPortY, frame.right
- viewPortX, frame.bottom - viewPortY);
}
/**
* Sets the layout on the Container. If a previous layout was already
* applied, this causes the views to animate to the new layout positions.
* Scroll positions will also be reset.
*
* @see FreeFlowLayout
* @param newLayout
*/
public void setLayout(FreeFlowLayout newLayout) {
if (newLayout == mLayout || newLayout == null) {
return;
}
stopScrolling();
oldLayout = mLayout;
mLayout = newLayout;
shouldRecalculateScrollWhenComputingLayout = true;
if (mAdapter != null) {
mLayout.setAdapter(mAdapter);
}
dispatchLayoutChanging(oldLayout, newLayout);
markLayoutDirty = true;
viewPortX = 0;
viewPortY = 0;
logLifecycleEvent("Setting layout");
requestLayout();
}
/**
* Stops the scrolling immediately
*/
public void stopScrolling() {
if (!scroller.isFinished()) {
scroller.forceFinished(true);
}
removeCallbacks(flingRunnable);
resetAllCallbacks();
mTouchMode = TOUCH_MODE_REST;
if (mOnTouchModeChangedListener != null) {
mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
}
}
/**
* Resets all Runnables that are checking on various statuses
*/
protected void resetAllCallbacks() {
if (mPendingCheckForTap != null) {
removeCallbacks(mPendingCheckForTap);
mPendingCheckForTap = null;
}
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
mPendingCheckForLongPress = null;
}
if (mTouchModeReset != null) {
removeCallbacks(mTouchModeReset);
mTouchModeReset = null;
}
if (mPerformClick != null) {
removeCallbacks(mPerformClick);
mPerformClick = null;
}
}
/**
* @return The layout currently applied to the Container
*/
public FreeFlowLayout getLayout() {
return mLayout;
}
/**
* Computes the Rectangle that defines the ViewPort. The Container tries to
* keep the view at the top left of the old layout visible in the new
* layout.
*
*
*/
protected void computeViewPort(FreeFlowLayout newLayout) {
if (mLayout == null || frames == null || frames.size() == 0) {
viewPortX = 0;
viewPortY = 0;
return;
}
Object data = null;
int lowestSection = Integer.MAX_VALUE;
int lowestPosition = Integer.MAX_VALUE;
// Find the frame of of the first item in the first section in the
// current set of frames defining the viewport
// Changing layout will then keep this item in the viewport of the new
// layout
// TODO: Need to make sure this item is actually being shown in the
// viewport and not just in some offscreen buffer
for (FreeFlowItem fd : frames.values()) {
if (fd.itemSection < lowestSection
|| (fd.itemSection == lowestSection && fd.itemIndex < lowestPosition)) {
data = fd.data;
lowestSection = fd.itemSection;
lowestPosition = fd.itemIndex;
}
}
FreeFlowItem freeflowItem = newLayout.getFreeFlowItemForItem(data);
freeflowItem = FreeFlowItem.clone(freeflowItem);
if (freeflowItem == null) {
viewPortX = 0;
viewPortY = 0;
return;
}
Rect vpFrame = freeflowItem.frame;
viewPortX = vpFrame.left;
viewPortY = vpFrame.top;
mScrollableWidth = mLayout.getContentWidth() - getWidth();
mScrollableHeight = mLayout.getContentHeight() - getHeight();
if (mScrollableWidth < 0) {
mScrollableWidth = 0;
}
if (mScrollableHeight < 0) {
mScrollableHeight = 0;
}
if (viewPortX > mScrollableWidth)
viewPortX = mScrollableWidth;
if (viewPortY > mScrollableHeight)
viewPortY = mScrollableHeight;
}
/**
* Returns the actual frame for a view as its on stage. The FreeFlowItem's
* frame object always represents the position it wants to be in but actual
* frame may be different based on animation etc.
*
* @param freeflowItem
* The freeflowItem to get the <code>Frame</code> for
* @return The Frame for the freeflowItem or null if that view doesn't exist
*/
public Rect getActualFrame(final FreeFlowItem freeflowItem) {
View v = freeflowItem.view;
if (v == null) {
return null;
}
Rect of = new Rect();
of.left = (int) (v.getLeft() + v.getTranslationX());
of.top = (int) (v.getTop() + v.getTranslationY());
of.right = (int) (v.getRight() + v.getTranslationX());
of.bottom = (int) (v.getBottom() + v.getTranslationY());
return of;
}
/**
* Returns the <code>FreeFlowItem</code> representing the data passed in IF
* that item is being rendered in the Container.
*
* @param dataItem
* The data object being rendered in a View managed by the
* Container, null otherwise
* @return
*/
public FreeFlowItem getFreeFlowItem(Object dataItem) {
for (FreeFlowItem item : frames.values()) {
if (item.data.equals(dataItem)) {
return item;
}
}
return null;
}
/**
* TODO: This should be renamed to layoutInvalidated, since the layout isn't
* changed
*/
public void layoutChanged() {
logLifecycleEvent("layoutChanged");
markLayoutDirty = true;
dispatchDataChanged();
requestLayout();
}
protected boolean isAnimatingChanges = false;
private void animateChanges(LayoutChangeset changeSet) {
logLifecycleEvent("animating changes: " + changeSet.toString());
if (changeSet.added.size() == 0 && changeSet.removed.size() == 0
&& changeSet.moved.size() == 0) {
return;
}
for (FreeFlowItem freeflowItem : changeSet.getAdded()) {
addAndMeasureViewIfNeeded(freeflowItem);
doLayout(freeflowItem);
}
if (isAnimatingChanges) {
layoutAnimator.cancel();
}
isAnimatingChanges = true;
dispatchAnimationsStarting();
layoutAnimator.animateChanges(changeSet, this);
}
/**
* This method is called by the <code>LayoutAnimator</code> instance once
* all transition animations have been completed.
*
* @param anim
* The LayoutAnimator instance that reported change complete.
*/
public void onLayoutChangeAnimationsCompleted(FreeFlowLayoutAnimator anim) {
// preventLayout = false;
isAnimatingChanges = false;
logLifecycleEvent("layout change animations complete");
for (FreeFlowItem freeflowItem : anim.getChangeSet().getRemoved()) {
View v = freeflowItem.view;
removeView(v);
returnItemToPoolIfNeeded(freeflowItem);
}
dispatchLayoutChangeAnimationsComplete();
// changeSet = null;
}
public LayoutChangeset getViewChanges(Map<Object, FreeFlowItem> oldFrames,
Map<Object, FreeFlowItem> newFrames) {
return getViewChanges(oldFrames, newFrames, false);
}
public LayoutChangeset getViewChanges(Map<Object, FreeFlowItem> oldFrames,
Map<Object, FreeFlowItem> newFrames, boolean moveEvenIfSame) {
// cleanupViews();
LayoutChangeset change = new LayoutChangeset();
if (oldFrames == null) {
markAdapterDirty = false;
for (FreeFlowItem freeflowItem : newFrames.values()) {
change.addToAdded(freeflowItem);
}
return change;
}
if (markAdapterDirty) {
markAdapterDirty = false;
for (FreeFlowItem freeflowItem : newFrames.values()) {
change.addToAdded(freeflowItem);
}
for (FreeFlowItem freeflowItem : oldFrames.values()) {
change.addToDeleted(freeflowItem);
}
return change;
}
Iterator<?> it = newFrames.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<?, ?> m = (Map.Entry<?, ?>) it.next();
FreeFlowItem freeflowItem = (FreeFlowItem) m.getValue();
if (oldFrames.get(m.getKey()) != null) {
FreeFlowItem old = oldFrames.remove(m.getKey());
freeflowItem.view = old.view;
// if (moveEvenIfSame || !old.compareRect(((FreeFlowItem)
// m.getValue()).frame)) {
if (moveEvenIfSame
|| !old.frame
.equals(((FreeFlowItem) m.getValue()).frame)) {
change.addToMoved(freeflowItem,
getActualFrame(freeflowItem));
}
} else {
change.addToAdded(freeflowItem);
}
}
for (FreeFlowItem freeflowItem : oldFrames.values()) {
change.addToDeleted(freeflowItem);
}
frames = newFrames;
return change;
}
@Override
public void requestLayout() {
if (!preventLayout) {
/**
* Ends up with a call to <code>onMeasure</code> where all the logic
* lives
*/
super.requestLayout();
}
}
/**
* Sets the adapter for the this CollectionView.All view pools will be
* cleared at this point and all views on the stage will be cleared
*
* @param adapter
* The {@link com.marshalchen.common.uimodule.freeflow.core.SectionedAdapter} that will populate this
* Collection
*/
public void setAdapter(SectionedAdapter adapter) {
if (adapter == mAdapter) {
return;
}
stopScrolling();
logLifecycleEvent("setting adapter");
markAdapterDirty = true;
viewPortX = 0;
viewPortY = 0;
shouldRecalculateScrollWhenComputingLayout = true;
this.mAdapter = adapter;
if (adapter != null) {
viewpool.initializeViewPool(adapter.getViewTypes());
}
if (mLayout != null) {
mLayout.setAdapter(mAdapter);
}
requestLayout();
}
public FreeFlowLayout getLayoutController() {
return mLayout;
}
/**
* The Viewport defines the rectangular "window" that the container is
* actually showing of the entire view.
*
* @return The left (x) of the viewport within the entire container
*/
public int getViewportLeft() {
return viewPortX;
}
/**
* The Viewport defines the rectangular "window" that the container is
* actually showing of the entire view.
*
* @return The top (y) of the viewport within the entire container
*
*/
public int getViewportTop() {
return viewPortY;
}
/**
* Indicates that we are not in the middle of a touch gesture
*/
public static final int TOUCH_MODE_REST = -1;
/**
* Indicates we just received the touch event and we are waiting to see if
* the it is a tap or a scroll gesture.
*/
public static final int TOUCH_MODE_DOWN = 0;
/**
* Indicates the touch has been recognized as a tap and we are now waiting
* to see if the touch is a longpress
*/
public static final int TOUCH_MODE_TAP = 1;
/**
* Indicates we have waited for everything we can wait for, but the user's
* finger is still down
*/
public static final int TOUCH_MODE_DONE_WAITING = 2;
/**
* Indicates the touch gesture is a scroll
*/
public static final int TOUCH_MODE_SCROLL = 3;
/**
* Indicates the view is in the process of being flung
*/
public static final int TOUCH_MODE_FLING = 4;
/**
* Indicates the touch gesture is an overscroll - a scroll beyond the
* beginning or end.
*/
public static final int TOUCH_MODE_OVERSCROLL = 5;
/**
* Indicates the view is being flung outside of normal content bounds and
* will spring back.
*/
public static final int TOUCH_MODE_OVERFLING = 6;
/**
* One of TOUCH_MODE_REST, TOUCH_MODE_DOWN, TOUCH_MODE_TAP,
* TOUCH_MODE_SCROLL, or TOUCH_MODE_DONE_WAITING
*/
int mTouchMode = TOUCH_MODE_REST;
/**
* The duration for which the scroller will wait before deciding whether the
* user was actually trying to stop the scroll or swuipe again to increase
* the velocity
*/
protected final int FLYWHEEL_TIMEOUT = 40;
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
if (mLayout == null) {
return false;
}
// flag to check if laid out items are wide or tall enough
// to require scrolling
boolean canScroll = false;
if (mLayout.horizontalScrollEnabled()
&& this.mLayout.getContentWidth() > getWidth()) {
canScroll = true;
}
if (mLayout.verticalScrollEnabled()
&& mLayout.getContentHeight() > getHeight()) {
canScroll = true;
}
switch (event.getAction()) {
case (MotionEvent.ACTION_DOWN):
touchDown(event);
break;
case (MotionEvent.ACTION_MOVE):
if (canScroll) {
touchMove(event);
}
break;
case (MotionEvent.ACTION_UP):
touchUp(event);
break;
case (MotionEvent.ACTION_CANCEL):
touchCancel(event);
break;
}
if (!canScroll) {
return true;
}
if (mVelocityTracker == null && canScroll) {
mVelocityTracker = VelocityTracker.obtain();
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
return true;
}
protected void touchDown(MotionEvent event) {
if(isAnimatingChanges){
layoutAnimator.onContainerTouchDown(event);
}
/*
* Recompute this just to be safe. TODO: We should optimize this to be
* only calculated when a data or layout change happens
*/
mScrollableHeight = mLayout.getContentHeight() - getHeight();
mScrollableWidth = mLayout.getContentWidth() - getWidth();
if (mTouchMode == TOUCH_MODE_FLING) {
// Wait for some time to see if the user is just trying
// to speed up the scroll
postDelayed(new Runnable() {
@Override
public void run() {
if (mTouchMode == TOUCH_MODE_DOWN) {
if (mTouchMode == TOUCH_MODE_DOWN) {
scroller.forceFinished(true);
}
}
}
}, FLYWHEEL_TIMEOUT);
}
beginTouchAt = ViewUtils.getItemAt(frames,
(int) (viewPortX + event.getX()),
(int) (viewPortY + event.getY()));
deltaX = event.getX();
deltaY = event.getY();
mTouchMode = TOUCH_MODE_DOWN;
if (mOnTouchModeChangedListener != null) {
mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
}
if (mPendingCheckForTap != null) {
removeCallbacks(mPendingCheckForTap);
mPendingCheckForLongPress = null;
}
if (beginTouchAt != null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
}
protected void touchMove(MotionEvent event) {
float xDiff = event.getX() - deltaX;
float yDiff = event.getY() - deltaY;
double distance = Math.sqrt(xDiff * xDiff + yDiff * yDiff);
if (mLayout.verticalScrollEnabled()) {
if (yDiff > 0 && viewPortY == 0) {
if (mEdgeEffectsEnabled) {
float str = (float) distance / getHeight();
mTopEdge.onPull(str);
invalidate();
}
return;
}
if (yDiff < 0 && viewPortY == mScrollableHeight) {
if (mEdgeEffectsEnabled) {
float str = (float) distance / getHeight();
mBottomEdge.onPull(str);
invalidate();
}
return;
}
}
if (mLayout.horizontalScrollEnabled()) {
if (xDiff > 0 && viewPortX == 0) {
if (mEdgeEffectsEnabled) {
float str = (float) distance / getWidth();
mLeftEdge.onPull(str);
invalidate();
}
return;
}
if (xDiff < 0 && viewPortY == mScrollableWidth) {
if (mEdgeEffectsEnabled) {
float str = (float) distance / getWidth();
mRightEdge.onPull(str);
invalidate();
}
return;
}
}
if ((mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_REST)
&& distance > touchSlop) {
mTouchMode = TOUCH_MODE_SCROLL;
if (mOnTouchModeChangedListener != null) {
mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
}
if (mPendingCheckForTap != null) {
removeCallbacks(mPendingCheckForTap);
mPendingCheckForTap = null;
}
}
if (mTouchMode == TOUCH_MODE_SCROLL) {
moveViewportBy(event.getX() - deltaX, event.getY() - deltaY, false);
invokeOnItemScrollListeners();
deltaX = event.getX();
deltaY = event.getY();
}
}
protected void touchCancel(MotionEvent event) {
mTouchMode = TOUCH_MODE_REST;
if (mOnTouchModeChangedListener != null) {
mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
// requestLayout();
}
protected void touchUp(MotionEvent event) {
if ((mTouchMode == TOUCH_MODE_SCROLL || mTouchMode == TOUCH_MODE_OVERFLING)
&& mVelocityTracker != null) {
mVelocityTracker.computeCurrentVelocity(1000, maxFlingVelocity);
if (Math.abs(mVelocityTracker.getXVelocity()) > minFlingVelocity
|| Math.abs(mVelocityTracker.getYVelocity()) > minFlingVelocity) {
int maxX = mLayout.getContentWidth() - getWidth();
int maxY = mLayout.getContentHeight() - getHeight();
int allowedScrollOffset;
if (mTouchMode == TOUCH_MODE_SCROLL) {
allowedScrollOffset = 0;
} else {
allowedScrollOffset = overflingDistance;
}
scroller.fling(viewPortX, viewPortY,
-(int) mVelocityTracker.getXVelocity(),
-(int) mVelocityTracker.getYVelocity(), 0, maxX, 0,
maxY, allowedScrollOffset, allowedScrollOffset);
mTouchMode = TOUCH_MODE_FLING;
if (mOnTouchModeChangedListener != null) {
mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
}
post(flingRunnable);
} else {
mTouchMode = TOUCH_MODE_REST;
if (mOnTouchModeChangedListener != null) {
mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
}
}
} else if (mTouchMode == TOUCH_MODE_DOWN
|| mTouchMode == TOUCH_MODE_DONE_WAITING) {
if (mTouchModeReset != null) {
removeCallbacks(mTouchModeReset);
}
FreeFlowItem endTouchAt = ViewUtils.getItemAt(frames,
(int) (viewPortX + event.getX()),
(int) (viewPortY + event.getY()));
if (beginTouchAt != null && beginTouchAt.view != null
&& beginTouchAt == endTouchAt) {
beginTouchAt.view.setPressed(true);
mTouchModeReset = new Runnable() {
@Override
public void run() {
mTouchModeReset = null;
mTouchMode = TOUCH_MODE_REST;
if (mOnTouchModeChangedListener != null) {
mOnTouchModeChangedListener
.onTouchModeChanged(mTouchMode);
}
if (beginTouchAt != null && beginTouchAt.view != null) {
beginTouchAt.view.setPressed(false);
}
if (mChoiceActionMode == null
&& mOnItemSelectedListener != null) {
mOnItemSelectedListener.onItemSelected(
FreeFlowContainer.this,
selectedFreeFlowItem);
}
}
};
selectedFreeFlowItem = beginTouchAt;
postDelayed(mTouchModeReset,
ViewConfiguration.getPressedStateDuration());
mTouchMode = TOUCH_MODE_TAP;
mPerformClick = new PerformClick();
mPerformClick.run();
if (mOnTouchModeChangedListener != null) {
mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
}
} else {
mTouchMode = TOUCH_MODE_REST;
if (mOnTouchModeChangedListener != null) {
mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
}
}
}
}
public FreeFlowItem getSelectedFreeFlowItem() {
return selectedFreeFlowItem;
}
private Runnable flingRunnable = new Runnable() {
@Override
public void run() {
if (scroller.isFinished()) {
mTouchMode = TOUCH_MODE_REST;
if (mOnTouchModeChangedListener != null) {
mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
}
invokeOnItemScrollListeners();
return;
}
boolean more = scroller.computeScrollOffset();
if (mEdgeEffectsEnabled) {
checkEdgeEffectDuringScroll();
}
if (mLayout.horizontalScrollEnabled()) {
viewPortX = scroller.getCurrX();
}
if (mLayout.verticalScrollEnabled()) {
viewPortY = scroller.getCurrY();
}
moveViewport(true);
if (more) {
post(flingRunnable);
}
}
};
protected void checkEdgeEffectDuringScroll() {
if (mLeftEdge.isFinished() && viewPortX < 0
&& mLayout.horizontalScrollEnabled()) {
mLeftEdge.onAbsorb((int) scroller.getCurrVelocity());
}
if (mRightEdge.isFinished()
&& viewPortX > mLayout.getContentWidth() - getMeasuredWidth()
&& mLayout.horizontalScrollEnabled()) {
mRightEdge.onAbsorb((int) scroller.getCurrVelocity());
}
if (mTopEdge.isFinished() && viewPortY < 0
&& mLayout.verticalScrollEnabled()) {
mTopEdge.onAbsorb((int) scroller.getCurrVelocity());
}
if (mBottomEdge.isFinished()
&& viewPortY > mLayout.getContentHeight() - getMeasuredHeight()
&& mLayout.verticalScrollEnabled()) {
mBottomEdge.onAbsorb((int) scroller.getCurrVelocity());
}
}
protected void moveViewportBy(float movementX, float movementY,
boolean fling) {
if (mLayout.horizontalScrollEnabled()) {
viewPortX = (int) (viewPortX - movementX);
}
if (mLayout.verticalScrollEnabled()) {
viewPortY = (int) (viewPortY - movementY);
}
moveViewport(fling);
}
protected void moveViewPort(int left, int top, boolean isInFlingMode) {
viewPortX = left;
viewPortY = top;
moveViewport(isInFlingMode);
}
/**
* Will move viewport to viewPortX and viewPortY values
*
* @param isInFlingMode
* Setting this
*/
protected void moveViewport(boolean isInFlingMode) {
mScrollableWidth = mLayout.getContentWidth() - getWidth();
if (mScrollableWidth < 0) {
mScrollableWidth = 0;
}
mScrollableHeight = mLayout.getContentHeight() - getHeight();
if (mScrollableHeight < 0) {
mScrollableHeight = 0;
}
if (isInFlingMode) {
if (viewPortX < 0 || viewPortX > mScrollableWidth || viewPortY < 0
|| viewPortY > mScrollableHeight) {
mTouchMode = TOUCH_MODE_OVERFLING;
}
} else {
if (viewPortX < -overflingDistance) {
viewPortX = -overflingDistance;
} else if (viewPortX > mScrollableWidth + overflingDistance) {
viewPortX = (mScrollableWidth + overflingDistance);
}
if (viewPortY < (int) (-overflingDistance)) {
viewPortY = (int) -overflingDistance;
} else if (viewPortY > mScrollableHeight + overflingDistance) {
viewPortY = (int) (mScrollableHeight + overflingDistance);
}
if (mEdgeEffectsEnabled) {
if (viewPortX <= 0) {
mLeftEdge.onPull(viewPortX / (-overflingDistance));
} else if (viewPortX >= mScrollableWidth) {
mRightEdge.onPull((viewPortX - mScrollableWidth)
/ (-overflingDistance));
}
if (viewPortY <= 0) {
mTopEdge.onPull(viewPortY / (-overflingDistance));
} else if (viewPortY >= mScrollableHeight) {
mBottomEdge.onPull((viewPortY - mScrollableHeight)
/ (-overflingDistance));
}
}
}
LinkedHashMap<Object, FreeFlowItem> oldFrames = new LinkedHashMap<Object, FreeFlowItem>();
copyFrames(frames, oldFrames);
frames = new LinkedHashMap<Object, FreeFlowItem>();
copyFrames(mLayout.getItemProxies(viewPortX, viewPortY), frames);
LayoutChangeset changeSet = getViewChanges(oldFrames, frames, true);
for (FreeFlowItem freeflowItem : changeSet.added) {
addAndMeasureViewIfNeeded(freeflowItem);
doLayout(freeflowItem);
}
for (Pair<FreeFlowItem, Rect> freeflowItemPair : changeSet.moved) {
doLayout(freeflowItemPair.first);
}
for (FreeFlowItem freeflowItem : changeSet.removed) {
removeViewInLayout(freeflowItem.view);
returnItemToPoolIfNeeded(freeflowItem);
}
invalidate();
}
protected boolean mEdgeEffectsEnabled = true;
/**
* Controls whether the edge glows are enabled or not
*/
public void setEdgeEffectsEnabled(boolean val) {
mEdgeEffectsEnabled = val;
if (val) {
Context context = getContext();
setWillNotDraw(false);
mLeftEdge = new EdgeEffect(context);
mRightEdge = new EdgeEffect(context);
mTopEdge = new EdgeEffect(context);
mBottomEdge = new EdgeEffect(context);
} else {
setWillNotDraw(true);
mLeftEdge = mRightEdge = mTopEdge = mBottomEdge = null;
}
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
boolean needsInvalidate = false;
final int height = getMeasuredHeight() - getPaddingTop()
- getPaddingBottom();
final int width = getMeasuredWidth();
if (!mLeftEdge.isFinished()) {
final int restoreCount = canvas.save();
canvas.rotate(270);
canvas.translate(-height + getPaddingTop(), 0);// width);
mLeftEdge.setSize(height, width);
needsInvalidate = mLeftEdge.draw(canvas);
canvas.restoreToCount(restoreCount);
}
if (!mTopEdge.isFinished()) {
final int restoreCount = canvas.save();
mTopEdge.setSize(width, height);
needsInvalidate = mTopEdge.draw(canvas);
canvas.restoreToCount(restoreCount);
}
if (!mRightEdge.isFinished()) {
final int restoreCount = canvas.save();
canvas.rotate(90);
canvas.translate(0, -width);// width);
mRightEdge.setSize(height, width);
needsInvalidate = mRightEdge.draw(canvas);
canvas.restoreToCount(restoreCount);
}
if (!mBottomEdge.isFinished()) {
final int restoreCount = canvas.save();
canvas.rotate(180);
canvas.translate(-width + getPaddingTop(), -height);
mBottomEdge.setSize(width, height);
needsInvalidate = mBottomEdge.draw(canvas);
canvas.restoreToCount(restoreCount);
}
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
protected void returnItemToPoolIfNeeded(FreeFlowItem freeflowItem) {
View v = freeflowItem.view;
v.setTranslationX(0);
v.setTranslationY(0);
v.setRotation(0);
v.setScaleX(1f);
v.setScaleY(1f);
v.setAlpha(1);
viewpool.returnViewToPool(v);
}
public SectionedAdapter getAdapter() {
return mAdapter;
}
public void setLayoutAnimator(FreeFlowLayoutAnimator anim) {
layoutAnimator = anim;
}
public FreeFlowLayoutAnimator getLayoutAnimator() {
return layoutAnimator;
}
public Map<Object, FreeFlowItem> getFrames() {
return frames;
}
public void clearFrames() {
removeAllViews();
frames = null;
}
@Override
public boolean shouldDelayChildPressedState() {
return true;
}
public int getCheckedItemCount() {
return mCheckStates.size();
}
public ArrayList<IndexPath> getCheckedItemPositions() {
ArrayList<IndexPath> checked = new ArrayList<IndexPath>();
for (int i = 0; i < mCheckStates.size(); i++) {
checked.add(mCheckStates.keyAt(i));
}
return checked;
}
public void clearChoices() {
mCheckStates.clear();
}
/**
* Defines the choice behavior for the Container allowing multi-select etc.
*
* @see <a href=
* "http://developer.android.com/reference/android/widget/AbsListView.html#attr_android:choiceMode"
* >List View's Choice Mode</a>
*/
public void setChoiceMode(int choiceMode) {
mChoiceMode = choiceMode;
if (mChoiceActionMode != null) {
mChoiceActionMode.finish();
mChoiceActionMode = null;
}
if (mChoiceMode != CHOICE_MODE_NONE) {
if (mCheckStates == null) {
mCheckStates = new SimpleArrayMap<IndexPath, Boolean>();
}
if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
clearChoices();
setLongClickable(true);
}
}
}
boolean isLongClickable = false;
@Override
public void setLongClickable(boolean b) {
isLongClickable = b;
}
@Override
public boolean isLongClickable() {
return isLongClickable;
}
public void setMultiChoiceModeListener(MultiChoiceModeListener listener) {
if (mMultiChoiceModeCallback == null) {
mMultiChoiceModeCallback = new MultiChoiceModeWrapper();
}
mMultiChoiceModeCallback.setWrapped(listener);
}
final class CheckForTap implements Runnable {
@Override
public void run() {
if (mTouchMode == TOUCH_MODE_DOWN) {
mTouchMode = TOUCH_MODE_TAP;
if (mOnTouchModeChangedListener != null) {
mOnTouchModeChangedListener.onTouchModeChanged(mTouchMode);
}
if (beginTouchAt != null && beginTouchAt.view != null) {
beginTouchAt.view.setPressed(true);
// setPressed(true);
}
refreshDrawableState();
final int longPressTimeout = ViewConfiguration
.getLongPressTimeout();
final boolean longClickable = isLongClickable();
if (longClickable) {
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
postDelayed(mPendingCheckForLongPress, longPressTimeout);
} else {
mTouchMode = TOUCH_MODE_DONE_WAITING;
if (mOnTouchModeChangedListener != null) {
mOnTouchModeChangedListener
.onTouchModeChanged(mTouchMode);
}
}
}
}
}
private class CheckForLongPress implements Runnable {
@Override
public void run() {
if (beginTouchAt == null) {
// Assuming child that was being long pressed
// is no longer valid
return;
}
mCheckStates.clear();
final View child = beginTouchAt.view;
if (child != null) {
boolean handled = false;
// if (!mDataChanged) {
handled = performLongPress();
// }
if (handled) {
mTouchMode = TOUCH_MODE_REST;
if (mOnTouchModeChangedListener != null) {
mOnTouchModeChangedListener
.onTouchModeChanged(mTouchMode);
}
// setPressed(false);
child.setPressed(false);
} else {
mTouchMode = TOUCH_MODE_DONE_WAITING;
if (mOnTouchModeChangedListener != null) {
mOnTouchModeChangedListener
.onTouchModeChanged(mTouchMode);
}
}
}
}
}
boolean performLongPress() {
// CHOICE_MODE_MULTIPLE_MODAL takes over long press.
if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
if (mChoiceActionMode == null
&& (mChoiceActionMode = startActionMode(mMultiChoiceModeCallback)) != null) {
setItemChecked(beginTouchAt.itemSection,
beginTouchAt.itemIndex, true);
updateOnScreenCheckedViews();
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return true;
}
boolean handled = false;
final long longPressId = mAdapter.getItemId(beginTouchAt.itemSection,
beginTouchAt.itemSection);
if (mOnItemLongClickListener != null) {
handled = mOnItemLongClickListener.onItemLongClick(this,
beginTouchAt.view, beginTouchAt.itemSection,
beginTouchAt.itemIndex, longPressId);
}
if (!handled) {
mContextMenuInfo = createContextMenuInfo(beginTouchAt.view,
beginTouchAt.itemSection, beginTouchAt.itemIndex,
longPressId);
handled = super.showContextMenuForChild(this);
}
if (handled) {
updateOnScreenCheckedViews();
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}
ContextMenuInfo createContextMenuInfo(View view, int sectionIndex,
int positionInSection, long id) {
return new AbsLayoutContainerContextMenuInfo(view, sectionIndex,
positionInSection, id);
}
class MultiChoiceModeWrapper implements MultiChoiceModeListener {
private MultiChoiceModeListener mWrapped;
public void setWrapped(MultiChoiceModeListener wrapped) {
mWrapped = wrapped;
}
public boolean hasWrappedCallback() {
return mWrapped != null;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
if (mWrapped.onCreateActionMode(mode, menu)) {
// Initialize checked graphic state?
setLongClickable(false);
return true;
}
return false;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return mWrapped.onPrepareActionMode(mode, menu);
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return mWrapped.onActionItemClicked(mode, item);
}
@Override
public void onDestroyActionMode(ActionMode mode) {
mWrapped.onDestroyActionMode(mode);
mChoiceActionMode = null;
// Ending selection mode means deselecting everything.
clearChoices();
updateOnScreenCheckedViews();
// rememberSyncState();
requestLayout();
setLongClickable(true);
}
@Override
public void onItemCheckedStateChanged(ActionMode mode, int section,
int position, long id, boolean checked) {
mWrapped.onItemCheckedStateChanged(mode, section, position, id,
checked);
// If there are no items selected we no longer need the selection
// mode.
if (getCheckedItemCount() == 0) {
mode.finish();
}
}
}
public interface OnTouchModeChangedListener {
void onTouchModeChanged(int touchMode);
}
public interface MultiChoiceModeListener extends ActionMode.Callback {
/**
* Called when an item is checked or unchecked during selection mode.
*
* @param mode
* The {@link android.view.ActionMode} providing the selection mode
* @param section
* The Section of the item that was checked
* @param position
* Adapter position of the item in the section that was
* checked or unchecked
* @param id
* Adapter ID of the item that was checked or unchecked
* @param checked
* <code>true</code> if the item is now checked,
* <code>false</code> if the item is now unchecked.
*/
public void onItemCheckedStateChanged(ActionMode mode, int section,
int position, long id, boolean checked);
}
public void setItemChecked(int sectionIndex, int positionInSection,
boolean value) {
if (mChoiceMode == CHOICE_MODE_NONE) {
return;
}
// Start selection mode if needed. We don't need to if we're unchecking
// something.
if (value && mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL
&& mChoiceActionMode == null) {
if (mMultiChoiceModeCallback == null
|| !mMultiChoiceModeCallback.hasWrappedCallback()) {
throw new IllegalStateException(
"Container: attempted to start selection mode "
+ "for CHOICE_MODE_MULTIPLE_MODAL but no choice mode callback was "
+ "supplied. Call setMultiChoiceModeListener to set a callback.");
}
mChoiceActionMode = startActionMode(mMultiChoiceModeCallback);
}
if (mChoiceMode == CHOICE_MODE_MULTIPLE
|| mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
setCheckedValue(sectionIndex, positionInSection, value);
if (mChoiceActionMode != null) {
final long id = mAdapter.getItemId(sectionIndex,
positionInSection);
mMultiChoiceModeCallback.onItemCheckedStateChanged(
mChoiceActionMode, sectionIndex, positionInSection, id,
value);
}
} else {
setCheckedValue(sectionIndex, positionInSection, value);
}
// if (!mInLayout && !mBlockLayoutRequests) {
// mDataChanged = true;
// rememberSyncState();
requestLayout();
// }
}
@Override
public boolean performItemClick(View view, int section, int position,
long id) {
boolean handled = false;
boolean dispatchItemClick = true;
if (mChoiceMode != CHOICE_MODE_NONE) {
handled = true;
boolean checkedStateChanged = false;
if (mChoiceMode == CHOICE_MODE_MULTIPLE
|| (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode != null)) {
boolean checked = isChecked(section, position);
checked = !checked;
setCheckedValue(section, position, checked);
if (mChoiceActionMode != null) {
mMultiChoiceModeCallback.onItemCheckedStateChanged(
mChoiceActionMode, section, position, id, checked);
dispatchItemClick = false;
}
checkedStateChanged = true;
} else if (mChoiceMode == CHOICE_MODE_SINGLE) {
boolean checked = !isChecked(section, position);
if (checked) {
setCheckedValue(section, position, checked);
}
checkedStateChanged = true;
}
if (checkedStateChanged) {
updateOnScreenCheckedViews();
}
}
if (dispatchItemClick) {
handled |= super.performItemClick(view, section, position, id);
}
return handled;
}
private class PerformClick implements Runnable {
@Override
public void run() {
// if (mDataChanged) return;
View view = beginTouchAt.view;
if (view != null) {
performItemClick(view, beginTouchAt.itemSection,
beginTouchAt.itemIndex, mAdapter.getItemId(
beginTouchAt.itemSection,
beginTouchAt.itemIndex));
}
// }
}
}
/**
* Perform a quick, in-place update of the checked or activated state on all
* visible item views. This should only be called when a valid choice mode
* is active.
*/
private void updateOnScreenCheckedViews() {
Iterator<?> it = frames.entrySet().iterator();
View child = null;
while (it.hasNext()) {
Map.Entry<?, FreeFlowItem> pairs = (Map.Entry<?, FreeFlowItem>) it
.next();
child = pairs.getValue().view;
boolean isChecked = isChecked(pairs.getValue().itemSection,
pairs.getValue().itemIndex);
if (child instanceof Checkable) {
((Checkable) child).setChecked(isChecked);
} else {
child.setActivated(isChecked);
}
}
}
public boolean isChecked(int sectionIndex, int positionInSection) {
for (int i = 0; i < mCheckStates.size(); i++) {
IndexPath p = mCheckStates.keyAt(i);
if (p.section == sectionIndex
&& p.positionInSection == positionInSection) {
return true;
}
}
return false;
}
/**
* Updates the internal ArrayMap keeping track of checked states. Will not
* update the check UI.
*/
protected void setCheckedValue(int sectionIndex, int positionInSection,
boolean val) {
int foundAtIndex = -1;
for (int i = 0; i < mCheckStates.size(); i++) {
IndexPath p = mCheckStates.keyAt(i);
if (p.section == sectionIndex
&& p.positionInSection == positionInSection) {
foundAtIndex = i;
break;
}
}
if (foundAtIndex > -1 && val == false) {
mCheckStates.removeAt(foundAtIndex);
} else if (foundAtIndex == -1 && val == true) {
IndexPath pos = new IndexPath(sectionIndex, positionInSection);
mCheckStates.put(pos, true);
}
}
public void addScrollListener(OnScrollListener listener) {
if (!scrollListeners.contains(listener))
scrollListeners.add(listener);
}
public void removeScrollListener(OnScrollListener listener) {
scrollListeners.remove(listener);
}
public void scrollToItem(int sectionIndex, int itemIndex, boolean animate) {
Section section;
if (sectionIndex > mAdapter.getNumberOfSections() || sectionIndex < 0
|| (section = mAdapter.getSection(sectionIndex)) == null) {
return;
}
if (itemIndex < 0 || itemIndex > section.getDataCount()) {
return;
}
FreeFlowItem freeflowItem = mLayout.getFreeFlowItemForItem(section
.getDataAtIndex(itemIndex));
freeflowItem = FreeFlowItem.clone(freeflowItem);
int newVPX = freeflowItem.frame.left;
int newVPY = freeflowItem.frame.top;
if (newVPX > mLayout.getContentWidth() - getMeasuredWidth())
newVPX = mLayout.getContentWidth() - getMeasuredWidth();
if (newVPY > mLayout.getContentHeight() - getMeasuredHeight())
newVPY = mLayout.getContentHeight() - getMeasuredHeight();
if (animate) {
scroller.startScroll(viewPortX, viewPortY, (newVPX - viewPortX),
(newVPY - viewPortY), 1500);
post(flingRunnable);
} else {
moveViewportBy((viewPortX - newVPX), (viewPortY - newVPY), false);
invokeOnItemScrollListeners();
}
}
/**
* Returns the percentage of width scrolled. The values range from 0 to 1
*
* @return
*/
public float getScrollPercentX() {
if (mLayout == null || mAdapter == null)
return 0;
float w = mLayout.getContentWidth();
float scrollableWidth = w - getWidth();
if (scrollableWidth == 0)
return 0;
return viewPortX / scrollableWidth;
}
/**
* Returns the percentage of height scrolled. The values range from 0 to 1
*
* @return
*/
public float getScrollPercentY() {
if (mLayout == null || mAdapter == null)
return 0;
float ht = mLayout.getContentHeight();
float scrollableHeight = ht - getHeight();
if (scrollableHeight == 0)
return 0;
return viewPortY / scrollableHeight;
}
protected void invokeOnItemScrollListeners() {
for (OnScrollListener l : scrollListeners) {
l.onScroll(this);
}
}
protected void reportScrollStateChange(int state) {
// TODO:
}
public interface OnScrollListener {
public int SCROLL_STATE_IDLE = 0;
public int SCROLL_STATE_TOUCH_SCROLL = 1;
public int SCROLL_STATE_FLING = 2;
public void onScroll(FreeFlowContainer container);
}
/******** DEBUGGING HELPERS *******/
/**
* A flag for conditionally printing Container lifecycle events to LogCat
* for debugging
*/
public boolean logDebugEvents = false;
/**
* A utility method for debugging lifecycle events and putting them in the
* log messages
*
* @param msg
*/
private void logLifecycleEvent(String msg) {
Logs.d("ContainerLifecycleEvent", msg);
}
}