GeorgeYang'Blog

my technology blog

安卓自定义CoordinatorLayout.Behavior实现标题滑动变色

阅读:647 创建时间:17-05-20 02:24:43 tags:android

标题变色常规做法

标题变色常规做法:

 recyclerview.addOnScrollListener(new RecyclerView.OnScrollListener() {
     .... 
 }

但,我要来弄个不一样的标题变色。

CoordinatorLayout

CoordinatorLayout是用来协调其子view们之间动作的一个父view,而Behavior就是用来给CoordinatorLayout的子view们实现交互的。 如果不熟悉CoordinatorLayout,先看入门文章:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0717/3196.html

其中android.support.v7.widget.AppBarLayout自带默认的Behavior,类是:android.support.design.widget.AppBarLayout.Behavior,所以AppBarLayout可以不用给他定义Behavior,而RecyclerView等具备滑动功能的需要指定

  app:layout_behavior="@string/appbar_scrolling_view_behavior"

让其具备引导其他view联动的功能。

AppBarLayout部分源码:

 @CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
 public class AppBarLayout extends LinearLayout {
     ...
 }

CollapsingToolbarLayout可以实现折叠效果和图片的视差效果。

准备自定义Behavior

自定义Behavior的时候,我们需要关心的两组四个方法,为什么分为两组呢?看一下下面两种情况

 1.某个view监听另一个view的状态变化,例如大小、位置、显示状态等
 这种情况使用:layoutDependsOn和onDependentViewChanged方法,

 2.某个view监听CoordinatorLayout里的滑动状态
 这种情况使用:onStartNestedScroll和onNestedPreScroll方法。

具体两种方法的使用,请参考:http://blog.csdn.net/qibin0506/article/details/50290421

如果你已经掌握了上面两种方法,那么还要系统记住Nested机制:

Nested机制

它要求CoordinatorLayout包含了一个实现了NestedScrollingChild接口的滚动视图控件,比如v7包中的RecyclerView,设置Behavior属性的Child View会随着这个控件的滚动而发生变化,涉及到的方法有:

 public class ScrollBehavior extends CoordinatorLayout.Behavior<View> {
     //返回true,继续往下处理滑动。
     onStartNestedScroll(View child, View target, int nestedScrollAxes)

     //滑动,手指按住每移动一像素都会触发
     onNestedPreScroll(View target, int dx, int dy, int[] consumed)

     //惯性准备飞动
     onNestedPreFling(View target, float velocityX, float velocityY)

     onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)

     //惯性飞动中
     onNestedFling(View target, float velocityX, float velocityY, boolean consumed)

     //结束了一个流程
     onStopNestedScroll(View target)
 }

还可以自行处理整个touchevent:

     //是否拦截事件分发自行处理,返回true,后面可处理onTouchEvent
     //可以参考android.support.design.widget.HeaderBehavior
     @Override
     public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) {
         return super.onInterceptTouchEvent(parent, child, ev);
     }

开始编码

为了实现AppBarLayout跟随RecyclerView滑动变色,布局时给AppBarLayout定义我们的Behavior,

 <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:fitsSystemWindows="true"
                                                  android:orientation="vertical">
         <android.support.design.widget.AppBarLayout
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:background="@android:color/white"
             app:layout_behavior="cn.georgeyang.libdemo.MyScrollBehavior"
             >

             <android.support.design.widget.CollapsingToolbarLayout
                 android:id="@+id/collapsingtoolbarlayout"
                 android:layout_width="match_parent"
                 android:layout_height="300dp"
                 app:contentScrim="#ffffff"
                 app:expandedTitleMarginStart="10dp"
                 app:layout_scrollFlags="scroll|exitUntilCollapsed">

                 <ImageView
                     android:layout_width="match_parent"
                     android:layout_height="300dp"
                     android:background="@mipmap/ic_launcher"
                     app:layout_collapseMode="parallax"
                     app:layout_collapseParallaxMultiplier="0.7" />

                 <android.support.v7.widget.Toolbar
                     android:layout_width="match_parent"
                     android:layout_height="?attr/actionBarSize"
                     app:layout_collapseMode="pin"

                     >

                     <TextView
                         android:layout_width="match_parent"
                         android:text="test"
                         android:gravity="center"
                         android:layout_height="?attr/actionBarSize"/>

                     </android.support.v7.widget.Toolbar>


             </android.support.design.widget.CollapsingToolbarLayout>

         </android.support.design.widget.AppBarLayout>


         <android.support.v7.widget.RecyclerView
             android:id="@+id/recyclerView"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             />

     </android.support.design.widget.CoordinatorLayout>

