模仿淘宝商品详情页,实现上下两个FrameLayout切换,并解决在两个Fragment中使用ScrollView冲突解决

实现淘宝详情页上下两个界面弹性切换。在此做个笔记,可以直接复制使用。

    import android.annotation.SuppressLint;
    import android.content.Context;
    import android.support.v4.view.GestureDetectorCompat;
    import android.support.v4.view.ViewCompat;
    import android.support.v4.widget.ViewDragHelper;
    import android.util.AttributeSet;
    import android.view.GestureDetector.SimpleOnGestureListener;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.ViewGroup;
    
    /**
     * 这是一个viewGroup容器,实现上下两个frameLayout拖动切换
     *
     * @author ncj
     */
    @SuppressLint("NewApi")
    public class DragLayout extends ViewGroup {

        /* 拖拽工具类 */
        private final ViewDragHelper mDragHelper;
        private GestureDetectorCompat gestureDetector;

        /* 上下两个frameLayout,在Activity中注入fragment */
        private View frameView1, frameView2;
        private int viewHeight;
        private static final int VEL_THRESHOLD = 100; // 滑动速度的阈值,超过这个绝对值认为是上下
        private static final int DISTANCE_THRESHOLD = 100; // 单位是像素,当上下滑动速度不够时,通过这个        阈        值来判定是应该粘到顶部还是底部
        private int downTop1; // 手指按下的时候,frameView1的getTop值
        private ShowNextPageNotifier nextPageListener; // 手指松开是否加载下一页的notifier

        public DragLayout(Context context) {
            this(context, null);
        }

        public DragLayout(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }

        public DragLayout(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            mDragHelper = ViewDragHelper
                    .create(this, 10f, new DragHelperCallback());
            mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_BOTTOM);
            gestureDetector = new GestureDetectorCompat(context,
                    new YScrollDetector());
        }

        @Override
        protected void onFinishInflate() {
            // 跟findviewbyId一样,初始化上下两个view
            frameView1 = getChildAt(0);
            frameView2 = getChildAt(1);
        }

        class YScrollDetector extends SimpleOnGestureListener {

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx,
                            float dy) {
        // 垂直滑动时dy>dx,才被认定是上下拖动
        return Math.abs(dy) > Math.abs(dx);
    }
}

        @Override
        public void computeScroll() {
            if (mDragHelper.continueSettling(true)) {
                ViewCompat.postInvalidateOnAnimation(this);
            }
        }

        /**
         * 这是拖拽效果的主要逻辑
         */
        private class DragHelperCallback extends ViewDragHelper.Callback {

            @Override
            public void onViewPositionChanged(View changedView, int left, int top,
                                              int dx, int dy) {
                int childIndex = 1;
                if (changedView == frameView2) {
                    childIndex = 2;
                }

                // 一个view位置改变,另一个view的位置要跟进
                onViewPosChanged(childIndex, top);
            }

            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                // 两个子View都需要跟踪,返回true
                return true;
            }

            @Override
            public int getViewVerticalDragRange(View child) {
                // 这个用来控制拖拽过程中松手后,自动滑行的速度,暂时给一个随意的数值
                return 1;
            }

            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                // 滑动松开后,需要向上或者乡下粘到特定的位置
                animTopOrBottom(releasedChild, yvel);
            }

            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                int finalTop = top;
                if (child == frameView1) {
                    // 拖动的时第一个view
                    if (top > 0) {
                        // 不让第一个view往下拖,因为顶部会白板
                        finalTop = 0;
                    }
                } else if (child == frameView2) {
                    // 拖动的时第二个view
                    if (top < 0) {
                             // 不让第二个view网上拖,因为底部会白板
                        finalTop = 0;
                    }
                }

                // finalTop代表的是理论上应该拖动到的位置。此处计算拖动的距离除以一个参数(3),是让滑动的速度变慢。数值越大,滑动的越慢
                return child.getTop() + (finalTop - child.getTop()) / 3;
            }
        }

        /**
         * 滑动时view位置改变协调处理
         *
         * @param viewIndex
         *            滑动view的index(1或2)
         * @param posTop
         *            滑动View的top位置
         */
        private void onViewPosChanged(int viewIndex, int posTop) {
            if (viewIndex == 1) {
                int offsetTopBottom = viewHeight + frameView1.getTop()
                        - frameView2.getTop();
                frameView2.offsetTopAndBottom(offsetTopBottom);
            } else if (viewIndex == 2) {
                int offsetTopBottom = frameView2.getTop() - viewHeight
                        - frameView1.getTop();
                        frameView1.offsetTopAndBottom(offsetTopBottom);
            }

            // 有的时候会默认白板,这个很恶心。后面有时间再优化
            invalidate();
        }

        private void animTopOrBottom(View releasedChild, float yvel) {
            int finalTop = 0; // 默认是粘到最顶端
            if (releasedChild == frameView1) {
                // 拖动第一个view松手
                if (yvel < -VEL_THRESHOLD
                        || (downTop1 == 0 && frameView1.getTop() < -DISTANCE_THRESHOLD)) {
                    // 向上的速度足够大,就滑动到顶端
                    // 向上滑动的距离超过某个阈值,就滑动到顶端
                    finalTop = -viewHeight;

                    // 下一页可以初始化了
                    if (null != nextPageListener) {
                        nextPageListener.onDragNext();
                    }
                }
            } else {
                // 拖动第二个view松手
                if (yvel > VEL_THRESHOLD
                || (downTop1 == -viewHeight && releasedChild.getTop() > DISTANCE_THRESHOLD)) {
                    // 保持原地不动
                    finalTop = viewHeight;
                }
            }

            if (mDragHelper.smoothSlideViewTo(releasedChild, 0, finalTop)) {
                ViewCompat.postInvalidateOnAnimation(this);
            }
        }

        /* touch事件的拦截与处理都交给mDraghelper来处理 */
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {

            if (frameView1.getBottom() > 0 && frameView1.getTop() < 0) {
                // view粘到顶部或底部,正在动画中的时候,不处理touch事件
                return false;
            }

            boolean yScroll = gestureDetector.onTouchEvent(ev);
            boolean shouldIntercept = mDragHelper.shouldInterceptTouchEvent(ev);
            int action = ev.getActionMasked();

            if (action == MotionEvent.ACTION_DOWN) {
                // action_down时就让mDragHelper开始工作,否则有时候导致异常 他大爷的
                mDragHelper.processTouchEvent(ev);
                downTop1 = frameView1.getTop();
            }

            return shouldIntercept && yScroll;
        }

        @Override
        public boolean onTouchEvent(MotionEvent e) {
            // 统一交给mDragHelper处理,由DragHelperCallback实现拖动效果
            mDragHelper.processTouchEvent(e); // 该行代码可能会抛异常,正式发布时请将这行代码加上try catch
            return true;
        }

        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            // 只在初始化的时候调用
            // 一些参数作为全局变量保存起来
            frameView1.layout(l, 0, r, b - t);
            frameView2.layout(l, 0, r, b - t);

            viewHeight = frameView1.getMeasuredHeight();
            frameView2.offsetTopAndBottom(viewHeight);
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            measureChildren(widthMeasureSpec, heightMeasureSpec);

            int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
            int maxHeight = MeasureSpec.getSize(heightMeasureSpec);
            setMeasuredDimension(
                    resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
                    resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
        }

        /**
         * 这是View的方法,该方法不支持android低版本(2.2、2.3)的操作系统,所以手动复制过来以免强制退出
         */
        public static int resolveSizeAndState(int size, int measureSpec,
                                              int childMeasuredState) {
            int result = size;
            int specMode = MeasureSpec.getMode(measureSpec);
            int specSize = MeasureSpec.getSize(measureSpec);
            switch (specMode) {
                case MeasureSpec.UNSPECIFIED:
                    result = size;
                    break;
                case MeasureSpec.AT_MOST:
                    if (specSize < size) {
                        result = specSize | MEASURED_STATE_TOO_SMALL;
                    } else {
                        result = size;
                   }
                    break;
                case MeasureSpec.EXACTLY:
                    result = specSize;
                    break;
            }
            return result | (childMeasuredState & MEASURED_STATE_MASK);
        }

        public void setNextPageListener(ShowNextPageNotifier nextPageListener) {
            this.nextPageListener = nextPageListener;
        }

        public interface ShowNextPageNotifier {
            public void onDragNext();
        }
    }

