效果:
目标:利用CoordinatorLayout实现自定义behavior ,顶部的TitleBar和底部的ActionBar实现能够随着手势自由显示和隐藏
activity中使用了recyclerview 如果对recyclerview不熟悉可以参考官网:
https://developer.android.google.cn/guide/topics/ui/layout/recyclerview
所谓的behavior是与CoordinatorLayout进行联动的一些行为 注意观察 所有的behavior实际上都直接或者间接继承自CoordinatorLayout.Behavior
因此 如果要使用Behavior那么CoordinatorLayout是必须嵌套在外部的
介绍几个重要方法
public class TranslationBehaviorIntroduce extends FloatingActionButton.Behavior {
/**
* 表示是否给应用了Behavior 的View 指定一个依赖的布局,通常,当依赖的View 布局发生变化时
* 不管被被依赖View 的顺序怎样,被依赖的View也会重新布局
*
* @param parent FloatingActionButton的直接父容器应该是个CoordinatorLayout
* @param child 绑定behavior 的View
* @param dependency 依赖的view
* @return dependency是指定的实例类型 返回true,否则返回false
*/
@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull FloatingActionButton child, @NonNull View dependency) {
return super.layoutDependsOn(parent, child, dependency);
}
//FloatingActionButton的方法
//当被依赖的View 状态(如:位置、大小)发生变化时,这个方法被调用
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, View dependency) {
return super.onDependentViewChanged(parent, child, dependency);
}
/**
* 当CoordinatorLayout的子view尝试滚动的时候被调用
* Called when a descendant of the CoordinatorLayout attempts to initiate a nested scroll.
*
* 当返回值为true的时候表明 coordinatorLayout 充当nested scroll parent 处理这次滑动,
* 需要注意的是只有从此方法返回true的行为才会接收后续嵌套的滚动事件
* (如:onNestedPreScroll、onNestedScroll等)
*
* Any Behavior associated with any direct child of the CoordinatorLayout may respond
* to this event and return true to indicate that the CoordinatorLayout should act as
* a nested scrolling parent for this scroll. Only Behaviors that return true from
* this method will receive subsequent nested scroll events.
*
* @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
* associated with
* @param child the child view of the CoordinatorLayout this Behavior is associated with
* @param directTargetChild the child view of the CoordinatorLayout that either is or
* contains the target of the nested scroll operation
* @param target the descendant view of the CoordinatorLayout initiating the nested scroll
*
* axes是非常重要的参数 可以判断横向竖向滑动
* @param axes the axes that this nested scroll applies to. See
* {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
* {@link ViewCompat#SCROLL_AXIS_VERTICAL}
* @param type the type of input which cause this scroll event
* @return true if the Behavior wishes to accept this nested scroll
*
* @see NestedScrollingParent2#onStartNestedScroll(View, View, int, int)
*/
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull FloatingActionButton child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type);
}
/**
* 嵌套滚动发生之前被调用
* 在nested scroll child 消费掉自己的滚动距离之前,嵌套滚动每次被nested scroll child
* 更新都会调用onNestedPreScroll。注意有个重要的参数consumed,可以修改这个数组表示你消费
* 了多少距离。假设用户垂直滑动了100px,child 做了90px 的位移,你需要把 consumed[1]的值改成90,
* 这样coordinatorLayout就能知道只处理剩下的10px的滚动。
* Called when a nested scroll in progress is about to update, before the target has
* consumed any of the scrolled distance.
*
* Any Behavior associated with the direct child of the CoordinatorLayout may elect
* to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
* that returned true will receive subsequent nested scroll events for that nested scroll.
*
*
* onNestedPreScroll
is called each time the nested scroll is updated
* by the nested scrolling child, before the nested scrolling child has consumed the scroll
* distance itself. Each Behavior responding to the nested scroll will receive the
* same values. The CoordinatorLayout will report as consumed the maximum number
* of pixels in either direction that any Behavior responding to the nested scroll reported
* as consumed.
*
* @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
* associated with
* @param child the child view of the CoordinatorLayout this Behavior is associated with
* @param target the descendant view of the CoordinatorLayout performing the nested scroll
* @param dx the raw horizontal number of pixels that the user attempted to scroll
* 用户水平方向的滚动距离
* @param dy the raw vertical number of pixels that the user attempted to scroll
* 用户竖直方向的滚动距离
* @param consumed out parameter. consumed[0] should be set to the distance of dx that
* was consumed, consumed[1] should be set to the distance of dy that
* was consumed
* @param type the type of input which cause this scroll event
*
* @see NestedScrollingParent2#onNestedPreScroll(View, int, int, int[], int)
*/
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull FloatingActionButton child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
}
/**
* 进行嵌套滚动时被调用
* Called when a nested scroll in progress has updated and the target has scrolled or
* attempted to scroll.
*
* Any Behavior associated with the direct child of the CoordinatorLayout may elect
* to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
* that returned true will receive subsequent nested scroll events for that nested scroll.
*
*
* onNestedScroll
is called each time the nested scroll is updated by the
* nested scrolling child, with both consumed and unconsumed components of the scroll
* supplied in pixels. Each Behavior responding to the nested scroll will receive the
* same values.
*
*
* @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
* associated with
* @param child the child view of the CoordinatorLayout this Behavior is associated with
* @param target the descendant view of the CoordinatorLayout performing the nested scroll
* @param dxConsumed horizontal pixels consumed by the target's own scrolling operation
* @param dyConsumed vertical pixels consumed by the target's own scrolling operation
* @param dxUnconsumed horizontal pixels not consumed by the target's own scrolling
* operation, but requested by the user
* @param dyUnconsumed vertical pixels not consumed by the target's own scrolling operation,
* but requested by the user
* @param type the type of input which cause this scroll event
*
* @see NestedScrollingParent2#onNestedScroll(View, int, int, int, int, int)
*/
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull FloatingActionButton child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
}
/**
* 嵌套滚动结束时被调用,这是一个清除滚动状态等的好时机
* Called when a nested scroll has ended.
*
* Any Behavior associated with any direct child of the CoordinatorLayout may elect
* to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
* that returned true will receive subsequent nested scroll events for that nested scroll.
*
*
* onStopNestedScroll
marks the end of a single nested scroll event
* sequence. This is a good place to clean up any state related to the nested scroll.
*
*
* @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
* associated with
* @param child the child view of the CoordinatorLayout this Behavior is associated with
* @param target the descendant view of the CoordinatorLayout that initiated
* the nested scroll
* @param type the type of input which cause this scroll event
*
* @see NestedScrollingParent2#onStopNestedScroll(View, int)
*/
@Override
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull FloatingActionButton child, @NonNull View target, int type) {
super.onStopNestedScroll(coordinatorLayout, child, target, type);
}
/**
*
* 用户松开手指并且会发生惯性动作之前调用,参数提供了速度信息,可以根据这些速度信息
* 决定最终状态,比如滚动Header,是让Header处于展开状态还是折叠状态。返回true 表
* 示消费了fling.
* Called when a nested scrolling child is about to start a fling.
*
* Any Behavior associated with the direct child of the CoordinatorLayout may elect
* to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
* that returned true will receive subsequent nested scroll events for that nested scroll.
*
*
* onNestedPreFling
is called when the current nested scrolling child view
* detects the proper conditions for a fling, but it has not acted on it yet. A
* Behavior can return true to indicate that it consumed the fling. If at least one
* Behavior returns true, the fling should not be acted upon by the child.
*
* @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
* associated with
* @param child the child view of the CoordinatorLayout this Behavior is associated with
* @param target the descendant view of the CoordinatorLayout performing the nested scroll
* @param velocityX horizontal velocity of the attempted fling
* @param velocityY vertical velocity of the attempted fling
* @return true if the Behavior consumed the fling
*
* @see NestedScrollingParent#onNestedPreFling(View, float, float)
*/
@Override
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull FloatingActionButton child, @NonNull View target, float velocityX, float velocityY) {
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
//摆放子 View 的时候调用,可以重写这个方法对子View 进行重新布局
@Override
public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child, int layoutDirection) {
return super.onLayoutChild(parent, child, layoutDirection);
}
}
非常简单 直接使用系统定义好的behavior即可
1.将Toolbar嵌套在AppBarLayout
2.给ToolBar加上属性app:layout_scrollFlags=“scroll|enterAlways|snap”
3.给RecyclerView加上属性app:layout_behavior="@string/appbar_scrolling_view_behavior"
思路 监听子view的滑动 如果是向上滑动 显示FloatingActionButton ActionBar反之显示出FloatingActionButton ActionBar
自定义TranslationBehavior 继承 FloatingActionButton.Behavior 实现两个重要的监听滚动的方法
然后再xml中指定behavior为刚刚自定义的behavior
xml
<?xml version="1.0" encoding="utf-8"?><!-- 注意 使用CoordinatorLayout design RecyclerView需要在gradle文件配置引用 -->
<!-- CoordinatorLayout可以协调直接子控件 Coordinator是协调员的意思-->
<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"
tools:context=".MainActivity">
<!-- AppBarLayout十分依赖CoordinatorLayout来实现协调功能 AppBarLayout继承自LinearLayout -->
<!-- 通过AppBarLayout包裹Toolbar 让Toolbar可以具有某些滑动效果 -->
<!-- title部分 -->
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 添加app:layout_scrollFlags="scroll|enterAlways|snap" 可以让titleBar随着recyclerView向上向下滚动 -->
<androidx.appcompat.widget.Toolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:title="知乎首页"
app:titleTextColor="#FFFFFF" />
</com.google.android.material.appbar.AppBarLayout>
<!-- 主体部分 -->
<!-- 加上app:layout_behavior="@string/appbar_scrolling_view_behavior" 使得头部titleBar 底部actionBar不会遮挡 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<!-- 浮动button -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="70dp"
android:src="@drawable/ic_android"
app:layout_behavior=".TranslationBehavior" />
<!-- 加上app:layout_behavior="@string/bottom_sheet_behavior"使得actionBar自动沉到底部 -->
<LinearLayout
android:id="@+id/bottom_tab_layout"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom"
android:background="@android:color/white"
app:layout_behavior=".TranslationBehavior">
<!-- ?actionBarSize 如果不知道是什么意思 也可以自己指定数值 -->
<ImageView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:src="@drawable/ic_android" />
<ImageView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:src="@drawable/ic_android" />
<ImageView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:src="@drawable/ic_android" />
<ImageView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:src="@drawable/ic_android" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
public class TranslationBehavior extends CoordinatorLayout.Behavior {
/**
* 报错
* Caused by: android.view.InflateException: Binary XML file line #38: Could not inflate Behavior subclass com.example.translationbehavior.TranslationBehavior
* Caused by: java.lang.RuntimeException: Could not inflate Behavior subclass com.example.translationbehavior.TranslationBehavior
* at androidx.coordinatorlayout.widget.CoordinatorLayout.parseBehavior(CoordinatorLayout.java:622)
* at androidx.coordinatorlayout.widget.CoordinatorLayout$LayoutParams.(CoordinatorLayout.java:2805)
* 在parseBehavior中断点 发现执行clazz.getConstructor(CONSTRUCTOR_PARAMS);时报错
* 跟踪CONSTRUCTOR_PARAMS
* static final Class>[] CONSTRUCTOR_PARAMS = new Class>[] {
* Context.class,
* AttributeSet.class
* };
* 发现我们需要构造函数 且必须是public的
*/
public TranslationBehavior(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
}
/**
* 当CoordinatorLayout的子view尝试滚动的时候被调用
*
* 当返回值为true的时候表明 coordinatorLayout 充当nested scroll parent 处理这次滑动,
* 需要注意的是只有从此方法返回true的行为才会接收后续嵌套的滚动事件
* (如:onNestedPreScroll、onNestedScroll等)
*
* @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
* associated with
* @param child the child view of the CoordinatorLayout this Behavior is associated with
* @param directTargetChild the child view of the CoordinatorLayout that either is or
* contains the target of the nested scroll operation
* @param target the descendant view of the CoordinatorLayout initiating the nested scroll
*
* axes是非常重要的参数 可以判断横向竖向滑动
* @param axes the axes that this nested scroll applies to. See
* {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
* {@link ViewCompat#SCROLL_AXIS_VERTICAL}
* @param type the type of input which cause this scroll event
* @return true if the Behavior wishes to accept this nested scroll
* @see NestedScrollingParent2#onStartNestedScroll(View, View, int, int)
*/
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type);
//只在竖向scroll发生的时候接受后续滑动事件
return axes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
//滑动发生时调用
private boolean isAnimating = false;
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
Log.e("TAG", "dyConsumed -> " + dyConsumed + " dyUnconsumed -> " + dyUnconsumed);
child.animate().setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
isAnimating = true;
}
@Override
public void onAnimationEnd(Animator animation) {
isAnimating = false;
}
@Override
public void onAnimationCancel(Animator animation) {
isAnimating = false;
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
if (!isAnimating) {
floatingButtonAnimate(dyConsumed, child);
}
}
private void floatingButtonAnimate(int dyConsumed, View child) {
if (dyConsumed > 0) {
//如果是向上滑动 隐藏ActionBar
int translationY = ((CoordinatorLayout.LayoutParams) child.getLayoutParams()).bottomMargin + child.getMeasuredHeight();
child.animate().translationY(translationY).setDuration(500).start();
} else if (dyConsumed < 0) {
// 如果是向下滑动 显示出ActionBar
child.animate().translationY(0).setDuration(500).start();
}
}
}
activity
//本节内容 利用CoordinateLayout实现自定义Behavior
public class MainActivity extends AppCompatActivity {
RecyclerView mRecyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecyclerView = findViewById(R.id.recycler_view);
//必须指定一个LayoutManager
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setAdapter(new RecyclerView.Adapter() {
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
//创建ViewHolder时 不要将item直接填充到parent(mRecyclerView) 否则抛出异常
// ViewHolder views must not be attached when created. Ensure that you are not passing 'true' to the attachToRoot parameter of LayoutInflater.inflate(..., boolean attachToRoot
//即attachToRoot = false
View itemView = LayoutInflater.from(MainActivity.this).inflate(R.layout.recycler_item, parent, false);
return new ViewHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
((TextView) holder.itemView).setText("RecyclerView ->" + position);
}
@Override
public int getItemCount() {
return 50;
}
});
}
private static class ViewHolder extends RecyclerView.ViewHolder {
ViewHolder(View itemView) {
super(itemView);
}
}
}
红橙Darren Material Design - 自定义Behavior原文链接
https://www.jianshu.com/p/f3c81cf485e5
原作者的实现方式和我不同
1.我的实现方式:
我观察到所有的behavior实际上都继承自CoordinatorLayout.Behavior 因此 我没有继承FloatingActionButton.Behavior 而是直接继承CoordinatorLayout.Behavior 这样意味着所有的View都可以使用这个自定义Behavior
但是缺点是丢失了一些指定Behavior的特性 比如FloatingActionButton.Behavior
而且在这种情况下 底部的ActionBar必须使用自定义Behavior 而不能使用系统的Behavior(app:layout_behavior="@string/bottom_sheet_behavior") 因此我们需要手动给bottom_tab_layout添加属性android:layout_gravity=“bottom” 以达到原先的效果
最后 如果ActionBar和FloatingActionButton的动画效果不一样 我们势必要拆成两个behavior 但是我认为为了符合指责单一原则 这样是有必要的
但是优点是通用 FloatingActionButton+ActionBar都可以使用这个Behavior
2.红橙Darren的实现方式
他的实现直接继承于FloatingActionButton的Behavior 因此只有FloatingActionButton可以使用该Behavior
但是他在内部先找到了底部的ActionBar然后 在FloatingActionButton发生scroll的同时 对ActionBar进行动画操作
这样很方便 而且ActionBar本身还可以使用系统的Behavior(app:layout_behavior="@string/bottom_sheet_behavior")
最后FloatingActionButton与ActionBar还能够实现不一样的动画效果。
但是 感觉上这个Behavior就不够纯粹了 或者说不够通用 似乎不符合指责单一原则
总之 对比两种方式 各有优劣,不知道读者怎么看呢?