先实现Behavior后开始贴上变色的代码

 /**
  * 滑动变色行为控制器
  * Created by george.yang on 17/5/18.
  */
 public class MyScrollBehavior extends CoordinatorLayout.Behavior<View> {
     public MyScrollBehavior(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 }

  /**
      * 获取滑动距离
      * @param target
      * @return
      */
 public int getScrollY(View target) {
         int scrollY = 0;
         if (target instanceof NestedScrollView) {
             scrollY = target.getScrollY();
         } else if (target instanceof ScrollView) {
             scrollY = target.getScrollY();
         } else if (target instanceof RecyclerView) {
             RecyclerView recyclerView = (RecyclerView) target;
             RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
             if (layoutManager instanceof LinearLayoutManager) {
                 LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
                 int position = linearLayoutManager.findFirstVisibleItemPosition();
                 View firstVisiableChildView = layoutManager.findViewByPosition(position);
                 int itemHeight = firstVisiableChildView.getHeight();
                 scrollY = (position) * itemHeight - firstVisiableChildView.getTop();
             } else {
                 scrollY = recyclerView.computeVerticalScrollOffset();
             }
         }
         return scrollY;
     }

     /**
      * 设置透明度,其中超过400像素,变成完全不透明
      * @param target 参考
      * @param child 变色的目标
      */
     private void setAlpha(View target,View child) {
         int scrollY = getScrollY(target);
         if (scrollY>400) {
             child.setBackgroundColor(0xFFFFFFFF);
         } else {
             float persent = scrollY * 1f / 400;
             int alpha = (int) (255 * persent);
             int color = Color.argb(alpha,255,255,255);
             child.setBackgroundColor(color);
         }
     }

开始处理滑动变色:

     /**
      * 手指按住滑动时响应
      * @param coordinatorLayout
      * @param child MyScrollBehavior的对象
      * @param target 手指移动的对象
      * @param dx
      * @param dy
      * @param consumed
      */
     @Override
     public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
         super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
         setAlpha(target,child);
     }

