美文网首页
模仿淘宝商品详情页,实现上下两个FrameLayout切换,并解

模仿淘宝商品详情页,实现上下两个FrameLayout切换,并解

作者: 丶剑起沧澜 | 来源:发表于2018-02-02 15:45 被阅读0次

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

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

    布局中使用:

        <DragLayout
            android:id="@+id/slideLl"
             android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <FrameLayout
                android:id="@+id/one"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
    
            <FrameLayout
                android:id="@+id/two"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
        </DragLayout>
    

    在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可以直接在布局中使用,使用方式为:

        <com.ncj.mvp.demo.view.DragScrollView
                android:id="@+id/drag_scroll_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scrollbars="none">
    
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:orientation="vertical">
                    //你的内容
                </LinearLayout>
         </com.ncj.mvp.demo.view.DragScrollView>
    

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

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

    初始化后添加:

        scrollView.setMode(DragScrollView.BOTTOM_MODE);
    

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

    相关文章

      网友评论

          本文标题:模仿淘宝商品详情页,实现上下两个FrameLayout切换,并解

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