效果展示
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;
}
});
}
}