代码写到这里,运行上去,手指按住滑动已经实现变色了,但是,如果快速滑动,出现惯性滑动时,就不能变色了,因为没处理onNestedFling的情况。让我们看看appbarlayout是怎么做的:

 @Override
         public boolean onNestedFling(final CoordinatorLayout coordinatorLayout,
                 final AppBarLayout child, View target, float velocityX, float velocityY,
                 boolean consumed) {
             boolean flung = false;

             if (!consumed) {
                 // It has been consumed so let's fling ourselves
                 flung = fling(coordinatorLayout, child, -child.getTotalScrollRange(),
                         0, -velocityY);
             } else {
                 // If we're scrolling up and the child also consumed the fling. We'll fake scroll
                 // up to our 'collapsed' offset
                 if (velocityY < 0) {
                     // We're scrolling down
                     final int targetScroll = -child.getTotalScrollRange()
                             + child.getDownNestedPreScrollRange();
                     if (getTopBottomOffsetForScrollingSibling() < targetScroll) {
                         // If we're currently not expanded more than the target scroll, we'll
                         // animate a fling
                         animateOffsetTo(coordinatorLayout, child, targetScroll, velocityY);
                         flung = true;
                     }
                 } else {
                     // We're scrolling up
                     final int targetScroll = -child.getUpNestedPreScrollRange();
                     if (getTopBottomOffsetForScrollingSibling() > targetScroll) {
                         // If we're currently not expanded less than the target scroll, we'll
                         // animate a fling
                         animateOffsetTo(coordinatorLayout, child, targetScroll, velocityY);
                         flung = true;
                     }
                 }
             }

             mWasNestedFlung = flung;
             return flung;
         }

         private void animateOffsetTo(final CoordinatorLayout coordinatorLayout,
                 final AppBarLayout child, final int offset, float velocity) {
             final int distance = Math.abs(getTopBottomOffsetForScrollingSibling() - offset);

             final int duration;
             velocity = Math.abs(velocity);
             if (velocity > 0) {
                 duration = 3 * Math.round(1000 * (distance / velocity));
             } else {
                 final float distanceRatio = (float) distance / child.getHeight();
                 duration = (int) ((distanceRatio + 1) * 150);
             }

             animateOffsetWithDuration(coordinatorLayout, child, offset, duration);
         }

         private void animateOffsetWithDuration(final CoordinatorLayout coordinatorLayout,
                 final AppBarLayout child, final int offset, final int duration) {
             final int currentOffset = getTopBottomOffsetForScrollingSibling();
             if (currentOffset == offset) {
                 if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) {
                     mOffsetAnimator.cancel();
                 }
                 return;
             }

             if (mOffsetAnimator == null) {
                 mOffsetAnimator = ViewUtils.createAnimator();
                 mOffsetAnimator.setInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);
                 mOffsetAnimator.addUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
                     @Override
                     public void onAnimationUpdate(ValueAnimatorCompat animator) {
                         setHeaderTopBottomOffset(coordinatorLayout, child,
                                 animator.getAnimatedIntValue());
                     }
                 });
             } else {
                 mOffsetAnimator.cancel();
             }

             mOffsetAnimator.setDuration(Math.min(duration, MAX_OFFSET_ANIMATION_DURATION));
             mOffsetAnimator.setIntValues(currentOffset, offset);
             mOffsetAnimator.start();
         }

其中有两个重要的方法:child.getTotalScrollRange()、child.getDownNestedPreScrollRange();这两个方法也是计算apptablayout滑动高度的,我们要做的是滑动标题变色,不能参考它的方法,继续看源码,其中AppBarLayout.Behavior有一个方法:

 @Override
         void onFlingFinished(CoordinatorLayout parent, AppBarLayout layout) {
             // At the end of a manual fling, check to see if we need to snap to the edge-child
             snapToChildIfNeeded(parent, layout);
         }

该方法是重新了父类的HeaderBehavior的onFlingFinished方法,阅读HeaderBehavior源码可知道具体流程是:

 1.onInterceptTouchEvent return 上下滑动大于等于mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
 2.onTouchEvent case MotionEvent.ACTION_UP:
 3.mScroller = ScrollerCompat.create(layout.getContext());
 使用mScroller滑动,直到停止,其中使用ViewCompat.postOnAnimation(layout, mFlingRunnable);一直移动view的位置

综合上面两段源码,我们要写的是:

 1.重写onNestedFling即时更新透明度
 2.使用ViewCompat.postOnAnimation(layout, mFlingRunnable),mFlingRunnable里处理透明度
 3.RecyclerView或其他view滑动高度和滑动完成判断(重要环节)

