Android 仿京东商品详情页下拉加载商品详情DragSlideLayout

       上一篇中给大家介绍了一下自定义View和ViewGroup的流程,并用FlowLayout这个例子给大家演示了一下自定义ViewGroup中onMeasure和onLayout的使用:https://blog.csdn.net/u013107751/article/details/81701606 

       今天就写个简单的小例子跟大家一起探讨一下自定义View中的事件处理,我们先来看一下要完成效果:

        京东商品详情页大体就是进去的时候展示上面头部商品价格,下单预计送达时间,商品部分评价等等,然后是上滑到底部的时候如果在继续往下滑就滑进商品详情页.根据这个需求,想来想去现有的控件好像都没有能很好解决的,这个时候我们就要考虑使用自定义的ViewGroup了.通过自定义一个ViewGroup然后放入头部一个RecyclerView和一个底部商品详情介绍可以满足该功能:

public class DragSlideLayout extends ViewGroup {

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

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

    public DragSlideLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }
   
}

定义一个DragSlideLayout继承ViewGroup,重写onMeasure,onLayout这两个方法是必须重写的,不然子View会显示不出来,onFinishInflate则是在XML加载完成的时候我们用来获取DragSlideLayout里面包含的子View的,onInterceptTouchEvent和onTouchEvent则是用来处理拖动的时候的事件处理的,至于对事件的分发机制还不太熟的童鞋可以先移步:https://mp.csdn.net/postedit/81667157 这边看一下ViewGroup的事件分发机制,这边不做过多的详解.

接下来我们先运用在XML中:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="#fff">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="商品详情页" />
        </FrameLayout>

        <fun.hxy.com.jddragslidelayout.DragSlideLayout
            android:id="@+id/tbl"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
            <slide.VerticalTopRecyclerView
                android:id="@+id/header"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scrollbars="none" />
             <slide.VerticalBottomRecyclerView
                android:id="@+id/footer"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scrollbars="none" />
        </fun.hxy.com.jddragslidelayout.DragSlideLayout>

    </LinearLayout>
</FrameLayout>

然后在MainActivity中填充一下RecyclerView的数据

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RecyclerView footer = (RecyclerView) findViewById(R.id.footer);
        RecyclerView header = (RecyclerView) findViewById(R.id.header);
        footer.setLayoutManager(new LinearLayoutManager(this));
        header.setLayoutManager(new LinearLayoutManager(this));
        List list = new ArrayList();
        for (int i = 0; i < 30; i++) {
            list.add("我是商品"+i);
        }
        List list2 = new ArrayList();
        for (int i = 0; i < 30; i++) {
            list2.add("我是头部"+i);
        }
        footer.setAdapter(new MyRecycleAdapter<String>(this,list) {
            @Override
            public void getItemView(MyRecycleViewHolder holder, int position, String item) {
                holder.setText(R.id.tv_name,item);
            }

            @Override
            public int getItemResource() {
                return R.layout.item;
            }
        });
        header.setAdapter(new MyRecycleAdapter<String>(this,list2) {
            @Override
            public void getItemView(MyRecycleViewHolder holder, int position, String item) {
                holder.setText(R.id.tv_name,item);
                holder.getView(R.id.tv_name).setBackgroundColor(Color.YELLOW);
            }

            @Override
            public int getItemResource() {
                return R.layout.item;
            }
        });
    }

MyRecycleAdapter只是自己在重写的一个RecyclerView的adapter为了简化代码,我相信大家都有这种控件,这边就不做过多的解释,好了,我们现在运行一下看一下效果

我们可以看到现在是白茫茫的一片,什么都没有,这是因为我们重写的onMeasure和onLayout都是空实现,现在我们来写一下这两个方法

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

onMeasure很简单,让子View计算一下自己的宽高,然后ViewGroup自己计算一下自己的宽高,然后要onLayout,我们首先在

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

然后onLayout:

    if (changed){
            if (headerRecycler.getTop() == 0) {
                // 只在初始化的时候调用
                // 一些参数作为全局变量保存起来
                headerRecycler.layout(l, 0, r, b - t);
                footRecycler.layout(l, 0, r, b - t);

                viewHeight = headerRecycler.getMeasuredHeight();
                footRecycler.offsetTopAndBottom(viewHeight);
            } else {
                // 如果已被初始化,这次onLayout只需要将之前的状态存入即可
                headerRecycler.layout(l, headerRecycler.getTop(), r, headerRecycler.getBottom());
                footRecycler.layout(l, footRecycler.getTop(), r, footRecycler.getBottom());
            }
        }

让两个子View上下摆放,然后我在来运行一下看看效果:

我们发现,只显示了头部的RecyclerView的数据,底部的拉不出来,这是为什么呢?别急,我们这不是还有两个重写的方法没实现的吗,我们现在来写一下onInterceptTouchEvent和onTouchEvent这两个方法

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        // 统一交给mDragHelper处理,由DragHelperCallback实现拖动效果
        try {
            mDragHelper.processTouchEvent(e);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return true;
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (headerRecycler.getBottom() > 0 && headerRecycler.getTop() < 0) {
            // view粘到顶部或底部,正在动画中的时候,不处理touch事件
            return false;
        }

        boolean yScroll = gestureDetector.onTouchEvent(ev); //判断是否是竖直方向的滑动
        boolean shouldIntercept = mDragHelper.shouldInterceptTouchEvent(ev);//是否拦截子View的触摸事件
        int action = ev.getActionMasked();

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

        return shouldIntercept && yScroll;
    }

