老规矩,还是先上效果图
前面我也写过一篇关于UC浏览器首页滑动动画效果的文章UC浏览器首页滑动动画实现,只不过这篇文章是通过自定义View的方式实现这个滑动效果。最近在看Behavior
相关的东西,所以使用Behavior
又实现了一次UC浏览器主页的滑动效果,使用Behavior
实现相比较自定义View的实现方式还是要简单方便很多。
View结构分析
UC首页滑动过程中可以分为四个View
在参与滑动,具体的分析流程可以参见UC浏览器首页滑动动画实现这篇文章的分析,这里简要罗列下:
1. UCViewTitle:
首页标题栏视图(UC首页显示UC头条
)
2. UCViewHeader:
首页头部导航视图(UC首页显示各个网站ICON入口)
3. UCViewContent:
首页内容视图(UC首页显示新闻内容的列表)
4. UCViewTab:
首页内容Tab
导航视图(UC首页显示新闻分类的View)
Behavior
既然已经决定通过Behavior
实现此效果,那下面几个概念就必须要弄清楚:
1. Behavior
必须作用于CoordinatorLayout
直接子View
才会生效
2. Behavior
其实是对嵌套滑动的应用,因为CoordinatorLayout
其实是实现嵌套滑动,最终对嵌套滑动的执行交给Behavior
来实现,所以Behavior
的滑动处理必须要有能触发嵌套滑动的子View
触发才会起作用
关于嵌套滑动
Android
实现嵌套滑动只需要实现NestedScrollingParent
和NestedScrollingChild
这两个接口即可- 在嵌套滑动过程中
子View(实现NestedScrollingChild接口)
会将自身的滑动情况通知父View(实现NestedScrollingParent接口),不一定是直接父View
,父View
做完相关动作之后再通知子View
,也就是子View
其实是整个嵌套滑动的发起者 CoordinatorLayout
实现了NestedScrollingParent
接口作为嵌套滑动的父View
,因此如果要处理Behavior
中对于滑动的相关处理,就需要有一个嵌套滑动的子View
来触发这个Behavior
实现
- 上面分析UC首页时发现有个显示新闻的列表,因此我们可以用
RecyclerView
作为列表,因为RecyclerView
实现了NestedScrollingChild
接口,可以作为嵌套滑动的子View
- 因为是多个视图的同时滑动处理,所以在实现
Behavior
时需要选择一个依赖,这里我选择前面说过的UCViewHeader
作为其他视图Behavior
的依赖 - 在看了
AppBarLayout
的源码之后,发现其子类ScrollingViewBehavior
继承至HeaderScrollingViewBehavior
,在查看源码之后发现如下几个类可以抽出来为我们所用HeaderScrollingViewBehavior
,ViewOffsetBehavior
,ViewOffsetHelper
HeaderScrollingViewBehavior:
继承该类后,应用此Behavior
的View
布局时会自动在其依赖View
的下方ViewOffsetBehavior:
继承该类后,应用此Behavior
的View
在布局时会自动进行移动处理
UCViewTitleBehavior实现
UCViewTitle
在初始时是不可见的,我采用设置其TopMargin
为其-height
让其不可见,然后在滑动过程中再慢慢滑动到可见,当其完全可见时滑动结束,因此滑动结束时UCViewTitle
滑动的距离为UCViewTitle
的高度值
下面的代码中涉及到Behavior
在处理滑动过程中一些函数的实现及作用这里就不再说明,不清楚滑动过程中各个函数的作用可以参考我的上篇文章自定义Behavior实现快速返回效果,这篇文章里有介绍Behavior
中各个函数的作用
同时UCViewTitle
与其依赖UCViewHeader
为反向滑动,关键代码如下:
public class UCViewTitleBehavior extends ViewOffsetBehavior<View> {
...
@Override
public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
//因为UCViewTitle默认是在屏幕外不可见,所以在UCViewTitle进行布局的时候设置其topMargin让其不可见
((CoordinatorLayout.LayoutParams) child.getLayoutParams()).topMargin = -child.getMeasuredHeight();
return super.onLayoutChild(parent, child, layoutDirection);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return isDependOn(dependency);
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
//因为UCViewTitle与UCViewHeader的滑动方向相反
//所以当依赖UCViewHeader发生变化时,只需要时设置反向的translationY即可
child.setTranslationY(-dependency.getTranslationY());
return false;
}
private boolean isDependOn(View dependency) {
//确定UCViewHeader作为依赖
return dependency != null && dependency.getId() == R.id.news_view_header_layout;
}
}
UCViewTabBehavior实现
上面已经说了,当UCViewTitle
完全可见时即代表整个滑动结束。因此在这个过程中UCViewTab
整个滑动的距离即为UCViewHeader
的高度减去UCViewTitle
的高度。而且因为是同向滑动,所以在依赖位置发生变化时,我们只需要根据依赖视图因滑动而产生的translationY
计算出UCViewTitle
的translationY
即可。计算方式见下面代码和注释:
public class UCViewTabBehavior extends HeaderScrollingViewBehavior {
private int mTitleViewHeight = 0;
...
@Override
protected void layoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
//UCViewTitle高度
mTitleViewHeight = parent.findViewById(R.id.news_view_title_layout).getMeasuredHeight();
super.layoutChild(parent, child, layoutDirection);
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
//UCViewTab要滑动的距离为Header的高度减去TitleView的高度
float offsetRange = mTitleViewHeight - dependency.getMeasuredHeight();
//当Header向上滑动mTitleViewHeight高度后,即滑动完成
int headerOffsetRange = -mTitleViewHeight;
if(dependency.getTranslationY() == headerOffsetRange) {//Header已经上滑结束
child.setTranslationY(offsetRange);
} else if(dependency.getTranslationY() == 0) {//下滑结束,也是初始化的状态
child.setTranslationY(0);
} else {
//UCViewTab与UCViewHeader为同向滑动
//根据依赖UCViewHeader的滑动比例计算当前UCViewTab应该要滑动的值translationY,依赖的translationY为正值则其也为正值反之亦然
child.setTranslationY(dependency.getTranslationY() / (headerOffsetRange * 1.0f) * offsetRange);
}
return false;
}
...
}
UCViewContentBehavior实现
与UCViewTab
一样,UCViewContent
与依赖UCViewHeader
也是同向滑动,其滑动过程中translationY
的计算方式也是一样的。只是滑动过程中UCViewContent
的滑动总距离为依赖UCViewHeader
的高度减去UCViewTab
的高度和UCViewTitle
高度,其对应的Behavior
实现关键代码如下:
public class UCViewContentBehavior extends HeaderScrollingViewBehavior {
private int mTitleViewHeight = 0;
private int mTabViewHeight = 0;
...
@Override
protected void layoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
mTitleViewHeight = parent.findViewById(R.id.news_view_title_layout).getMeasuredHeight();
mTabViewHeight = parent.findViewById(R.id.news_view_tab_layout).getMeasuredHeight();
super.layoutChild(parent, child, layoutDirection);
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
int headerOffsetRange = -mTitleViewHeight;
//因为UCViewContent与依赖UCViewHeader为同向滑动
//所以UCViewHeader向上滑即translationY为负数时,UCViewContent也向上滑其translationY也为负数
//所以UCViewHeader向上滑即translationY为正数时,UCViewContent也向上滑其translationY也为正数
//而headerOffsetRange为负数,getScrollRange(dependency)为正数,所以最前面要加上一个负号
//计算方式与UCViewTab的计算方式一样
child.setTranslationY(-dependency.getTranslationY() / (headerOffsetRange * 1.0f) * getScrollRange(dependency));
return false;
}
@Override
protected int getScrollRange(View dependency) {
if(isDependency(dependency)) {
//UCViewHeader的高度,减去UCViewTab和UCViewTitle的高度就是UCViewContent要滑动的高度
return dependency.getMeasuredHeight() - mTitleViewHeight - mTabViewHeight;
}
return super.getScrollRange(dependency);
}
...
}
UCViewHeaderBehavior实现
在UCViewHeader
需要处理好何时滑动结束,何时可以滑动,松开手指时该如何处理
在我的实现中以每次实际滑动距离的1/4作为UCViewHeader
的滑动值,而在松开手指时如果滑动达到整个滑动距离的1/4则会自动滑动到结束,否则则会自动滑动到初始位置,下面是该Behavior
的完整代码
public class UCViewHeaderBehavior extends ViewOffsetBehavior<View> {
private int mTitleViewHeight = 0;
private OverScroller mOverScroller;
private WeakReference<View> mChild;
public static final int STATE_OPENED = 0;
public static final int STATE_CLOSED = 1;
public static final int DURATION_SHORT = 300;
public static final int DURATION_LONG = 600;
private int mCurState = STATE_OPENED;
public UCViewHeaderBehavior() {
super();
}
public UCViewHeaderBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
mOverScroller = new OverScroller(context);
}
@Override
protected void layoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
super.layoutChild(parent, child, layoutDirection);
mTitleViewHeight = parent.findViewById(R.id.news_view_title_layout).getMeasuredHeight();
mChild = new WeakReference<>(child);
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
//开始滑动的条件,垂直方向滑动,滑动未结束
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL && canScroll(child, 0) && !isClosed(child);
}
/**
* 当前是否可以滑动
* @param child
* @param pendingDy Y轴方向滑动的translationY
* @return
*/
private boolean canScroll(View child, float pendingDy) {
int pendingTranslationY = (int) (child.getTranslationY() - pendingDy);
if (pendingTranslationY >= getHeaderOffsetRange() && pendingTranslationY <= 0) {
return true;
}
return false;
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
//开始滑动之前的逻辑处理
//dy>0 向上滑
//dy<0 向下滑
float halfOfDis = dy / 4.0f;//每次以滑动的1/4作为滑动距离进行滑动
if (!canScroll(child, halfOfDis)) {//滑动结束
if(halfOfDis > 0) {
child.setVisibility(View.GONE);//滑动结束后,隐藏此视图
child.setTranslationY(getHeaderOffsetRange());
} else {
child.setTranslationY(0);
}
} else {//滑动未结束
if(halfOfDis <= 0) {
child.setVisibility(View.VISIBLE);
}
//滑动
child.setTranslationY(child.getTranslationY() - halfOfDis);
}
//消耗掉当前垂直方向上的滑动距离
consumed[1] = dy;
}
/**
* 向上滑动过程时translationY的最小值
* @return
*/
private int getHeaderOffsetRange() {
return -mTitleViewHeight;
}
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_UP) {
//对松开手指时进行处理,如果松开时滑动滑动了1/4则自动滑动到结束,否则则回归原位
handlerActionUp(child);
}
return super.onInterceptTouchEvent(parent, child, ev);
}
private void handlerActionUp(View child) {
if (mFlingRunnable != null) {
child.removeCallbacks(mFlingRunnable);
mFlingRunnable = null;
}
mFlingRunnable = new FlingRunnable(child);
if (child.getTranslationY() < getHeaderOffsetRange() / 4.0f) {
mFlingRunnable.scrollToClosed(DURATION_SHORT);
} else {
mFlingRunnable.scrollToOpen(DURATION_SHORT);
}
}
private void onFlingFinished(View layout) {
boolean isClosed = isClosed(layout);
mCurState = isClosed ? STATE_CLOSED : STATE_OPENED;
if(isClosed) {
layout.setVisibility(View.GONE);
}
}
/**
* 是否滑动结束
* @param child
* @return
*/
private boolean isClosed(View child) {
return child.getTranslationY() == getHeaderOffsetRange();
}
public boolean isClosed() {
return mCurState == STATE_CLOSED;
}
public void openPager() {
openPager(DURATION_LONG);
}
/**
* @param duration open animation duration
*/
public void openPager(int duration) {
View child = mChild.get();
if (isClosed() && child != null) {
if(child.getVisibility() == View.GONE) {
child.setVisibility(View.VISIBLE);
}
if (mFlingRunnable != null) {
child.removeCallbacks(mFlingRunnable);
mFlingRunnable = null;
}
mFlingRunnable = new FlingRunnable(child);
mFlingRunnable.scrollToOpen(duration);
}
}
public void closePager() {
closePager(DURATION_LONG);
}
/**
* @param duration close animation duration
*/
public void closePager(int duration) {
View child = mChild.get();
if (!isClosed()) {
if (mFlingRunnable != null) {
child.removeCallbacks(mFlingRunnable);
mFlingRunnable = null;
}
mFlingRunnable = new FlingRunnable(child);
mFlingRunnable.scrollToClosed(duration);
}
}
private FlingRunnable mFlingRunnable;
private class FlingRunnable implements Runnable {
private final View mLayout;
FlingRunnable(View layout) {
mLayout = layout;
}
public void scrollToClosed(int duration) {
float curTranslationY = ViewCompat.getTranslationY(mLayout);
float dy = getHeaderOffsetRange() - curTranslationY;
mOverScroller.startScroll(0, Math.round(curTranslationY - 0.1f), 0, Math.round(dy + 0.1f), duration);
start();
}
public void scrollToOpen(int duration) {
float curTranslationY = ViewCompat.getTranslationY(mLayout);
mOverScroller.startScroll(0, (int) curTranslationY, 0, (int) -curTranslationY, duration);
start();
}
private void start() {
if (mOverScroller.computeScrollOffset()) {
mFlingRunnable = new FlingRunnable(mLayout);
ViewCompat.postOnAnimation(mLayout, mFlingRunnable);
} else {
onFlingFinished(mLayout);
}
}
@Override
public void run() {
if (mLayout != null && mOverScroller != null) {
if (mOverScroller.computeScrollOffset()) {
ViewCompat.setTranslationY(mLayout, mOverScroller.getCurrY());
ViewCompat.postOnAnimation(mLayout, this);
} else {
onFlingFinished(mLayout);
}
}
}
}
}
尾
上面就是涉及到的四个视图对应的Behavior
,从上面代码的介绍可以看出使用Behavior
实现该效果比自定义View实现该效果要简单省事很多而且难度也不大,可见掌握Behavior
对开发者来说是很有必要的。
大家如果有问题可以加QQ群交流:106510493