最后源码

 /**
  * 滑动变色行为控制器
  * Created by george.yang on 17/5/18.
  */
 public class MyScrollBehavior extends CoordinatorLayout.Behavior<View> {
     public MyScrollBehavior(Context context, AttributeSet attrs) {
         super(context, attrs);
     }

     @Override
     public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
         return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
     }

     public int scrollWhiteHeight = 400;

     public void setScrollWhiteHeight(int scrollWhiteHeight) {
         this.scrollWhiteHeight = scrollWhiteHeight;
     }

     /**
      *
      * @param coordinatorLayout
      * @param child MyScrollBehavior的对象
      * @param target 手指移动的对象
      * @param dx
      * @param dy
      * @param consumed
      */
     @Override
     public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
         super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
         setAlpha(target,child);
     }

     /**
      * 获取滑动距离
      * @param target
      * @return
      */
     public int getScrollY(View target) {
         int scrollY = 0;
         if (target instanceof NestedScrollView) {
             scrollY = target.getScrollY();
         } else if (target instanceof ScrollView) {
             scrollY = target.getScrollY();
         } else if (target instanceof RecyclerView) {
             RecyclerView recyclerView = (RecyclerView) target;
             RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
             if (layoutManager instanceof LinearLayoutManager) {
                 LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
                 int position = linearLayoutManager.findFirstVisibleItemPosition();
                 View firstVisiableChildView = layoutManager.findViewByPosition(position);
                 int itemHeight = firstVisiableChildView.getHeight();
                 scrollY = (position) * itemHeight - firstVisiableChildView.getTop();
             } else {
                 scrollY = recyclerView.computeVerticalScrollOffset();
             }
         }
         return scrollY;
     }

     /**
      * 背景色头透明修改
      * @param target 参考
      * @param child 变色的目标
      */
     private void setAlpha(View target,View child) {
         int scrollY = getScrollY(target);
         if (scrollY>=scrollWhiteHeight) {
             child.setBackgroundColor(0xFFFFFFFF);
         } else {
             float p = scrollY * 1f / scrollWhiteHeight;
             int alpha = (int) (255 * p);
             int color = Color.argb(alpha,255,255,255);
             child.setBackgroundColor(color);
         }
     }

     /**
      * 还在滑动中?
      * @param view
      * @return
      */
     private boolean computeScrollOffset(View view) {
         boolean ret = false;
         try {
             if (view instanceof RecyclerView) {
                 RecyclerView recyclerView = (RecyclerView) view;
                 ret = recyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
 //                Field flingerField = RecyclerView.class.getDeclaredField("mViewFlinger");
 //                flingerField.setAccessible(true);
 //                Object viewFlinger = flingerField.get(view);
 //                Field scrollerField = viewFlinger.getClass().getDeclaredField("mScroller");
 //                scrollerField.setAccessible(true);
 //                ScrollerCompat mScroller = (ScrollerCompat) scrollerField.get(viewFlinger);
 //                ret =  mScroller.computeScrollOffset();
             } else if (view instanceof NestedScrollView) {
                 //... mScroller
             }
         } catch (Exception e) {
             e.printStackTrace();
         }
         return ret;
     }

     private FlingRunnable mFlingRunnable;
     @Override
     public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY, boolean consumed) {
         if (mFlingRunnable != null) {
             child.removeCallbacks(mFlingRunnable);
             mFlingRunnable = null;
         }

         mFlingRunnable = new FlingRunnable(target,child);
         ViewCompat.postOnAnimation(child, mFlingRunnable);

         return true;
     }

     private class FlingRunnable implements Runnable {
         private final View mTarget,mChild;

         FlingRunnable(View target,View child) {
             mTarget = target;
             mChild = child;
         }

         @Override
         public void run() {
             if (computeScrollOffset(mTarget)) {
                 setAlpha(mTarget,mChild);
                 ViewCompat.postOnAnimation(mChild, this);
             } else {
                 setAlpha(mTarget,mChild);
             }
         }
     }
 }

参考:

http://blog.csdn.net/qibin0506/article/details/50290421

http://blog.csdn.net/wangbaochu/article/details/49446469

http://blog.csdn.net/yanzhenjie1003/article/details/51946749

https://www.sitepoint.com/material-design-android-design-support-library/

http://www.jianshu.com/p/8396b74de317

http://zhaochenpu.github.io/2016/05/14/CollapsingToolbarLayout%E5%AE%9E%E7%94%A8/