这里引入两个类ViewDragHelper和GestureDetector,这两个又是什么东西呢?我们来看一下

ViewDragHelper 是收录在 v4 兼容包中一个工具类,它的目的是辅助自定义 ViewGroup。ViewDragHelper 针对 ViewGroup 中的拖拽和重新定位 views 操作时提供了一系列非常有用的方法和状态追踪.用这个类就不用我们自己在onTouchEvent实现拖拽的方法了

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

        // 决定了是否需要捕获这个 child,只有捕获了才能进行下面的拖拽行为
        @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);
        }

        // 修整 child 垂直方向上的坐标,top 指 child 要移动到的坐标,dy 相对上次的偏移量
        @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;
        }
    }

这就是ViewDragHelper的回调,主要就是用来处理手指触摸放手回弹等等.

GestureDetector:这也是自定义View中的一个神器,通常用来处理手指点击,滑动,长按等事件,我们这边只用到了onScroll方法用来判断手指的滑动方向

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

处理完这波手势操作我们在来看一下效果;

咦!!!滑是滑得动了,可是好像跟我们要的效果不太一样啊,明明头部的RecyclerView的数据都还没展示完呢就滑到底部去了???这是为什么呢???

别急,这是因为我们现在默认把事件全部都交给父View处理了,内部的RecyclerView接收不到事件了,我们现在来改写一下RecyclerView的事件处理,让父View该拦截的时候在拦截,不该拦截的时候就不拦截了:

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            downY = ev.getRawY();
            needConsumeTouch = true; // 默认情况下,RecyclerView内部的滚动优先,默认情况下由该RecyclerView去消费touch事件
            allowDragBottom = isAtBottom();
        } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
            allowDragBottom = isAtBottom();

            if (!needConsumeTouch) {
                // 在最顶端且向上拉了,则这个touch事件交给父类去处理
                getParent().requestDisallowInterceptTouchEvent(false);
                return false;
            } else if (allowDragBottom) {
                // needConsumeTouch尚未被定性,此处给其定性
                // 允许拖动到底部的下一页,而且又向上拖动了,就将touch事件交给父view
                if (downY - ev.getRawY()> 2) {
                    // flag设置,由父类去消费
                    needConsumeTouch = false;
                    getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
                }
            }
        }

        // 通知父view是否要处理touch事件
        getParent().requestDisallowInterceptTouchEvent(needConsumeTouch);
        return super.dispatchTouchEvent(ev);
    }

    /**
     * 判断listView是否在顶部
     *
     * @return 是否在顶部
     */
    private boolean isAtBottom() {
        boolean resultValue = false;
        int childNum = getChildCount();
        if (childNum == 0) {
            // 没有child,肯定在顶部
            resultValue = true;
        } else {
            LayoutManager layoutManager = getLayoutManager();
            if (layoutManager instanceof LinearLayoutManager) {
                LinearLayoutManager manager = (LinearLayoutManager) layoutManager;
                int childCount = manager.getItemCount();
                if (manager.findLastVisibleItemPosition() == childCount-1) {
                    // 根据第一个childView来判定是否在顶部
                    View lastView = manager.getChildAt(childNum -1);
                    if (Math.abs(lastView.getBottom() - getBottom()) < 2) {
                        resultValue = true;
                    }
                }
            }
        }
        return resultValue;
    }

这是头部的RecyclerView,判断滑动到底部了然后又继续往上滑就让父布局处理事件,否则自己处理事件

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            downY = ev.getRawY();
            needConsumeTouch = true; // 默认情况下,RecyclerView内部的滚动优先,默认情况下由该RecyclerView去消费touch事件
            allowDragTop = isAtTop();
        } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
            allowDragTop = isAtTop();

            if (!needConsumeTouch) {
                // 在最顶端且向上拉了,则这个touch事件交给父类去处理
                getParent().requestDisallowInterceptTouchEvent(false);
                return false;
            } else if (allowDragTop) {
                // needConsumeTouch尚未被定性,此处给其定性
                // 允许拖动到底部的下一页,而且又向上拖动了,就将touch事件交给父view
                if (ev.getRawY() - downY > 2) {
                    // flag设置,由父类去消费
                    needConsumeTouch = false;
                    getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
                }
            }
        }

        // 通知父view是否要处理touch事件
        getParent().requestDisallowInterceptTouchEvent(needConsumeTouch);
        return super.dispatchTouchEvent(ev);
    }

    /**
     * 判断listView是否在顶部
     *
     * @return 是否在顶部
     */
    private boolean isAtTop() {
        boolean resultValue = false;
        int childNum = getChildCount();
        if (childNum == 0) {
            // 没有child,肯定在顶部
            resultValue = true;
        } else {
            LayoutManager layoutManager = getLayoutManager();
            if (layoutManager instanceof LinearLayoutManager) {
                LinearLayoutManager manager = (LinearLayoutManager) layoutManager;
                if (manager.findFirstVisibleItemPosition() == 0) {
                    // 根据第一个childView来判定是否在顶部
                    View firstView = getChildAt(0);
                    if (Math.abs(firstView.getTop() - getTop()) < 2) {
                        resultValue = true;
                    }
                }
            }
        }
        return resultValue;
    }

这是底部的RecyclerView,判断滑动到顶部了然后又继续往下滑就让父布局处理事件,否则自己处理事件,完整源码已上传至github:https://github.com/huang7855196/JDDragSlideLayout 欢迎大家指点

猜你喜欢

转载自blog.csdn.net/u013107751/article/details/81741252