使用CoordinatorLayout打造各类炫酷的效果javascript
自定义Behavior —— 仿知乎,FloatActionButton隐藏与展现php
一步步带你读懂 CoordinatorLayout 源码android
ViewPager,ScrollView 嵌套ViewPager滑动冲突解决github
自定义 behavior - 完美仿 QQ 浏览器首页,美团商家详情页浏览器
记得两年前的时候,曾写过自定义 behavior 的文章 自定义 Behavior -仿新浪微博发现页的实现,到如今差很少有一万多的阅读量吧。微信
今天,对该 behavior 进行升级,相对于两年前的 behavior,增长了如下功能app
咱们先来看一下新浪微博发现页的效果:ide
接下来咱们在来看一下咱们两年前仿照新浪微博实现的效果
仿 QQ 浏览器
仿美团商家详情页面的:
有两种状态,open 和 close 状态。
从效果图,咱们能够看到 在 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 文件,并指定相应的 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。
说这么多,总结以下
以为效果还不错的,能够动手扫一扫关注个人微信公众号,或者到个人 github 上面 star,谢谢