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

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

使用说明


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


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


  1. 第一步,分别在 xml 文件中,为 header 部分, content 部分指定我们对应的 behavior
  2. 第二部分,在代码里面设置一些配置参数


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


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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:background="@android:color/holo_blue_light"
    android:fitsSystemWindows="true">
    <!-- Header 部分-->
    <FrameLayout
        android:id="@+id/id_uc_news_header_pager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/behavior_qq_browser_header_pager">
        <com.xj.qqbroswer.behavior.base.NestedLinearLayout
            android:layout_width="match_parent"
            android:layout_height="@dimen/header_height"
            android:orientation="vertical">
            <TextView
                android:id="@+id/news_tv_header_pager"
                style="@style/TextAppearance.AppCompat.Title"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_gravity="center_vertical"
                android:gravity="center"
                android:text="QQBrowser Header"
                android:textColor="@android:color/white" />
        </com.xj.qqbroswer.behavior.base.NestedLinearLayout>
    </FrameLayout>
    <!-- ContentProvide 部分-->
    <LinearLayout
        android:id="@+id/behavior_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_behavior="@string/behavior_contents">
        <android.support.design.widget.TabLayout
            android:id="@+id/id_uc_news_tab"
            android:layout_width="match_parent"
            android:layout_height="@dimen/tabs_height"
            android:background="@color/colorPrimary"
            app:tabGravity="fill"
            app:tabIndicatorColor="@color/colorPrimaryLight"
            app:tabSelectedTextColor="@color/colorPrimaryLight"
            app:tabTextColor="@color/colorPrimaryIcons" />
        <android.support.v4.view.ViewPager
            android:id="@+id/id_uc_news_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#F0F4C3">
        </android.support.v4.view.ViewPager>
    </LinearLayout>
    <!--search 部分-->
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="@dimen/header_title_height"
        app:layout_behavior="@string/behavior_search">
        <android.support.v7.widget.SearchView
            android:layout_width="match_parent"
            android:layout_height="30dp"
            android:layout_centerVertical="true"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="50dp"
            android:background="@android:color/white"
            app:defaultQueryHint="搜索"
            app:queryHint="搜索">
        </android.support.v7.widget.SearchView>
        <android.support.v7.widget.AppCompatImageView
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_marginRight="10dp"
            android:src="@mipmap/camera"
            android:tint="@android:color/white" />
    </RelativeLayout>
</android.support.design.widget.CoordinatorLayout>


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


private void initBehavior() {
    Resources resources = DemoApplication.getAppContext().getResources();
    mHeaderBehavior = (QQBrowserHeaderBehavior) ((CoordinatorLayout.LayoutParams) findViewById(R.id.id_uc_news_header_pager).getLayoutParams()).getBehavior();
    mHeaderBehavior.setPagerStateListener(new QQBrowserHeaderBehavior.OnPagerStateListener() {
        @Override
        public void onPagerClosed() {
            if (BuildConfig.DEBUG) {
                Log.d(TAG, "onPagerClosed: ");
            }
            Snackbar.make(mNewsPager, "pager closed", Snackbar.LENGTH_SHORT).show();
            setFragmentRefreshEnabled(true);
            setViewPagerScrollEnable(mNewsPager, true);
        }
        @Override
        public void onScrollChange(boolean isUp, int dy, int type) {
        }
        @Override
        public void onPagerOpened() {
            Snackbar.make(mNewsPager, "pager opened", Snackbar.LENGTH_SHORT).show();
            setFragmentRefreshEnabled(false);
        }
    });
    // 设置为 header height 的相反数
    mHeaderBehavior.setHeaderOffsetRange(-resources.getDimensionPixelOffset(R.dimen.header_height));
    // 设置 header close 的时候是否能够通过滑动打开
    mHeaderBehavior.setCouldScroollOpen(false);
    mContentBehavior = (QQBrowserContentBehavior) ((CoordinatorLayout.LayoutParams) findViewById(R.id.behavior_content).getLayoutParams()).getBehavior();
    // 设置依赖于哪一个 id,这里要设置为 Header layout id
    mContentBehavior.setDependsLayoutId(R.id.id_uc_news_header_pager);
    // 设置 content 部分最终停留的位置
    mContentBehavior.setFinalY(resources.getDimensionPixelOffset(R.dimen.header_title_height));
}


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 的回调


/**
 * callback for HeaderPager 's state
 */
public interface OnPagerStateListener {
    /**
     * do callback when pager closed
     */
    void onPagerClosed();
    /**
     * when scrooll, it would call back
     *
     * @param isUp  isScroollUp
     * @param dy   child.getTanslationY
     * @param type touch or not touch, TYPE_TOUCH, TYPE_NON_TOUCH
     */
    void onScrollChange(boolean isUp, int dy, @ViewCompat.NestedScrollType int type);
    /**
     * do callback when pager opened
     */
    void onPagerOpened();
}


主要有三个方法,第一个方法,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 进行滑动。


