四时宝库

程序员的知识宝库

material之Behavior的学习(material base)

效果展示

Material中提供了一个协调布局CoordinatorLayout,使我们能够轻松的实现一些联动交互效果。如上图页面向上滑动时,顶部的图片渐渐消失,标题栏固定在顶部,底部的选项卡向下移动隐藏,右下角第一个圆形浮动按钮缩小隐藏,第二个带文字的浮动按钮向右移动隐藏。这样页面只剩下标题栏和内容,可浏览的内容最大化。当页面向下滑动时,底部选项卡向上滑动显示出来,右边的浮动按钮一个放大显示,一个向左移动显示,分别与隐藏时的动画相反。

效果实现

这样一个联动效果实现起来很复杂吧?在material之前,要实现这样的效果确实很复杂,而现在我们只需要在xml中写几个配置属性就可以了,非常方便

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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"
  app:statusBarBackground="@android:color/transparent"
  tools:context=".MainActivity">

  <com.google.android.material.appbar.AppBarLayout
    android:id="@+id/app_bar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white"
    android:fitsSystemWindows="true"
    app:statusBarForeground="@android:color/transparent">

    <com.google.android.material.appbar.CollapsingToolbarLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      app:collapsedTitleTextAppearance="@style/titleText"
      app:contentScrim="@color/purple_700"
      app:expandedTitleTextAppearance="@style/titleText"
      app:layout_scrollFlags="scroll|exitUntilCollapsed"
      app:statusBarScrim="@android:color/transparent"
      app:title="龙儿筝"
      app:titleEnabled="true"
      app:toolbarId="@id/toolbar">

      <com.google.android.material.imageview.ShapeableImageView
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:fitsSystemWindows="true"
        android:scaleType="centerCrop"
        android:src="@drawable/a"
        app:layout_collapseMode="parallax" />

      <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:fitsSystemWindows="true"
        app:layout_collapseMode="pin" />
    </com.google.android.material.appbar.CollapsingToolbarLayout>
  </com.google.android.material.appbar.AppBarLayout>

  <androidx.core.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">

    <androidx.appcompat.widget.LinearLayoutCompat
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:background="@android:color/white"
      android:orientation="vertical"
      app:divider="@android:drawable/divider_horizontal_textfield"
      app:showDividers="middle">

      <com.google.android.material.textview.MaterialTextView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="龙儿筝1"
        android:textSize="48sp" />
      <!-- 省略其它的TextView -->
    </androidx.appcompat.widget.LinearLayoutCompat>
  </androidx.core.widget.NestedScrollView>

  <com.google.android.material.floatingactionbutton.FloatingActionButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    android:layout_marginEnd="16dp"
    android:layout_marginBottom="160dp"
    android:src="@drawable/ic_android"
    app:fabSize="mini"
    app:layout_behavior=".HideScaleViewOnScrollBehavior" />

  <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
    android:id="@+id/fab2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    android:layout_marginEnd="16dp"
    android:layout_marginBottom="80dp"
    android:text="龙儿筝"
    app:icon="@drawable/ic_android"
    app:layout_behavior=".HideRightViewOnScrollBehavior" />

  <com.google.android.material.bottomnavigation.BottomNavigationView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom"
    app:labelVisibilityMode="labeled"
    app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
    app:menu="@menu/menu_bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
  • 配置ImageView的layout_collapseMode属性为parallax,使其滑动时有视觉差效果
  • 配置Toolbar的layout_collapseMode属性为pin,使其能悬浮在顶部
  • 配置NestedScrollView的layout_behavior为ScrollingViewBehavior,使其为主滑动组件,其它组件都是依赖它来进行移动
  • 配置FloatingActionButton的layout_behavior为HideScaleViewOnScrollBehavior,缩放隐藏
  • 配置ExtendedFloatingActionButton的layout_behavior为HideRightViewOnScrollBehavior,右移隐藏
  • 配置BottomNavigationView的layout_behavior为HideBottomViewOnScrollBehavior,下移隐藏

其中ScrollingViewBehavior和HideBottomViewOnScrollBehavior是系统提供了,HideScaleViewOnScrollBehavior和HideRightViewOnScrollBehavior是自定义的。

HideBottomViewOnScrollBehavior源码分析

public class HideBottomViewOnScrollBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {

  protected static final int ENTER_ANIMATION_DURATION = 225;
  protected static final int EXIT_ANIMATION_DURATION = 175;

  private static final int STATE_SCROLLED_DOWN = 1;
  private static final int STATE_SCROLLED_UP = 2;

  private int height = 0;
  private int currentState = STATE_SCROLLED_UP;
  private int additionalHiddenOffsetY = 0;
  @Nullable private ViewPropertyAnimator currentAnimator;

  public HideBottomViewOnScrollBehavior() {}

  public HideBottomViewOnScrollBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  @Override
  public boolean onLayoutChild(
      @NonNull CoordinatorLayout parent, @NonNull V child, int layoutDirection) {
    ViewGroup.MarginLayoutParams paramsCompat =
        (ViewGroup.MarginLayoutParams) child.getLayoutParams();
    height = child.getMeasuredHeight() + paramsCompat.bottomMargin;
    return super.onLayoutChild(parent, child, layoutDirection);
  }

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

  @Override
  public void onNestedScroll(
      CoordinatorLayout coordinatorLayout,
      @NonNull V child,
      @NonNull View target,
      int dxConsumed,
      int dyConsumed,
      int dxUnconsumed,
      int dyUnconsumed,
      int type,
      @NonNull int[] consumed) {
    if (dyConsumed > 0) {
      slideDown(child);
    } else if (dyConsumed < 0) {
      slideUp(child);
    }
  }

