@xujun94
2017-05-12T07:12:08.000000Z
字数 20358
阅读 1984
我们先来看一下新浪微博发现页的效果:

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

我们这里先定义两种状态,open 和 close 状态。
从效果图,我们可以看到 在 open 状态下,我们向上滑动 ViewPager 里面的 RecyclerView 的 时候,RecyclerView 并不会向上移动(RecyclerView 的滑动事件交给 外部的容器处理,被被全部消费掉了),而是整个布局(指 Header + Tab +ViewPager)会向上偏移 。当 Tab 滑动到顶部的时候,我们向上滑动 ViewPager 里面的 RecyclerView 的时候,RecyclerView 可以正常向上滑动,即此时外部容器没有拦截滑动事件。
同时我们可以看到在 open 状态的时候,我们是不支持下拉刷新的,这个比较容易实现,监听页面的状态,如果是 open 状态,我们设置 SwipeRefreshLayout setEnabled 为 false,这样不会 拦截事件,在页面 close 的时候,设置 SwipeRefreshLayout setEnabled 为 TRUE,这样就可以支持下拉刷新了。
基于上面的分析,我们这里可以把整个效果划分为两个部分,第一部分为 Header,第二部分为 Tab+ViewPager。下文统一把第一部分称为 Header,第二部分称为 Content 。

