[关闭]
@xujun94 2017-05-12T15:12:08.000000Z 字数 20358 阅读 1812

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

效果图

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

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

仿新浪微博效果图

实现思路分析

我们这里先定义两种状态,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 部分的实现

Header 部分实现的两个关键点在于

  1. 在页面状态为 open 的时候,ViewPager 里面的 RecyclerView 向上滑动的时候,消费其滑动事件,并整体向上移动。在页面状态为 close 的时候,不消耗 RecyclerView 的 滑动事件
  2. 在页面状态为 open 的时候,向上滑动 Header 的时候,整体向上偏移。

第一个关键点的实现

这里区分页面状态是 open 还是 close 状态是通过 Header 是否移除屏幕来区分的,即 child.getTranslationY() == getHeaderOffsetRange() 。

  1. private boolean isClosed(View child) {
  2. boolean isClosed = child.getTranslationY() == getHeaderOffsetRange();
  3. return isClosed;
  4. }

NestedScrolling 机制深入解析博客中,我们对 NestedScrolling 机制做了如下的总结。

而 RecyclerView 也是 Scrolling Child (实现了 NestedScrollingChild 接口),RecyclerView 在开始滑动的 时候会先调用 CoordinatorLayout 的 startNestedScroll 方法,而 CoordinatorLayout 会 调用子 View 的 Behavior 的 startNestedScroll 方法。并且只有 boolean startNestedScroll 返回 TRUE 的 时候,才会调用接下里 Behavior 中的 onNestedPreScroll 和 onNestedScroll 方法。

所以,我们在 WeiboHeaderPagerBehavior 的 onStartNestedScroll 方法可以这样写,可以确保 只拦截垂直方向上的滚动事件,且当前状态是打开的并且还可以继续向上收缩的时候还会拦截

  1. @Override
  2. public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View
  3. directTargetChild, View target, int nestedScrollAxes) {
  4. if (BuildConfig.DEBUG) {
  5. Log.d(TAG, "onStartNestedScroll: nestedScrollAxes=" + nestedScrollAxes);
  6. }
  7. boolean canScroll = canScroll(child, 0);
  8. //拦截垂直方向上的滚动事件且当前状态是打开的并且还可以继续向上收缩
  9. return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && canScroll &&
  10. !isClosed(child);
  11. }

拦截事件之后,我们需要在 RecyclerView 滑动之前消耗事件,并且移动 Header,让其向上偏移。

  1. @Override
  2. public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target,
  3. int dx, int dy, int[] consumed) {
  4. super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
  5. //dy>0 scroll up;dy<0,scroll down
  6. Log.i(TAG, "onNestedPreScroll: dy=" + dy);
  7. float halfOfDis = dy;
  8. // 不能滑动了,直接给 Header 设置 终值,防止出错
  9. if (!canScroll(child, halfOfDis)) {
  10. child.setTranslationY(halfOfDis > 0 ? getHeaderOffsetRange() : 0);
  11. } else {
  12. child.setTranslationY(child.getTranslationY() - halfOfDis);
  13. }
  14. //consumed all scroll behavior after we started Nested Scrolling
  15. consumed[1] = dy;
  16. }

当然,我们也需要处理 Fling 事件,在页面没有完全关闭的 时候,消费所有 fling 事件。

  1. @Override
  2. public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
  3. float velocityX, float velocityY) {
  4. // consumed the flinging behavior until Closed
  5. return !isClosed(child);
  6. }

