- 侧滑效果[第三篇]:侧滑框架SmartSwipe之封装
- 侧滑效果[第十四篇]:侧滑框架SmartSwipe之开门侧滑返回
- 你见过微信侧滑返回的联动效果,但开门效果、百叶窗效果见过吗?
- 侧滑效果[第七篇]:侧滑框架SmartSwipe之侧滑透明效果
- 侧滑效果[第四篇]:侧滑框架SmartSwipe之抽屉效果
- 侧滑效果[第六篇]:侧滑框架SmartSwipe之弹性拉伸效果
- 侧滑效果[第十篇]:侧滑框架SmartSwipe之仿手机QQ侧滑
- 侧滑效果[第十五篇]:侧滑框架SmartSwipe之全局返回
- 侧滑效果[第十七篇]:侧滑框架SmartSwipe之互斥组
- 侧滑效果[第五篇]:侧滑框架SmartSwipe之弹性留白效果
SmartSwipe侧滑框架是杭州的某大佬整理的一套框架,SmartSwipe的架构思想还是比较强的,我觉得可以被选为三方框架来使用,当然,一般使用的时候需要避免不必要的三方框架,所以我将SmartSwipe的封装代码整理了出来。
工欲善其事,必先利其器。SmartSwipe侧滑框架可以被当做一个工具使用,所以就没必要矫情的自己写一个工具了。
SmartSwipe侧滑框架的github地址如下:
https://github.com/luckybilly/SmartSwipe
在第二篇文章中,为了了解侧滑原理,我解读了DrawerLayout
源码,其中有两个重要的类:
- DrawerLayout:抽屉布局
- ViewDragHelper:抽屉被拖拽的帮助类
在SmartSwipe框架中,SmartSwipeWrapper
就相当于抽屉布局,当然作者把它比作成一个包装器
,一个实现侧滑效果的包装器
,SwipeHelper
就是ViewDragHelper
的改装版,作者基于ViewDragHelper
进行了改造。
下面贴出这两个类的代码:
SmartSwipeWrapper.java
/**
* a wrapper to wrap the content view, handle motion events to do swipe business by {@link SwipeHelper} and {@link SwipeConsumer}
* @author billy.qi
*/
public class SmartSwipeWrapper extends ViewGroup {
protected SwipeHelper mHelper;
protected View mContentView;
protected final List<SwipeHelper> mHelpers = new LinkedList<>();
protected final List<SwipeConsumer> mConsumers = new LinkedList<>();
protected boolean mInflateFromXml;
public SmartSwipeWrapper(Context context) {
this(context, null, 0);
}
public SmartSwipeWrapper(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SmartSwipeWrapper(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public SmartSwipeWrapper(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mHelper = null;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mHelper != null) {
return mHelper.shouldInterceptTouchEvent(ev);
} else {
for (SwipeHelper helper : mHelpers) {
if (helper.shouldInterceptTouchEvent(ev)) {
mHelper = helper;
return true;
}
}
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mHelper != null) {
mHelper.processTouchEvent(event);
} else {
for (SwipeHelper helper : mHelpers) {
helper.processTouchEvent(event);
if (helper.getDragState() == SwipeHelper.STATE_DRAGGING) {
mHelper = helper;
return true;
}
}
}
return true;
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
for (SwipeConsumer consumer : mConsumers) {
if (consumer != null) {
consumer.dispatchDraw(canvas);
}
}
}
public void drawChild(Canvas canvas, View child) {
drawChild(canvas, child, getDrawingTime());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (SwipeConsumer consumer : mConsumers) {
if (consumer != null) {
consumer.onDraw(canvas);
}
}
}
private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1);
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
final ViewGroup.LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final ViewGroup.LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
final int width = Math.max(0, getMeasuredWidth());
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
final int height = Math.max(0, getMeasuredHeight());
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
for (SwipeConsumer consumer : mConsumers) {
if (consumer != null) {
consumer.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
boolean layoutByConsumer = false;
if (mHelper != null) {
layoutByConsumer = mHelper.getSwipeConsumer().onLayout(changed, left, top, right, bottom);
} else {
for (SwipeConsumer consumer : mConsumers) {
if (consumer != null && consumer.onLayout(changed, left, top, right, bottom)) {
layoutByConsumer = true;
}
}
}
if (!layoutByConsumer) {
if (mContentView != null) {
mContentView.layout(0, 0, mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight());
}
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//compat for xml usage
mInflateFromXml = true;
int childCount = getChildCount();
if (childCount > 0 && mContentView == null) {
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
if (layoutParams instanceof LayoutParams) {
final int gravity = ((LayoutParams) layoutParams).gravity;
if (gravity == LayoutParams.UNSPECIFIED_GRAVITY) {
setContentView(child);
break;
}
}
}
}
}
@Override
public void computeScroll() {
if (!mHelpers.isEmpty() ) {
boolean shouldContinue = false;
for (SwipeHelper helper : mHelpers) {
if (helper.continueSettling()) {
shouldContinue = true;
}
}
if (shouldContinue) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
@Override
public boolean canScrollVertically(int direction) {
for (SwipeConsumer consumer : mConsumers) {
if (direction < 0 && consumer.isTopEnable() && !consumer.isTopLocked()) {
if (consumer.getDirection() == SwipeConsumer.DIRECTION_TOP && consumer.getProgress() >= 1) {
return false;
}
return true;
} else if (direction > 0 && consumer.isBottomEnable() && !consumer.isBottomLocked()) {
if (consumer.getDirection() == SwipeConsumer.DIRECTION_BOTTOM && consumer.getProgress() >= 1) {
return false;
}
return true;
}
}
return super.canScrollVertically(direction);
}
@Override
public boolean canScrollHorizontally(int direction) {
for (SwipeConsumer consumer : mConsumers) {
if (direction < 0 && consumer.isLeftEnable() && !consumer.isLeftLocked()) {
if (consumer.getDirection() == SwipeConsumer.DIRECTION_LEFT && consumer.getProgress() >= 1) {
return false;
}
return true;
} else if (direction > 0 && consumer.isRightEnable() && !consumer.isRightLocked()) {
if (consumer.getDirection() == SwipeConsumer.DIRECTION_RIGHT && consumer.getProgress() >= 1) {
return false;
}
return true;
}
}
return super.canScrollHorizontally(direction);
}
public <T extends SwipeConsumer> T addConsumer(T consumer) {
if (consumer != null) {
this.mConsumers.add(consumer);
SwipeHelper helper = consumer.getSwipeHelper();
if (helper == null) {
helper = SwipeHelper.create(this, consumer.getSensitivity(), consumer, consumer.getInterpolator());
}
consumer.onAttachToWrapper(this, helper);
mHelpers.add(helper);
}
return consumer;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
for (SwipeConsumer consumer : mConsumers) {
consumer.close();
}
}
public SmartSwipeWrapper removeAllConsumers() {
Iterator<SwipeConsumer> iterator = mConsumers.iterator();
while (iterator.hasNext()) {
SwipeConsumer consumer = iterator.next();
iterator.remove();
if (consumer != null) {
consumer.onDetachFromWrapper();
SwipeHelper swipeHelper = consumer.getSwipeHelper();
mHelpers.remove(swipeHelper);
if (mHelper == swipeHelper) {
mHelper = null;
}
}
}
return this;
}
public SmartSwipeWrapper removeConsumer(SwipeConsumer consumer) {
boolean removed = mConsumers.remove(consumer);
if (removed) {
consumer.onDetachFromWrapper();
SwipeHelper swipeHelper = consumer.getSwipeHelper();
mHelpers.remove(swipeHelper);
if (mHelper == swipeHelper) {
mHelper = null;
}
}
return this;
}
public SwipeConsumer getConsumerByType(Class<? extends SwipeConsumer> clazz) {
for (SwipeConsumer consumer : mConsumers) {
if (consumer != null && consumer.getClass() == clazz) {
return consumer;
}
}
return null;
}
public void setContentView(View contentView) {
if (contentView == null || this.mContentView == contentView) {
return;
}
this.mContentView = contentView;
if (contentView.getParent() == null) {
addView(contentView);
}
}
public View getContentView() {
return mContentView;
}
public List<SwipeConsumer> getAllConsumers() {
return mConsumers;
}
public SmartSwipeWrapper enableDirection(int direction) {
return enableDirection(direction, true);
}
public SmartSwipeWrapper enableDirection(int direction, boolean enable) {
for (SwipeConsumer consumer : mConsumers) {
consumer.enableDirection(direction, enable);
}
return this;
}
public boolean isInflateFromXml() {
return mInflateFromXml;
}
public void consumeInflateFromXml() {
this.mInflateFromXml = false;
}
@Override
public boolean shouldDelayChildPressedState() {
return false;
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
if (lp instanceof LayoutParams) {
return new LayoutParams((LayoutParams) lp);
} else if (lp instanceof MarginLayoutParams) {
return new LayoutParams((MarginLayoutParams) lp);
}
return new LayoutParams(lp);
}
public static class LayoutParams extends MarginLayoutParams {
/**
* Value for {@link #gravity} indicating that a gravity has not been
* explicitly specified.
*/
public static final int UNSPECIFIED_GRAVITY = 0;
/**
* The gravity to apply with the View to which these layout parameters
* are associated.
*/
public int gravity = UNSPECIFIED_GRAVITY;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.SmartSwipeWrapper_Layout);
gravity = a.getInt(R.styleable.SmartSwipeWrapper_Layout_swipe_gravity, UNSPECIFIED_GRAVITY);
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
/**
* Creates a new set of layout parameters with the specified width, height
* and weight.
*
* @param width the width, either {@link #MATCH_PARENT},
* {@link #WRAP_CONTENT} or a fixed size in pixels
* @param height the height, either {@link #MATCH_PARENT},
* {@link #WRAP_CONTENT} or a fixed size in pixels
* @param gravity the gravity
*
* @see android.view.Gravity
*/
public LayoutParams(int width, int height, int gravity) {
super(width, height);
this.gravity = gravity;
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.MarginLayoutParams source) {
super(source);
}
/**
* Copy constructor. Clones the width, height, margin values, and
* gravity of the source.
*
* @param source The layout params to copy from.
*/
public LayoutParams(LayoutParams source) {
super(source);
this.gravity = source.gravity;
}
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH);
}
@Override
public void onStopNestedScroll(View child) {
onStopNestedScroll(child, ViewCompat.TYPE_TOUCH);
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, ViewCompat.TYPE_TOUCH);
}
/////////////////////////////////////////
//
// support for NestedScrollingParent2
//
/////////////////////////////////////////
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
if ((axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0) {
for (SwipeConsumer consumer : mConsumers) {
if (consumer.isTopEnable() || consumer.isBottomEnable()) {
return true;
}
}
} else if ((axes & ViewCompat.SCROLL_AXIS_HORIZONTAL) != 0) {
for (SwipeConsumer consumer : mConsumers) {
if (consumer.isLeftEnable() || consumer.isRightEnable()) {
return true;
}
}
}
return false;
}
private static final int NESTED_TYPE_INVALID = -1;
protected int mCurNestedType = NESTED_TYPE_INVALID;
protected boolean mNestedFlyConsumed;
public void onNestedScrollAccepted(View child, View target, int axes, int type) {
mNestedFlyConsumed = false;
mCurNestedType = type;
helperOnNestedScrollAccepted(child, target, axes, type);
}
public void onStopNestedScroll(View target, int type) {
helperOnStopNestedScroll(target, type);
if (type == mCurNestedType) {
mCurNestedType = NESTED_TYPE_INVALID;
if (mHelper != null) {
mHelper.nestedScrollingRelease();
}
}
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return false;
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
return false;
}
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
if (dxUnconsumed != 0 || dyUnconsumed != 0) {
if (type == ViewCompat.TYPE_NON_TOUCH) {
//fling nested scroll has not been consumed
requestDisallowInterceptTouchEvent(false);
}
int[] consumed = new int[2];
wrapperNestedScroll(dxUnconsumed, dyUnconsumed, consumed, type);
dxConsumed += consumed[0];
dyConsumed += consumed[1];
dxUnconsumed -= consumed[0];
dyUnconsumed -= consumed[1];
}
helperOnNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
}
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
if (mHelper != null && mHelper.getSwipeConsumer().getDirection() != SwipeConsumer.DIRECTION_NONE) {
wrapperNestedScroll(dx, dy, consumed, type);
}
helperOnNestedPreScroll(target, dx, dy, consumed, type);
}
private void wrapperNestedScroll(int dxUnconsumed, int dyUnconsumed, int[] consumed, int type) {
if (mCurNestedType == NESTED_TYPE_INVALID) {
//resolve problem: miss a call of: onStartNestedScroll(type = 1) and onNestedScrollAccepted(type=0)
// time line like this:
// onStartNestedScroll(type=0)
// onNestedScrollAccepted(type=0)
// some onNestedPreScroll/onNestedScroll(type=0)...
// onStopNestedScroll(type=0)
// some onNestedPreScroll/onNestedScroll(type=1)...
// onStopNestedScroll(type=1)
mCurNestedType = type;
mNestedFlyConsumed = false;
}
boolean fly = type == ViewCompat.TYPE_NON_TOUCH;
if (mHelper != null) {
if (fly) {
if (!mNestedFlyConsumed) {
if (mHelper.getSwipeConsumer().getProgress() >= 1) {
mNestedFlyConsumed = true;
mHelper.nestedScrollingRelease();
} else {
mHelper.nestedScrollingDrag(-dxUnconsumed, -dyUnconsumed, consumed, fly);
}
}
} else {
mHelper.nestedScrollingDrag(-dxUnconsumed, -dyUnconsumed, consumed, fly);
}
} else {
for (SwipeHelper helper : mHelpers) {
if (helper != null) {
//try to determined which SwipeHelper will handle this fake drag via nested scroll
if (helper.nestedScrollingDrag(-dxUnconsumed, -dyUnconsumed, consumed, type == ViewCompat.TYPE_NON_TOUCH)) {
mHelper = helper;
break;
}
}
}
}
}
protected void helperOnNestedScrollAccepted(View child, View target, int axes, int type) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
super.onNestedScrollAccepted(child, target, axes);
}
}
protected void helperOnNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
super.onNestedPreScroll(target, dx, dy, consumed);
}
}
protected void helperOnNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
}
}
protected void helperOnStopNestedScroll(View target, int type) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
super.onStopNestedScroll(target);
}
}
}
SwipeHelper.java
/**
* This class is copy and modified from ViewDragHelper
* 1. mCapturedView removed. use mClampedDistanceX and mClampedDistanceY instead
* 2. Callback removed. use {@link SwipeConsumer} to consume the motion event
* @author billy.qi
*/
public class SwipeHelper {
private static final String TAG = "SwipeHelper";
/**
* A null/invalid pointer ID.
*/
public static final int INVALID_POINTER = -1;
public static final int POINTER_NESTED_SCROLL = -2;
public static final int POINTER_NESTED_FLY = -3;
/**
* A view is not currently being dragged or animating as a result of a fling/snap.
*/
public static final int STATE_IDLE = 0;
/**
* A view is currently being dragged. The position is currently changing as a result
* of user input or simulated user input.
*/
public static final int STATE_DRAGGING = 1;
/**
* A view is currently settling into place as a result of a fling or
* predefined non-interactive motion.
*/
public static final int STATE_SETTLING = 2;
public static final int STATE_NONE_TOUCH = 3;
private final ViewConfiguration viewConfiguration;
private int maxSettleDuration = 600; // ms
// Current drag state; idle, dragging or settling
private int mDragState;
// Distance to travel before a drag may begin
private int mTouchSlop;
// Last known position/pointer tracking
private int mActivePointerId = INVALID_POINTER;
private float[] mInitialMotionX;
private float[] mInitialMotionY;
private float[] mLastMotionX;
private float[] mLastMotionY;
private int mPointersDown;
private VelocityTracker mVelocityTracker;
private float mMaxVelocity;
private float mMinVelocity;
private OverScroller mScroller;
private final SwipeConsumer mSwipeConsumer;
// private View mCapturedView;
private boolean mReleaseInProgress;
private final ViewGroup mParentView;
private int mClampedDistanceX;
private int mClampedDistanceY;
/**
* Default interpolator defining the animation curve for mScroller
*/
private static final Interpolator sInterpolator = new Interpolator() {
@Override
public float getInterpolation(float t) {
t -= 1.0f;
return t * t * t * t * t + 1.0f;
}
};
/**
* Factory method to create a new SwipeHelper.
*
* @param forParent Parent view to monitor
* @param consumer Callback to provide information and receive events
* @param interpolator interpolator for animation
* @return a new SwipeHelper instance
*/
public static SwipeHelper create(ViewGroup forParent, SwipeConsumer consumer, Interpolator interpolator) {
return new SwipeHelper(forParent.getContext(), forParent, consumer, interpolator);
}
public static SwipeHelper create(ViewGroup forParent, SwipeConsumer consumer) {
return create(forParent, consumer, null);
}
/**
* Factory method to create a new SwipeHelper.
*
* @param forParent Parent view to monitor
* @param sensitivity Multiplier for how sensitive the helper should be about detecting
* the start of a drag. Larger values are more sensitive. 1.0f is normal.
* @param consumer Callback to provide information and receive events
* @param interpolator interpolator for animation
* @return a new SwipeHelper instance
*/
public static SwipeHelper create(ViewGroup forParent, float sensitivity, SwipeConsumer consumer, Interpolator interpolator) {
final SwipeHelper helper = create(forParent, consumer, interpolator);
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
return helper;
}
public static SwipeHelper create(ViewGroup forParent, float sensitivity, SwipeConsumer cb) {
return create(forParent, sensitivity, cb, null);
}
public void setSensitivity(float sensitivity) {
mTouchSlop = (int) (viewConfiguration.getScaledTouchSlop() * (1 / sensitivity));
}
/**
* Apps should use SwipeHelper.create() to get a new instance.
* This will allow VDH to use internal compatibility implementations for different
* platform versions.
*
* @param context Context to initialize config-dependent params from
* @param forParent Parent view to monitor
* @param interpolator interpolator for animation
*/
private SwipeHelper(Context context, ViewGroup forParent, SwipeConsumer cb, Interpolator interpolator) {
if (forParent == null) {
throw new IllegalArgumentException("Parent view may not be null");
}
if (cb == null) {
throw new IllegalArgumentException("Callback may not be null");
}
mParentView = forParent;
mSwipeConsumer = cb;
viewConfiguration = ViewConfiguration.get(context);
mTouchSlop = viewConfiguration.getScaledTouchSlop();
mMaxVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
mMinVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
setInterpolator(context, interpolator);
}
public void setInterpolator(Context context, Interpolator interpolator) {
if (interpolator == null) {
interpolator = sInterpolator;
}
if (mScroller != null) {
abort();
mScroller = null;
}
mScroller = new OverScroller(context, interpolator);
}
/**
* Set the minimum velocity that will be detected as having a magnitude greater than zero
* in pixels per second. Callback methods accepting a velocity will be clamped appropriately.
*
* @param minVel Minimum velocity to detect
* @return this
*/
public SwipeHelper setMinVelocity(float minVel) {
mMinVelocity = minVel;
return this;
}
/**
* Return the currently configured minimum velocity. Any flings with a magnitude less
* than this value in pixels per second. Callback methods accepting a velocity will receive
* zero as a velocity value if the real detected velocity was below this threshold.
*
* @return the minimum velocity that will be detected
*/
public float getMinVelocity() {
return mMinVelocity;
}
/**
* Retrieve the current drag state of this helper. This will return one of
* {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING} or {@link #STATE_NONE_TOUCH}.
* @return The current drag state
*/
public int getDragState() {
return mDragState;
}
/**
* @return The ID of the pointer currently dragging
* or {@link #INVALID_POINTER}.
*/
public int getActivePointerId() {
return mActivePointerId;
}
/**
* @return The minimum distance in pixels that the user must travel to initiate a drag
*/
public int getTouchSlop() {
return mTouchSlop;
}
/**
* The result of a call to this method is equivalent to
* {@link #processTouchEvent(android.view.MotionEvent)} receiving an ACTION_CANCEL event.
*/
public void cancel() {
mActivePointerId = INVALID_POINTER;
clearMotionHistory();
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
/**
* {@link #cancel()}, but also abort all motion in progress and snap to the end of any
* animation.
*/
public void abort() {
cancel();
if (mDragState == STATE_SETTLING || mDragState == STATE_NONE_TOUCH) {
final int oldX = mScroller.getCurrX();
final int oldY = mScroller.getCurrY();
mScroller.abortAnimation();
final int newX = mScroller.getCurrX();
final int newY = mScroller.getCurrY();
mSwipeConsumer.onSwipeDistanceChanged(newX, newY, newX - oldX, newY - oldY);
}
setDragState(STATE_IDLE);
}
/**
* Animate the view <code>child</code> to the given (left, top) position.
* If this method returns true, the caller should invoke {@link #continueSettling()}
* on each subsequent frame to continue the motion until it returns false. If this method
* returns false there is no further work to do to complete the movement.
*
* @param startX start x position
* @param startY start y position
* @param finalX Final x position
* @param finalY Final y position
* @return true if animation should continue through {@link #continueSettling()} calls
*/
public boolean smoothSlideTo(int startX, int startY, int finalX, int finalY) {
mClampedDistanceX = startX;
mClampedDistanceY = startY;
return smoothSlideTo(finalX, finalY);
}
public boolean smoothSlideTo(int finalX, int finalY) {
boolean continueSliding;
if (mVelocityTracker != null) {
continueSliding = smoothSettleCapturedViewTo(finalX, finalY,
(int) mVelocityTracker.getXVelocity(mActivePointerId),
(int) mVelocityTracker.getYVelocity(mActivePointerId));
} else {
continueSliding = smoothSettleCapturedViewTo(finalX, finalY, 0, 0);
}
mActivePointerId = INVALID_POINTER;
return continueSliding;
}
/**
* Settle the captured view at the given (left, top) position.
* The appropriate velocity from prior motion will be taken into account.
* If this method returns true, the caller should invoke {@link #continueSettling()}
* on each subsequent frame to continue the motion until it returns false. If this method
* returns false there is no further work to do to complete the movement.
*
* @param finalX Settled left edge position for the captured view
* @param finalY Settled top edge position for the captured view
* @return true if animation should continue through {@link #continueSettling()} calls
*/
public boolean settleCapturedViewAt(int finalX, int finalY) {
if (!mReleaseInProgress) {
throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to "
+ "Callback#onViewReleased");
}
return smoothSettleCapturedViewTo(finalX, finalY,
(int) mVelocityTracker.getXVelocity(mActivePointerId),
(int) mVelocityTracker.getYVelocity(mActivePointerId));
}
/**
* Settle the captured view at the given (left, top) position.
*
* @param finalX Target left position for the captured view
* @param finalY Target top position for the captured view
* @param xvel Horizontal velocity
* @param yvel Vertical velocity
* @return true if animation should continue through {@link #continueSettling()} calls
*/
private boolean smoothSettleCapturedViewTo(int finalX, int finalY, int xvel, int yvel) {
final int startX = mClampedDistanceX;
final int startTop = mClampedDistanceY;
final int dx = finalX - startX;
final int dy = finalY - startTop;
mScroller.abortAnimation();
if (dx == 0 && dy == 0) {
setDragState(STATE_SETTLING);
mSwipeConsumer.onSwipeDistanceChanged(finalX, finalY, dx, dy);
setDragState(STATE_IDLE);
return false;
}
final int duration = computeSettleDuration(dx, dy, xvel, yvel);
mScroller.startScroll(startX, startTop, dx, dy, duration);
setDragState(STATE_SETTLING);
return true;
}
private int computeSettleDuration(int dx, int dy, int xvel, int yvel) {
xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity);
yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity);
final int absDx = Math.abs(dx);
final int absDy = Math.abs(dy);
final int absXVel = Math.abs(xvel);
final int absYVel = Math.abs(yvel);
final int addedVel = absXVel + absYVel;
final int addedDistance = absDx + absDy;
final float xweight = xvel != 0 ? (float) absXVel / addedVel :
(float) absDx / addedDistance;
final float yweight = yvel != 0 ? (float) absYVel / addedVel :
(float) absDy / addedDistance;
int xduration = computeAxisDuration(dx, xvel, mSwipeConsumer.getHorizontalRange(dx, dy));
int yduration = computeAxisDuration(dy, yvel, mSwipeConsumer.getVerticalRange(dx, dy));
return (int) (xduration * xweight + yduration * yweight);
}
private int computeAxisDuration(int delta, int velocity, int motionRange) {
if (delta == 0) {
return 0;
}
final int width = mParentView.getWidth();
final int halfWidth = width >> 1;
final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width);
final float distance = halfWidth + halfWidth
* distanceInfluenceForSnapDuration(distanceRatio);
int duration;
velocity = Math.abs(velocity);
if (velocity > 0) {
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
} else {
final float range = (float) Math.abs(delta) / motionRange;
duration = (int) (range * maxSettleDuration);
}
return Math.min(duration, maxSettleDuration);
}
/**
* Clamp the magnitude of value for absMin and absMax.
* If the value is below the minimum, it will be clamped to zero.
* If the value is above the maximum, it will be clamped to the maximum.
*
* @param value Value to clamp
* @param absMin Absolute value of the minimum significant value to return
* @param absMax Absolute value of the maximum value to return
* @return The clamped value with the same sign as <code>value</code>
*/
private int clampMag(int value, int absMin, int absMax) {
final int absValue = Math.abs(value);
if (absValue < absMin) {
return 0;
}
if (absValue > absMax) {
return value > 0 ? absMax : -absMax;
}
return value;
}
/**
* Clamp the magnitude of value for absMin and absMax.
* If the value is below the minimum, it will be clamped to zero.
* If the value is above the maximum, it will be clamped to the maximum.
*
* @param value Value to clamp
* @param absMin Absolute value of the minimum significant value to return
* @param absMax Absolute value of the maximum value to return
* @return The clamped value with the same sign as <code>value</code>
*/
private float clampMag(float value, float absMin, float absMax) {
final float absValue = Math.abs(value);
if (absValue < absMin) {
return 0;
}
if (absValue > absMax) {
return value > 0 ? absMax : -absMax;
}
return value;
}
private float distanceInfluenceForSnapDuration(float f) {
f -= 0.5f; // center the values about 0.
f *= 0.3f * (float) Math.PI / 2.0f;
return (float) Math.sin(f);
}
public boolean continueSettling() {
if (mDragState == STATE_SETTLING) {
boolean keepGoing = mScroller.computeScrollOffset();
final int x = mScroller.getCurrX();
final int y = mScroller.getCurrY();
final int dx = x - mClampedDistanceX;
final int dy = y - mClampedDistanceY;
if (dx != 0) {
mClampedDistanceX = x;
}
if (dy != 0) {
mClampedDistanceY = y;
}
if (dx != 0 || dy != 0) {
mSwipeConsumer.onSwipeDistanceChanged(x, y, dx, dy);
}
if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) {
// Close enough. The interpolator/scroller might think we're still moving
// but the user sure doesn't.
mScroller.abortAnimation();
keepGoing = false;
}
if (!keepGoing) {
setDragState(STATE_IDLE);
}
}
return mDragState == STATE_SETTLING;
}
/**
* Like all callback events this must happen on the UI thread, but release
* involves some extra semantics. During a release (mReleaseInProgress)
* is the only time it is valid to call {@link #settleCapturedViewAt(int, int)}
* @param xvel x velocity
* @param yvel y velocity
*/
public void dispatchViewReleased(float xvel, float yvel) {
mReleaseInProgress = true;
mSwipeConsumer.onSwipeReleased(xvel, yvel);
mReleaseInProgress = false;
if (mDragState == STATE_DRAGGING) {
// onViewReleased didn't call a method that would have changed this. Go idle.
setDragState(STATE_IDLE);
}
}
private void clearMotionHistory() {
if (mInitialMotionX == null) {
return;
}
Arrays.fill(mInitialMotionX, 0);
Arrays.fill(mInitialMotionY, 0);
Arrays.fill(mLastMotionX, 0);
Arrays.fill(mLastMotionY, 0);
mPointersDown = 0;
}
private void clearMotionHistory(int pointerId) {
if (mInitialMotionX == null || !isPointerDown(pointerId)) {
return;
}
mInitialMotionX[pointerId] = 0;
mInitialMotionY[pointerId] = 0;
mLastMotionX[pointerId] = 0;
mLastMotionY[pointerId] = 0;
mPointersDown &= ~(1 << pointerId);
}
private void ensureMotionHistorySizeForId(int pointerId) {
if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) {
float[] imx = new float[pointerId + 1];
float[] imy = new float[pointerId + 1];
float[] lmx = new float[pointerId + 1];
float[] lmy = new float[pointerId + 1];
if (mInitialMotionX != null) {
System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length);
System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length);
System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length);
System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length);
}
mInitialMotionX = imx;
mInitialMotionY = imy;
mLastMotionX = lmx;
mLastMotionY = lmy;
}
}
private void saveInitialMotion(float x, float y, int pointerId) {
ensureMotionHistorySizeForId(pointerId);
mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
mPointersDown |= 1 << pointerId;
}
private void saveLastMotion(MotionEvent ev) {
final int pointerCount = ev.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
final int pointerId = ev.getPointerId(i);
// If pointer is invalid then skip saving on ACTION_MOVE.
if (!isValidPointerForActionMove(pointerId)) {
continue;
}
final float x = ev.getX(i);
final float y = ev.getY(i);
mLastMotionX[pointerId] = x;
mLastMotionY[pointerId] = y;
}
}
/**
* Check if the given pointer ID represents a pointer that is currently down (to the best
* of the SwipeHelper's knowledge).
*
* <p>The state used to report this information is populated by the methods
* {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or
* {@link #processTouchEvent(android.view.MotionEvent)}. If one of these methods has not
* been called for all relevant MotionEvents to track, the information reported
* by this method may be stale or incorrect.</p>
*
* @param pointerId pointer ID to check; corresponds to IDs provided by MotionEvent
* @return true if the pointer with the given ID is still down
*/
public boolean isPointerDown(int pointerId) {
return (mPointersDown & 1 << pointerId) != 0;
}
void setDragState(int state) {
if (mDragState != state) {
mDragState = state;
mSwipeConsumer.onStateChanged(state);
// if (mDragState == STATE_IDLE) {
// mClampedDistanceX = mClampedDistanceY = 0;
// }
}
}
/**
* Attempt to capture the view with the given pointer ID. The callback will be involved.
* This will put us into the "dragging" state. If we've already captured this view with
* this pointer this method will immediately return true without consulting the callback.
*
* @param pointerId Pointer to capture with
* @return true if capture was successful
*/
private boolean trySwipe(int pointerId, boolean settling, float downX, float downY, float dx, float dy) {
return trySwipe(pointerId, settling, downX, downY, dx, dy, true);
}
private boolean trySwipe(int pointerId, boolean settling, float downX, float downY, float dx, float dy, boolean touchMode) {
if (mActivePointerId == pointerId) {
// Already done!
return true;
}
boolean swipe;
if (settling || mDragState == STATE_SETTLING) {
swipe = mSwipeConsumer.tryAcceptSettling(pointerId, downX, downY);
} else {
swipe = mSwipeConsumer.tryAcceptMoving(pointerId, downX, downY, dx, dy);
}
if (swipe) {
mActivePointerId = pointerId;
float initX = 0;
float initY = 0;
if (pointerId >= 0 && pointerId < mInitialMotionX.length && pointerId < mInitialMotionY.length) {
initX = mInitialMotionX[pointerId];
initY = mInitialMotionY[pointerId];
}
mSwipeConsumer.onSwipeAccepted(pointerId, settling, initX, initY);
mClampedDistanceX = mSwipeConsumer.clampDistanceHorizontal(0, 0);
mClampedDistanceY = mSwipeConsumer.clampDistanceVertical(0, 0);
setDragState(touchMode ? STATE_DRAGGING : STATE_NONE_TOUCH);
return true;
}
return false;
}
/**
* Check if this event as provided to the parent view's onInterceptTouchEvent should
* cause the parent to intercept the touch event stream.
*
* @param ev MotionEvent provided to onInterceptTouchEvent
* @return true if the parent view should return true from onInterceptTouchEvent
*/
public boolean shouldInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
final int actionIndex = ev.getActionIndex();
if (action == MotionEvent.ACTION_DOWN) {
// Reset things for a new event stream, just in case we didn't get
// the whole previous stream.
cancel();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = ev.getPointerId(0);
saveInitialMotion(x, y, pointerId);
// Catch a settling view if possible.
if (mDragState == STATE_SETTLING || mDragState == STATE_NONE_TOUCH) {
trySwipe(pointerId, true, x, y, 0, 0);
}
break;
}
case MotionEvent.ACTION_POINTER_DOWN: {
final int pointerId = ev.getPointerId(actionIndex);
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
saveInitialMotion(x, y, pointerId);
// A SwipeHelper can only manipulate one view at a time.
if (mDragState == STATE_SETTLING || mDragState == STATE_NONE_TOUCH) {
// Catch a settling view if possible.
trySwipe(pointerId, true, x, y, 0, 0);
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (mInitialMotionX == null || mInitialMotionY == null) {
break;
}
// First to cross a touch slop over a draggable view wins. Also report edge drags.
final int pointerCount = ev.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
final int pointerId = ev.getPointerId(i);
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(pointerId)) {
continue;
}
final float x = ev.getX(i);
final float y = ev.getY(i);
float downX = mInitialMotionX[pointerId];
float downY = mInitialMotionY[pointerId];
final float dx = x - downX;
final float dy = y - downY;
final boolean pastSlop = checkTouchSlop(dx, dy);
if (pastSlop) {
final int hDragRange = mSwipeConsumer.getHorizontalRange(dx, dy);
final int vDragRange = mSwipeConsumer.getVerticalRange(dx, dy);
if (hDragRange == 0 && vDragRange == 0) {
continue;
}
}
if (pastSlop && trySwipe(pointerId, false, downX, downY, dx, dy)) {
break;
}
}
saveLastMotion(ev);
break;
}
case MotionEvent.ACTION_POINTER_UP: {
final int pointerId = ev.getPointerId(actionIndex);
clearMotionHistory(pointerId);
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
cancel();
break;
}
default:
}
return mDragState == STATE_DRAGGING;
}
/**
* Process a touch event received by the parent view. This method will dispatch callback events
* as needed before returning. The parent view's onTouchEvent implementation should call this.
*
* @param ev The touch event received by the parent view
*/
public void processTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
final int actionIndex = ev.getActionIndex();
if (action == MotionEvent.ACTION_DOWN && mDragState != STATE_DRAGGING) {
// Reset things for a new event stream, just in case we didn't get
// the whole previous stream.
cancel();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = ev.getPointerId(0);
saveInitialMotion(x, y, pointerId);
// Since the parent is already directly processing this touch event,
// there is no reason to delay for a slop before dragging.
// Start immediately if possible.
if (mDragState != STATE_DRAGGING) {
trySwipe(pointerId, mDragState == STATE_SETTLING || mDragState == STATE_NONE_TOUCH, x, y, 0, 0);
}
break;
}
case MotionEvent.ACTION_POINTER_DOWN: {
final int pointerId = ev.getPointerId(actionIndex);
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
saveInitialMotion(x, y, pointerId);
if (mDragState == STATE_DRAGGING) {
trySwipe(pointerId, true, x, y, 0, 0);
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (mDragState == STATE_DRAGGING) {
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(mActivePointerId)) {
break;
}
final int index = ev.findPointerIndex(mActivePointerId);
if (index < 0) {
break;
}
final float x = ev.getX(index);
final float y = ev.getY(index);
final int idx = (int) (x - mLastMotionX[mActivePointerId]);
final int idy = (int) (y - mLastMotionY[mActivePointerId]);
dragTo(mClampedDistanceX + idx, mClampedDistanceY + idy, idx, idy);
saveLastMotion(ev);
} else {
// Check to see if any pointer is now over a draggable view.
final int pointerCount = ev.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
final int pointerId = ev.getPointerId(i);
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(pointerId)) {
continue;
}
final float x = ev.getX(i);
final float y = ev.getY(i);
float downX = mInitialMotionX[pointerId];
float downY = mInitialMotionY[pointerId];
final float dx = x - downX;
final float dy = y - downY;
if (checkTouchSlop(dx, dy) && trySwipe(pointerId, false, downX, downY, dx, dy)) {
break;
}
}
saveLastMotion(ev);
}
break;
}
case MotionEvent.ACTION_POINTER_UP: {
final int pointerId = ev.getPointerId(actionIndex);
if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) {
// Try to find another pointer that's still holding on to the captured view.
int newActivePointer = INVALID_POINTER;
final int pointerCount = ev.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
final int id = ev.getPointerId(i);
if (id == mActivePointerId) {
// This one's going away, skip.
continue;
}
if (!isValidPointerForActionMove(id)) {
continue;
}
if (trySwipe(id, true, mInitialMotionX[id], mInitialMotionX[id], 0, 0)) {
newActivePointer = mActivePointerId;
break;
}
}
if (newActivePointer == INVALID_POINTER) {
// We didn't find another pointer still touching the view, release it.
releaseViewForPointerUp();
}
}
clearMotionHistory(pointerId);
break;
}
case MotionEvent.ACTION_UP: {
if (mDragState == STATE_DRAGGING) {
releaseViewForPointerUp();
}
cancel();
break;
}
case MotionEvent.ACTION_CANCEL: {
if (mDragState == STATE_DRAGGING) {
dispatchViewReleased(0, 0);
}
cancel();
break;
}
default:
}
}
/**
* Check if we've crossed a reasonable touch slop for the given child view.
* If the child cannot be dragged along the horizontal or vertical axis, motion
* along that axis will not count toward the slop check.
*
* @param dx Motion since initial position along X axis
* @param dy Motion since initial position along Y axis
* @return true if the touch slop has been crossed
*/
private boolean checkTouchSlop(float dx, float dy) {
final boolean checkHorizontal = mSwipeConsumer.getHorizontalRange(dx, dy) > 0;
final boolean checkVertical = mSwipeConsumer.getVerticalRange(dx, dy) > 0;
if (checkHorizontal && checkVertical) {
return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
} else if (checkHorizontal) {
return Math.abs(dx) > mTouchSlop;
} else if (checkVertical) {
return Math.abs(dy) > mTouchSlop;
}
return false;
}
private void releaseViewForPointerUp() {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
final float xvel = clampMag(
mVelocityTracker.getXVelocity(mActivePointerId),
mMinVelocity, mMaxVelocity);
final float yvel = clampMag(
mVelocityTracker.getYVelocity(mActivePointerId),
mMinVelocity, mMaxVelocity);
dispatchViewReleased(xvel, yvel);
}
public boolean nestedScrollingDrag(int dx, int dy, int[] consumed, boolean fly) {
if (mDragState == STATE_IDLE && !trySwipe(fly ? POINTER_NESTED_FLY : POINTER_NESTED_SCROLL, false, 0, 0, dx, dy, false)) {
return false;
}
int clampedX = 0, clampedY = 0;
if (mClampedDistanceX != 0 || dx != 0) {
clampedX = mSwipeConsumer.clampDistanceHorizontal(mClampedDistanceX + dx, dx);
consumed[0] = mClampedDistanceX - clampedX;
}
if (mClampedDistanceY != 0 || dy != 0) {
clampedY = mSwipeConsumer.clampDistanceVertical(mClampedDistanceY + dy, dy);
consumed[1] = mClampedDistanceY - clampedY;
}
if (mClampedDistanceX == 0 && mClampedDistanceY == 0 && consumed[0] == 0 && consumed[1] == 0) {
mActivePointerId = INVALID_POINTER;
setDragState(STATE_IDLE);
return false;
} else {
dragTo(clampedX, clampedY, -consumed[0], -consumed[1]);
return true;
}
}
public void nestedScrollingRelease() {
if (mDragState == STATE_NONE_TOUCH) {
dispatchViewReleased(0, 0);
}
}
private void dragTo(int x, int y, int dx, int dy) {
int clampedX = x;
int clampedY = y;
final int oldX = mClampedDistanceX;
final int oldY = mClampedDistanceY;
if (dx != 0) {
clampedX = mSwipeConsumer.clampDistanceHorizontal(x, dx);
mClampedDistanceX = clampedX;
}
if (dy != 0) {
clampedY = mSwipeConsumer.clampDistanceVertical(y, dy);
mClampedDistanceY = clampedY;
}
if (dx != 0 || dy != 0) {
final int clampedDx = clampedX - oldX;
final int clampedDy = clampedY - oldY;
mSwipeConsumer.onSwipeDistanceChanged(clampedX, clampedY, clampedDx, clampedDy);
}
}
public SwipeConsumer getSwipeConsumer() {
return mSwipeConsumer;
}
private boolean isValidPointerForActionMove(int pointerId) {
if (!isPointerDown(pointerId)) {
Log.e(TAG, "Ignoring pointerId=" + pointerId + " because ACTION_DOWN was not received "
+ "for this pointer before ACTION_MOVE. It likely happened because "
+ " SwipeHelper did not receive all the events in the event stream.");
return false;
}
return true;
}
public int getMaxSettleDuration() {
return maxSettleDuration;
}
public void setMaxSettleDuration(int maxSettleDuration) {
this.maxSettleDuration = maxSettleDuration;
}
}
下面看一下目录结构,如下:

