android 仿ios滑动删除_自定义 behavior 完美仿 QQ 浏览器首页,美团商家详情页

前言

记得两年前的时候,曾写过自定义 behavior 的文章 自定义 Behavior -仿新浪微博发现页的实现,到现在差不多有一万多的阅读量吧。

81ae23de84d0fd034c3b926645122225.png
image

今天,对该 behavior 进行升级,相对于两年前的 behavior,增加了以下功能

  • 级联滑动过程中增加监听回调,方便外部根据滑动距离,进行相应的动画,展现炫酷的 UI,通过 setPagerStateListener 设置回调监听

  • 在滑动到顶部的时候,可以设置是否能够滑动将 Head 滑动下来,方法为 setCouldScroollOpen

  • 手指在 header 部分惯性滑动的时候,增加 fling 回调,可根据需要,是否滑动 content 部分的 list ,方法为 setOnHeaderFlingListener

  • HeaderBehavior ,ContentBehavior 代码优化,与业务逻辑剥离开,方便复用。


使用说明

效果图

我们先来看一下新浪微博发现页的效果:

39320a65199d443cf6cbdacd32bc2631.gif

接下来我们在来看一下我们两年前仿照新浪微博实现的效果

4d4b602018204002604af7c56e4868c3.gif
仿新浪微博效果图

仿 QQ 浏览器

a68cb00527d58fe46e0eb5933d374fe3.gif
image

仿美图商家详情页面的:

9999ffd28661243dc96a175c53da2fda.gif
image

分析说明:

98a3829f99c7bd35d06af2fe51ab0302.png
image

有两种状态,open 和  close 状态。

1)open 状态指 Tab+ViewPager 还没有滑动到顶部的时候,header 还 没有被完全移除屏幕的时候

2)close 状态指 Tab+ViewPager 滑动到顶部的时候,Header 被移除屏幕的时候

从效果图,我们可以看到 在 open 状态下,我们向上滑动 ViewPager 里面的 RecyclerView  的 时候,RecyclerView 并不会向上移动(RecyclerView 的滑动事件交给 外部的容器处理,被被全部消费掉了),而是整个布局(指 Header + Tab +ViewPager)会向上偏移。当 Tab 滑动到顶部的时候,我们向上滑动 ViewPager 里面的 RecyclerView 的时候,RecyclerView  可以正常向上滑动,即此时外部容器没有拦截滑动事件

同时我们可以看到在 open 状态的时候,我们是不支持下拉刷新的,这个比较容易实现,监听页面的状态,如果是 open 状态,我们设置 SwipeRefreshLayout setEnabled 为 false,这样不会 拦截事件,在页面 close 的时候,设置 SwipeRefreshLayout setEnabled 为 TRUE,这样就可以支持下拉刷新了。

基于上面的分析,我们这里可以把整个效果划分为三个部分

第一部分 Header 部分:在 Header 部分还没有滑动到顶部的时候(即 open 的时候),跟随手指滑动
第二部分 Content 部分:我们向上滑动的时候,当Header 处于 open 状态,这时候 Header 向上滑动,  content 部分的 recyclerView 不会滑动,当 header 处于 close 状态,content 部分向上滑动, RecyclerView 向上滑动。当我们向下滑动的时候,header 并不会随着滑动,只会滑动 content 部分的 recyclerView
第三部分 search 部分:当我们向上滑动的时候,Search 部分会随着滑动,最终停留在固定的位置.

我们把这三部分的关系定义为 Content 依赖于  Header。Header 移动的时候,Content 跟着 移动。所以,我们在处理滑动事件的时候,只需要处理好 Header 部分的 Behavior 就oK了,Content 部分的 Behavior 不需要处理滑动事件,只需依赖于  Header ,跟着做相应的移动即可。Search 部分的 behavior 也不需要处理滑动事件,只需依赖与 Header,跟着做相应的移动。

至于具体怎么实现的,可以看自定义 Behavior -仿新浪微博发现页的实现,核心思想差不多,这里不再重复。

使用说明

这里我们已仿 QQ 浏览器 demo 进行说明:

