Android仿网易严选商品详情页

仿照网易严选商品详情页面,整个页面分为两个部分,上面一部分是Native的ScrollView,下面一部分则是WebView,其目的是为了可以进行分步加载。滑动到ScrollView底部时,继续向上拖动,可以加载下面的WebView部分。反之,滑动到WebView顶部时,继续向下拖动,可以展示上面的ScrollView部分。其中,在向上或者向下拖动的时候,增加了一些阻力。另外,还使用了自定义控件辅助神器ViewDragHelper,可以使滑动比较流畅。

一、自定义View

总体的实现思路是对ScrollView和WebView的dispatchTouchEvent 方法进行重写,当在ScrollView的顶部并且向上拉,或者是在WebView的底部向下拉时,自身不消费事件,让父容器拦截事件并处理,父容器Touch事件的拦截与处理都交给ViewDragHelper来处理。

1.自定义ViewGroup

public class GoodsDetailVerticalSlideView extends ViewGroup {

    private static final int VEL_THRESHOLD = 6000;// 滑动速度的阈值,超过这个绝对值认为是上下
    private int DISTANCE_THRESHOLD = 75;// 单位是dp,当上下滑动速度不够时,通过这个阈值来判定是应该粘到顶部还是底部
    private OnPullListener onPullListener;// 页面上拉或者下拉监听器
    private OnShowPreviousPageListener onShowPreviousPageListener;// 手指松开是否加载上一页的监听器
    private OnShowNextPageListener onShowNextPageListener; // 手指松开是否加载下一页的监听器
    private ViewDragHelper mDragHelper;
    private GestureDetectorCompat mGestureDetector;// 手势识别,处理手指在触摸屏上的滑动
    private View view1;
    private View view2;
    private int viewHeight;
    private int currentPage;// 当前第几页
    private int pageIndex;// 页码标记

    /**
     * 设置页面上拉或者下拉监听
     * @param onPullListener
     */
    public void setOnPullListener(OnPullListener onPullListener) {
        this.onPullListener = onPullListener;
    }

    /**
     * 设置加载上一页监听
     * @param onShowPreviousPageListener
     */
    public void setOnShowPreviousPageListener(OnShowPreviousPageListener onShowPreviousPageListener) {
        this.onShowPreviousPageListener = onShowPreviousPageListener;
    }

    /**
     * 设置加载下一页监听
     * @param onShowNextPageListener
     */
    public void setOnShowNextPageListener(OnShowNextPageListener onShowNextPageListener) {
        this.onShowNextPageListener = onShowNextPageListener;
    }

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

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