该目录结构中的代码仅仅是SmartSwipe
框架的一部分,也是SmartSwipe
框架的基本封装代码。
除了以上两个重要的文件之外,还有其它文件。
【侧滑监听接口】
SwipeListener
/**
* listen swipe state of {@link SwipeConsumer} via {@link SwipeConsumer#addListener(SwipeListener)}
* @author billy.qi
* @see SimpleSwipeListener
*/
public interface SwipeListener {
/**
* Depending on whether SwipeConsumer has been added to the wrapper through {@link SmartSwipeWrapper#addConsumer(SwipeConsumer)},
* This method will be called in 2 cases : <br>
* 1. not added: called when {@link SmartSwipeWrapper#addConsumer(SwipeConsumer)} <br>
* 2. already added: called when added to SwipeConsumer via {@link SwipeConsumer#addListener(SwipeListener)} <br>
*
* This callback method is useful to program auto open or auto close action when SwipeConsumer attached to SmartSwipeWrapper before SwipeConsumer attached to SmartSwipeWrapper
*
* @param wrapper SmartSwipeWrapper the SwipeConsumer add to
* @param consumer the SwipeConsumer this listener add to
* @see SmartSwipeWrapper#addConsumer(SwipeConsumer)
* @see SwipeConsumer#onAttachToWrapper(SmartSwipeWrapper, SwipeHelper)
* @see SwipeConsumer#addListener(SwipeListener)
*/
void onConsumerAttachedToWrapper(SmartSwipeWrapper wrapper, SwipeConsumer consumer);
void onConsumerDetachedFromWrapper(SmartSwipeWrapper wrapper, SwipeConsumer consumer);
void onSwipeStateChanged(SmartSwipeWrapper wrapper, SwipeConsumer consumer, int state, int direction, float progress);
void onSwipeStart(SmartSwipeWrapper wrapper, SwipeConsumer consumer, int direction);
void onSwipeProcess(SmartSwipeWrapper wrapper, SwipeConsumer consumer, int direction, boolean settling, float progress);
void onSwipeRelease(SmartSwipeWrapper wrapper, SwipeConsumer consumer, int direction, float progress, float xVelocity, float yVelocity);
void onSwipeOpened(SmartSwipeWrapper wrapper, SwipeConsumer consumer, int direction);
void onSwipeClosed(SmartSwipeWrapper wrapper, SwipeConsumer consumer, int direction);
}
【侧滑监听实现类】
SimpleSwipeListener
/**
* @author billy.qi
*/
public class SimpleSwipeListener implements SwipeListener {
@Override
public void onConsumerAttachedToWrapper(SmartSwipeWrapper wrapper, SwipeConsumer consumer) {
}
@Override
public void onConsumerDetachedFromWrapper(SmartSwipeWrapper wrapper, SwipeConsumer consumer) {
}
@Override
public void onSwipeStateChanged(SmartSwipeWrapper wrapper, SwipeConsumer consumer, int state, int direction, float progress) {
}
@Override
public void onSwipeStart(SmartSwipeWrapper wrapper, SwipeConsumer consumer, int direction) {
}
@Override
public void onSwipeProcess(SmartSwipeWrapper wrapper, SwipeConsumer consumer, int direction, boolean settling, float progress) {
}
@Override
public void onSwipeRelease(SmartSwipeWrapper wrapper, SwipeConsumer consumer, int direction, float progress, float xVelocity, float yVelocity) {
}
@Override
public void onSwipeOpened(SmartSwipeWrapper wrapper, SwipeConsumer consumer, int direction) {
}
@Override
public void onSwipeClosed(SmartSwipeWrapper wrapper, SwipeConsumer consumer, int direction) {
}
}
【滑动距离计算器监听】
SwipeDistanceCalculator
/**
* swipe distance is the same as pointer move distance by default, this calculator can change the role
* @author billy.qi
*/
public interface SwipeDistanceCalculator {
/**
* calculate swipe distance
* @param swipeDistance pointer move distance
* @param progress current {@link SwipeConsumer} opening progress, value: (from 0F to 1F + {@link SwipeConsumer#getOverSwipeFactor()})
* @return the distance of calculate result for {@link SwipeConsumer} to do business
*/
int calculateSwipeDistance(int swipeDistance, float progress);
/**
* calculate the open distance by this calculator`s role
* @param openDistance the original open distance
* @return calculated open distance
*/
int calculateSwipeOpenDistance(int openDistance);
}
【滑动工具类】
SwipeUtil
/**
* utils
* @author billy.qi
*/
public class SwipeUtil {
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = Math.max(2, CPU_COUNT - 1);
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT + 1;
private static final int KEEP_ALIVE = 10;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "SmartSwipe #" + mCount.getAndIncrement());
}
};
private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<Runnable>(128);
private static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
public static void runInThreadPool(Runnable runnable) {
THREAD_POOL_EXECUTOR.execute(runnable);
}
/**
* return the reverse direction for the given direction
* @param direction the given direction, must be one of: {@link SwipeConsumer#DIRECTION_LEFT}
* /{@link SwipeConsumer#DIRECTION_RIGHT}
* /{@link SwipeConsumer#DIRECTION_TOP}
* /{@link SwipeConsumer#DIRECTION_BOTTOM}
* @return the reverse direction
*/
public static int getReverseDirection(int direction) {
if ((direction & SwipeConsumer.DIRECTION_HORIZONTAL) != 0) {
return (direction ^ SwipeConsumer.DIRECTION_HORIZONTAL) & SwipeConsumer.DIRECTION_HORIZONTAL;
} else {
return (direction ^ SwipeConsumer.DIRECTION_VERTICAL) & SwipeConsumer.DIRECTION_VERTICAL;
}
}
}
【SmartSwipe框架的核心类】
SmartSwipe
SmartSwipe
是SmartSwipe框架最最最核心类之一,它的作用是使用wrap
方法包装一个指定的View(或Activity),最终生成一个侧滑布局类SmartSwipeWrapper
,最后将侧滑布局类SmartSwipeWrapper
添加到被指定的View(或Activity)中。
代码如下:
/**
* A smart swipe util to wrap a view and consume swipe event to do some business via {@link SwipeConsumer}
* classic usage:
* <pre>
* SmartSwipe.wrap(view) //specific the view to wrap
* .addConsumer(new StretchConsumer()) // add consumer to consume swipe event
* .enableVertical(); //enable consumer`s direction(s)
* </pre>
* @author billy.qi
*/
public class SmartSwipe {
/**
* wrap an activity
* the content view is: android.R.id.content
* @param activity activity
* @return the wrapper
*/
public static SmartSwipeWrapper wrap(Activity activity) {
SmartSwipeWrapper wrapper = peekWrapperFor(activity);
if (wrapper != null) {
return wrapper;
}
View decorView = activity.getWindow().getDecorView();
if (decorView instanceof ViewGroup) {
ViewGroup group = (ViewGroup) decorView;
int childCount = group.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = group.getChildAt(i);
if (child.findViewById(android.R.id.content) != null) {
return wrap(child);
}
}
}
View contentView = decorView.findViewById(android.R.id.content);
return wrap(contentView);
}
/**
* peek wrapper for the specific activity, return the origin {@link SmartSwipeWrapper} if exists, else return null
* @param activity activity
* @return the wrapper if exists, otherwise returns null
*/
public static SmartSwipeWrapper peekWrapperFor(Activity activity) {
View decorView = activity.getWindow().getDecorView();
View contentView = decorView.findViewById(android.R.id.content);
while (contentView != null && contentView != decorView) {
if (contentView.getParent() instanceof SmartSwipeWrapper) {
return (SmartSwipeWrapper) contentView.getParent();
}
contentView = (View) contentView.getParent();
}
return null;
}
/**
* wrap a view in activity, view id is specified
* if already wrapped, returns the original wrapper
* @param activity activity
* @param viewId the id of view to be wrapped
* @return the original wrapper or create a new wrapper to wrap the view and replace its place into parent
*/
public static SmartSwipeWrapper wrap(Activity activity, int viewId) {
if (activity != null) {
View view = activity.findViewById(viewId);
if (view != null) {
return wrap(view);
}
}
return null;
}
/**
* wrap a view
* @param view the view to be wrapped
* @return the original wrapper or create a new wrapper to wrap the view and replace its place into parent
*/
public static SmartSwipeWrapper wrap(View view) {
SmartSwipeWrapper wrapper = peekWrapperFor(view);
if (wrapper != null) {
return wrapper;
}
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if (view.getParent() != null) {
ViewGroup viewParent = (ViewGroup) view.getParent();
wrapper = createNewWrapper(view.getContext());
int index = viewParent.indexOfChild(view);
viewParent.removeView(view);
viewParent.addView(wrapper, index, layoutParams);
} else {
wrapper = createNewWrapper(view.getContext());
wrapper.setLayoutParams(layoutParams);
}
wrapper.setContentView(view);
return wrapper;
}
/**
* get wrapper of the specific view
* @param view view to find wrapper
* @return the original wrapper of the specific view
*/
public static SmartSwipeWrapper peekWrapperFor(View view) {
if (view.getParent() instanceof SmartSwipeWrapper) {
return (SmartSwipeWrapper) view.getParent();
}
return null;
}
/**
* switch direction enable for all {@link SwipeConsumer} that already added to the wrapper
* @param view the view which be wrapped
* @param enable true: to enable, false: to disable
* @param direction direction to enable or disable
*/
public static void switchDirectionEnable(View view, boolean enable, int direction) {
enableOrDisableFor(peekWrapperFor(view), enable, direction);
}
public static void switchDirectionEnable(Activity activity, boolean enable, int direction) {
enableOrDisableFor(peekWrapperFor(activity), enable, direction);
}
private static void enableOrDisableFor(SmartSwipeWrapper wrapper, boolean enable, int direction) {
if (wrapper != null) {
wrapper.enableDirection(direction, enable);
}
}
public static int dp2px(int dp, Context context){
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
}
public static int ensureBetween(int origin, int min, int max) {
return Math.max(min, Math.min(origin, max));
}
public static float ensureBetween(float origin, float min, float max) {
return Math.max(min, Math.min(origin, max));
}
public static double ensureBetween(double origin, double min, double max) {
return Math.max(min, Math.min(origin, max));
}
private static IWrapperFactory factory;
private static SmartSwipeWrapper createNewWrapper(Context context) {
final IWrapperFactory factory = SmartSwipe.factory;
if (factory != null) {
SmartSwipeWrapper wrapper = factory.createWrapper(context);
if (wrapper != null) {
return wrapper;
}
}
return new SmartSwipeWrapper(context);
}
/**
* set the factory of {@link SmartSwipeWrapper}
* by default, create the {@link SmartSwipeWrapper} instance to wrap if {@link #factory} is null
* @param factory the factory
* @see #createNewWrapper(Context)
*/
public static void setFactory(IWrapperFactory factory) {
SmartSwipe.factory = factory;
}
public interface IWrapperFactory {
SmartSwipeWrapper createWrapper(Context context);
}
static {
//set wrapper factory automatically
try {
//android x
boolean success = initFactoryByClassName("com.billy.android.swipe.androidx.WrapperFactory");
if (!success) {
//android support
initFactoryByClassName( "com.billy.android.swipe.support.WrapperFactory");
}
} catch(Throwable e) {
e.printStackTrace();
}
}
private static boolean initFactoryByClassName(String factoryClassName) {
Class<?> clazz;
try {
clazz = Class.forName(factoryClassName);
if (clazz != null) {
Object o = clazz.getConstructor().newInstance();
if (o instanceof IWrapperFactory) {
setFactory((IWrapperFactory) o);
}
}
return true;
} catch(Exception ignored) {
}
return false;
}
}
【SmartSwipe框架的核心类】
SwipeConsumer
SwipeConsumer
是一个抽象类,用于实现侧滑的各种状态,与SwipeHelper
结合使用,最终完成侧滑事件
,当侧滑状态改变时,又与SwipeDistanceCalculator
接口结合动态计算滑动距离。作者将它命名为侧滑事件的消费者
,它是一个抽象类,注定不能被直接使用,由于Java的多态特性,它可以被任意扩展,也就是说,这个抽象类会衍生出任意侧滑事件的消费者
,可以说成自定义侧滑效果
。
代码如下:
/**
* contains content view and at most 4 drawer view
* drawer view shows above content view
* default release mode is {@link #RELEASE_MODE_AUTO_OPEN_CLOSE}
*
* @author billy.qi
*/
public class DrawerConsumer extends SwipeConsumer implements View.OnClickListener {
protected final View[] mDrawerViews = new View[4];
protected View mCurDrawerView;
protected int l, t, r, b;
protected int mScrimColor = 0;
protected int mShadowColor = 0;
protected ScrimView mScrimView;
protected int mShadowSize;
protected boolean mDrawerViewRequired = true;
protected boolean mShowScrimAndShadowOutsideContentView;
public DrawerConsumer() {
//set default release mode
setReleaseMode(SwipeConsumer.RELEASE_MODE_AUTO_OPEN_CLOSE);
}
@Override
public void onAttachToWrapper(SmartSwipeWrapper wrapper, SwipeHelper swipeHelper) {
super.onAttachToWrapper(wrapper, swipeHelper);
for (int i = 0; i < mDrawerViews.length; i++) {
attachDrawerView(i);
}
if (mShadowSize == 0) {
//10dp by default
mShadowSize = SmartSwipe.dp2px(10, wrapper.getContext());
}
}
@Override
protected void initChildrenFormXml() {
final SmartSwipeWrapper wrapper = mWrapper;
int childCount = wrapper.getChildCount();
View contentView = wrapper.getContentView();
for (int i = 0; i < childCount; i++) {
View child = wrapper.getChildAt(i);
if (child == contentView || !(child.getLayoutParams() instanceof SmartSwipeWrapper.LayoutParams)) {
continue;
}
final int gravity = ((SmartSwipeWrapper.LayoutParams) child.getLayoutParams()).gravity;
if (mDrawerViews[0] == null && (gravity & DIRECTION_LEFT) == DIRECTION_LEFT) {
// This child is a left drawer
setLeftDrawerView(child);
mWrapper.consumeInflateFromXml();
}
if (mDrawerViews[1] == null && (gravity & DIRECTION_RIGHT) == DIRECTION_RIGHT) {
// This child is a right drawer
setRightDrawerView(child);
mWrapper.consumeInflateFromXml();
}
if (mDrawerViews[2] == null && (gravity & DIRECTION_TOP) == DIRECTION_TOP) {
// This child is a top drawer
setTopDrawerView(child);
mWrapper.consumeInflateFromXml();
}
if (mDrawerViews[3] == null && (gravity & DIRECTION_BOTTOM) == DIRECTION_BOTTOM) {
// This child is a bottom drawer
setBottomDrawerView(child);
mWrapper.consumeInflateFromXml();
}
}
}
@Override
public void onDetachFromWrapper() {
super.onDetachFromWrapper();
if (mScrimView != null) {
mWrapper.removeView(mScrimView);
mScrimView.setOnClickListener(null);
mScrimView = null;
}
for (View drawerView : mDrawerViews) {
if (drawerView != null) {
mWrapper.removeView(drawerView);
}
}
mCurDrawerView = null;
}
@Override
protected void onOpened() {
super.onOpened();
if (mScrimView != null && !mShowScrimAndShadowOutsideContentView) {
mScrimView.setOnClickListener(this);
}
}
@Override
protected void onClosed() {
super.onClosed();
if (mCurDrawerView != null) {
changeDrawerViewVisibility(INVISIBLE);
}
if (mScrimView != null) {
mScrimView.setOnClickListener(null);
mScrimView.setClickable(false);
mScrimView.setFocusable(false);
mScrimView.setVisibility(GONE);
}
}
@Override
public boolean tryAcceptMoving(int pointerId, float downX, float downY, float dx, float dy) {
boolean handle = super.tryAcceptMoving(pointerId, downX, downY, dx, dy);
if (handle && mCachedSwipeDistanceX == 0 && mCachedSwipeDistanceY == 0) {
if (mDrawerViewRequired && getDrawerView(mDirection) == null) {
handle = false;
}
}
return handle;
}
@Override
public void onSwipeAccepted(int activePointerId, boolean settling, float initialMotionX, float initialMotionY) {
if (mCachedSwipeDistanceX == 0 && mCachedSwipeDistanceY == 0) {
changeDrawerViewVisibility(INVISIBLE);
mCurDrawerView = getDrawerView(mDirection);
changeDrawerViewVisibility(VISIBLE);
}
int w = mWidth;
int h = mHeight;
if (mCurDrawerView != null) {
w = mCurDrawerView.getMeasuredWidth();
h = mCurDrawerView.getMeasuredHeight();
} else if (mDrawerViewRequired) {
return;
}
if (!mOpenDistanceSpecified) {
if ((mDirection & DIRECTION_HORIZONTAL) > 0) {
mOpenDistance = w;
} else {
mOpenDistance = h;
}
}
calculateDrawerDirectionInitPosition(mDirection, w, h);
changeDrawerViewVisibility(VISIBLE);
initScrimView();
layoutChildren();
orderChildren();
super.onSwipeAccepted(activePointerId, settling, initialMotionX, initialMotionY);
}
protected void changeDrawerViewVisibility(int visibility) {
if (mCurDrawerView != null) {
mCurDrawerView.setVisibility(visibility);
}
}
@Override
public void setCurrentStateAsClosed() {
mCurDrawerView = null;
super.setCurrentStateAsClosed();
}
protected void initScrimView() {
if (mScrimColor != 0 || mShadowColor != 0 && mShadowSize > 0) {
if (mScrimView == null) {
mScrimView = new ScrimView(mWrapper.getContext());
mWrapper.addView(mScrimView);
}
mScrimView.setScrimColor(mScrimColor);
if (mShadowColor != 0 && mShadowSize > 0) {
int shadowDirection = this.mDirection;
if (mShowScrimAndShadowOutsideContentView) {
shadowDirection = SwipeUtil.getReverseDirection(mDirection);
}
mScrimView.setDirection(this.mDirection, mShadowColor, shadowDirection, mShadowSize, mWidth, mHeight);
}
mScrimView.setVisibility(VISIBLE);
}
}
protected void calculateDrawerDirectionInitPosition(int direction, int w, int h) {
switch (direction) {
case DIRECTION_LEFT: l = -w; r = l + w; t = 0; b = h; break;
case DIRECTION_RIGHT: l = mWidth; r = l + w; t = 0; b = h; break;
case DIRECTION_TOP: l = 0; r = mWidth; t = -h; b = t + h; break;
case DIRECTION_BOTTOM: l = 0; r = mWidth; t = mHeight;b = t + h; break;
default: break;
}
}
@Override
public boolean onLayout(boolean changed, int left, int top, int right, int bottom) {
if (mWrapper != null) {
layoutChildren();
return true;
}
return false;
}
@Override
protected void onDisplayDistanceChanged(int distanceXToDisplay, int distanceYToDisplay, int dx, int dy) {
View drawerView = mCurDrawerView;
if (drawerView != null && drawerView.getParent() == mWrapper) {
boolean horizontal = (mDirection & DIRECTION_HORIZONTAL) > 0;
if (horizontal) {
ViewCompat.offsetLeftAndRight(drawerView, dx);
} else {
ViewCompat.offsetTopAndBottom(drawerView, dy);
}
layoutScrimView();
}
}
protected void orderChildren() {
if (mCurDrawerView != null) {
mCurDrawerView.bringToFront();
}
if (mScrimView != null) {
mScrimView.bringToFront();
}
}
protected void layoutChildren() {
layoutContentView(mWrapper.getContentView());
layoutDrawerView();
layoutScrimView();
}
protected void layoutContentView(View contentView) {
if (contentView != null) {
contentView.layout(0, 0, mWidth, mHeight);
}
}
protected void layoutDrawerView() {
if (mCurDrawerView != null && mCurDrawerView.getVisibility() == VISIBLE) {
mCurDrawerView.layout(l + mCurDisplayDistanceX, t + mCurDisplayDistanceY, r + mCurDisplayDistanceX, b + mCurDisplayDistanceY);
}
}
protected void layoutScrimView() {
if (mScrimView != null && mScrimView.getVisibility() == VISIBLE) {
int l = 0, r = mWidth, t = 0, b = mHeight;
if (mShowScrimAndShadowOutsideContentView) {
switch (mDirection) {
case DIRECTION_LEFT: r = mCurDisplayDistanceX; break;
case DIRECTION_RIGHT: l = r + mCurDisplayDistanceX; break;
case DIRECTION_TOP: b = mCurDisplayDistanceY; break;
case DIRECTION_BOTTOM: t = b + mCurDisplayDistanceY; break;
default:
}
} else {
switch (mDirection) {
case DIRECTION_LEFT: l = mCurDisplayDistanceX; break;
case DIRECTION_RIGHT: r = r + mCurDisplayDistanceX; break;
case DIRECTION_TOP: t = mCurDisplayDistanceY; break;
case DIRECTION_BOTTOM: b = b + mCurDisplayDistanceY; break;
default:
}
}
mScrimView.layout(l, t, r, b);
mScrimView.setProgress(mShowScrimAndShadowOutsideContentView ? (1 - mProgress) : mProgress);
}
}
@Override
protected void notifySwipeStart() {
if (mCurDrawerView instanceof SwipeListener) {
((SwipeListener)mCurDrawerView).onSwipeStart(mWrapper, this, mDirection);
}
super.notifySwipeStart();
}
@Override
protected void notifySwipeProgress(boolean settling) {
if (mCurDrawerView instanceof SwipeListener) {
((SwipeListener) mCurDrawerView).onSwipeProcess(mWrapper, this, mDirection, settling, mProgress);
}
super.notifySwipeProgress(settling);
}
@Override
protected void notifySwipeRelease(float xVelocity, float yVelocity) {
if (mCurDrawerView instanceof SwipeListener) {
((SwipeListener) mCurDrawerView).onSwipeRelease(mWrapper, this, mDirection, mProgress, xVelocity, yVelocity);
}
super.notifySwipeRelease(xVelocity, yVelocity);
}
public View getDrawerView(int direction) {
int viewIndex = -1;
switch (direction) {
default: break;
case DIRECTION_LEFT: viewIndex = 0; break;
case DIRECTION_RIGHT: viewIndex = 1; break;
case DIRECTION_TOP: viewIndex = 2; break;
case DIRECTION_BOTTOM: viewIndex = 3; break;
}
if (viewIndex < 0) {
return null;
}
return mDrawerViews[viewIndex];
}
public DrawerConsumer setLeftDrawerView(View drawerView) {
return setDrawerView(DIRECTION_LEFT, drawerView);
}
public DrawerConsumer setRightDrawerView(View drawerView) {
return setDrawerView(DIRECTION_RIGHT, drawerView);
}
public DrawerConsumer setTopDrawerView(View drawerView) {
return setDrawerView(DIRECTION_TOP, drawerView);
}
public DrawerConsumer setBottomDrawerView(View drawerView) {
return setDrawerView(DIRECTION_BOTTOM, drawerView);
}
public DrawerConsumer setHorizontalDrawerView(View drawerView) {
return setDrawerView(DIRECTION_HORIZONTAL, drawerView);
}
public DrawerConsumer setVerticalDrawerView(View drawerView) {
return setDrawerView(DIRECTION_VERTICAL, drawerView);
}
public DrawerConsumer setAllDirectionDrawerView(View drawerView) {
return setDrawerView(DIRECTION_ALL, drawerView);
}
/**
* set a extension to the direction, also set direction enable if drawerView is not null(otherwise, disable the direction)
* direction can be a single direction or mixed direction(eg: DIRECTION_LEFT | DIRECTION_RIGHT)
* @param direction direction to set
* @param drawerView view
* @return this
*/
public DrawerConsumer setDrawerView(int direction, View drawerView) {
enableDirection(direction, drawerView != null);
if ((direction & DIRECTION_LEFT) > 0) {
setOrUpdateDrawerView(0, drawerView);
}
if ((direction & DIRECTION_RIGHT) > 0) {
setOrUpdateDrawerView(1, drawerView);
}
if ((direction & DIRECTION_TOP) > 0) {
setOrUpdateDrawerView(2, drawerView);
}
if ((direction & DIRECTION_BOTTOM) > 0) {
setOrUpdateDrawerView(3, drawerView);
}
return this;
}
private void setOrUpdateDrawerView(int index, View drawerView) {
View oldView = mDrawerViews[index];
if (oldView == drawerView) {
return;
}
mDrawerViews[index] = drawerView;
attachDrawerView(index);
}
private void attachDrawerView(final int index) {
final View drawerView = mDrawerViews[index];
final SmartSwipeWrapper wrapper = mWrapper;
if (drawerView != null && wrapper != null && drawerView.getParent() != wrapper) {
if (drawerView.getParent() != null) {
((ViewGroup)drawerView.getParent()).removeView(drawerView);
}
int contentViewIndex = wrapper.indexOfChild(wrapper.getContentView());
if (contentViewIndex >= 0) {
ViewGroup.LayoutParams lp = drawerView.getLayoutParams();
if (lp == null) {
int w = FrameLayout.LayoutParams.WRAP_CONTENT, h = FrameLayout.LayoutParams.WRAP_CONTENT;
switch (index) {
default: break;
case 0: case 1: h = FrameLayout.LayoutParams.MATCH_PARENT; break;
case 2: case 3: w = FrameLayout.LayoutParams.MATCH_PARENT; break;
}
lp = new FrameLayout.LayoutParams(w, h);
drawerView.setLayoutParams(lp);
}
wrapper.addView(drawerView, contentViewIndex);
drawerView.setVisibility(INVISIBLE);
}
}
}
@Override
public int getOpenDistance() {
if (mCurDrawerView == null) {
return super.getOpenDistance();
}
if ((mDirection & DIRECTION_HORIZONTAL) > 0) {
return mCurDrawerView.getMeasuredWidth();
}
return mCurDrawerView.getMeasuredHeight();
}
/**
* Set a color to use for the scrim that obscures primary content while a drawer is open.
* @param color Color to use in 0xAARRGGBB format.
* @return this
*/
public DrawerConsumer setScrimColor(int color) {
mScrimColor = color;
return this;
}
/**
* Set a color to use for the shadow at the edge of content view while a drawer is open.
* @param shadowColor Color to use in 0xAARRGGBB format.
* @return this
*/
public DrawerConsumer setShadowColor(int shadowColor) {
mShadowColor = shadowColor;
return this;
}
public int getShadowSize() {
return mShadowSize;
}
/**
* set the size of shadow at the edge of content view while a drawer is open.
* @param size shadow size in pixel
* @return this
*/
public DrawerConsumer setShadowSize(int size) {
this.mShadowSize = size;
return this;
}
public boolean isDrawerViewRequired() {
return mDrawerViewRequired;
}
/**
* set the extension view as drawer is required or not
* it useful inside this sdk framework,
* developers who use this SDK do not call this function unless you really know what its mean
* @param required required or not
* @return this
*/
public DrawerConsumer setDrawerViewRequired(boolean required) {
this.mDrawerViewRequired = required;
return this;
}
public boolean isScrimAndShadowOutsideContentView() {
return mShowScrimAndShadowOutsideContentView;
}
public DrawerConsumer showScrimAndShadowOutsideContentView() {
this.mShowScrimAndShadowOutsideContentView = true;
return this;
}
public DrawerConsumer showScrimAndShadowInsideContentView() {
this.mShowScrimAndShadowOutsideContentView = false;
return this;
}
@Override
public void onClick(View v) {
if (getDragState() == SwipeHelper.STATE_IDLE && !mShowScrimAndShadowOutsideContentView && v == mScrimView) {
smoothClose();
}
}
}
【互斥组】
SwipeConsumerExclusiveGroup
管理一组SwipeConsumer
,在这个组内的SwipeConsumer
打开状态是互斥的,同时只能有0个或1个SwipeConsumer处于打开状态,打开一个,其它的都将自动关闭。
代码如下:
/**
* manage a group of SwipeConsumer(s), only single one SwipeConsumer can be mark as the current one, the original SwipeConsumer will be close
* @author billy.qi
*/
public class SwipeConsumerExclusiveGroup {
private List<SwipeConsumer> list = new LinkedList<>();
private SwipeConsumer curSwipeConsumer;
private boolean smooth;
private boolean lockOther = false;
public SwipeConsumerExclusiveGroup() {
this.smooth = true;
}
/**
* create a group, specific close mode smoothly or not
* @param smooth specific close mode smoothly or not
*/
public SwipeConsumerExclusiveGroup(boolean smooth) {
this.smooth = smooth;
}
public void markNoCurrent() {
if (curSwipeConsumer != null) {
curSwipeConsumer.close(smooth);
curSwipeConsumer = null;
}
if (lockOther) {
for (SwipeConsumer consumer : list) {
if (consumer.isAllDirectionsLocked()) {
consumer.unlockAllDirections();
}
}
}
}
public void markAsCurrent(SwipeConsumer consumer) {
markAsCurrent(consumer, smooth);
}
public void markAsCurrent(SwipeConsumer current, boolean smoothResetOrigin) {
if (this.curSwipeConsumer == current) {
return;
}
this.curSwipeConsumer = current;
for(SwipeConsumer consumer : list) {
if (consumer != curSwipeConsumer) {
if (lockOther && !consumer.isAllDirectionsLocked()) {
consumer.lockAllDirections();
}
consumer.close(smoothResetOrigin);
}
}
}
public void add(SwipeConsumer consumer) {
if (!list.contains(consumer)) {
list.add(consumer);
consumer.addListener(singleListener);
}
}
public void remove(SwipeConsumer consumer) {
if (consumer != null) {
list.remove(consumer);
consumer.removeListener(singleListener);
}
}
public void clear() {
while(!list.isEmpty()) {
SwipeConsumer consumer = list.remove(0);
if (consumer != null) {
consumer.removeListener(singleListener);
}
}
}
private SimpleSwipeListener singleListener = new SimpleSwipeListener() {
@Override
public void onSwipeOpened(SmartSwipeWrapper wrapper, SwipeConsumer consumer, int direction) {
markAsCurrent(consumer);
}
@Override
public void onSwipeClosed(SmartSwipeWrapper wrapper, SwipeConsumer consumer, int direction) {
if (consumer == curSwipeConsumer) {
markNoCurrent();
}
}
};
public boolean isLockOther() {
return lockOther;
}
public void setLockOther(boolean lockOther) {
this.lockOther = lockOther;
}
public SwipeConsumer getCurSwipeConsumer() {
return curSwipeConsumer;
}
public boolean isSmooth() {
return smooth;
}
public void setSmooth(boolean smooth) {
this.smooth = smooth;
}
}
【两个辅助类】
ScrimView和ViewCompat
这两个辅助类的代码,请在下一篇查看。
到这里,所有的封装代码已经完成了,至于侧滑消费者
的扩展后面的章节会讲到。
[本章完...]
网友评论