至于滑动到顶部的动画,我是通过 mOverScroller + FlingRunnable 来实现的 。

  1. public class WeiboHeaderPagerBehavior extends ViewOffsetBehavior {
  2. private static final String TAG = "UcNewsHeaderPager";
  3. public static final int STATE_OPENED = 0;
  4. public static final int STATE_CLOSED = 1;
  5. public static final int DURATION_SHORT = 300;
  6. public static final int DURATION_LONG = 600;
  7. private int mCurState = STATE_OPENED;
  8. private OnPagerStateListener mPagerStateListener;
  9. private OverScroller mOverScroller;
  10. private WeakReference<CoordinatorLayout> mParent;
  11. private WeakReference<View> mChild;
  12. public void setPagerStateListener(OnPagerStateListener pagerStateListener) {
  13. mPagerStateListener = pagerStateListener;
  14. }
  15. public WeiboHeaderPagerBehavior() {
  16. init();
  17. }
  18. public WeiboHeaderPagerBehavior(Context context, AttributeSet attrs) {
  19. super(context, attrs);
  20. init();
  21. }
  22. private void init() {
  23. mOverScroller = new OverScroller(BaseAPP.getAppContext());
  24. }
  25. @Override
  26. protected void layoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
  27. super.layoutChild(parent, child, layoutDirection);
  28. mParent = new WeakReference<CoordinatorLayout>(parent);
  29. mChild = new WeakReference<View>(child);
  30. }
  31. @Override
  32. public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View
  33. directTargetChild, View target, int nestedScrollAxes) {
  34. if (BuildConfig.DEBUG) {
  35. Log.d(TAG, "onStartNestedScroll: nestedScrollAxes=" + nestedScrollAxes);
  36. }
  37. boolean canScroll = canScroll(child, 0);
  38. //拦截垂直方向上的滚动事件且当前状态是打开的并且还可以继续向上收缩
  39. return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && canScroll &&
  40. !isClosed(child);
  41. }
  42. @Override
  43. public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
  44. float velocityX, float velocityY) {
  45. // consumed the flinging behavior until Closed
  46. boolean coumsed = !isClosed(child);
  47. Log.i(TAG, "onNestedPreFling: coumsed=" +coumsed);
  48. return coumsed;
  49. }
  50. @Override
  51. public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
  52. float velocityX, float velocityY, boolean consumed) {
  53. Log.i(TAG, "onNestedFling: velocityY=" +velocityY);
  54. return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY,
  55. consumed);
  56. }
  57. private boolean isClosed(View child) {
  58. boolean isClosed = child.getTranslationY() == getHeaderOffsetRange();
  59. return isClosed;
  60. }
  61. public boolean isClosed() {
  62. return mCurState == STATE_CLOSED;
  63. }
  64. private void changeState(int newState) {
  65. if (mCurState != newState) {
  66. mCurState = newState;
  67. if (mCurState == STATE_OPENED) {
  68. if (mPagerStateListener != null) {
  69. mPagerStateListener.onPagerOpened();
  70. }
  71. } else {
  72. if (mPagerStateListener != null) {
  73. mPagerStateListener.onPagerClosed();
  74. }
  75. }
  76. }
  77. }
  78. // 表示 Header TransLationY 的值是否达到我们指定的阀值, headerOffsetRange,到达了,返回 false,
  79. // 否则,返回 true。注意 TransLationY 是负数。
  80. private boolean canScroll(View child, float pendingDy) {
  81. int pendingTranslationY = (int) (child.getTranslationY() - pendingDy);
  82. int headerOffsetRange = getHeaderOffsetRange();
  83. if (pendingTranslationY >= headerOffsetRange && pendingTranslationY <= 0) {
  84. return true;
  85. }
  86. return false;
  87. }
  88. @Override
  89. public boolean onInterceptTouchEvent(CoordinatorLayout parent, final View child, MotionEvent
  90. ev) {
  91. boolean closed = isClosed();
  92. Log.i(TAG, "onInterceptTouchEvent: closed=" + closed);
  93. if (ev.getAction() == MotionEvent.ACTION_UP && !closed) {
  94. handleActionUp(parent,child);
  95. }
  96. return super.onInterceptTouchEvent(parent, child, ev);
  97. }
  98. @Override
  99. public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target,
  100. int dx, int dy, int[] consumed) {
  101. super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
  102. //dy>0 scroll up;dy<0,scroll down
  103. Log.i(TAG, "onNestedPreScroll: dy=" + dy);
  104. float halfOfDis = dy;
  105. // 不能滑动了,直接给 Header 设置 终值,防止出错
  106. if (!canScroll(child, halfOfDis)) {
  107. child.setTranslationY(halfOfDis > 0 ? getHeaderOffsetRange() : 0);
  108. } else {
  109. child.setTranslationY(child.getTranslationY() - halfOfDis);
  110. }
  111. //consumed all scroll behavior after we started Nested Scrolling
  112. consumed[1] = dy;
  113. }
  114. // 需要注意的是 Header 我们是通过 setTranslationY 来移出屏幕的,所以这个值是负数
  115. private int getHeaderOffsetRange() {
  116. return BaseAPP.getInstance().getResources().getDimensionPixelOffset(R.dimen
  117. .weibo_header_offset);
  118. }
  119. private void handleActionUp(CoordinatorLayout parent, final View child) {
  120. if (BuildConfig.DEBUG) {
  121. Log.d(TAG, "handleActionUp: ");
  122. }
  123. if (mFlingRunnable != null) {
  124. child.removeCallbacks(mFlingRunnable);
  125. mFlingRunnable = null;
  126. }
  127. mFlingRunnable = new FlingRunnable(parent, child);
  128. if (child.getTranslationY() < getHeaderOffsetRange() / 6.0f) {
  129. mFlingRunnable.scrollToClosed(DURATION_SHORT);
  130. } else {
  131. mFlingRunnable.scrollToOpen(DURATION_SHORT);
  132. }
  133. }
  134. private void onFlingFinished(CoordinatorLayout coordinatorLayout, View layout) {
  135. changeState(isClosed(layout) ? STATE_CLOSED : STATE_OPENED);
  136. }
  137. public void openPager() {
  138. openPager(DURATION_LONG);
  139. }
  140. /**
  141. * @param duration open animation duration
  142. */
  143. public void openPager(int duration) {
  144. View child = mChild.get();
  145. CoordinatorLayout parent = mParent.get();
  146. if (isClosed() && child != null) {
  147. if (mFlingRunnable != null) {
  148. child.removeCallbacks(mFlingRunnable);
  149. mFlingRunnable = null;
  150. }
  151. mFlingRunnable = new FlingRunnable(parent, child);
  152. mFlingRunnable.scrollToOpen(duration);
  153. }
  154. }
  155. public void closePager() {
  156. closePager(DURATION_LONG);
  157. }
  158. /**
  159. * @param duration close animation duration
  160. */
  161. public void closePager(int duration) {
  162. View child = mChild.get();
  163. CoordinatorLayout parent = mParent.get();
  164. if (!isClosed()) {
  165. if (mFlingRunnable != null) {
  166. child.removeCallbacks(mFlingRunnable);
  167. mFlingRunnable = null;
  168. }
  169. mFlingRunnable = new FlingRunnable(parent, child);
  170. mFlingRunnable.scrollToClosed(duration);
  171. }
  172. }
  173. private FlingRunnable mFlingRunnable;
  174. /**
  175. * For animation , Why not use {@link android.view.ViewPropertyAnimator } to play animation
  176. * is of the
  177. * other {@link CoordinatorLayout.Behavior} that depend on this could not receiving the
  178. * correct result of
  179. * {@link View#getTranslationY()} after animation finished for whatever reason that i don't know
  180. */
  181. private class FlingRunnable implements Runnable {
  182. private final CoordinatorLayout mParent;
  183. private final View mLayout;
  184. FlingRunnable(CoordinatorLayout parent, View layout) {
  185. mParent = parent;
  186. mLayout = layout;
  187. }
  188. public void scrollToClosed(int duration) {
  189. float curTranslationY = ViewCompat.getTranslationY(mLayout);
  190. float dy = getHeaderOffsetRange() - curTranslationY;
  191. if (BuildConfig.DEBUG) {
  192. Log.d(TAG, "scrollToClosed:offest:" + getHeaderOffsetRange());
  193. Log.d(TAG, "scrollToClosed: cur0:" + curTranslationY + ",end0:" + dy);
  194. Log.d(TAG, "scrollToClosed: cur:" + Math.round(curTranslationY) + ",end:" + Math
  195. .round(dy));
  196. Log.d(TAG, "scrollToClosed: cur1:" + (int) (curTranslationY) + ",end:" + (int) dy);
  197. }
  198. mOverScroller.startScroll(0, Math.round(curTranslationY - 0.1f), 0, Math.round(dy +
  199. 0.1f), duration);
  200. start();
  201. }
  202. public void scrollToOpen(int duration) {
  203. float curTranslationY = ViewCompat.getTranslationY(mLayout);
  204. mOverScroller.startScroll(0, (int) curTranslationY, 0, (int) -curTranslationY,
  205. duration);
  206. start();
  207. }
  208. private void start() {
  209. if (mOverScroller.computeScrollOffset()) {
  210. mFlingRunnable = new FlingRunnable(mParent, mLayout);
  211. ViewCompat.postOnAnimation(mLayout, mFlingRunnable);
  212. } else {
  213. onFlingFinished(mParent, mLayout);
  214. }
  215. }
  216. @Override
  217. public void run() {
  218. if (mLayout != null && mOverScroller != null) {
  219. if (mOverScroller.computeScrollOffset()) {
  220. if (BuildConfig.DEBUG) {
  221. Log.d(TAG, "run: " + mOverScroller.getCurrY());
  222. }
  223. ViewCompat.setTranslationY(mLayout, mOverScroller.getCurrY());
  224. ViewCompat.postOnAnimation(mLayout, this);
  225. } else {
  226. onFlingFinished(mParent, mLayout);
  227. }
  228. }
  229. }
  230. }
  231. /**
  232. * callback for HeaderPager 's state
  233. */
  234. public interface OnPagerStateListener {
  235. /**
  236. * do callback when pager closed
  237. */
  238. void onPagerClosed();
  239. /**
  240. * do callback when pager opened
  241. */
  242. void onPagerOpened();
  243. }
  244. }