我们一起来看一下怎样使用:简单来说,只需要两步:

 第一步,分别在 xml 文件中,为 header 部分, content 部分指定我们对应的 behavior

第二部分,在代码里面设置一些配置参数

第一步:编写 xml 文件,并指定相应的 behavior

 1<?xml  version="1.0" encoding="utf-8"?>
2<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" 3    xmlns:app="http://schemas.android.com/apk/res-auto" 4    android:layout_width="match_parent" 5    android:layout_height="match_parent" 6    android:background="@android:color/holo_blue_light" 7    android:fitsSystemWindows="true">
8
9    
10    <FrameLayout11        android:id="@+id/id_uc_news_header_pager"12        android:layout_width="match_parent"13        android:layout_height="wrap_content"14        app:layout_behavior="@string/behavior_qq_browser_header_pager">
15
16
17        <com.xj.qqbroswer.behavior.base.NestedLinearLayout18            android:layout_width="match_parent"19            android:layout_height="@dimen/header_height"20            android:orientation="vertical">
21
22            <TextView23                android:id="@+id/news_tv_header_pager"24                style="@style/TextAppearance.AppCompat.Title"25                android:layout_width="match_parent"26                android:layout_height="match_parent"27                android:layout_gravity="center_vertical"28                android:gravity="center"29                android:text="QQBrowser Header"30                android:textColor="@android:color/white" />
31
32
33        com.xj.qqbroswer.behavior.base.NestedLinearLayout>
34    FrameLayout>
35
36    
37    <LinearLayout38        android:id="@+id/behavior_content"39        android:layout_width="match_parent"40        android:layout_height="wrap_content"41        android:orientation="vertical"42        app:layout_behavior="@string/behavior_contents">
43
44        <android.support.design.widget.TabLayout45            android:id="@+id/id_uc_news_tab"46            android:layout_width="match_parent"47            android:layout_height="@dimen/tabs_height"48            android:background="@color/colorPrimary"49            app:tabGravity="fill"50            app:tabIndicatorColor="@color/colorPrimaryLight"51            app:tabSelectedTextColor="@color/colorPrimaryLight"52            app:tabTextColor="@color/colorPrimaryIcons" />
53
54        <android.support.v4.view.ViewPager55            android:id="@+id/id_uc_news_content"56            android:layout_width="match_parent"57            android:layout_height="match_parent"58            android:background="#F0F4C3">
59
60        android.support.v4.view.ViewPager>
61    LinearLayout>
62
63    
64    <RelativeLayout65        android:layout_width="match_parent"66        android:layout_height="@dimen/header_title_height"67        app:layout_behavior="@string/behavior_search">
68
69        <android.support.v7.widget.SearchView70            android:layout_width="match_parent"71            android:layout_height="30dp"72            android:layout_centerVertical="true"73            android:layout_marginLeft="10dp"74            android:layout_marginRight="50dp"![Android 技术人](http://upload-images.jianshu.io/upload_images/2050203-bf3eca3c1cf265e4.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)7576            android:background="@android:color/white"77            app:defaultQueryHint="搜索"78            app:queryHint="搜索">
79
80        android.support.v7.widget.SearchView>
81
82        <android.support.v7.widget.AppCompatImageView83            android:layout_width="30dp"84            android:layout_height="30dp"85            android:layout_alignParentRight="true"86            android:layout_centerVertical="true"87            android:layout_marginRight="10dp"88            android:src="@mipmap/camera"89            android:tint="@android:color/white" />
90
91    RelativeLayout>
92
93
94android.support.design.widget.CoordinatorLayout>

第二步:在代码里面动态设置一些参数

 1private void initBehavior() {
2    Resources resources = DemoApplication.getAppContext().getResources();
3    mHeaderBehavior = (QQBrowserHeaderBehavior) ((CoordinatorLayout.LayoutParams) findViewById(R.id.id_uc_news_header_pager).getLayoutParams()).getBehavior();
4    mHeaderBehavior.setPagerStateListener(new QQBrowserHeaderBehavior.OnPagerStateListener() {
5        @Override
6        public void onPagerClosed() {
7            if (BuildConfig.DEBUG) {
8                Log.d(TAG, "onPagerClosed: ");
9            }
10            Snackbar.make(mNewsPager, "pager closed", Snackbar.LENGTH_SHORT).show();
11            setFragmentRefreshEnabled(true);
12            setViewPagerScrollEnable(mNewsPager, true);
13        }
14
15        @Override
16        public void onScrollChange(boolean isUp, int dy, int type) {
17
18        }
19
20        @Override
21        public void onPagerOpened() {
22            Snackbar.make(mNewsPager, "pager opened", Snackbar.LENGTH_SHORT).show();
23            setFragmentRefreshEnabled(false);
24        }
25    });
26    // 设置为 header height 的相反数
27    mHeaderBehavior.setHeaderOffsetRange(-resources.getDimensionPixelOffset(R.dimen.header_height));
28    // 设置 header close 的时候是否能够通过滑动打开
29    mHeaderBehavior.setCouldScroollOpen(false);
30
31    mContentBehavior = (QQBrowserContentBehavior) ((CoordinatorLayout.LayoutParams) findViewById(R.id.behavior_content).getLayoutParams()).getBehavior();
32    // 设置依赖于哪一个 id,这里要设置为 Header layout id
33    mContentBehavior.setDependsLayoutId(R.id.id_uc_news_header_pager);
34    // 设置 content 部分最终停留的位置
35    mContentBehavior.setFinalY(resources.getDimensionPixelOffset(R.dimen.header_title_height));
36}

mHeaderBehavior.setHeaderOffsetRange 设置 Header 部分的偏移量,我们是通过 translationY 实现的,因此我们一般设置为 header 高度的相反数即可。
 mHeaderBehavior.setCouldScroollOpen(false) , 设置 header close 的时候是否能够通过滑动打开。

mContentBehavior.setDependsLayoutId(R.id.id_uc_news_header_pager);设置依赖于哪一个 id,这里要设置为 Header layout id。 mContentBehavior.setFinalY  设置 content 部分最终停留的位置。

我们来看一下 OnPagerStateListener 的回调

 1/** 2 * callback for HeaderPager 's state 3 */
4public interface OnPagerStateListener {
5    /** 6     * do callback when pager closed 7     */
8    void onPagerClosed();
9
10    /**11     * when scrooll, it would call back12     *13     * @param isUp  isScroollUp14     * @param dy   child.getTanslationY15     * @param type touch or not touch, TYPE_TOUCH, TYPE_NON_TOUCH16     */
17    void onScrollChange(boolean isUp, int dy, @ViewCompat.NestedScrollType int type);
18
19    /**20     * do callback when pager opened21     */
22    void onPagerOpened();
23}

主要有三个方法,第一个方法,onPagerClosed 当 header close 的时候,会回调,第二个方法,当 header 滑动距离变化的时候,会回调 onScrollChange 方法。它有三个参数, isUp 代表是否是向上滑动, dy 代表 header 的偏移量, type 代表类型是 touch 或者是非 touch 的(即 fling 滑动的)

如果你想要做一些酷炫的效果的话,你可以在 onScrollChange 方法中,根据滑动的距离,各个不同的 View 做相应的动画。

仿美图商家详情页面

步骤跟上面的仿 QQ 浏览器的步骤是一样的,这里不再重复相同的步骤,说几个关键点:
第一:在页面  header close 的时候,我们可以通过滑动打开header,这是通过调用 mHeaderBehavior.setCouldScroollOpen(true); 实现的。
第二:滑动 header, fling 的时候,可以看到 content 部分的 recyclerView 也在滑动,我们是通过 header 的 fling 事件做到的,在 onFlingStart 的时候手动调用 RecyclerView 的 smoothScrollBy 进行滑动。

 1mHeaderBehavior.setOnHeaderFlingListener(new HeaderFlingRunnable.OnHeaderFlingListener() {
2    @Override
3    public void onFlingFinish() {
4
5    }
6
7    @Override
8    public void onFlingStart(View child, View target, float velocityX, float velocityY) {
9        Log.i(TAG, "onFlingStart: velocityY =" + velocityY);
10        if (velocityY 0) {
11            mRecyclerView.smoothScrollBy(0, (int) Math.abs(velocityY), new AccelerateDecelerateInterpolator());
12        }
13
14    }
15
16    @Override
17    public void onHeaderClose() {
18
19    }
20
21    @Override
22    public void onHeaderOpen() {
23
24    }
25});

碰到的坑

header 部分无法响应滑动事件

我们是通过自定义一个 NestedLinearLayout ,重写它的 onTouchEvent 事件,通过 NestedScrolling 机制将事件传递给 NestedScrollingParent,即 CoordinatorLayout,而 NestedScrollingParent 会交给子 View 的 behavior 进行处理。

 1@Override
2public boolean onTouchEvent(MotionEvent event) {
3    mGestureDetector.onTouchEvent(event);
4    final int action = MotionEventCompat.getActionMasked(event);
5    switch (action) {
6        case MotionEvent.ACTION_DOWN:
7            startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL
8                    | ViewCompat.SCROLL_AXIS_VERTICAL);
9
10            break;
11        case MotionEvent.ACTION_MOVE:
12            int dy = (int) (event.getRawY() - lastY);
13            lastY = (int) event.getRawY();
14            //  dy 0 下拉
15            if (dy 0) { // 上滑的时候交给父类去处理
16                if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // 如果找到了支持嵌套滚动的父类
17                        && dispatchNestedPreScroll(0, -dy, consumed, offset)) {//
18                    // 父类进行了一部分滚动
19
20                }
21            } else {
22                if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // 如果找到了支持嵌套滚动的父类
23                        && dispatchNestedScroll(0, 0, 0, -dy, offset)) {//
24                    // 父类进行了一部分滚动
25
26                }
27            }
28            break;
29        case MotionEvent.ACTION_CANCEL:
30        case MotionEvent.ACTION_UP:
31            stopNestedScroll();
32            break;
33
34    }
35    return super.onTouchEvent(event);
36}