mHeaderBehavior.setOnHeaderFlingListener(new HeaderFlingRunnable.OnHeaderFlingListener() {
    @Override
    public void onFlingFinish() {
    }
    @Override
    public void onFlingStart(View child, View target, float velocityX, float velocityY) {
        Log.i(TAG, "onFlingStart: velocityY =" + velocityY);
        if (velocityY < 0) {
            mRecyclerView.smoothScrollBy(0, (int) Math.abs(velocityY), new AccelerateDecelerateInterpolator());
        }
    }
    @Override
    public void onHeaderClose() {
    }
    @Override
    public void onHeaderOpen() {
    }
});



碰到的坑


header 部分无法响应滑动事件


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


@Override
public boolean onTouchEvent(MotionEvent event) {
    mGestureDetector.onTouchEvent(event);
    final int action = MotionEventCompat.getActionMasked(event);
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL
                    | ViewCompat.SCROLL_AXIS_VERTICAL);
            break;
        case MotionEvent.ACTION_MOVE:
            int dy = (int) (event.getRawY() - lastY);
            lastY = (int) event.getRawY();
            //  dy < 0 上滑, dy>0 下拉
            if (dy < 0) { // 上滑的时候交给父类去处理
                if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // 如果找到了支持嵌套滚动的父类
                        && dispatchNestedPreScroll(0, -dy, consumed, offset)) {//
                    // 父类进行了一部分滚动
                }
            } else {
                if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // 如果找到了支持嵌套滚动的父类
                        && dispatchNestedScroll(0, 0, 0, -dy, offset)) {//
                    // 父类进行了一部分滚动
                }
            }
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            stopNestedScroll();
            break;
    }
    return super.onTouchEvent(event);
}


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


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


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


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


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


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


  • 如果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 事件,从而保证可以滑动。


@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mDownY = (int) event.getRawY();
            // 当开始滑动的时候,告诉父view
            startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL
                    | ViewCompat.SCROLL_AXIS_VERTICAL);
            break;
        case MotionEvent.ACTION_MOVE:
            // 确保不消耗 ACTION_DOWN 事件
            if (Math.abs(event.getRawY() - mDownY) > mScaledTouchSlop) {
                logD("onInterceptTouchEvent: ACTION_MOVE  mScaledTouchSlop =" + mScaledTouchSlop);
                return true;
            }
    }
    return super.onInterceptTouchEvent(event);
}


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


解决办法:


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


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


如何判断 header 是 fling 动作

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


public boolean onTouchEvent(MotionEvent event) {
    mGestureDetector.onTouchEvent(event);
}
        GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {
                return false;
            }
            -----// 省略若干代码
            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                Log.d(TAG, "onFling: velocityY =" + velocityY);
//                fling((int) velocityY);
                getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
                return false;
            }
        };
        mGestureDetector = new GestureDetector(getContext(), onGestureListener);

题外话


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


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


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


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


说这么多,总结如下


  • 遇到不会的多做笔记,尤其是涉及到原理的
  • 对代码要有敬畏之心,不多说,自己领悟取
  • 保持一颗谦卑之心

相关文章
|
Web App开发 存储 缓存
(新)Chrome浏览器自定义背景插件
(新)Chrome浏览器自定义背景插件
179 0
自定义 behavior - 完美仿 QQ 浏览器首页,美团商家详情页(一)
自定义 behavior - 完美仿 QQ 浏览器首页,美团商家详情页
|
6月前
|
测试技术
【sgUploadTileImage】自定义组件:浏览器端生成瓦片图,并转换为File文件序列上传瓦片图
【sgUploadTileImage】自定义组件:浏览器端生成瓦片图,并转换为File文件序列上传瓦片图
|
6月前
|
Windows
【报错】在浏览器输入localhost为什么启动的是Microsoft的IIS windows首页?
【报错】在浏览器输入localhost为什么启动的是Microsoft的IIS windows首页?
|
6月前
|
JavaScript
vue自定义浏览器滚动条样式
vue自定义浏览器滚动条样式
47 0
|
6月前
|
Web App开发 测试技术
软件测试/测试开发|edge浏览器首页及新标签页设置
软件测试/测试开发|edge浏览器首页及新标签页设置
|
Web App开发 UED C++
在chrome浏览器中调用IE浏览器并访问(openIE.reg自定义协议)
在chrome浏览器中调用IE浏览器并访问(openIE.reg自定义协议)
|
Web App开发 存储 前端开发
现在Chrome浏览器打开"掘金首页"竟开启了六个进程?
前言中已经提到的进程,对我来说有一点陌生。其实我对进程和线程的理解一直模模糊糊的,所以索性去认真学习了一下,发现大佬有文章在此。
428 0
|
Web App开发 JavaScript 前端开发
playwright自定义浏览器设备、时区、经纬度、userAgent、注入脚本
playwright自定义浏览器设备、时区、经纬度、userAgent、注入脚本
1302 0
|
16天前
|
JSON 移动开发 JavaScript
在浏览器执行js脚本的两种方式
【10月更文挑战第20天】本文介绍了在浏览器中执行HTTP请求的两种方式:`fetch`和`XMLHttpRequest`。`fetch`支持GET和POST请求,返回Promise对象,可以方便地处理异步操作。`XMLHttpRequest`则通过回调函数处理请求结果,适用于需要兼容旧浏览器的场景。文中还提供了具体的代码示例。
在浏览器执行js脚本的两种方式