需要实现的效果为:在页面状态为 open 的时候,向上滑动 Header 的时候,整体向上偏移,ViewPager 里面的 RecyclerView 向上滑动的时候,消费其滑动事件,并整体向上移动。在页面状态为 close 的时候,不消耗 RecyclerView 的 滑动事件。
在上一篇博客 一步步带你读懂 CoordinatorLayout 源码 中,我们有提到在 CoordinatorLayout中,我们可以通过 给子 View 自定义 Behavior 来处理事件。它是一个容器,实现了 NestedScrollingParent 接口。它并不会直接处理事件,而是会尽可能地交给子 View 的 Behavior 进行处理。因此,为了减少依赖,我们把这两部分的关系定义为 Content 依赖于 Header。Header 移动的时候,Content 跟着 移动。所以,我们在处理滑动事件的时候,只需要处理好 Header 部分的 Behavior 就oK了,Content 部分的 Behavior 不需要处理滑动事件,只需依赖于 Header ,跟着做相应的移动即可。
Header 部分实现的两个关键点在于
这里区分页面状态是 open 还是 close 状态是通过 Header 是否移除屏幕来区分的,即 child.getTranslationY() == getHeaderOffsetRange() 。
private boolean isClosed(View child) {boolean isClosed = child.getTranslationY() == getHeaderOffsetRange();return isClosed;}
在NestedScrolling 机制深入解析博客中,我们对 NestedScrolling 机制做了如下的总结。
而 RecyclerView 也是 Scrolling Child (实现了 NestedScrollingChild 接口),RecyclerView 在开始滑动的 时候会先调用 CoordinatorLayout 的 startNestedScroll 方法,而 CoordinatorLayout 会 调用子 View 的 Behavior 的 startNestedScroll 方法。并且只有 boolean startNestedScroll 返回 TRUE 的 时候,才会调用接下里 Behavior 中的 onNestedPreScroll 和 onNestedScroll 方法。
所以,我们在 WeiboHeaderPagerBehavior 的 onStartNestedScroll 方法可以这样写,可以确保 只拦截垂直方向上的滚动事件,且当前状态是打开的并且还可以继续向上收缩的时候还会拦截
@Overridepublic boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, ViewdirectTargetChild, View target, int nestedScrollAxes) {if (BuildConfig.DEBUG) {Log.d(TAG, "onStartNestedScroll: nestedScrollAxes=" + nestedScrollAxes);}boolean canScroll = canScroll(child, 0);//拦截垂直方向上的滚动事件且当前状态是打开的并且还可以继续向上收缩return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && canScroll &&!isClosed(child);}
拦截事件之后,我们需要在 RecyclerView 滑动之前消耗事件,并且移动 Header,让其向上偏移。
@Overridepublic void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target,int dx, int dy, int[] consumed) {super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);//dy>0 scroll up;dy<0,scroll downLog.i(TAG, "onNestedPreScroll: dy=" + dy);float halfOfDis = dy;// 不能滑动了,直接给 Header 设置 终值,防止出错if (!canScroll(child, halfOfDis)) {child.setTranslationY(halfOfDis > 0 ? getHeaderOffsetRange() : 0);} else {child.setTranslationY(child.getTranslationY() - halfOfDis);}//consumed all scroll behavior after we started Nested Scrollingconsumed[1] = dy;}
当然,我们也需要处理 Fling 事件,在页面没有完全关闭的 时候,消费所有 fling 事件。
@Overridepublic boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,float velocityX, float velocityY) {// consumed the flinging behavior until Closedreturn !isClosed(child);}
至于滑动到顶部的动画,我是通过 mOverScroller + FlingRunnable 来实现的 。
public class WeiboHeaderPagerBehavior extends ViewOffsetBehavior {private static final String TAG = "UcNewsHeaderPager";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;private OnPagerStateListener mPagerStateListener;private OverScroller mOverScroller;private WeakReference<CoordinatorLayout> mParent;private WeakReference<View> mChild;public void setPagerStateListener(OnPagerStateListener pagerStateListener) {mPagerStateListener = pagerStateListener;}public WeiboHeaderPagerBehavior() {init();}public WeiboHeaderPagerBehavior(Context context, AttributeSet attrs) {super(context, attrs);init();}private void init() {mOverScroller = new OverScroller(BaseAPP.getAppContext());}@Overrideprotected void layoutChild(CoordinatorLayout parent, View child, int layoutDirection) {super.layoutChild(parent, child, layoutDirection);mParent = new WeakReference<CoordinatorLayout>(parent);mChild = new WeakReference<View>(child);}@Overridepublic boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, ViewdirectTargetChild, View target, int nestedScrollAxes) {if (BuildConfig.DEBUG) {Log.d(TAG, "onStartNestedScroll: nestedScrollAxes=" + nestedScrollAxes);}boolean canScroll = canScroll(child, 0);//拦截垂直方向上的滚动事件且当前状态是打开的并且还可以继续向上收缩return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && canScroll &&!isClosed(child);}@Overridepublic boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,float velocityX, float velocityY) {// consumed the flinging behavior until Closedboolean coumsed = !isClosed(child);Log.i(TAG, "onNestedPreFling: coumsed=" +coumsed);return coumsed;}@Overridepublic boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,float velocityX, float velocityY, boolean consumed) {Log.i(TAG, "onNestedFling: velocityY=" +velocityY);return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY,consumed);}private boolean isClosed(View child) {boolean isClosed = child.getTranslationY() == getHeaderOffsetRange();return isClosed;}public boolean isClosed() {return mCurState == STATE_CLOSED;}private void changeState(int newState) {if (mCurState != newState) {mCurState = newState;if (mCurState == STATE_OPENED) {if (mPagerStateListener != null) {mPagerStateListener.onPagerOpened();}} else {if (mPagerStateListener != null) {mPagerStateListener.onPagerClosed();}}}}// 表示 Header TransLationY 的值是否达到我们指定的阀值, headerOffsetRange,到达了,返回 false,// 否则,返回 true。注意 TransLationY 是负数。private boolean canScroll(View child, float pendingDy) {int pendingTranslationY = (int) (child.getTranslationY() - pendingDy);int headerOffsetRange = getHeaderOffsetRange();if (pendingTranslationY >= headerOffsetRange && pendingTranslationY <= 0) {return true;}return false;}@Overridepublic boolean onInterceptTouchEvent(CoordinatorLayout parent, final View child, MotionEventev) {boolean closed = isClosed();Log.i(TAG, "onInterceptTouchEvent: closed=" + closed);if (ev.getAction() == MotionEvent.ACTION_UP && !closed) {handleActionUp(parent,child);}return super.onInterceptTouchEvent(parent, child, ev);}@Overridepublic void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target,int dx, int dy, int[] consumed) {super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);//dy>0 scroll up;dy<0,scroll downLog.i(TAG, "onNestedPreScroll: dy=" + dy);float halfOfDis = dy;// 不能滑动了,直接给 Header 设置 终值,防止出错if (!canScroll(child, halfOfDis)) {child.setTranslationY(halfOfDis > 0 ? getHeaderOffsetRange() : 0);} else {child.setTranslationY(child.getTranslationY() - halfOfDis);}//consumed all scroll behavior after we started Nested Scrollingconsumed[1] = dy;}// 需要注意的是 Header 我们是通过 setTranslationY 来移出屏幕的,所以这个值是负数private int getHeaderOffsetRange() {return BaseAPP.getInstance().getResources().getDimensionPixelOffset(R.dimen.weibo_header_offset);}private void handleActionUp(CoordinatorLayout parent, final View child) {if (BuildConfig.DEBUG) {Log.d(TAG, "handleActionUp: ");}if (mFlingRunnable != null) {child.removeCallbacks(mFlingRunnable);mFlingRunnable = null;}mFlingRunnable = new FlingRunnable(parent, child);if (child.getTranslationY() < getHeaderOffsetRange() / 6.0f) {mFlingRunnable.scrollToClosed(DURATION_SHORT);} else {mFlingRunnable.scrollToOpen(DURATION_SHORT);}}private void onFlingFinished(CoordinatorLayout coordinatorLayout, View layout) {changeState(isClosed(layout) ? STATE_CLOSED : STATE_OPENED);}public void openPager() {openPager(DURATION_LONG);}/*** @param duration open animation duration*/public void openPager(int duration) {View child = mChild.get();CoordinatorLayout parent = mParent.get();if (isClosed() && child != null) {if (mFlingRunnable != null) {child.removeCallbacks(mFlingRunnable);mFlingRunnable = null;}mFlingRunnable = new FlingRunnable(parent, child);mFlingRunnable.scrollToOpen(duration);}}public void closePager() {closePager(DURATION_LONG);}/*** @param duration close animation duration*/public void closePager(int duration) {View child = mChild.get();CoordinatorLayout parent = mParent.get();if (!isClosed()) {if (mFlingRunnable != null) {child.removeCallbacks(mFlingRunnable);mFlingRunnable = null;}mFlingRunnable = new FlingRunnable(parent, child);mFlingRunnable.scrollToClosed(duration);}}private FlingRunnable mFlingRunnable;/*** For animation , Why not use {@link android.view.ViewPropertyAnimator } to play animation* is of the* other {@link CoordinatorLayout.Behavior} that depend on this could not receiving the* correct result of* {@link View#getTranslationY()} after animation finished for whatever reason that i don't know*/private class FlingRunnable implements Runnable {private final CoordinatorLayout mParent;private final View mLayout;FlingRunnable(CoordinatorLayout parent, View layout) {mParent = parent;mLayout = layout;}public void scrollToClosed(int duration) {float curTranslationY = ViewCompat.getTranslationY(mLayout);float dy = getHeaderOffsetRange() - curTranslationY;if (BuildConfig.DEBUG) {Log.d(TAG, "scrollToClosed:offest:" + getHeaderOffsetRange());Log.d(TAG, "scrollToClosed: cur0:" + curTranslationY + ",end0:" + dy);Log.d(TAG, "scrollToClosed: cur:" + Math.round(curTranslationY) + ",end:" + Math.round(dy));Log.d(TAG, "scrollToClosed: cur1:" + (int) (curTranslationY) + ",end:" + (int) dy);}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(mParent, mLayout);ViewCompat.postOnAnimation(mLayout, mFlingRunnable);} else {onFlingFinished(mParent, mLayout);}}@Overridepublic void run() {if (mLayout != null && mOverScroller != null) {if (mOverScroller.computeScrollOffset()) {if (BuildConfig.DEBUG) {Log.d(TAG, "run: " + mOverScroller.getCurrY());}ViewCompat.setTranslationY(mLayout, mOverScroller.getCurrY());ViewCompat.postOnAnimation(mLayout, this);} else {onFlingFinished(mParent, mLayout);}}}}/*** callback for HeaderPager 's state*/public interface OnPagerStateListener {/*** do callback when pager closed*/void onPagerClosed();/*** do callback when pager opened*/void onPagerOpened();}}
在页面状态为 open 的时候,向上滑动 Header 的时候,整体向上偏移。
在第一个关键点的实现上,我们是通过自定义 Behavior 来处理 ViewPager 里面 RecyclerView 的移动的,那我们要怎样监听整个 Header 的滑动了。
那就是重写 LinearLayout,将滑动事件交给 ScrollingParent(这里是CoordinatorLayout) 去处理,CoordinatorLayout 再交给子 View 的 behavior 去处理。
public class NestedLinearLayout extends LinearLayout implements NestedScrollingChild {private static final String TAG = "NestedLinearLayout";private final int[] offset = new int[2];private final int[] consumed = new int[2];private NestedScrollingChildHelper mScrollingChildHelper;private int lastY;public NestedLinearLayout(Context context) {this(context, null);}public NestedLinearLayout(Context context, @Nullable AttributeSet attrs) {this(context, attrs, 0);}public NestedLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);initData();}private void initData() {if (mScrollingChildHelper == null) {mScrollingChildHelper = new NestedScrollingChildHelper(this);mScrollingChildHelper.setNestedScrollingEnabled(true);}}@Overridepublic boolean onInterceptTouchEvent(MotionEvent event) {switch (event.getAction()){case MotionEvent.ACTION_DOWN:lastY = (int) event.getRawY();// 当开始滑动的时候,告诉父viewstartNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL| ViewCompat.SCROLL_AXIS_VERTICAL);break;case MotionEvent.ACTION_MOVE:return true;}return super.onInterceptTouchEvent(event);}@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()){case MotionEvent.ACTION_MOVE:Log.i(TAG, "onTouchEvent: ACTION_MOVE=");int y = (int) (event.getRawY());int dy =lastY- y;lastY = y;Log.i(TAG, "onTouchEvent: lastY=" + lastY);Log.i(TAG, "onTouchEvent: dy=" + dy);// 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;}return true;}private NestedScrollingChildHelper getScrollingChildHelper() {return mScrollingChildHelper;}// 接口实现--------------------------------------------------@Overridepublic void setNestedScrollingEnabled(boolean enabled) {getScrollingChildHelper().setNestedScrollingEnabled(enabled);}@Overridepublic boolean isNestedScrollingEnabled() {return getScrollingChildHelper().isNestedScrollingEnabled();}@Overridepublic boolean startNestedScroll(int axes) {return getScrollingChildHelper().startNestedScroll(axes);}@Overridepublic void stopNestedScroll() {getScrollingChildHelper().stopNestedScroll();}@Overridepublic boolean hasNestedScrollingParent() {return getScrollingChildHelper().hasNestedScrollingParent();}@Overridepublic boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {return getScrollingChildHelper().dispatchNestedScroll(dxConsumed,dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);}@Overridepublic boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed,int[] offsetInWindow) {return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy,consumed, offsetInWindow);}@Overridepublic boolean dispatchNestedFling(float velocityX, float velocityY,boolean consumed) {return getScrollingChildHelper().dispatchNestedFling(velocityX,velocityY, consumed);}@Overridepublic boolean dispatchNestedPreFling(float velocityX, float velocityY) {return getScrollingChildHelper().dispatchNestedPreFling(velocityX,velocityY);}}
Content 部分的实现也主要有两个关键点
整体置于 Header 之下。这个我们可以参考 APPBarLayout 的 behavior,它是这样处理的。
/*** Copy from Android design library* <p/>* Created by xujun*/public abstract class HeaderScrollingViewBehavior extends ViewOffsetBehavior<View> {private final Rect mTempRect1 = new Rect();private final Rect mTempRect2 = new Rect();private int mVerticalLayoutGap = 0;private int mOverlayTop;public HeaderScrollingViewBehavior() {}public HeaderScrollingViewBehavior(Context context, AttributeSet attrs) {super(context, attrs);}@Overridepublic boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {final int childLpHeight = child.getLayoutParams().height;if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {// If the menu's height is set to match_parent/wrap_content then measure it// with the maximum visible heightfinal List<View> dependencies = parent.getDependencies(child);final View header = findFirstDependency(dependencies);if (header != null) {if (ViewCompat.getFitsSystemWindows(header) && !ViewCompat.getFitsSystemWindows(child)) {// If the header is fitting system windows then we need to also,// otherwise we'll get CoL's compatible measuringViewCompat.setFitsSystemWindows(child, true);if (ViewCompat.getFitsSystemWindows(child)) {// If the set succeeded, trigger a new layout and return truechild.requestLayout();return true;}}if (ViewCompat.isLaidOut(header)) {int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);if (availableHeight == 0) {// If the measure spec doesn't specify a size, use the current heightavailableHeight = parent.getHeight();}final int height = availableHeight - header.getMeasuredHeight() + getScrollRange(header);final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT ? View.MeasureSpec.EXACTLY : View.MeasureSpec.AT_MOST);// Now measure the scrolling view with the correct heightparent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);return true;}}}return false;}@Overrideprotected void layoutChild(final CoordinatorLayout parent, final View child, final int layoutDirection) {final List<View> dependencies = parent.getDependencies(child);final View header = findFirstDependency(dependencies);if (header != null) {final CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();final Rect available = mTempRect1;available.set(parent.getPaddingLeft() + lp.leftMargin, header.getBottom() + lp.topMargin,parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,parent.getHeight() + header.getBottom() - parent.getPaddingBottom() - lp.bottomMargin);final Rect out = mTempRect2;GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(), child.getMeasuredHeight(), available, out, layoutDirection);final int overlap = getOverlapPixelsForOffset(header);child.layout(out.left, out.top - overlap, out.right, out.bottom - overlap);mVerticalLayoutGap = out.top - header.getBottom();} else {// If we don't have a dependency, let super handle itsuper.layoutChild(parent, child, layoutDirection);mVerticalLayoutGap = 0;}}float getOverlapRatioForOffset(final View header) {return 1f;}final int getOverlapPixelsForOffset(final View header) {return mOverlayTop == 0? 0: MathUtils.constrain(Math.round(getOverlapRatioForOffset(header) * mOverlayTop),0, mOverlayTop);}private static int resolveGravity(int gravity) {return gravity == Gravity.NO_GRAVITY ? GravityCompat.START | Gravity.TOP : gravity;}protected abstract View findFirstDependency(List<View> views);protected int getScrollRange(View v) {return v.getMeasuredHeight();}/*** The gap between the top of the scrolling view and the bottom of the header layout in pixels.*/final int getVerticalLayoutGap() {return mVerticalLayoutGap;}/*** Set the distance that this view should overlap any {@link AppBarLayout}.** @param overlayTop the distance in px*/public final void setOverlayTop(int overlayTop) {mOverlayTop = overlayTop;}/*** Returns the distance that this view should overlap any {@link AppBarLayout}.*/public final int getOverlayTop() {return mOverlayTop;}}
这个基类的代码还是很好理解的,因为之前就说过了,正常来说被依赖的 View 会优先于依赖它的 View 处理,所以需要依赖的 View 可以在 measure/layout 的时候,找到依赖的 View 并获取到它的测量/布局的信息,这里的处理就是依靠着这种关系来实现的.
我们的实现类,需要重写的除了抽象方法 findFirstDependency 外,还需要重写 getScrollRange,我们把 Header
的 Id id_weibo_header 定义在 ids.xml 资源文件内,方便依赖的判断.

至于缩放的高度,根据 结果图 得知是 0,得出如下代码
public class WeiboContentBehavior extends HeaderScrollingViewBehavior {private static final String TAG = "WeiboContentBehavior";public WeiboContentBehavior() {}public WeiboContentBehavior(Context context, AttributeSet attrs) {super(context, attrs);}@Overridepublic boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {return isDependOn(dependency);}@Overridepublic boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {if (BuildConfig.DEBUG) {Log.d(TAG, "onDependentViewChanged");}offsetChildAsNeeded(parent, child, dependency);return false;}private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {float dependencyTranslationY = dependency.getTranslationY();int translationY = (int) (-dependencyTranslationY / (getHeaderOffsetRange() * 1.0f) *getScrollRange(dependency));Log.i(TAG, "offsetChildAsNeeded: translationY=" + translationY);child.setTranslationY(translationY);}@Overrideprotected View findFirstDependency(List<View> views) {for (int i = 0, z = views.size(); i < z; i++) {View view = views.get(i);if (isDependOn(view)) return view;}return null;}@Overrideprotected int getScrollRange(View v) {if (isDependOn(v)) {return Math.max(0, v.getMeasuredHeight() - getFinalHeight());} else {return super.getScrollRange(v);}}private int getHeaderOffsetRange() {return BaseAPP.getInstance().getResources().getDimensionPixelOffset(R.dimen.weibo_header_offset);}private int getFinalHeight() {Resources resources = BaseAPP.getInstance().getResources();return 0;}private boolean isDependOn(View dependency) {return dependency != null && dependency.getId() == R.id.id_weibo_header;}}
里面主要的逻辑就是 在 Header 位置发生变化的时候,会回调 onDependentViewChanged 方法,在该方法里面,做相应的偏移。TranslationY 是根据比例算出来的 translationY = (int) (-dependencyTranslationY / (getHeaderOffsetRange() * 1.0f) * getScrollRange(dependency));
最后,特别感谢写这篇博客 自定义Behavior的艺术探索-仿UC浏览器主页 的开发者,没有这篇博客作为参考,这种效果我很大几率是 实现 不了的。大家觉得效果还不错的话,顺手到 github 上面给我 star,谢谢。github 地址
参考文章: