/******************************************************************************* * 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 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.AnimationUtils; import android.view.animation.Interpolator; import android.widget.LinearLayout; import android.widget.Scroller; public class ActionsContentView extends ViewGroup { private static final String TAG = ActionsContentView.class.getSimpleName(); private static final boolean DEBUG = false; public interface OnActionsContentListener { public void onContentStateChanged(ActionsContentView v, boolean isContentShown); public void onContentStateInAction(ActionsContentView v, boolean isContentShowing); } private static final int FLING_MIN = 1000; /** * 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; public static final int EFFECTS_NONE = 0; public static final int EFFECTS_SCROLL_OPENING = 1 << 0; public static final int EFFECTS_SCROLL_CLOSING = 1 << 1; public static final int EFFECTS_SCROLL = EFFECTS_SCROLL_OPENING | EFFECTS_SCROLL_CLOSING; public static final int EFFECTS_FLING_OPENING = 1 << 2; public static final int EFFECTS_FLING_CLOSING = 1 << 3; public static final int EFFECTS_FLING = EFFECTS_FLING_OPENING | EFFECTS_FLING_CLOSING; public static final int EFFECTS_ALL = EFFECTS_SCROLL | EFFECTS_FLING; private final ContentScrollController mScrollController; 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; private int mEffects = EFFECTS_ALL; private OnActionsContentListener mOnActionsContentListener; 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); isSwipingEnabled = a.getBoolean(R.styleable.ActionsContentView_swiping_enabled, true); final int effectActionsRes = a.getResourceId(R.styleable.ActionsContentView_effect_actions, 0); final int effectContentRes = a.getResourceId(R.styleable.ActionsContentView_effect_content, 0); mEffects = a.getInt(R.styleable.ActionsContentView_effects, EFFECTS_ALL); final int effectsInterpolatorRes = a.getResourceId(R.styleable.ActionsContentView_effects_interpolator, 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, " swiping enabled: " + isSwipingEnabled); Log.d(TAG, " effects: " + mEffects); Log.d(TAG, " effect actions: " + effectActionsRes); Log.d(TAG, " effect content: " + effectContentRes); Log.d(TAG, " effects interpolator: " + effectsInterpolatorRes); } final Scroller effectsScroller; if (effectsInterpolatorRes > 0) { final Interpolator interpolator = AnimationUtils.loadInterpolator(getContext(), effectsInterpolatorRes); effectsScroller = new Scroller(context, interpolator); } else { effectsScroller = new Scroller(context); } mScrollController = new ContentScrollController(new Scroller(context), effectsScroller); mGestureDetector = new GestureDetector(context, mScrollController); 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 ) { viewActionsContainer.getController().setEffects(effectActionsRes); } if ( effectContentRes > 0 ) { viewContentContainer.getController().setEffects(effectContentRes); } } public void setOnActionsContentListener(OnActionsContentListener listener) { mOnActionsContentListener = listener; } public OnActionsContentListener getOnActionsContentListener() { return mOnActionsContentListener; } /** * 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 = getSpacingType(); ss.mSpacing = getSpacingWidth(); ss.mActionsSpacing = getActionsSpacingWidth(); ss.isShadowVisible = isShadowVisible(); ss.mShadowWidth = getShadowWidth(); ss.isSwipingEnabled = isSwipingEnabled(); ss.mFlingDuration = getFlingDuration(); ss.mFadeType = getFadeType(); ss.mFadeValue = getFadeValue(); ss.mSwipeType = getSwipingType(); ss.mSwipeEdgeWidth = getSwipingEdgeWidth(); return ss; } public void onRestoreInstanceState(Parcelable state) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } final SavedState ss = (SavedState)state; super.onRestoreInstanceState(ss.getSuperState()); mScrollController.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 ContainerController getActionsController() { return viewActionsContainer.getController(); } public ContainerController getContentController() { return viewContentContainer.getController(); } public boolean isActionsShown() { return !mScrollController.isContentShown(); } public void showActions() { mScrollController.hideContent(mFlingDuration); } public boolean isContentShown() { return mScrollController.isContentShown(); } public void showContent() { mScrollController.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; } @Override public boolean onTouchEvent(MotionEvent ev) { if (!isSwipingEnabled) return false; mGestureDetector.onTouchEvent(ev); final int action = ev.getAction(); if (action == MotionEvent.ACTION_UP) { if (mScrollController.onUp(ev)) return true; } return mScrollController.isHandled(); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (!isSwipingEnabled) return false; mGestureDetector.onTouchEvent(ev); final int action = ev.getAction(); if (action == MotionEvent.ACTION_UP) { if (mScrollController.onUp(ev)) return true; } // whether we should handle all following events by our view // and don't allow children to get them return mScrollController.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); } @SuppressLint("DrawAllocation") @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; mScrollController.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) { mScrollController.init(); } } private void updateScrollFactor() { if (viewActionsContainer == null || viewContentContainer == null) return; final float scrollFactor = mScrollController.getScrollFactor(); final boolean isOpening = mScrollController.isOpening(); final boolean enableEffects = mScrollController.isEffectsEnabled(); final int actionsFadeFactor; if ((mFadeType & FADE_ACTIONS) == FADE_ACTIONS) { actionsFadeFactor = (int) (scrollFactor * mFadeValue); } else { actionsFadeFactor = 0; } viewActionsContainer.getController().onScroll(scrollFactor, actionsFadeFactor, isOpening, enableEffects); final int contentFadeFactor; if ((mFadeType & FADE_CONTENT) == FADE_CONTENT) { contentFadeFactor = (int) ((1f - scrollFactor) * mFadeValue); } else { contentFadeFactor = 0; } viewContentContainer.getController().onScroll(1f - scrollFactor, contentFadeFactor, isOpening, enableEffects); } 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; /** * Used to fling to after fling touch event. */ private final Scroller mEffectsScroller; // using Boolean object to initialize while first scroll event private Boolean mHandleEvent = null; /** * Indicates whether we need initialize position of view after measuring is finished. */ private boolean isContentShown = true; private boolean isFlinging = false; private boolean isEffectsEnabled = false; public ContentScrollController(Scroller scroller, Scroller effectsScroller) { mScroller = scroller; mEffectsScroller = effectsScroller; } /** * 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; } public boolean isSwipeFinished() { if (!mScroller.isFinished()) { return false; } if (!mEffectsScroller.isFinished()) { return false; } final int x = viewContentContainer.getScrollX(); if (isContentShown && x != 0) return false; if (!isContentShown && x != -getRightBound()) return false; return true; } public boolean isOpening() { if (!mScroller.isFinished()) { return mScroller.getStartX() > mScroller.getFinalX(); } if (!mEffectsScroller.isFinished()) { return mEffectsScroller.getStartX() > mEffectsScroller.getFinalX(); } return !isContentShown; } public boolean isEffectsEnabled() { return isEffectsEnabled; } @Override public boolean onDown(MotionEvent e) { mHandleEvent = null; reset(); return false; } public boolean onUp(MotionEvent e) { if (isSwipeFinished()) 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) { isFlinging = false; isEffectsEnabled = false; reset(); // 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) { if (!mHandleEvent) { return false; } final float absVelocityX = Math.abs(velocityX); if (absVelocityX <= Math.abs(velocityY)) return false; if (absVelocityX < FLING_MIN) return false; isFlinging = true; if (velocityX < 0) showContent(mFlingDuration); else hideContent(mFlingDuration); return true; } 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; } scroll(false, 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; } scroll(true, duration); } public float getScrollFactor() { return 1f + (float) viewContentContainer.getScrollX() / (float) getRightBound(); } /** * Resets scroller controller. Stops flinging on current position. */ public void reset() { if (DEBUG) Log.d(TAG, "Scroller: reset"); if (!mScroller.isFinished()) { mScroller.forceFinished(true); } if (!mEffectsScroller.isFinished()) { mEffectsScroller.forceFinished(true); } } /** * Starts auto-scrolling to bound which is closer to current position. */ private void completeScrolling() { // preventing override of fling effect if (!mScroller.isFinished() || !mEffectsScroller.isFinished()) return; final int startX = viewContentContainer.getScrollX(); final int rightBound = getRightBound(); final int middle = -rightBound / 2; if (startX > middle) { showContent(mFlingDuration); } else { hideContent(mFlingDuration); } } private void scroll(boolean showContent, int duration) { reset(); final int startX = viewContentContainer.getScrollX(); final int dx = showContent ? -startX : -getRightBound() - startX; if (DEBUG) Log.d(TAG, "start scroller at " + startX + " for " + dx + " by " + duration); if (duration <= 0) { viewContentContainer.scrollBy(dx, 0); return; } isEffectsEnabled = startEffects(dx < 0, isFlinging); if (isEffectsEnabled) mEffectsScroller.startScroll(startX, 0, dx, 0, duration); else mScroller.startScroll(startX, 0, dx, 0, duration); if (mOnActionsContentListener != null) mOnActionsContentListener.onContentStateInAction(ActionsContentView.this, isContentShown); viewContentContainer.post(this); } /** * Scrolling content view according by given value. * @param dx */ private void scrollBy(int dx) { final int x = viewContentContainer.getScrollX(); isEffectsEnabled = startEffects(!isContentShown, false); 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; } if (DEBUG) Log.d(TAG, "scroll from " + x + " by " + dx + " [" + scrollBy + "]"); viewContentContainer.scrollBy(scrollBy, 0); } /** * Processes auto-scrolling to bound which is closer to current position. */ @Override public void run() { final Scroller scroller = isEffectsEnabled() ? mEffectsScroller : mScroller; if (scroller.isFinished()) { if (DEBUG) Log.d(TAG, "scroller is finished, done with fling"); if (mOnActionsContentListener != null) mOnActionsContentListener.onContentStateChanged(ActionsContentView.this, isContentShown); return; } final boolean more = scroller.computeScrollOffset(); final int x = scroller.getCurrX(); viewContentContainer.scrollTo(x, 0); if (more) { viewContentContainer.post(this); } else { if (mOnActionsContentListener != null) mOnActionsContentListener.onContentStateChanged(ActionsContentView.this, isContentShown); } } /** * 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; } } private boolean startEffects(boolean isOpening, boolean isFlinging) { final boolean enableEffects; if (mEffects == EFFECTS_NONE) { enableEffects = false; } else if (!isFlinging && (mEffects & EFFECTS_SCROLL) > 0) { if (isOpening && (mEffects & EFFECTS_SCROLL_OPENING) == EFFECTS_SCROLL_OPENING) enableEffects = true; else if (!isOpening && (mEffects & EFFECTS_SCROLL_CLOSING) == EFFECTS_SCROLL_CLOSING) enableEffects = true; else enableEffects = false; } else if (isFlinging && (mEffects & EFFECTS_FLING) > 0) { if (isOpening && (mEffects & EFFECTS_FLING_OPENING) == EFFECTS_FLING_OPENING) enableEffects = true; else if (!isOpening && (mEffects & EFFECTS_FLING_CLOSING) == EFFECTS_FLING_CLOSING) enableEffects = true; else enableEffects = false; } else { enableEffects = false; } return enableEffects; } }; }