UI之自定义Behavior实现AppBarLayout越界弹性效果

TheronCharl 8年前
   <h3>先上效果</h3>    <p style="text-align:center"><img src="https://simg.open-open.com/show/6f05d780340c23ab1ff0257106fd872d.gif"></p>    <h3>一、继承 AppBarLayout.Behavior</h3>    <p>AppBarLayout有一个默认的 Behavior ,即 AppBarLayout.Behavior , AppBarLayout.Behavior 已注解的方式设置给AppBarLayout。</p>    <pre>  <code class="language-java">@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)  public class AppBarLayout extends LinearLayout {      ...  }</code></pre>    <p>1.继承 AppBarLayout.Behavior 自定义 Behavior</p>    <p>我们可以继承 AppBarLayout.Behavior 并重新设置给AppBarLayout来修改AppBarLayout的默认滚动行为,实现AppBarLayout的弹性越界效果就可以通过这种方式实现。</p>    <p>继承 AppBarLayout.Behavior 需要重写构造方法</p>    <pre>  <code class="language-java">public class AppBarLayoutOverScrollViewBehavior extends AppBarLayout.Behavior {        public AppBarLayoutOverScrollViewBehavior() {      }        public AppBarLayoutOverScrollViewBehavior(Context context, AttributeSet attrs) {          super(context, attrs);      }    }</code></pre>    <p>2.将自定义的 Behavior 设置给AppBarLayout</p>    <p>可以通过两种方式将自定义的 Behavior 设置给AppBarLayout</p>    <ol>     <li> <p>在布局文件中设置</p> <pre>  <code class="language-java"><android.support.design.widget.AppBarLayout       ...       app:layout_behavior="packageName.AppBarLayoutOverScrollViewBehavior">   </android.support.design.widget.AppBarLayout></code></pre> </li>     <li> <p>在代码中设置</p> <pre>  <code class="language-java">AppBarLayout appBar = (AppBarLayout) findViewById(R.id.appbar);   CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);   params.setBehavior(new AppBarLayoutOverScrollViewBehavior());   appBar.setLayoutParams(params);</code></pre> </li>    </ol>    <p>设置完成后,自定义的 Behavior 就会生效,但是因为没有重写任何方法,所以AppBarLayout的滚动行为不会发生变化。</p>    <h3>二、 Behavior 中的回调方法分析</h3>    <p>将自定义的 Behavior 设置给AppBarLayout后,可以在自定义的 Behavior 中重写滚动相关回调方法</p>    <pre>  <code class="language-java">public class AppBarLayoutOverScrollViewBehavior extends AppBarLayout.Behavior {        ...        /**       * AppBarLayout布局时调用       *       * @param parent 父布局CoordinatorLayout       * @param abl 使用此Behavior的AppBarLayout       * @param layoutDirection 布局方向       * @return 返回true表示子View重新布局,返回false表示请求默认布局       */      @Override      public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl, int layoutDirection) {          return super.onLayoutChild(parent, abl, layoutDirection);      }        /**       * 当CoordinatorLayout的子View尝试发起嵌套滚动时调用       *       * @param parent 父布局CoordinatorLayout       * @param child 使用此Behavior的AppBarLayout       * @param directTargetChild CoordinatorLayout的子View,或者是包含嵌套滚动操作的目标View       * @param target 发起嵌套滚动的目标View(即AppBarLayout下面的ScrollView或RecyclerView)       * @param nestedScrollAxes 嵌套滚动的方向       * @return 返回true表示接受滚动       */      @Override      public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {          return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);      }        /**       * 当嵌套滚动已由CoordinatorLayout接受时调用       *       * @param coordinatorLayout 父布局CoordinatorLayout       * @param child 使用此Behavior的AppBarLayout       * @param directTargetChild CoordinatorLayout的子View,或者是包含嵌套滚动操作的目标View       * @param target 发起嵌套滚动的目标View(即AppBarLayout下面的ScrollView或RecyclerView)       * @param nestedScrollAxes 嵌套滚动的方向       */      @Override      public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {          super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);      }        /**       * 当准备开始嵌套滚动时调用       *       * @param coordinatorLayout 父布局CoordinatorLayout       * @param child 使用此Behavior的AppBarLayout       * @param target 发起嵌套滚动的目标View(即AppBarLayout下面的ScrollView或RecyclerView)       * @param dx 用户在水平方向上滑动的像素数       * @param dy 用户在垂直方向上滑动的像素数       * @param consumed 输出参数,consumed[0]为水平方向应该消耗的距离,consumed[1]为垂直方向应该消耗的距离       */      @Override      public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {          super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);      }        /**       * 嵌套滚动时调用       *       * @param coordinatorLayout 父布局CoordinatorLayout       * @param child 使用此Behavior的AppBarLayout       * @param target 发起嵌套滚动的目标View(即AppBarLayout下面的ScrollView或RecyclerView)       * @param dxConsumed 由目标View滚动操作消耗的水平像素数       * @param dyConsumed 由目标View滚动操作消耗的垂直像素数       * @param dxUnconsumed 由用户请求但是目标View滚动操作未消耗的水平像素数       * @param dyUnconsumed 由用户请求但是目标View滚动操作未消耗的垂直像素数       */      @Override      public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {          super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);      }        /**       * 当嵌套滚动的子View准备快速滚动时调用       *       * @param coordinatorLayout 父布局CoordinatorLayout       * @param child 使用此Behavior的AppBarLayout       * @param target 发起嵌套滚动的目标View(即AppBarLayout下面的ScrollView或RecyclerView)       * @param velocityX 水平方向的速度       * @param velocityY 垂直方向的速度       * @return 如果Behavior消耗了快速滚动返回true       */      @Override      public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY) {          return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);      }        /**       * 当嵌套滚动的子View快速滚动时调用       *       * @param coordinatorLayout 父布局CoordinatorLayout       * @param child 使用此Behavior的AppBarLayout       * @param target 发起嵌套滚动的目标View(即AppBarLayout下面的ScrollView或RecyclerView)       * @param velocityX 水平方向的速度       * @param velocityY 垂直方向的速度       * @param consumed 如果嵌套的子View消耗了快速滚动则为true       * @return 如果Behavior消耗了快速滚动返回true       */      @Override      public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {          return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);      }        /**       * 当定制滚动时调用       *       * @param coordinatorLayout 父布局CoordinatorLayout       * @param abl 使用此Behavior的AppBarLayout       * @param target 发起嵌套滚动的目标View(即AppBarLayout下面的ScrollView或RecyclerView)       */      @Override      public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target) {          super.onStopNestedScroll(coordinatorLayout, abl, target);      }  }</code></pre>    <p>可以通过打印log来观察AppBarLayout在滚动时 Behavior 中回调方法的调用情况。</p>    <p>通过观察可以发现:</p>    <ol>     <li>上滑时      <ul>       <li>当AppBarLayout由展开到收起时,会依次调用onStartNestedScroll()->onNestedScrollAccepted()->onNestedPreScroll()->onStopNestedScroll()</li>       <li>当AppBarLayout收起后继续向上滑动时,会依次调用onStartNestedScroll()->onNestedScrollAccepted()->onNestedPreScroll()->onNestedScroll()->onStopNestedScroll()</li>      </ul> </li>     <li>下滑时      <ul>       <li>当AppBarLayout全部展开时(即未到顶部时),会依次调用onStartNestedScroll()->onNestedScrollAccepted()->onNestedPreScroll()->onNestedScroll()->onStopNestedScroll()</li>       <li>当AppBarLayout全部展开时(即到顶部时),继续向下滑动屏幕,会依次调用onStartNestedScroll()->onNestedScrollAccepted()->onNestedPreScroll()->onNestedScroll()->onStopNestedScroll()</li>      </ul> </li>     <li>当有快速滑动时会在onStopNestedScroll()前依次调用onNestedPreFling()->onNestedFling()</li>    </ol>    <p>所以要修改AppBarLayout的越界行为可以重写onNestedPreScroll()或onNestedScroll(),因为AppBarLayout收起时不会调用onNestedScroll(),所以只能选择重写onNestedPreScroll(),具体原因下面会有说明。</p>    <h3>三、重写 Behavior 的相关方法</h3>    <p>1.获取越界时需要改变尺寸的View</p>    <p>布局时会调用onLayoutChild(),所以在该方法中可获取需要改变尺寸的View,可以使用View的findViewWithTag方法获取指定的View,并初始化属性。</p>    <pre>  <code class="language-java">public class AppBarLayoutOverScrollViewBehavior extends AppBarLayout.Behavior {      private static final String TAG = "overScroll";      private View mTargetView;       // 目标View      private int mParentHeight;      // AppBarLayout的初始高度      private int mTargetViewHeight;  // 目标View的高度        @Override      public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl, int layoutDirection) {          boolean handled = super.onLayoutChild(parent, abl, layoutDirection);          // 需要在调用过super.onLayoutChild()方法之后获取          if (mTargetView == null) {              mTargetView = parent.findViewWithTag(TAG);              if (mTargetView != null) {                  initial(abl);              }          }          return handled;      }        private void initial(AppBarLayout abl) {          // 必须设置ClipChildren为false,这样目标View在放大时才能超出布局的范围          abl.setClipChildren(false);          mParentHeight = abl.getHeight();          mTargetViewHeight = mTargetView.getHeight();      }        ...    }</code></pre>    <p>需要在布局文件或代码中给目标View指定tag,如下:</p>    <pre>  <code class="language-java"><android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"      xmlns:app="http://schemas.android.com/apk/res-auto"      xmlns:tools="http://schemas.android.com/tools"      android:layout_width="match_parent"      android:layout_height="match_parent"      android:fitsSystemWindows="true">        <android.support.design.widget.AppBarLayout          android:id="@+id/appbar"          android:layout_width="match_parent"          android:layout_height="wrap_content"          android:fitsSystemWindows="true"          android:theme="@style/AppTheme.AppBarOverlay"          android:transitionName="picture"          app:layout_behavior="com.zly.exifviewer.widget.behavior.AppBarLayoutOverScrollViewBehavior"          tools:targetApi="lollipop">            <android.support.design.widget.CollapsingToolbarLayout              android:id="@+id/collapsingToolbarLayout"              android:layout_width="match_parent"              android:layout_height="wrap_content"              app:contentScrim="@color/colorPrimary"              app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"              app:statusBarScrim="@color/colorPrimaryDark">                <ImageView                  android:id="@+id/siv_picture"                  android:layout_width="match_parent"                  android:layout_height="200dp"                  android:fitsSystemWindows="true"                  android:foreground="@drawable/shape_fg_picture"                  android:scaleType="centerCrop"                  android:tag="overScroll"                  app:layout_collapseMode="parallax"                  tools:src="@android:drawable/sym_def_app_icon" />                <android.support.v7.widget.Toolbar                  android:id="@+id/toolbar"                  android:layout_width="match_parent"                  android:layout_height="?attr/actionBarSize"                  app:contentInsetEnd="64dp"                  app:layout_collapseMode="pin"                  app:popupTheme="@style/AppTheme.PopupOverlay" />          </android.support.design.widget.CollapsingToolbarLayout>      </android.support.design.widget.AppBarLayout>        <android.support.v4.widget.NestedScrollView          android:layout_width="match_parent"          android:layout_height="wrap_content"          app:layout_behavior="@string/appbar_scrolling_view_behavior">            ...        </android.support.v4.widget.NestedScrollView>    </android.support.design.widget.CoordinatorLayout></code></pre>    <p>2.下滑处理</p>    <p>重写onNestedPreScroll()修改AppBarLayou滑动的顶部后的行为</p>    <pre>  <code class="language-java">private static final float TARGET_HEIGHT = 500; // 最大滑动距离  private float mTotalDy;     // 总滑动的像素数  private float mLastScale;   // 最终放大比例  private int mLastBottom;    // AppBarLayout的最终Bottom值    @Override  public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {      // 1.mTargetView不为null      // 2.是向下滑动,dy<0表示向下滑动      // 3.AppBarLayout已经完全展开,child.getBottom() >= mParentHeight      if (mTargetView != null && dy < 0 && child.getBottom() >= mParentHeight) {          // 累加垂直方向上滑动的像素数          mTotalDy += -dy;          // 不能大于最大滑动距离          mTotalDy = Math.min(mTotalDy, TARGET_HEIGHT);          // 计算目标View缩放比例,不能小于1          mLastScale = Math.max(1f, 1f + mTotalDy / TARGET_HEIGHT);          // 缩放目标View          ViewCompat.setScaleX(mTargetView, mLastScale);          ViewCompat.setScaleY(mTargetView, mLastScale);          // 计算目标View放大后增加的高度          mLastBottom = mParentHeight + (int) (mTargetViewHeight / 2 * (mLastScale - 1));          // 修改AppBarLayout的高度          child.setBottom(mLastBottom);      } else {          super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);      }  }</code></pre>    <p>此时可以实现下滑越界时目标View放大,AppBarLayout变高的效果。</p>    <p>3.上滑处理</p>    <p>下滑时目标View放大,AppBarLayout变高,如果此时用户不松开手指,直接上滑,需要目标View缩小,并且AppBarLayout变高。</p>    <p>默认情况下AppBarLayout的滑动是通过修改top和bottom实现的,所以上滑时,AppBarLayout为整体向上移动, 高度不会发生改变,并且AppBarLayout下面的ScrollView也会向上滚动 ;而我们需要的是在AppBarLayout的高度大于原始高度时, 减小AppBarLayout的高度,top不发生改变,并且AppBarLayout下面的ScrollView不会向上滚动 。</p>    <p>AppBarLayout上滑时不会调用onNestedScroll(),所以只能在onNestedPreScroll()方法中修改,这也是为什么选择onNestedPreScroll()方法的原因</p>    <pre>  <code class="language-java">@Override  public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {      if (mTargetView != null && dy < 0 && child.getBottom() >= mParentHeight) {          ...      } else       // 1.mTargetView不为null      // 2.是向上滑动,dy>0表示向下滑动      // 3.AppBarLayout尚未恢复到原始高度child.getBottom() > mParentHeight      if (mTargetView != null && dy > 0 && child.getBottom() > mParentHeight) {          // 累减垂直方向上滑动的像素数          mTotalDy -= dy;          // 计算目标View缩放比例,不能小于1          mLastScale = Math.max(1f, 1f + mTotalDy / TARGET_HEIGHT);          // 缩放目标View          ViewCompat.setScaleX(mTargetView, mLastScale);          ViewCompat.setScaleY(mTargetView, mLastScale);          // 计算目标View缩小后减少的高度          mLastBottom = mParentHeight + (int) (mTargetViewHeight / 2 * (mLastScale - 1));          // 修改AppBarLayout的高度          child.setBottom(mLastBottom);          // 保持target不滑动          target.setScrollY(0);      } else {          super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);      }  }</code></pre>    <p>与上滑的逻辑基本一直,所以可写为一个方法</p>    <pre>  <code class="language-java">@Override  public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {      if (mTargetView != null && ((dy < 0 && child.getBottom() >= mParentHeight) || (dy > 0 && child.getBottom() > mParentHeight))) {          scale(child, target, dy);      } else {          super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);      }  }    private void scale(AppBarLayout abl, View target, int dy) {      mTotalDy += -dy;      mTotalDy = Math.min(mTotalDy, TARGET_HEIGHT);      mLastScale = Math.max(1f, 1f + mTotalDy / TARGET_HEIGHT);      ViewCompat.setScaleX(mTargetView, mLastScale);      ViewCompat.setScaleY(mTargetView, mLastScale);      mLastBottom = mParentHeight + (int) (mTargetViewHeight / 2 * (mLastScale - 1));      abl.setBottom(mLastBottom);      target.setScrollY(0);  }</code></pre>    <p>4.还原</p>    <p>当AppBarLayout处于越界时,如果用户松开手指,此时应该让目标View和AppBarLayout都还原到原始状态,重写onStopNestedScroll()方法</p>    <pre>  <code class="language-java">@Override  public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target) {      recovery(abl);      super.onStopNestedScroll(coordinatorLayout, abl, target);  }    private void recovery(final AppBarLayout abl) {      if (mTotalDy > 0) {          mTotalDy = 0;          // 使用属性动画还原          ValueAnimator anim = ValueAnimator.ofFloat(mLastScale, 1f).setDuration(200);          anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {              @Override              public void onAnimationUpdate(ValueAnimator animation) {                  float value = (float) animation.getAnimatedValue();                  ViewCompat.setScaleX(mTargetView, value);                  ViewCompat.setScaleY(mTargetView, value);                  abl.setBottom((int) (mLastBottom - (mLastBottom - mParentHeight) * animation.getAnimatedFraction()));              }          });          anim.start();      }  }</code></pre>    <p>5.优化</p>    <p>由于用户在滑动时有可能触发快速滑动,会导致在AppBarLayout收起后触发还原动画,重新修改AppBarLayout的Bottom,从而显示错误,所以当发生快速滑动时需要禁止还原动画,直接还原到初始状态</p>    <pre>  <code class="language-java">private boolean isAnimate;  //是否有动画    @Override  public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {      // 开始滑动时,启用动画      isAnimate = true;      return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);  }    @Override  public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY) {      // 如果触发了快速滚动且垂直方向上速度大于100,则禁用动画      if (velocityY > 100) {          isAnimate = false;      }      return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);  }    private void recovery(final AppBarLayout abl) {      if (mTotalDy > 0) {          mTotalDy = 0;          if (isAnimate) {              ValueAnimator anim = ValueAnimator.ofFloat(mLastScale, 1f).setDuration(200);              anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {                  @Override                  public void onAnimationUpdate(ValueAnimator animation) {                      float value = (float) animation.getAnimatedValue();                      ViewCompat.setScaleX(mTargetView, value);                      ViewCompat.setScaleY(mTargetView, value);                      abl.setBottom((int) (mLastBottom - (mLastBottom - mParentHeight) * animation.getAnimatedFraction()));                  }              });              anim.start();          } else {              ViewCompat.setScaleX(mTargetView, 1f);              ViewCompat.setScaleY(mTargetView, 1f);              abl.setBottom(mParentHeight);          }      }  }</code></pre>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/bb3fe452e1f5</p>    <p> </p>