第二个关键点的实现

在页面状态为 open 的时候,向上滑动 Header 的时候,整体向上偏移。

在第一个关键点的实现上,我们是通过自定义 Behavior 来处理 ViewPager 里面 RecyclerView 的移动的,那我们要怎样监听整个 Header 的滑动了。

那就是重写 LinearLayout,将滑动事件交给 ScrollingParent(这里是CoordinatorLayout) 去处理,CoordinatorLayout 再交给子 View 的 behavior 去处理。

  1. public class NestedLinearLayout extends LinearLayout implements NestedScrollingChild {
  2. private static final String TAG = "NestedLinearLayout";
  3. private final int[] offset = new int[2];
  4. private final int[] consumed = new int[2];
  5. private NestedScrollingChildHelper mScrollingChildHelper;
  6. private int lastY;
  7. public NestedLinearLayout(Context context) {
  8. this(context, null);
  9. }
  10. public NestedLinearLayout(Context context, @Nullable AttributeSet attrs) {
  11. this(context, attrs, 0);
  12. }
  13. public NestedLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
  14. super(context, attrs, defStyleAttr);
  15. initData();
  16. }
  17. private void initData() {
  18. if (mScrollingChildHelper == null) {
  19. mScrollingChildHelper = new NestedScrollingChildHelper(this);
  20. mScrollingChildHelper.setNestedScrollingEnabled(true);
  21. }
  22. }
  23. @Override
  24. public boolean onInterceptTouchEvent(MotionEvent event) {
  25. switch (event.getAction()){
  26. case MotionEvent.ACTION_DOWN:
  27. lastY = (int) event.getRawY();
  28. // 当开始滑动的时候,告诉父view
  29. startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL
  30. | ViewCompat.SCROLL_AXIS_VERTICAL);
  31. break;
  32. case MotionEvent.ACTION_MOVE:
  33. return true;
  34. }
  35. return super.onInterceptTouchEvent(event);
  36. }
  37. @Override
  38. public boolean onTouchEvent(MotionEvent event) {
  39. switch (event.getAction()){
  40. case MotionEvent.ACTION_MOVE:
  41. Log.i(TAG, "onTouchEvent: ACTION_MOVE=");
  42. int y = (int) (event.getRawY());
  43. int dy =lastY- y;
  44. lastY = y;
  45. Log.i(TAG, "onTouchEvent: lastY=" + lastY);
  46. Log.i(TAG, "onTouchEvent: dy=" + dy);
  47. // dy < 0 下拉, dy>0 赏花
  48. if (dy >0) { // 上滑的时候才交给父类去处理
  49. if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // 如果找到了支持嵌套滚动的父类
  50. && dispatchNestedPreScroll(0, dy, consumed, offset)) {//
  51. // 父类进行了一部分滚动
  52. }
  53. }else{
  54. if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // 如果找到了支持嵌套滚动的父类
  55. && dispatchNestedScroll(0, 0, 0,dy, offset)) {//
  56. // 父类进行了一部分滚动
  57. }
  58. }
  59. break;
  60. }
  61. return true;
  62. }
  63. private NestedScrollingChildHelper getScrollingChildHelper() {
  64. return mScrollingChildHelper;
  65. }
  66. // 接口实现--------------------------------------------------
  67. @Override
  68. public void setNestedScrollingEnabled(boolean enabled) {
  69. getScrollingChildHelper().setNestedScrollingEnabled(enabled);
  70. }
  71. @Override
  72. public boolean isNestedScrollingEnabled() {
  73. return getScrollingChildHelper().isNestedScrollingEnabled();
  74. }
  75. @Override
  76. public boolean startNestedScroll(int axes) {
  77. return getScrollingChildHelper().startNestedScroll(axes);
  78. }
  79. @Override
  80. public void stopNestedScroll() {
  81. getScrollingChildHelper().stopNestedScroll();
  82. }
  83. @Override
  84. public boolean hasNestedScrollingParent() {
  85. return getScrollingChildHelper().hasNestedScrollingParent();
  86. }
  87. @Override
  88. public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
  89. int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
  90. return getScrollingChildHelper().dispatchNestedScroll(dxConsumed,
  91. dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
  92. }
  93. @Override
  94. public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed,
  95. int[] offsetInWindow) {
  96. return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy,
  97. consumed, offsetInWindow);
  98. }
  99. @Override
  100. public boolean dispatchNestedFling(float velocityX, float velocityY,
  101. boolean consumed) {
  102. return getScrollingChildHelper().dispatchNestedFling(velocityX,
  103. velocityY, consumed);
  104. }
  105. @Override
  106. public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
  107. return getScrollingChildHelper().dispatchNestedPreFling(velocityX,
  108. velocityY);
  109. }
  110. }