布局中使用:

    

        

        
    

在Activity中使用:

/**

* 框架
*/
public class DragActivity extends BaseActivity implements ShowNextPageNotifier {
     @BindView(R.id.dl_layout)
     DragLayout dragLayout;

     private FragmentProductOne mOneFrament;
     private FragmentProductTwo mTwoFrament;

    @Override
    protected int getResId() {
        return R.layout.activity_product_detail;
    }

    @Override
    protected void initView() {
        mOneFrament = new FragmentProductOne();
        mTwoFrament = new FragmentProductTwo();
        getSupportFragmentManager().beginTransaction()
                .add(R.id.fy_fragment_one, mOneFrament)
                .add(R.id.fy_fragment_two, mTwoFrament).commit();
        dragLayout.setNextPageListener(this);
    }




    @Override
  public void onDragNext() {
  }
}

到这里不算完事,大家可以看到淘宝页会有两个能滑动的界面,使用的布局中存在着ScrollView,这样就会有滑动手势冲突,在这里给大家自定义一个使用的ScrollView 能完美解决这个问题:

    import android.content.Context;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.ViewConfiguration;
    import android.widget.ScrollView;

    /**
     * Created by ncj on 2018/2/6.
     */

    public class DragScrollView extends ScrollView {
        private static final int TOUCH_IDLE = 0;
        private static final int TOUCH_INNER_CONSIME = 1; // touch事件由ScrollView内部消费
        private static final int TOUCH_DRAG_LAYOUT = 2; // touch事件由上层的DragLayout去消费

        public static final int TOP_MODEL = 1;
        public static final int BOTTOM_MODE = 2;

        private int model = 1;
        boolean isAtBottom; // 按下的时候是否在底部
        boolean isAtTop;    // 按下的时候是否在顶部
        private int mTouchSlop = 4; // 判定为滑动的阈值,单位是像素
        private int scrollMode;
        private float downY;

        public DragScrollView(Context arg0) {
            this(arg0, null);
        }

        public DragScrollView(Context arg0, AttributeSet arg1) {
            this(arg0, arg1, 0);
        }

        public DragScrollView(Context arg0, AttributeSet arg1, int arg2) {
            super(arg0, arg1, arg2);
            ViewConfiguration configuration = ViewConfiguration.get(getContext());
            mTouchSlop = configuration.getScaledTouchSlop();
        }

        @Override
        protected void onFinishInflate() {
            super.onFinishInflate();

            //子View一定要Clickable才行,否则onInterceptTouchEvent工作不按正常来
            getChildAt(0).setClickable(true);
        }

        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                downY = ev.getRawY();
                isAtBottom = isAtBottom();
                isAtTop = isAtTop();
                scrollMode = TOUCH_IDLE;
                getParent().requestDisallowInterceptTouchEvent(true);
            } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
                if (scrollMode == TOUCH_IDLE) {
                    float yOffset = downY - ev.getRawY();
                    float yDistance = Math.abs(yOffset);
                    if (yDistance > mTouchSlop) {
                        if (yOffset > 0 && isAtBottom && (model == BOTTOM_MODE)) {
                            scrollMode = TOUCH_DRAG_LAYOUT;
                            getParent().requestDisallowInterceptTouchEvent(false);
                            return true;
                        } else if (yOffset < 0 && isAtTop && (model == TOP_MODEL)) {
                            scrollMode = TOUCH_DRAG_LAYOUT;
                            getParent().requestDisallowInterceptTouchEvent(false);
                            return true;
                        } else {
                            scrollMode = TOUCH_INNER_CONSIME;
                        }
                    }
                }
            }
            return super.onInterceptTouchEvent(ev);
        }

        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            if (scrollMode == TOUCH_DRAG_LAYOUT) {
                return false;
            }
            return super.onTouchEvent(ev);
        }

        private boolean isAtBottom() {
            return getScrollY() + getMeasuredHeight() >= computeVerticalScrollRange() - 2;
        }

        private boolean isAtTop() {
            return getScrollY() == 0;
        }

        public void setMode(int model) {
            this.model = model;
        }

    }

DragScrollView可以直接在布局中使用,使用方式为:

    

            
                //你的内容
            
     

使用的时候在第一个Fragment里面添加

    @BindView(R.id.drag_scroll_view)
      DragScrollView scrollView;

初始化后添加:

    scrollView.setMode(DragScrollView.BOTTOM_MODE);

这样就能解决两个上下页面有滑动事件的冲突问题!

你可能感兴趣的:(模仿淘宝商品详情页,实现上下两个FrameLayout切换,并解决在两个Fragment中使用ScrollView冲突解决)