  public void slideUp(@NonNull V child) {
    if (currentState == STATE_SCROLLED_UP) {
      return;
    }

    if (currentAnimator != null) {
      currentAnimator.cancel();
      child.clearAnimation();
    }
    currentState = STATE_SCROLLED_UP;
    animateChildTo(
        child, 0, ENTER_ANIMATION_DURATION, AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
  }

  public void slideDown(@NonNull V child) {
    if (currentState == STATE_SCROLLED_DOWN) {
      return;
    }

    if (currentAnimator != null) {
      currentAnimator.cancel();
      child.clearAnimation();
    }
    currentState = STATE_SCROLLED_DOWN;
    animateChildTo(
        child,
        height + additionalHiddenOffsetY,
        EXIT_ANIMATION_DURATION,
        AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR);
  }

  private void animateChildTo(
      @NonNull V child, int targetY, long duration, TimeInterpolator interpolator) {
    currentAnimator =
        child
            .animate()
            .translationY(targetY)
            .setInterpolator(interpolator)
            .setDuration(duration)
            .setListener(
                new AnimatorListenerAdapter() {
                  @Override
                  public void onAnimationEnd(Animator animation) {
                    currentAnimator = null;
                  }
                });
  }
}
  • onLayoutChild方法初始化动画移动的距离
  • onStartNestedScroll方法监听垂直方向的滑动
  • onNestedScroll页面向下滑动隐藏控件,向上滑动显示控件
  • animateChildTo真正的隐藏显示动画

核心思想就是判断向上滑动还是向下滑动,再通过动画隐藏或显示依赖的控件。

自定义Behavior

我们参考HideBottomViewOnScrollBehavior,很容易就能实现自己的隐藏动画效果。接下来我们实现一个滑动时向右移动和缩放的隐藏Behavior。逻辑很简单,代码就不分析了。

向右移动隐藏Behavior

public class HideRightViewOnScrollBehavior<V extends View> extends Behavior<V> {
    protected static final int ENTER_ANIMATION_DURATION = 225;
    protected static final int EXIT_ANIMATION_DURATION = 175;
    private static final int STATE_SCROLLED_OUT = 1;
    private static final int STATE_SCROLLED_IN = 2;
    private int target = 0;
    private int currentState = STATE_SCROLLED_IN;
    @Nullable
    private ViewPropertyAnimator currentAnimator;

    public HideRightViewOnScrollBehavior() {
    }

    public HideRightViewOnScrollBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull V child, int layoutDirection) {
        ViewGroup.MarginLayoutParams paramsCompat = (ViewGroup.MarginLayoutParams) child.getLayoutParams();
        target = child.getMeasuredWidth() + paramsCompat.getMarginEnd();
        return super.onLayoutChild(parent, child, layoutDirection);
    }

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

    @Override
    public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) {
        if (dyConsumed > 0) {
            animateOut(child);
        } else if (dyConsumed < 0) {
            animateIn(child);
        }
    }

    public void animateIn(@NonNull V child) {
        if (currentState == STATE_SCROLLED_IN) {
            return;
        }
        if (currentAnimator != null) {
            currentAnimator.cancel();
            child.clearAnimation();
        }
        currentState = STATE_SCROLLED_IN;
        animateChildTo(child, 0, ENTER_ANIMATION_DURATION, AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
    }

    public void animateOut(@NonNull V child) {
        if (currentState == STATE_SCROLLED_OUT) {
            return;
        }
        if (currentAnimator != null) {
            currentAnimator.cancel();
            child.clearAnimation();
        }
        currentState = STATE_SCROLLED_OUT;
        animateChildTo(child, target, EXIT_ANIMATION_DURATION, AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR);
    }

    private void animateChildTo(@NonNull V child, int target, long duration, TimeInterpolator interpolator) {
        currentAnimator = child.animate().translationX(target).setInterpolator(interpolator)
                .setDuration(duration).setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        currentAnimator = null;
                    }
                });
    }
}

缩放隐藏Behavior

public class HideScaleViewOnScrollBehavior<V extends View> extends Behavior<V> {
    protected static final int ENTER_ANIMATION_DURATION = 225;
    protected static final int EXIT_ANIMATION_DURATION = 175;
    private static final int STATE_SCROLLED_OUT = 1;
    private static final int STATE_SCROLLED_IN = 2;
    private int currentState = STATE_SCROLLED_IN;
    @Nullable
    private ViewPropertyAnimator currentAnimator;

    public HideScaleViewOnScrollBehavior() {
    }

    public HideScaleViewOnScrollBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    @Override
    public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) {
        if (dyConsumed > 0) {
            animateOut(child);
        } else if (dyConsumed < 0) {
            animateIn(child);
        }
    }

    public void animateIn(@NonNull V child) {
        if (currentState == STATE_SCROLLED_IN) {
            return;
        }
        if (currentAnimator != null) {
            currentAnimator.cancel();
            child.clearAnimation();
        }
        currentState = STATE_SCROLLED_IN;
        animateChildTo(child, 1, ENTER_ANIMATION_DURATION, AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
    }

    public void animateOut(@NonNull V child) {
        if (currentState == STATE_SCROLLED_OUT) {
            return;
        }
        if (currentAnimator != null) {
            currentAnimator.cancel();
            child.clearAnimation();
        }
        currentState = STATE_SCROLLED_OUT;
        animateChildTo(child, 0, EXIT_ANIMATION_DURATION, AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR);
    }

    private void animateChildTo(@NonNull V child, int targetY, long duration, TimeInterpolator interpolator) {
        currentAnimator = child.animate().scaleX(targetY).scaleY(targetY)
                .setInterpolator(interpolator).setDuration(duration).setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        currentAnimator = null;
                    }
                });
    }
}

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接