红橙Darren视频笔记 CoordinatorLayout:实现自定义behavior

效果:
红橙Darren视频笔记 CoordinatorLayout:实现自定义behavior_第1张图片
目标:利用CoordinatorLayout实现自定义behavior ,顶部的TitleBar和底部的ActionBar实现能够随着手势自由显示和隐藏
activity中使用了recyclerview 如果对recyclerview不熟悉可以参考官网:
https://developer.android.google.cn/guide/topics/ui/layout/recyclerview
所谓的behavior是与CoordinatorLayout进行联动的一些行为 注意观察 所有的behavior实际上都直接或者间接继承自CoordinatorLayout.Behavior
因此 如果要使用Behavior那么CoordinatorLayout是必须嵌套在外部的

一 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); } }

二 实现顶部TitleBar自由滚动

非常简单 直接使用系统定义好的behavior即可
1.将Toolbar嵌套在AppBarLayout
2.给ToolBar加上属性app:layout_scrollFlags=“scroll|enterAlways|snap”
3.给RecyclerView加上属性app:layout_behavior="@string/appbar_scrolling_view_behavior"

三 实现底部FloatingActionButton+ActionBar

思路 监听子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>

自定义Behavior

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的demo的不同之处

红橙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就不够纯粹了 或者说不够通用 似乎不符合指责单一原则
总之 对比两种方式 各有优劣,不知道读者怎么看呢?

你可能感兴趣的:(自定义view,android)