    public GoodsDetailVerticalSlideView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        DISTANCE_THRESHOLD = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DISTANCE_THRESHOLD, getResources().getDisplayMetrics());
        // 在自定义ViewGroup时,ViewDragHelper可以用来拖拽和设置子View的位置(在ViewGroup范围内)。
        mDragHelper = ViewDragHelper.create(this, 10.0f, new DragCallBack());
        mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_BOTTOM);
        mGestureDetector = new GestureDetectorCompat(getContext(), new YScrollDetector());
        currentPage = 1;
    }

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

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (view1 == null) view1 = getChildAt(0);
        if (view2 == null) view2 = getChildAt(1);
        //当滑到第二页时,第二页的top为0,第一页为负数。
        if (view1.getTop() == 0) {
            view1.layout(0, 0, r, b);
            view2.layout(0, 0, r, b);
            viewHeight = view1.getMeasuredHeight();
            view2.offsetTopAndBottom(viewHeight);// view2向下移动到view1的底部
        } else {
            view1.layout(view1.getLeft(), view1.getTop(), view1.getRight(), view1.getBottom());
            view2.layout(view2.getLeft(), view2.getTop(), view2.getRight(), view2.getBottom());
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
    }

    // Touch事件的拦截与处理都交给mDragHelper来处理
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (view1.getBottom() > 0 && view1.getTop() < 0) {
            // view粘到顶部或底部,正在动画中的时候,不处理Touch事件
            return false;
        }

        boolean shouldIntercept = false;
        boolean yScroll = mGestureDetector.onTouchEvent(ev);
        try {
            shouldIntercept = mDragHelper.shouldInterceptTouchEvent(ev);
            //修复导致OnTouchEvent中pointerIndex out of range的异常
            int action = ev.getActionMasked();
            if (action == MotionEvent.ACTION_DOWN) {
                mDragHelper.processTouchEvent(ev);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return shouldIntercept && yScroll;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        try {
            mDragHelper.processTouchEvent(event);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }

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

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            // 由于拖拽导致被捕获View的位置发生改变时进行回调
            if (changedView == view1) {
                view2.offsetTopAndBottom(dy);
                if (onPullListener != null && currentPage == 1) {
                    onPullListener.onPull(1, top);
                }
            }
            if (changedView == view2) {
                view1.offsetTopAndBottom(dy);
                if (onPullListener != null && currentPage == 2) {
                    onPullListener.onPull(2, top);
                }
            }

            // 如果不重绘,拖动的时候,其他View会不显示
            ViewCompat.postInvalidateOnAnimation(GoodsDetailVerticalSlideView.this);
        }

        @Override
        public int getViewVerticalDragRange(View child) {
            // 这个用来控制拖拽过程中松手后,自动滑行的速度
            return child.getHeight();
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            // 滑动松开后,需要向上或者向下粘到特定的位置, 默认是粘到最顶端
            int finalTop = 0;
            if (releasedChild == view1) {
                // 拖动view1松手
                if (yvel < -VEL_THRESHOLD || releasedChild.getTop() < -DISTANCE_THRESHOLD) {
                    // 向上的速度足够大或者向上滑动的距离超过某个阈值,就滑动到view2顶端
                    finalTop = -viewHeight;
                }
            } else {
                // 拖动view2松手
                if (yvel > VEL_THRESHOLD || releasedChild.getTop() > DISTANCE_THRESHOLD) {
                    // 向下的速度足够大或者向下滑动的距离超过某个阈值,就滑动到view1顶端
                    finalTop = viewHeight;
                }
            }

            //触发缓慢滚动
            //将给定子View平滑移动到给定位置,会回调continueSettling(boolean)方法,在内部是用的ScrollerCompat来实现滑动的。
            //如果返回true,表明动画应该继续,所以调用者应该调用continueSettling(boolean)在每个后续帧继续动作,直到它返回false。
            if (mDragHelper.smoothSlideViewTo(releasedChild, 0, finalTop)) {
                ViewCompat.postInvalidateOnAnimation(GoodsDetailVerticalSlideView.this);
            }
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            // 限制被拖动的子View在垂直方向的移动,可以用作边界约束
            // 阻尼滑动,让滑动位移变为1/2,除数越大阻力越大
            return child.getTop() + dy / 2;
        }
    }

    @Override
    public void computeScroll() {
        // 判断smoothSlideViewTo触发的continueSettling(boolean)的返回值
        if (mDragHelper.continueSettling(true)) {
            // 如果当前被捕获的子View还需要继续移动,则进行重绘直到它返回false,返回false表示不用后续操作就能完成这个动作了。
            ViewCompat.postInvalidateOnAnimation(this);
            if (view2.getTop() == 0) {
                currentPage = 2;
                if (onShowNextPageListener != null && pageIndex != 2) {
                    onShowNextPageListener.onShowNextPage();
                    pageIndex =2;
                }
            } else if (view1.getTop() == 0) {
                currentPage = 1;
                if (onShowPreviousPageListener != null && pageIndex != 1) {
                    onShowPreviousPageListener.onShowPreviousPage();
                    pageIndex =1;
                }
            }
        }
    }

    /** 滚动到view1顶部 */
    public void smoothSlideToFirstPageTop() {
        if (currentPage == 2) {
            //触发缓慢滚动
            if (mDragHelper.smoothSlideViewTo(view2, 0, viewHeight)) {
                ViewCompat.postInvalidateOnAnimation(this);
            }
        }
    }

    /** 滚动到view2顶部 */
    public void smoothSlideToSecondPageTop() {
        if (currentPage == 1) {
            //触发缓慢滚动
            if (mDragHelper.smoothSlideViewTo(view1, 0, -viewHeight)) {
                ViewCompat.postInvalidateOnAnimation(this);
            }
        }
    }

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

    public interface OnPullListener{
        void onPull(int currentPage, int top);
    }

    public interface OnShowPreviousPageListener{
        void onShowPreviousPage();
    }

    public interface OnShowNextPageListener {
        void onShowNextPage();
    }
}

其中,有一些回调监听Listener,在具体业务逻辑处理的时候,可以跟Activity进行相应的交互,其余部分基本都有代码注释了。

2.自定义ScrollView

public class GoodsDetailScrollView extends ScrollView {

    private float downX;
    private float downY;

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

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

    public GoodsDetailScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = ev.getX();
                downY = ev.getY();
                //如果滑动到了最底部,就允许继续向上滑动加载下一页,否者不允许
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                float dx = ev.getX() - downX;
                float dy = ev.getY() - downY;
                boolean allowParentTouchEvent;
                if (Math.abs(dy) > Math.abs(dx)) {
                    if (dy > 0) {
                        //ScrollView顶部下拉时需要放大图片,自身消费事件
                        allowParentTouchEvent = false;
                    } else {
                        //位于底部时上拉,让父View消费事件
                        allowParentTouchEvent = isBottom();
                    }
                } else {
                    //水平方向滑动,自身消费事件
                    allowParentTouchEvent = false;
                }
                getParent().requestDisallowInterceptTouchEvent(!allowParentTouchEvent);
        }
        return super.dispatchTouchEvent(ev);
    }

    public boolean isTop() {
        return !canScrollVertically(-1);
    }

    public boolean isBottom() {
        return !canScrollVertically(1);
    }

    public void goTop() {
        scrollTo(0, 0);
    }
}