Content 部分的实现

Content 部分的实现也主要有两个关键点

第一个关键点的实现

整体置于 Header 之下。这个我们可以参考 APPBarLayout 的 behavior,它是这样处理的。

  1. /**
  2. * Copy from Android design library
  3. * <p/>
  4. * Created by xujun
  5. */
  6. public abstract class HeaderScrollingViewBehavior extends ViewOffsetBehavior<View> {
  7. private final Rect mTempRect1 = new Rect();
  8. private final Rect mTempRect2 = new Rect();
  9. private int mVerticalLayoutGap = 0;
  10. private int mOverlayTop;
  11. public HeaderScrollingViewBehavior() {
  12. }
  13. public HeaderScrollingViewBehavior(Context context, AttributeSet attrs) {
  14. super(context, attrs);
  15. }
  16. @Override
  17. public boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
  18. final int childLpHeight = child.getLayoutParams().height;
  19. if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
  20. // If the menu's height is set to match_parent/wrap_content then measure it
  21. // with the maximum visible height
  22. final List<View> dependencies = parent.getDependencies(child);
  23. final View header = findFirstDependency(dependencies);
  24. if (header != null) {
  25. if (ViewCompat.getFitsSystemWindows(header) && !ViewCompat.getFitsSystemWindows(child)) {
  26. // If the header is fitting system windows then we need to also,
  27. // otherwise we'll get CoL's compatible measuring
  28. ViewCompat.setFitsSystemWindows(child, true);
  29. if (ViewCompat.getFitsSystemWindows(child)) {
  30. // If the set succeeded, trigger a new layout and return true
  31. child.requestLayout();
  32. return true;
  33. }
  34. }
  35. if (ViewCompat.isLaidOut(header)) {
  36. int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
  37. if (availableHeight == 0) {
  38. // If the measure spec doesn't specify a size, use the current height
  39. availableHeight = parent.getHeight();
  40. }
  41. final int height = availableHeight - header.getMeasuredHeight() + getScrollRange(header);
  42. final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,
  43. childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT ? View.MeasureSpec.EXACTLY : View.MeasureSpec.AT_MOST);
  44. // Now measure the scrolling view with the correct height
  45. parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);
  46. return true;
  47. }
  48. }
  49. }
  50. return false;
  51. }
  52. @Override
  53. protected void layoutChild(final CoordinatorLayout parent, final View child, final int layoutDirection) {
  54. final List<View> dependencies = parent.getDependencies(child);
  55. final View header = findFirstDependency(dependencies);
  56. if (header != null) {
  57. final CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
  58. final Rect available = mTempRect1;
  59. available.set(parent.getPaddingLeft() + lp.leftMargin, header.getBottom() + lp.topMargin,
  60. parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
  61. parent.getHeight() + header.getBottom() - parent.getPaddingBottom() - lp.bottomMargin);
  62. final Rect out = mTempRect2;
  63. GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(), child.getMeasuredHeight(), available, out, layoutDirection);
  64. final int overlap = getOverlapPixelsForOffset(header);
  65. child.layout(out.left, out.top - overlap, out.right, out.bottom - overlap);
  66. mVerticalLayoutGap = out.top - header.getBottom();
  67. } else {
  68. // If we don't have a dependency, let super handle it
  69. super.layoutChild(parent, child, layoutDirection);
  70. mVerticalLayoutGap = 0;
  71. }
  72. }
  73. float getOverlapRatioForOffset(final View header) {
  74. return 1f;
  75. }
  76. final int getOverlapPixelsForOffset(final View header) {
  77. return mOverlayTop == 0
  78. ? 0
  79. : MathUtils.constrain(Math.round(getOverlapRatioForOffset(header) * mOverlayTop),
  80. 0, mOverlayTop);
  81. }
  82. private static int resolveGravity(int gravity) {
  83. return gravity == Gravity.NO_GRAVITY ? GravityCompat.START | Gravity.TOP : gravity;
  84. }
  85. protected abstract View findFirstDependency(List<View> views);
  86. protected int getScrollRange(View v) {
  87. return v.getMeasuredHeight();
  88. }
  89. /**
  90. * The gap between the top of the scrolling view and the bottom of the header layout in pixels.
  91. */
  92. final int getVerticalLayoutGap() {
  93. return mVerticalLayoutGap;
  94. }
  95. /**
  96. * Set the distance that this view should overlap any {@link AppBarLayout}.
  97. *
  98. * @param overlayTop the distance in px
  99. */
  100. public final void setOverlayTop(int overlayTop) {
  101. mOverlayTop = overlayTop;
  102. }
  103. /**
  104. * Returns the distance that this view should overlap any {@link AppBarLayout}.
  105. */
  106. public final int getOverlayTop() {
  107. return mOverlayTop;
  108. }
  109. }

