美文网首页高级UI安卓Android
侧滑效果[第三篇]:侧滑框架SmartSwipe之封装

侧滑效果[第三篇]:侧滑框架SmartSwipe之封装

作者: NoBugException | 来源:发表于2019-11-11 10:57 被阅读0次

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;
    }
}

下面看一下目录结构,如下:

图片.png

该目录结构中的代码仅仅是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

这两个辅助类的代码,请在下一篇查看。

到这里,所有的封装代码已经完成了,至于侧滑消费者的扩展后面的章节会讲到。

[本章完...]

相关文章

网友评论

    本文标题:侧滑效果[第三篇]:侧滑框架SmartSwipe之封装

    本文链接:https://www.haomeiwen.com/subject/jcudbctx.html