其中,可以根据自身业务逻辑的需要,对dispatchTouchEvent事件分发做相应的调整。

3.自定义WebView

public class GoodsDetailWebView extends WebView {

    private float downX;
    private float downY;

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

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

    public GoodsDetailWebView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = ev.getX();
                downY = ev.getY();
                //如果滑动到了最顶部,就允许继续向下滑动加载上一页,否者不允许
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                float dx = ev.getX() - downX;
                float dy = ev.getY() - downY;
                boolean allowParentTouchEvent;
                if (Math.abs(dy) > Math.abs(dx)) {
                    if (dy > 0) {
                        //位于顶部时下拉,让父View消费事件
                        allowParentTouchEvent = isTop();
                    } else {
                        //向上滑动,自身消费事件
                        allowParentTouchEvent = false;
                    }
                } else {
                    //水平方向滑动,自身消费事件
                    allowParentTouchEvent = false;
                }
                getParent().requestDisallowInterceptTouchEvent(!allowParentTouchEvent);
        }
        return super.dispatchTouchEvent(ev);
    }

    public boolean isTop() {
        return getScrollY() <= 0;
    }

    public boolean isBottom() {
        return getHeight() + getScrollY() >= getContentHeight() * getScale();
    }

    public void goTop() {
        scrollTo(0, 0);
    }
}

同样的,可以根据自身业务逻辑的需要,对dispatchTouchEvent事件分发做相应的调整。

二、如何使用

1.在Activity中使用GoodsDetailVerticalSlideView控件

(1)使用GoodsDetailVerticalSlideView控件,内部包含两个子View,分别表示第一部分ScrollView和第二部分WebView,可以先使用FrameLayout占位,然后在代码中使用Fragment替换。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white">

    <com.my.widgets.GoodsDetailVerticalSlideView
        android:id="@+id/goods_detail_vertical_slide_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <FrameLayout
            android:id="@+id/layout_goods_scrollview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

        <FrameLayout
            android:id="@+id/layout_goods_webview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

    </com.my.widgets.GoodsDetailVerticalSlideView>

</RelativeLayout>

(2)当然,也可以直接使用GoodsDetailScrollView和GoodsDetailWebView

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white">

    <com.my.widgets.GoodsDetailVerticalSlideView
        android:id="@+id/goods_detail_vertical_slide_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.my.widgets.GoodsDetailScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            ......

        </com.my.widgets.GoodsDetailScrollView>

        <com.my.widgets.GoodsDetailWebView
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            ......

        </com.my.widgets.GoodsDetailWebView>

    </com.my.widgets.GoodsDetailVerticalSlideView>

</RelativeLayout>

2.在Fragment中使用GoodsDetailScrollView和GoodsDetailWebView

这边有个注意点就是上拉或者下拉的时候,我们一般都会给用户展示一个文字和图片的指示器来提示用户如何操作,我们只需要把指示器放在上面一部分的ScrollView布局里面即可,然后根据目前正在展示哪一部分进行显示/隐藏以及文字图片变化就可以了,这样可以使我们的整个拖动效果看起来比较流畅。
(1)Fragment中使用GoodsDetailScrollView

<?xml version="1.0" encoding="utf-8"?>
<com.my.widgets.GoodsDetailScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/goods_detail_scrollview"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

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

        ......

       <!-- 此处应有上拉/下拉指示器 --!>

    </LinearLayout>

</com.my.widgets.GoodsDetailScrollView>

(2)Fragment中使用GoodsDetailWebView

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.my.widgets.GoodsDetailWebView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </com.my.widgets.GoodsDetailWebView>

</RelativeLayout>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,050评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,943评论 4 60
  • 又到毕业季,朋友圈里,空间里挂满了毕业照和一些感伤的文字,那所有的动态无一不让我感慨,是啊,时间过得真快呢。 转眼...
    你笑起来真美阅读 207评论 0 2
  • 霜天浩渺,谁解相思扣? 一枕秋香风雨后。 尽染红情绿意,不负清新淡黄柳。 半坡诱,云随半坡走。 歌浪水、水生皱。 ...
    纳兰蕙若阅读 2,153评论 61 159
  • 来, 做一件荒唐的事, 敞开怀抱, 一次,爱上十个姑娘。 脉搏跳动十次, 每次, 心动一个姑娘。
    渡厄肚饿阅读 203评论 0 0