/******************************************************************************* * Copyright 2012 Steven Rudenko * * 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 shared.ui.actionscontentview; import com.actionbarsherlock.R; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Rect; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.LinearLayout; import android.widget.Scroller; @SuppressLint("DrawAllocation") public class ActionsContentView extends ViewGroup { private static final String TAG = ActionsContentView.class.getSimpleName(); private static final boolean DEBUG = false; /** * Spacing will be calculated as offset from right bound of view. */ public static final int SPACING_RIGHT_OFFSET = 0; /** * Spacing will be calculated as right bound of actions bar container. */ public static final int SPACING_ACTIONS_WIDTH = 1; /** * Fade is disabled. */ public static final int FADE_NONE = 0; /** * Fade applies to actions container. */ public static final int FADE_ACTIONS = 1; /** * Fade applies to content container. */ public static final int FADE_CONTENT = 2; /** * Fade applies to every container. */ public static final int FADE_BOTH = 3; /** * Swiping will be handled at any point of screen. */ public static final int SWIPING_ALL = 0; /** * Swiping will be handled starting from screen edge only. */ public static final int SWIPING_EDGE = 1; private final ContentScrollController mContentScrollController; private final GestureDetector mGestureDetector; private final View viewShadow; private final ActionsLayout viewActionsContainer; private final ContentLayout viewContentContainer; /** * Spacing type. */ private int mSpacingType = SPACING_RIGHT_OFFSET; /** * Value of spacing to use. */ private int mSpacing; /** * Value of actions container spacing to use. */ private int mActionsSpacing; /** * Value of shadow width. */ private int mShadowWidth = 0; /** * Indicates how long flinging will take time in milliseconds. */ private int mFlingDuration = 250; /** * Fade type. */ private int mFadeType = FADE_NONE; /** * Max fade value. */ private int mFadeValue; /** * Indicates whether swiping is enabled or not. */ private boolean isSwipingEnabled = true; /** * Swiping type. */ private int mSwipeType = FADE_NONE; /** * Swiping edge width. */ private int mSwipeEdgeWidth; /** * Indicates whether refresh of content position should be done on next layout calculation. */ private boolean mForceRefresh = false; public ActionsContentView(Context context) { this(context, null); } public ActionsContentView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ActionsContentView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setClipChildren(false); setClipToPadding(false); // reading attributes final TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ActionsContentView); mSpacingType = a.getInteger(R.styleable.ActionsContentView_spacing_type, SPACING_RIGHT_OFFSET); final int spacingDefault = context.getResources().getDimensionPixelSize(R.dimen.default_actionscontentview_spacing); mSpacing = a.getDimensionPixelSize(R.styleable.ActionsContentView_spacing, spacingDefault); final int actionsSpacingDefault = context.getResources().getDimensionPixelSize(R.dimen.default_actionscontentview_actions_spacing); mActionsSpacing = a.getDimensionPixelSize(R.styleable.ActionsContentView_actions_spacing, actionsSpacingDefault); final int actionsLayout = a.getResourceId(R.styleable.ActionsContentView_actions_layout, 0); final int contentLayout = a.getResourceId(R.styleable.ActionsContentView_content_layout, 0); mShadowWidth = a.getDimensionPixelSize(R.styleable.ActionsContentView_shadow_width, 0); final int shadowDrawableRes = a.getResourceId(R.styleable.ActionsContentView_shadow_drawable, 0); mFadeType = a.getInteger(R.styleable.ActionsContentView_fade_type, FADE_NONE); final int fadeValueDefault = context.getResources().getInteger(R.integer.default_actionscontentview_fade_max_value); mFadeValue = (int) a.getInt(R.styleable.ActionsContentView_fade_max_value, fadeValueDefault); setFadeValue(mFadeValue); final int flingDurationDefault = context.getResources().getInteger(R.integer.default_actionscontentview_fling_duration); mFlingDuration = a.getInteger(R.styleable.ActionsContentView_fling_duration, flingDurationDefault); mSwipeType = a.getInteger(R.styleable.ActionsContentView_swiping_type, SWIPING_EDGE); final int swipingEdgeWidthDefault = context.getResources().getDimensionPixelSize(R.dimen.default_actionscontentview_swiping_edge_width); mSwipeEdgeWidth = a.getDimensionPixelSize(R.styleable.ActionsContentView_swiping_edge_width, swipingEdgeWidthDefault); final int effectActionsRes = a.getResourceId(R.styleable.ActionsContentView_effect_actions, 0); final int effectContentRes = a.getResourceId(R.styleable.ActionsContentView_effect_content, 0); a.recycle(); if (DEBUG) { Log.d(TAG, "Values from layout"); Log.d(TAG, " spacing type: " + mSpacingType); Log.d(TAG, " spacing value: " + mSpacing); Log.d(TAG, " actions spacing value: " + mActionsSpacing); Log.d(TAG, " actions layout id: " + actionsLayout); Log.d(TAG, " content layout id: " + contentLayout); Log.d(TAG, " shadow drawable: " + shadowDrawableRes); Log.d(TAG, " shadow width: " + mShadowWidth); Log.d(TAG, " fade type: " + mFadeType); Log.d(TAG, " fade max value: " + mFadeValue); Log.d(TAG, " fling duration: " + mFlingDuration); Log.d(TAG, " swiping type: " + mSwipeType); Log.d(TAG, " swiping edge width: " + mSwipeEdgeWidth); Log.d(TAG, " effect actions: " + effectActionsRes); Log.d(TAG, " effect content: " + effectContentRes); } mContentScrollController = new ContentScrollController(new Scroller(context)); mGestureDetector = new GestureDetector(context, mContentScrollController); mGestureDetector.setIsLongpressEnabled(true); final LayoutInflater inflater = LayoutInflater.from(context); viewActionsContainer = new ActionsLayout(context); if (actionsLayout != 0) inflater.inflate(actionsLayout, viewActionsContainer, true); super.addView(viewActionsContainer, 0, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); viewContentContainer = new ContentLayout(context); viewContentContainer.setOnSwipeListener(new ContentLayout.OnSwipeListener() { @Override public void onSwipe(int scrollPosition) { updateScrollFactor(); } }); viewShadow = new View(context); viewShadow.setBackgroundResource(shadowDrawableRes); final LinearLayout.LayoutParams shadowParams = new LinearLayout.LayoutParams(mShadowWidth, LinearLayout.LayoutParams.MATCH_PARENT); viewShadow.setLayoutParams(shadowParams); viewContentContainer.addView(viewShadow); if (mShadowWidth <= 0 || shadowDrawableRes == 0) { viewShadow.setVisibility(GONE); } if (contentLayout != 0) inflater.inflate(contentLayout, viewContentContainer, true); super.addView(viewContentContainer, 1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); if ( effectActionsRes > 0 ) { setActionEffects(effectActionsRes); } if ( effectContentRes > 0 ) { setContentEffects(effectContentRes); } } /** * This method is not supported and throws an UnsupportedOperationException when called. * * @param child Ignored. * * @throws UnsupportedOperationException Every time this method is invoked. */ @Override public void addView(View child) { throw new UnsupportedOperationException("addView(View) is not supported in " + TAG); } /** * This method is not supported and throws an UnsupportedOperationException when called. * * @param child Ignored. * @param index Ignored. * * @throws UnsupportedOperationException Every time this method is invoked. */ @Override public void addView(View child, int index) { throw new UnsupportedOperationException("addView(View, int) is not supported in " + TAG); } /** * This method is not supported and throws an UnsupportedOperationException when called. * * @param child Ignored. * @param params Ignored. * * @throws UnsupportedOperationException Every time this method is invoked. */ @Override public void addView(View child, LayoutParams params) { throw new UnsupportedOperationException("addView(View, LayoutParams) is not supported in " + TAG); } /** * This method is not supported and throws an UnsupportedOperationException when called. * * @param child Ignored. * @param index Ignored. * @param params Ignored. * * @throws UnsupportedOperationException Every time this method is invoked. */ @Override public void addView(View child, int index, LayoutParams params) { throw new UnsupportedOperationException("addView(View, int, LayoutParams) is not supported in " + TAG); } /** * This method is not supported and throws an UnsupportedOperationException when called. * * @param child Ignored. * * @throws UnsupportedOperationException Every time this method is invoked. */ @Override public void removeView(View child) { throw new UnsupportedOperationException("removeView(View) is not supported in " + TAG); } /** * This method is not supported and throws an UnsupportedOperationException when called. * * @param index Ignored. * * @throws UnsupportedOperationException Every time this method is invoked. */ @Override public void removeViewAt(int index) { throw new UnsupportedOperationException("removeViewAt(int) is not supported in " + TAG); } /** * This method is not supported and throws an UnsupportedOperationException when called. * * @throws UnsupportedOperationException Every time this method is invoked. */ @Override public void removeAllViews() { throw new UnsupportedOperationException("removeAllViews() is not supported in " + TAG); } public Parcelable onSaveInstanceState() { final Parcelable superState = super.onSaveInstanceState(); final SavedState ss = new SavedState(superState); ss.isContentShown = isContentShown(); ss.mSpacingType = mSpacingType; ss.mSpacing = mSpacing; ss.mActionsSpacing = mActionsSpacing; ss.isShadowVisible = isShadowVisible(); ss.mShadowWidth = mShadowWidth; //isSwipingEnabled() ss.isSwipingEnabled = isSwipingEnabled; ss.mFlingDuration = mFlingDuration; ss.mFadeType = mFadeType; ss.mFadeValue = mFadeValue; ss.mSwipeType = mSwipeType; ss.mSwipeEdgeWidth = mSwipeEdgeWidth; return ss; } public void onRestoreInstanceState(Parcelable state) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } final SavedState ss = (SavedState)state; super.onRestoreInstanceState(ss.getSuperState()); mContentScrollController.isContentShown = ss.isContentShown; mSpacingType = ss.mSpacingType; mSpacing = ss.mSpacing; mActionsSpacing = ss.mActionsSpacing; isSwipingEnabled = ss.isSwipingEnabled; mSwipeType = ss.mSwipeType; mSwipeEdgeWidth = ss.mSwipeEdgeWidth; mFlingDuration = ss.mFlingDuration; mFadeType = ss.mFadeType; mFadeValue = ss.mFadeValue; viewShadow.setVisibility(ss.isShadowVisible ? VISIBLE : GONE); // this will call requestLayout() to calculate layout according to values setShadowWidth(ss.mShadowWidth); } public ViewGroup getActionsContainer() { return viewActionsContainer; } public ViewGroup getContentContainer() { return viewContentContainer; } public boolean isActionsShown() { return !mContentScrollController.isContentShown(); } public void showActions() { mContentScrollController.hideContent(mFlingDuration); } public boolean isContentShown() { return mContentScrollController.isContentShown(); } public void showContent() { mContentScrollController.showContent(mFlingDuration); } public void toggleActions() { if (isActionsShown()) showContent(); else showActions(); } public void setSpacingType(int type) { if (mSpacingType == type) return; if (type != SPACING_RIGHT_OFFSET && type != SPACING_ACTIONS_WIDTH) return; if (DEBUG) Log.d(TAG, "- spacing type: " + type); mSpacingType = type; mForceRefresh = true; requestLayout(); } public int getSpacingType() { return mSpacingType; } public void setSpacingWidth(int width) { if (mSpacing == width) return; if (DEBUG) Log.d(TAG, "- spacing width: " + width); mSpacing = width; mForceRefresh = true; requestLayout(); } public int getSpacingWidth() { return mSpacing; } public void setActionsSpacingWidth(int width) { if (mActionsSpacing == width) return; mActionsSpacing = width; mForceRefresh = true; requestLayout(); } public int getActionsSpacingWidth() { return mActionsSpacing; } public void setShadowVisible(boolean visible) { viewShadow.setVisibility(visible ? VISIBLE : GONE); mForceRefresh = true; requestLayout(); } public boolean isShadowVisible() { return viewShadow.getVisibility() == VISIBLE; } public void setShadowWidth(int width) { if (mShadowWidth == width) return; if (DEBUG) Log.d(TAG, "- shadow width: " + width); mShadowWidth = width; viewShadow.getLayoutParams().width = mShadowWidth; mForceRefresh = true; requestLayout(); } public int getShadowWidth() { return mShadowWidth; } public void setFlingDuration(int duration) { mFlingDuration = duration; } public int getFlingDuration() { return mFlingDuration; } public void setFadeType(int type) { if (type != FADE_NONE && type != FADE_ACTIONS && type != FADE_CONTENT && type != FADE_BOTH) return; mFadeType = type; updateScrollFactor(); } public int getFadeType() { return mFadeType; } public void setFadeValue(int value) { if (value < 0) value = 0; else if (value > 255) value = 255; mFadeValue = value; updateScrollFactor(); } public int getFadeValue() { return mFadeValue; } public boolean isSwipingEnabled() { return isSwipingEnabled; } public void setSwipingEnabled(boolean enabled) { isSwipingEnabled = enabled; } public void setSwipingType(int type) { if (type != SWIPING_ALL && type != SWIPING_EDGE) return; mSwipeType = type; } public int getSwipingType() { return mSwipeType; } public void setSwipingEdgeWidth(int width) { mSwipeEdgeWidth = width; } public int getSwipingEdgeWidth() { return mSwipeEdgeWidth; } public void setActionEffects(int effectsId) { final Animation effects = AnimationUtils.loadAnimation(getContext(), effectsId); setActionEffects(effects); } public void setActionEffects(Animation effects) { viewActionsContainer.setEffects(effects); } public Animation getActionEffects() { return viewActionsContainer.getEffects(); } public void setContentEffects(int effectsId) { final Animation effects = AnimationUtils.loadAnimation(getContext(), effectsId); setContentEffects(effects); } public void setContentEffects(Animation effects) { viewContentContainer.setEffects(effects); } public Animation getContentEffects() { return viewContentContainer.getEffects(); } @Override public boolean onTouchEvent(MotionEvent ev) { if (!isSwipingEnabled) return false; mGestureDetector.onTouchEvent(ev); final int action = ev.getAction(); // if current touch event should be handled if (mContentScrollController.isHandled()) { if (action == MotionEvent.ACTION_UP) mContentScrollController.onUp(ev); return true; } return false; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (!isSwipingEnabled) return false; mGestureDetector.onTouchEvent(ev); // whether we should handle all following events by our view // and don't allow children to get them return mContentScrollController.isHandled(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int width = MeasureSpec.getSize(widthMeasureSpec); final int height = MeasureSpec.getSize(heightMeasureSpec); if (DEBUG) Log.d(TAG, "width: " + width + " height: " + height); final int childrenCount = getChildCount(); for (int i=0; i<childrenCount; ++i) { final View v = getChildAt(i); if (v == viewActionsContainer) { // setting size of actions according to spacing parameters if (mSpacingType == SPACING_ACTIONS_WIDTH) viewActionsContainer.measure(MeasureSpec.makeMeasureSpec(mSpacing, MeasureSpec.EXACTLY), heightMeasureSpec); else // all other situations are handled as SPACING_RIGHT_OFFSET viewActionsContainer.measure(MeasureSpec.makeMeasureSpec(width - mSpacing, MeasureSpec.EXACTLY), heightMeasureSpec); } else if (v == viewContentContainer) { final int shadowWidth = isShadowVisible() ? mShadowWidth : 0; final int contentWidth = MeasureSpec.getSize(widthMeasureSpec) - mActionsSpacing + shadowWidth; v.measure(MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY), heightMeasureSpec); } else { v.measure(widthMeasureSpec, heightMeasureSpec); } } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (DEBUG) { final Rect layout = new Rect(l, t, r, b); Log.d(TAG, "layout: " + layout.toShortString()); } // putting every child view to top-left corner final int childrenCount = getChildCount(); for (int i=0; i<childrenCount; ++i) { final View v = getChildAt(i); if (v == viewContentContainer) { final int shadowWidth = isShadowVisible() ? mShadowWidth : 0; v.layout(mActionsSpacing - shadowWidth, 0, mActionsSpacing + v.getMeasuredWidth(), v.getMeasuredHeight()); } else { v.layout(0, 0, v.getMeasuredWidth(), v.getMeasuredHeight()); } } if (mForceRefresh) { mForceRefresh = false; mContentScrollController.init(); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // set correct position of content view after view size was changed if (w != oldw || h != oldh) { mContentScrollController.init(); } } private void updateScrollFactor() { if ( viewActionsContainer == null ) return; final float scrollFactor = mContentScrollController.getScrollFactor(); final int actionsFadeFactor; if ((mFadeType & FADE_ACTIONS) == FADE_ACTIONS) { actionsFadeFactor = (int) (scrollFactor * mFadeValue); } else { actionsFadeFactor = 0; } viewActionsContainer.onScroll(scrollFactor, actionsFadeFactor); final int contentFadeFactor; if ((mFadeType & FADE_CONTENT) == FADE_CONTENT) { contentFadeFactor = (int) ((1f - scrollFactor) * mFadeValue); } else { contentFadeFactor = 0; } viewContentContainer.onScroll(1f - scrollFactor, contentFadeFactor); } public static class SavedState extends BaseSavedState { /** * Indicates whether content was shown while saving state. */ private boolean isContentShown; /** * Spacing type. */ private int mSpacingType = SPACING_RIGHT_OFFSET; /** * Value of spacing to use. */ private int mSpacing; /** * Value of actions container spacing to use. */ private int mActionsSpacing; /** * Indicates whether shadow is visible. */ private boolean isShadowVisible; /** * Value of shadow width. */ private int mShadowWidth = 0; /** * Indicates whether swiping is enabled or not. */ private boolean isSwipingEnabled = true; /** * Indicates how long flinging will take time in milliseconds. */ private int mFlingDuration = 250; /** * Fade type. */ private int mFadeType = FADE_NONE; /** * Max fade value. */ private int mFadeValue; /** * Swiping type. */ private int mSwipeType = FADE_NONE; /** * Swiping edge width. */ private int mSwipeEdgeWidth; public SavedState(Parcelable superState) { super(superState); } public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(isContentShown ? 1 : 0); out.writeInt(mSpacingType); out.writeInt(mSpacing); out.writeInt(mActionsSpacing); out.writeInt(isShadowVisible ? 1 : 0); out.writeInt(mShadowWidth); out.writeInt(isSwipingEnabled ? 1 : 0); out.writeInt(mFlingDuration); out.writeInt(mFadeType); out.writeInt(mFadeValue); out.writeInt(mSwipeType); out.writeInt(mSwipeEdgeWidth); } public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { public SavedState[] newArray(int size) { return new SavedState[size]; } @Override public SavedState createFromParcel(Parcel source) { return new SavedState(source); } }; SavedState(Parcel in) { super(in); isContentShown = in.readInt() == 1; mSpacingType = in.readInt(); mSpacing = in.readInt(); mActionsSpacing = in.readInt(); isShadowVisible = in.readInt() == 1; mShadowWidth = in.readInt(); isSwipingEnabled = in.readInt() == 1; mFlingDuration = in.readInt(); mFadeType = in.readInt(); mFadeValue = in.readInt(); mSwipeType = in.readInt(); mSwipeEdgeWidth = in.readInt(); } } /** * Used to handle scrolling events and scroll content container * on top of actions one. * @author steven * */ private class ContentScrollController implements GestureDetector.OnGestureListener, Runnable { /** * Used to auto-scroll to closest bound on touch up event. */ private final Scroller mScroller; // using Boolean object to initialize while first scroll event private Boolean mHandleEvent = null; private int mLastFlingX = 0; /** * Indicates whether we need initialize position of view after measuring is finished. */ private boolean isContentShown = true; public ContentScrollController(Scroller scroller) { mScroller = scroller; } /** * Initializes visibility of content after views measuring is finished. */ public void init() { if (DEBUG) Log.d(TAG, "Scroller: init"); if (isContentShown) showContent(0); else hideContent(0); updateScrollFactor(); } /** * Returns handling lock value. It indicates whether all events * should be marked as handled. * @return */ public boolean isHandled() { return mHandleEvent != null && mHandleEvent; } @Override public boolean onDown(MotionEvent e) { mHandleEvent = null; reset(); return false; } public boolean onUp(MotionEvent e) { if (!isHandled()) return false; mHandleEvent = null; completeScrolling(); return true; } @Override public boolean onSingleTapUp(MotionEvent e) { return false; } @Override public void onShowPress(MotionEvent e) { // No-op } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // if there is first scroll event after touch down if (mHandleEvent == null) { if (Math.abs(distanceX) < Math.abs(distanceY)) { // if first event is more scroll by Y axis than X one // ignore all events until event up mHandleEvent = Boolean.FALSE; } else { final int contentLeftBound = viewContentContainer.getLeft() - viewContentContainer.getScrollX() + mShadowWidth; final int firstTouchX = (int) e1.getX(); if (DEBUG) { Log.d(TAG, "Scroller: first touch: " + firstTouchX + ", " + e1.getY()); Log.d(TAG, "Content left bound: " + contentLeftBound); } // if content is not shown we handle all horizontal swipes // it content shown and there is edge mode we should check start // swiping area first if (mSwipeType == SWIPING_ALL || (isContentShown && firstTouchX <= mSwipeEdgeWidth || (!isContentShown && firstTouchX >= contentLeftBound))) { // handle all events of scrolling by X axis mHandleEvent = Boolean.TRUE; scrollBy((int) distanceX); } else { mHandleEvent = Boolean.FALSE; } } } else if (mHandleEvent) { // it is not first event we should handle as scrolling by X axis scrollBy((int) distanceX); } return mHandleEvent; } @Override public void onLongPress(MotionEvent e) { // No-op } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // does not work because onDown() method returns false always return false; } public boolean isContentShown() { return isContentShown; } public void hideContent(int duration) { if (DEBUG) Log.d(TAG, "Scroller: hide content by " + duration + "ms"); isContentShown = false; if (viewContentContainer.getMeasuredWidth() == 0 || viewContentContainer.getMeasuredHeight() == 0) { return; } final int startX = viewContentContainer.getScrollX(); final int dx = getRightBound() + startX; fling(startX, dx, duration); } public void showContent(int duration) { if (DEBUG) Log.d(TAG, "Scroller: show content by " + duration + "ms"); isContentShown = true; if (viewContentContainer.getMeasuredWidth() == 0 || viewContentContainer.getMeasuredHeight() == 0) { return; } final int startX = viewContentContainer.getScrollX(); final int dx = startX; fling(startX, dx, duration); } public float getScrollFactor() { return 1f + (float) viewContentContainer.getScrollX() / (float) getRightBound(); } /** * Processes auto-scrolling to bound which is closer to current position. */ @Override public void run() { if (mScroller.isFinished()) { if (DEBUG) Log.d(TAG, "scroller is finished, done with fling"); return; } final boolean more = mScroller.computeScrollOffset(); final int x = mScroller.getCurrX(); final int diff = mLastFlingX - x; if (diff != 0) { viewContentContainer.scrollBy(diff, 0); mLastFlingX = x; } if (more) { viewContentContainer.post(this); } } /** * Resets scroller controller. Stops flinging on current position. */ public void reset() { if (DEBUG) Log.d(TAG, "Scroller: reset"); if (!mScroller.isFinished()) { mScroller.forceFinished(true); } } /** * Starts auto-scrolling to bound which is closer to current position. */ private void completeScrolling() { final int startX = viewContentContainer.getScrollX(); final int rightBound = getRightBound(); final int middle = -rightBound / 2; if (startX > middle) { showContent(mFlingDuration); } else { hideContent(mFlingDuration); } } private void fling(int startX, int dx, int duration) { reset(); if (dx == 0) return; if (duration <= 0) { viewContentContainer.scrollBy(-dx, 0); return; } mScroller.startScroll(startX, 0, dx, 0, duration); if (DEBUG) Log.d(TAG, "starting fling at " + startX + " for " + dx + " by " + duration); mLastFlingX = startX; viewContentContainer.post(this); } /** * Scrolling content view according by given value. * @param dx */ private void scrollBy(int dx) { final int x = viewContentContainer.getScrollX(); if (DEBUG) Log.d(TAG, "scroll from " + x + " by " + dx); final int scrollBy; if (dx < 0) { // scrolling right final int rightBound = getRightBound(); if (x + dx < -rightBound) scrollBy = -rightBound - x; else scrollBy = dx; } else { // scrolling left // don't scroll if we are at left bound if (x == 0) return; if (x + dx > 0) scrollBy = -x; else scrollBy = dx; } viewContentContainer.scrollBy(scrollBy, 0); } /** * Returns right bound (limit) for scroller. * @return right bound (limit) for scroller. */ private int getRightBound() { if (mSpacingType == SPACING_ACTIONS_WIDTH) { return mSpacing - mActionsSpacing; } else { // all other situations are handled as SPACING_RIGHT_OFFSET return getWidth() - mSpacing - mActionsSpacing; } } }; }