当我们给 header 的子 View 设置点击事件的时候,无法滑动 header

对 Android 事件分发机制有一定了解的,都知道,在 Android 中,默认的事件传递机制是这样的,

当TouchEvent发生时,首先Activity将TouchEvent传递给最顶层的View,TouchEvent最先到达最顶层 view 的 dispatchTouchEvent ,然后由  dispatchTouchEvent 方法进行分发。

1)如果dispatchTouchEvent返回true 消费事件,事件终结。)

2)如果dispatchTouchEvent返回 false ,则回传给父View的onTouchEvent事件处理;

onTouchEvent事件返回true,事件终结,返回false,交给父View的OnTouchEvent方法处理

3)如果dispatchTouchEvent返回super的话,默认会调用自己的onInterceptTouchEvent方法

默认的情况下interceptTouchEvent回调用super方法,super方法默认返回false,所以会交给子View的onDispatchTouchEvent方法处理
如果 interceptTouchEvent 返回 true ,也就是拦截掉了,则交给它的 onTouchEvent 来处理
如果 interceptTouchEvent 返回 false ,那么就传递给子 view ,由子 view 的 dispatchTouchEvent 再来开始这个事件的分发。

因此,当我们给子 View 设置点击事件的时候,由于默认的 parent 没有拦截事件,会走到子 View 的 onToucheEvent 事件中,由于设置了点击事件,事件被消费了,所以不会回调父 View onTouchEvent 中的 ACTION_MOVE 事件。