这个基类的代码还是很好理解的,因为之前就说过了,正常来说被依赖的 View 会优先于依赖它的 View 处理,所以需要依赖的 View 可以在 measure/layout 的时候,找到依赖的 View 并获取到它的测量/布局的信息,这里的处理就是依靠着这种关系来实现的.

我们的实现类,需要重写的除了抽象方法 findFirstDependency 外,还需要重写 getScrollRange,我们把 Header
的 Id id_weibo_header 定义在 ids.xml 资源文件内,方便依赖的判断.

至于缩放的高度,根据 结果图 得知是 0,得出如下代码

  1. public class WeiboContentBehavior extends HeaderScrollingViewBehavior {
  2. private static final String TAG = "WeiboContentBehavior";
  3. public WeiboContentBehavior() {
  4. }
  5. public WeiboContentBehavior(Context context, AttributeSet attrs) {
  6. super(context, attrs);
  7. }
  8. @Override
  9. public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
  10. return isDependOn(dependency);
  11. }
  12. @Override
  13. public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
  14. if (BuildConfig.DEBUG) {
  15. Log.d(TAG, "onDependentViewChanged");
  16. }
  17. offsetChildAsNeeded(parent, child, dependency);
  18. return false;
  19. }
  20. private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {
  21. float dependencyTranslationY = dependency.getTranslationY();
  22. int translationY = (int) (-dependencyTranslationY / (getHeaderOffsetRange() * 1.0f) *
  23. getScrollRange(dependency));
  24. Log.i(TAG, "offsetChildAsNeeded: translationY=" + translationY);
  25. child.setTranslationY(translationY);
  26. }
  27. @Override
  28. protected View findFirstDependency(List<View> views) {
  29. for (int i = 0, z = views.size(); i < z; i++) {
  30. View view = views.get(i);
  31. if (isDependOn(view)) return view;
  32. }
  33. return null;
  34. }
  35. @Override
  36. protected int getScrollRange(View v) {
  37. if (isDependOn(v)) {
  38. return Math.max(0, v.getMeasuredHeight() - getFinalHeight());
  39. } else {
  40. return super.getScrollRange(v);
  41. }
  42. }
  43. private int getHeaderOffsetRange() {
  44. return BaseAPP.getInstance().getResources().getDimensionPixelOffset(R.dimen
  45. .weibo_header_offset);
  46. }
  47. private int getFinalHeight() {
  48. Resources resources = BaseAPP.getInstance().getResources();
  49. return 0;
  50. }
  51. private boolean isDependOn(View dependency) {
  52. return dependency != null && dependency.getId() == R.id.id_weibo_header;
  53. }
  54. }

里面主要的逻辑就是 在 Header 位置发生变化的时候,会回调 onDependentViewChanged 方法,在该方法里面,做相应的偏移。TranslationY 是根据比例算出来的 translationY = (int) (-dependencyTranslationY / (getHeaderOffsetRange() * 1.0f) * getScrollRange(dependency));


题外话

最后,特别感谢写这篇博客 自定义Behavior的艺术探索-仿UC浏览器主页 的开发者,没有这篇博客作为参考,这种效果我很大几率是 实现 不了的。大家觉得效果还不错的话,顺手到 github 上面给我 star,谢谢。github 地址


参考文章:

自定义Behavior的艺术探索-仿UC浏览器主页

github 地址

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注