解决办法: 重写 NestedLinearLayout 的 onInterceptToucheEvent 事件,当是 ACTION_MOVE 事件的时候,返回 true,拦截,这样会调用自己的 onTouchEvent 事件,从而保证可以滑动。

 1@Override
2public boolean onInterceptTouchEvent(MotionEvent event) {
3    switch (event.getAction()) {
4        case MotionEvent.ACTION_DOWN:
5            mDownY = (int) event.getRawY();
6            // 当开始滑动的时候,告诉父view
7            startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL
8                    | ViewCompat.SCROLL_AXIS_VERTICAL);
9            break;
10        case MotionEvent.ACTION_MOVE:
11            // 确保不消耗 ACTION_DOWN 事件
12            if (Math.abs(event.getRawY() - mDownY) > mScaledTouchSlop) {
13                logD("onInterceptTouchEvent: ACTION_MOVE  mScaledTouchSlop =" + mScaledTouchSlop);
14                return true;
15            }
16    }
17    return super.onInterceptTouchEvent(event);
18}

但这里还有一个坑,正常一个点击事件,会促发 ACTION_DOWN, ACTION_MOVE, ACTION_UP,如果我们直接在 ACTION_MOVE 里面返回 true,将会导致子 View 的 onClick 事件失效。

解决办法:

1 if (Math.abs(event.getRawY() - mDownY) > mScaledTouchSlop) {
2        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
3        mScaledTouchSlop = configuration.getScaledTouchSlop();
4        return true;
5}

关于滑动冲突解决的,可以看我以前的一篇博客:ViewPager,ScrollView 嵌套ViewPager滑动冲突解决

如何判断  header 是 fling 动作

我们这里通过手势处理器 GestureDetector 做到的,当然你也可以通过 VelocityTracker 计算,只不过比较繁琐

 1public boolean onTouchEvent(MotionEvent event) {
2    mGestureDetector.onTouchEvent(event);
3}
4
5        GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
6
7            @Override 8            public boolean onDown(MotionEvent e) {
9                return false;
10            }
11
12            -----// 省略若干代码
13
14            @Override15            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
16                Log.d(TAG, "onFling: velocityY =" + velocityY);
17//                fling((int) velocityY);
18                getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
19                return false;
20            }
21        };
22
23        mGestureDetector = new GestureDetector(getContext(), onGestureListener);

题外话

有时候,做一些笔记真的挺重要的。

这一次写这一篇博客,是因为在项目中要做类似的效果。刚开始,真的没什么思路。但清楚得记得两年前写过类似的文章,具体实现原理早已忘光。我查看了两年前的博客,整理了一下思路,将代码搬到项目中,发现了一些坑。修修补补,把坑都填了。

试想一下,如果当初没有将原理记录下来,这个效果,真的挺难实现的。如果你对 Coordinatorlayout , behavior,NestedScroll 机制这些不熟悉,你根本就无法实现。两年前写 自定义 Behavior -仿新浪微博发现页的实现 这篇博客的时候,收到挺多私信的,有一些反馈说他们做这个效果做了两个多星期还是无法实现,挺感谢我写这篇博客的。因此,从现在起,不妨尝试一下多做一下笔记。真的,好记性不如烂笔头。

第二点感触比较深的是,刚开始,我看了我两年前写的代码,我一开始的反应,我去,这是什么垃圾代码。确实,很多地方写得挺烂的,behavior 耦合业务逻辑,很难复用,也不好维护。因此,这一次,我在空闲的时间将 behavior 抽离出来,以后要实现类似的效果,轻松实现, biu biu biu。

说这么多,总结如下

1 遇到不会的多做笔记,尤其是涉及到原理的

2 对代码要有敬畏之心,不多说,自己领悟去

3 保持一颗谦卑之心


推荐阅读

该系列还有一系列文章,有兴趣的可以跳转到原文链接,那里有相应的链接

使用CoordinatorLayout打造各种炫酷的效果

自定义Behavior —— 仿知乎,FloatActionButton隐藏与展示

NestedScrolling 机制深入解析

一步步带你读懂 CoordinatorLayout 源码

自定义 Behavior -仿新浪微博发现页的实现

ViewPager,ScrollView 嵌套ViewPager滑动冲突解决

自定义 behavior - 完美仿 QQ 浏览器首页,美团商家详情页


觉得效果还不错的,可以动手扫一扫关注我的微信公众号,或者到我的  github 上面 star,谢谢

d0b981907b8c7d2db4ec7f10c1a4e2f3.png
Android 技术人
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值