behavior_behaviors - CSDN
精华内容
参与话题
  • 简介Behavior的中文翻译是”行为”的意思.Behavior是Android新出的Design库里新增的布局概念。Behavior只有是CoordinatorLayout的直接子View才有意义。可以为任何View添加一个Behavior。 Material Design里面的...

    一.简介

    Behavior的中文翻译是”行为”的意思.

    Behavior是Android新出的Design库里新增的布局概念。Behavior只有是CoordinatorLayout的直接子View才有意义。可以为任何View添加一个Behavior。

    Material Design里面的CoordinatorLayout是一个非常强大的控件,它接管了child组件之间的交互。让你滑动交互使用更加方便简单,效果也更加强大,不需要像以前那样自己处理一坨什么乱七八槽的滑动,事件传递之类的处理了。

    Behavior是一系列回调。让你有机会以非侵入的为View添加动态的依赖布局,和处理父布局(CoordinatorLayout)滑动手势的机会。如果我们想实现控件之间任意的交互效果,完全可以通过自定义 Behavior 的方式达到。

    在学习自定义behavior之前我们先看一看官方内置的behavior
    这里写图片描述

    二.BottomSheetBehavior

    BottomSheetBehavior实现的效果就是一个布局底部弹出,类似于饿了么查看购物车的效果,这种效果我们可以有很多种方式实现,使用BottomSheetBehavior你会发现简直只需要一行代码.

    这里写图片描述

    1.页面布局

    <?xml version="1.0" encoding="utf-8"?>
    <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:id="@+id/activity_group_car_boottom_sheet"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.testdemo.king.kingtestdemo.GroupCarBoottomSheetActivity">
    
    
        <LinearLayout
            android:visibility="visible"
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <Button
                android:id="@+id/bt1"
                android:text="显示bottomsheetBehavior"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="wrap_content" />
            <Button
                android:id="@+id/bt2"
                android:text="显示bottomsheetBehaviorDialog"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="wrap_content" />
        </LinearLayout>
        <RelativeLayout
    
            android:id="@+id/design_bottom_sheet"
            android:layout_width="match_parent"
            android:layout_gravity="center"
            android:layout_height="match_parent"
            android:background="@color/colorAccent"
            app:behavior_hideable="true"
            app:behavior_peekHeight="100dp"
            app:elevation="4dp"
            app:layout_behavior="@string/bottom_sheet_behavior">
    
            <TextView
                android:id="@+id/bottomsheet_text"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:text="展示Bottom Sheets"
                android:textColor="#FFFFFF" />
    
        </RelativeLayout>
    
    
    </android.support.design.widget.CoordinatorLayout>
    

    属性说明

    //折叠的高度
    app:behavior_peekHeight="10dp"          setPeekHeight
    //是否可以隐藏
    app:behavior_hideable="true"            setHideable
    //是否跳过折叠状态
    app:behavior_skipCollapsed="true"       setSkipCollapsed

    2.代码实现

    只需要获取到这个behavior

            bottomSheetBehavior = BottomSheetBehavior.from((View)rlBottom);
    

    然后点击的时候按钮时候控制其隐藏和出现即可

    case R.id.bt1:
                    if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
                        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
                    } else {
                        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
                    }
                    break;

    状态说明:

    • STATE_COLLAPSED: 关闭Bottom Sheets,显示peekHeight的高度,默认是0
    • STATE_DRAGGING: 用户拖拽Bottom Sheets时的状态
    • STATE_SETTLING: 当Bottom Sheets view释放时记录的状态。
    • STATE_EXPANDED: 当Bottom Sheets 展开的状态
    • STATE_HIDDEN: 当Bottom Sheets 隐藏的状态

    3.状态监听

    我们可以通过监听状态的改变做一些自定义的操作,比如这样.
    这里写图片描述

      bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
                @Override
                public void onStateChanged(@NonNull View bottomSheet, int newState) {
    //                /这里是bottomSheet 状态的改变,根据slideOffset可以做一些动画
                }
    
                @Override
                public void onSlide(@NonNull View bottomSheet, float slideOffset) {
                    //这里是拖拽中的回调,根据slideOffset可以做一些动画
                    Log.e("king",slideOffset+"");
                    if(slideOffset>0){
                        if(slideOffset>0.5)slideOffset=0.5f;
                        ViewGroup.LayoutParams para1;
                        para1 = bottomSheet.getLayoutParams();
                        para1.width = (int) (rlBottomWidth*(0.5+slideOffset));
                        bottomSheet.setLayoutParams(para1);
                    }
    
    
                }
            });

    三.BottomSheetDialog

    BottomSheetDialog是一个基于bottomSheetbehavior实现的dialog

    代码实现

     private void initBottomSheetDialog() {
            dialog = new BottomSheetDialog(this);
            View dialogView = LayoutInflater.from(this).inflate(R.layout.item_text, null, false);
            ((TextView) dialogView.findViewById(R.id.text)).setText("bottomsheetDialog");
            dialog.setContentView(dialogView);
            dialog.hide();
        }

    然后只需要调用dialog.hide();或者show()方法就能实现dialog的显示隐藏了.

    这里写图片描述

    当然,BottomSheetDialog也能调用状态监听,具体实现如下

    private void setBehaviorCallback() {
            View view = dialog.getDelegate().findViewById(android.support.design.R.id.design_bottom_sheet);
            final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(view);
            bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
                @Override
                public void onStateChanged(@NonNull View bottomSheet, int newState) {
                    if (newState == BottomSheetBehavior.STATE_HIDDEN) {
                        dialog.dismiss();
                        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
                    }
                }
    
                @Override
                public void onSlide(@NonNull View bottomSheet, float slideOffset) {
                }
            });
        }

    四.SwipeDismissBehavior

    SwipeDismissBehavior是一个实现侧滑删除的效果,比较简单

    private void initSwipeDismissBehavior() {
            SwipeDismissBehavior<View> swipe = new SwipeDismissBehavior();
            swipe.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_ANY);
    
            swipe.setListener(
                    new SwipeDismissBehavior.OnDismissListener() {
                        @Override
                        public void onDismiss(View view) {
    
                        }
    
                        @Override
                        public void onDragStateChanged(int state) {
                        }
                    });
    
            CoordinatorLayout.LayoutParams coordinatorParams =
                    (CoordinatorLayout.LayoutParams)tv1 .getLayoutParams();
    
            coordinatorParams.setBehavior(swipe);
        }

    五.AppBarLayout$ScrollingViewBehavior

    ScrollingViewBehavior的简单应用大家参考一下这篇文章吧
    http://blog.csdn.net/aqi00/article/details/56834285

    系统默认提供的behavior毕竟只能实现一部分功能,下一篇博客我们将介绍如何使用自定义behavior来实现复杂的功能.

    展开全文
  • ​ 我们之前简单介绍了AppBarLayout,CollapsingToolbarLayout的使用,他们的都是作为CoordinatorLayout的...今天我们介绍一下CoordinatorLayout中的Behavior。在CoorindatorLayout中其实没有做太多的事情,它就是一个Vi

    ​ 我们之前简单介绍了AppBarLayout,CollapsingToolbarLayout的使用,他们的都是作为CoordinatorLayout的子View 使用的。如果没有CoordinatorLayout作为父View,它们是没有任何效果的。今天我们介绍一下CoordinatorLayout中的Behavior。在CoorindatorLayout中其实没有做太多的事情,它就是一个ViewGroup,类似FrameLayout;它的核心就在于Behavior类。我们给CoordinatorLayout的子View设置一个Behavior,就可以接管该View自身的一些事件与其他的View之间的交互事件。包括 touch,measure,layout等事件。

    ​ 我们使用的AppBarLayout有属于自己的Behavior,它是在AppBarLayout中声明的内部类,有兴趣的去看看AppBarLayout的代码就很容易发现。

    简单定义Behavior

    ​ 我们没有办法直接使用Behavior,需要我们自己定义自己的Behavior。如何定义自己的内部类呢?只需要继承Behavior就OK了。就像下面这样:

    public class MyBehavior extends CoordinatorLayout.Behavior<View> {
        //用来在代码里实例化Behavior的构造
        public MyBehavior() {
        }
    
        //如果要在布局中使用Behavior,这个构造是必须的
        public MyBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    }

    ​ 这里有个注意地方,Behavior只能作用在View上的,所以泛型上用的是View 。我们都知道View中 用setTag,getTag用来保存临时数据,也有onSaveInstanceState,onRestoreInstanceState来保存相关实例的状态,Behavior也有,用法跟View相当类似,这里不介绍了,一般用不到。

    使用自定义的Behavior

    ​ 第一种方式:我们自定义的可以通过在布局中使用app:layout_behavior的来指定,例如,我们在NestedScrollView设定的Android提供的Behavior,app:layout_behavior="@string/appbar_scrolling_view_behavior">,其中的string是Behavior的绝对路径。

    ​ 第二种方式:如果你要用到自定义View/ViewGroup中,可以通过@CoordinatorLayout.DefaultBehavior()注解指定Behavior;这样我们的View就会默认使用这个Behavior。比如在AppBarLayout的使用方式:

    @CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
    public class AppBarLayout extends LinearLayout {
        //省略具体实现代码
    }

    ​ 第三种方式:通过代码设定Behavior,前提是你的指定的View必须是CoordinatorLayout的子View,如下:

    MyBehavior myBehavior = new MyBehavior();
    //我们的View必须是CoordinatorLayout的子View,否则我们获取不到CoordinatorLayout.LayoutParams
    CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) 我们的View.getLayoutParams();
    params.setBehavior(myBehavior);

    Behavior中的依赖

    ​ Behavior可以设置使用Behavior的View依赖的View。比如我们的View A 使用了我们自定义的Behavior MyBehavior,我们可以通过使用MyBehavior设定我们的 View A依赖于某个View B,这样我们可以获取到View B的各种状态,根据View B的状态去设定我们的View A 。

    ​ 我们通过 覆写BehaviorlayoutDependsOn去指定依赖的View。例如像这样:

    /**
     * 指定依赖的View,在这里指定依赖的View之后,
     * 我们去{@linkplain #onDependentViewChanged(CoordinatorLayout, View, View)}里去处理这两个依赖的View之间的关系
     *
     * @param parent
     * @param child      使用该Behavior的View
     * @param dependency 依赖的View
     * @return 当指定的View是我们需要的View时,返回true
     */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency instanceof AppBarLayout;
    }

    也可以像这样指定某个View,通过id去验证是不是我们依赖的View :

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency.getId() == R.id.dependency_view_id;
    }

    ​ 指定完依赖的View 之后,我们去覆写onDependentViewChanged方法去获取我们依赖的View的各个状态,并且设置我们自己的View状态,例如:

    /**
     * 当我们指定依赖的View 有变化时,调用这个方法
     * @param parent
     * @param child 使用该Behavior的View
     * @param dependency 依赖的View
     * @return
     */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        final View finalChild = child;
        AppBarLayout finalDependency = (AppBarLayout) dependency;
        finalDependency.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
            @Override
            public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                float tranlationY = Math.abs(verticalOffset / 3);
                finalChild.setTranslationY(tranlationY);
            }
        });
        return true;
    }

    这里我利用了 AppBarLayout CollapsingToolbarLayout 的进一步使用中的示例,如果兴趣的同学可以看一下。那么整个Behavior就是如下这样了:

    public class MyBehavior extends CoordinatorLayout.Behavior<View> {
    
        //用来在代码里实例化我们自定义的Behavior的构造
        public MyBehavior() {
        }
    
        //如果要在布局中使用Behavior,这个构造是必须的
        public MyBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        /**
         * 指定依赖的View,在这里指定依赖的View之后,
         * 我们去{@linkplain #onDependentViewChanged(CoordinatorLayout, View, View)}里去处理这两个依赖的View之间的关系
         *
         * @param parent
         * @param child      使用该Behavior的View
         * @param dependency 依赖的View
         * @return 当指定的View是我们需要的View时,返回true
         */
        @Override
        public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
            return dependency instanceof AppBarLayout;
        }
    
        /**
         * 当我们指定依赖的View 有变化时,调用这个方法
         * @param parent
         * @param child 使用该Behavior的View
         * @param dependency 依赖的View
         * @return
         */
        @Override
        public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
            final View finalChild = child;
            AppBarLayout finalDependency = (AppBarLayout) dependency;
            finalDependency.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
                @Override
                public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                    float tranlationY = Math.abs(verticalOffset / 3);
                    finalChild.setTranslationY(tranlationY);
                }
            });
            return true;
        }
    }

    我们设定了依赖的View 是AppBarLayout,我们监听AppBarLayout的上下折叠的偏移,根据它的偏移来设置我们的View的状态。效果如下:

    我们可以看到我们的下面粉红色的View,会随着我们的AppBarLayout的折叠展开会滑出滑进屏幕。根据这个效果结合我们AppBarLayout的app:layout_scrollFlags 就可以做出类似知乎首页的那种功能了。

    监听滑动

    ​ 如果我们需要监听一下CoordinatorLayout中子View的滑动,而不使用依赖View,那么怎么办呢?

    要处理这个问题我们至少需要了解两个方法onStartNestedScroll,onNestedPreScroll这两个方法:

    /**
     * 这里返回true,才会接收到后续滑动事件
     *
     * @param coordinatorLayout
     * @param child             使用该Behavior的View
     * @param directTargetChild Coordinator的子View,它可能包含一些滑动的操作
     * @param target            初始化滑动动作的View
     * @param nestedScrollAxes  滑动的轴,根据这个值来判断是纵向滑动还是横向滑动;
     *                          {@linkplain ViewCompat#SCROLL_AXIS_VERTICAL} 竖向;
     *                          {@linkplain ViewCompat#SCROLL_AXIS_HORIZONTAL} 横向
     * @return
     */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return  super.onStartNestedScroll(coordinatorLayout,child,directTargetChild,target,nestedScrollAxes);
    }
    
    /**
     * 这里可以获取到CoordinatorLayout的滑动
     *
     * @param coordinatorLayout
     * @param child             使用该Behavior的View
     * @param target            执行滑动动作的View
     * @param dx                横向滑动的距离,以像素为单位
     * @param dy                纵向滑动的距离,以像素为单位
     * @param consumed
     */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }

    如果现在我们有一个Button想让它随着一个ScrollView 的滑动而滑动,怎么办呢?通过上面的两个方法就可以实现。我们自己定义一个ScrollBehavior,覆写上面的方法就OK了:

    public class ScrollBehavior extends CoordinatorLayout.Behavior<View>{
    
        public ScrollBehavior() {
        }
    
        public ScrollBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        /**
         * 这里返回true,才会接收到后续滑动事件
         *
         * @param coordinatorLayout
         * @param child             使用该Behavior的View
         * @param directTargetChild Coordinator的子View,它可能包含一些滑动的操作
         * @param target            初始化滑动动作的View
         * @param nestedScrollAxes  滑动的轴,根据这个值来判断是纵向滑动还是横向滑动;
         *                          {@linkplain ViewCompat#SCROLL_AXIS_VERTICAL} 竖向;
         *                          {@linkplain ViewCompat#SCROLL_AXIS_HORIZONTAL} 横向
         * @return
         */
        @Override
        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
            return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
        }
    
        /**
         * 这里可以获取到CoordinatorLayout的滑动
         *
         * @param coordinatorLayout
         * @param child             使用该Behavior的View
         * @param target            执行滑动动作的View
         * @param dx                横向滑动的距离,以像素为单位
         * @param dy                纵向滑动的距离,以像素为单位
         * @param consumed
         */
        @Override
        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
            super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
            final View finalChild = child;
            if (target instanceof NestedScrollView){
                NestedScrollView view = (NestedScrollView) target;
                view.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
                    @Override
                    public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                        finalChild.setTranslationY(scrollY/5);
                    }
                });
            }
        }
    }

    这里我们在onNestedPreScroll 里对我们的ScrollView进行的了滑动监听,这样就避免去拦截它的onNestedFling了,onNestedPreScroll中的 target 表示的是CoordinatorLayout子View中执行滑动操作的View。

    具体效果如下:

    其实还有其它的几个方法还是比较重要的,比如:onNestedFlingonNestedPreFling,等等,有兴趣的朋友可以去研究一下。

    展开全文
  • Behavior Designer 中文版教程

    千次阅读 2017-08-14 08:52:42
    Behavior Designer 概述 Behavior Designer 是一个行为树插件!是为了让设计师,程序员,美术人员方便使用的可视化编辑器!Behavior Designer 提供了强大的 API 可以让你轻松的创建 tasks(任务),配合 uScript 和 ...

    Behavior Designer 概述

    中文版PDF下载地址:http://download.csdn.net/download/mango9126/9840488

    Behavior Designer 是一个行为树插件!是为了让设计师,程序员,美术人员方便使用的可视化编辑器!Behavior Designer 提供了强大的 API 可以让你轻松的创建 tasks(任务),配合 uScript 和 PlayMaker 这样的插件,可以不费吹灰之力就能够创建出强大的 AI 系统,而无需写一行代码!
    本指南将介绍所有 Behavior Designer 的功能特性!如果你还不了解什么是行为树(behavior trees)请查看"行为树的概述"!依赖于 Behavior Designer你完全可以不用关心什么是行为树(behavior trees)但是,如果你了解了一些 behavior trees!这将有助于你使用 Behavior Designer,包括一些常用组件,例如:tasks(任务),action(行为),composite(复合),conditional(条件),decorator(修饰符)!
    当你第一次打开 Behavior Designer ,会出现下面的窗口:

    这里一共分为四大部分:下图中的第一个部分是主要操作区,用来创建你的行为树!第二部分是面板属性区,这里可以编辑一个行为树的特定属性,添加新任务,创建新的变量,或者编辑 tasks(任务)的参数。第三部分是工具栏,你可以添加/删除行为树,锁定当前行为树,查看所有行为树,等等!第四部分是调试工具栏。你可以启动/停止,逐步调试,暂停,查看错误!

    第一部分是主要的设计工作的区域!在这个区域,你可以创建新的 Task(任务)和设计这些 Task(任务)的行为树。第一步要做的就是创建一个行为树(behavior tree),通过右键选择"Add Behavior Tree"可以创建新的 behavior tree 行为树,或者通过上方的工具栏 Lock(锁定)旁边的加号添加一个新的行为树!
    一旦行为树创建完毕,你就可以开始添加 tasks(任务)了。添加一个 task 可以通过右键点击空白区,或者左侧 2 区中的 Tasks 标签来创建!一旦你的task 创建完毕,你将看到类似于下面图中的效果!



    这里的 Entry task 是默认添加的,作为根节点进行后续的 Task 添加!这里能看到添加的 Sequence(队列)节点有错误,这是因为这个 Sequence必须有后续子节点,只要添加了子节点,这个错误就会消失了!现在我们已经有了第一个 Task,接下来让我们多添加几个:


    你可以创建 Sequence(序列)节点,或者 selector(选择器)节点,他们都是 task(任务)。通过多个这种节点的组合,你可以创建很深的层次结构!
    如果在创建过程中你不小心搞错了什么,那么选择有错误的节点,delete 删除掉即可!
    Behavior Designer 的执行顺序是从左到右的顺序执行,并且是深度优先。上图中的执行顺序如下:
    SequenceA,SelectorA、SequenceB ActionA、ActionB ActionC,SelectorB,ActionD ActionE


    现在我们已经有了一个基础的行为树,让我们改改参数看看!选择 ActionC 节点然后查看左侧的属性面板,选到 Inspector 视图!你可以再这里重命名这个 Task(任务)名称,设置参数,或者输入一个注释在(Comment)。
    在左侧的界面中有出了 Inspector 还有其他三个标签(Behavior,Tasks,Variables)Variables 面板允许你创建共享变量,并改变变量值!关于这个面板的具体信息,在后面章节介绍!Tasks 面板列出了所有可用的 tasks(任务)这里的 tasks 和你在空白区右键弹出的菜单中的 tasks 是一样的!这个列表可以通过上方的搜索框输入,快速定位到你想要创建的 tasks 任务,包括 action(行为),composite(复合),conditional(条件),decorator(修饰符)!类型!最后一个面板,behavior 面板,显示的是当前行为树 Behavior Tree 组件的属性,这个属性也会出现在所捆绑的 Gameobject 的属性面板上!各个面板具体介绍请看后续章节介绍!


    最后的是工具栏,工具栏提供了例如添加/删除等基础的行为树操作的工具!上图中 1 号位置的箭头是用来预览不同行为树,如果在同一个 Gameobject上添加了多个行为树的话!2 号位置用来显示所有行为树,并可以选择某个行为树,3 号位置是当前场景中拥有行为树的 Gameobject,可以展开下拉菜单并查看和选择!4 号位置也是用来选择当前 Gameobject 上的不同行为树,如果有多个的话,5 号是删除当前行为树,6 号增加一个行为树,7 号锁定当前行为树试图,不过因为在场景中点击其他资源导致行为树设计面板中的内容切换! 8号是保存当前行为树,9 号是导出当前行为树作为一个 Scriptable资源以便其他行为树调用,10 号截图,11 号偏好设置,包括了一些基础设置!

    什么是行为树 Behavior Tree ?
    行为树在人工智能游戏中很受欢迎。像《光晕 2》就是一个使用行为树并火起来的游戏!行为树的有点就是很容易理解并且是可视化的编辑!


    下面来了解一下行为树:有四种不同类型的 task(任务): 包括 action(行为),composite(复合),conditional(条件),decorator(修饰符)!action(行为)可能很容易理解,因为他们在某种程度上改变游戏的状态和结果。 
    conditional(条件)用来判断某些游戏属性是否合适!例如:在上图中的行为树中,有两个 conditional(条件)节点,两个 action(行为)节点前两个 conditional(条件)用来检查是否需有敌人,并确保是否有足够的子弹。如果这些条件都是真的,纳闷这两个 task(任务)将被执行,并执行后续任务,如果有 conditional(条件)不满足,则不会执行后续操作,直接返回上层的 Sequence,并结束本次行为树的执行!之后的是一个并行队列(parallel),下面的两个 action(行为)第一个负责计算设计伤害,第二个负责播放射击动画,他们是同事发生的!这里你完全可以把后面的两个 action(行为)作为单独的一个行为树!以此类推,编辑出负责的,嵌套的行为树!
    composite(复合):从上图中可以看出,Sequence 和 parallel 属于 composite(复合)节点。一个是顺序执行,一个是并列执行!
    decorator(修饰符):这个类型的节点只能有一个子节点。它的功能是修改子任务的行为。在上面的例子中,我们没有使用 decorator(修饰符),如果你需要类似于打断操作的话会用得到这个 decorator(修饰符)类型!举个例子:一个收集资源的操作,它可能有一个中断节点,这个节点判断是否被攻击,如果被攻击则中断收集资源操作!decorator(修饰符)的另一个应用场合是重复执行子任务 X 次,或者执行子任务直到完成! 
    行为树还有一个重要话题,那就是返回状态!有时候一个 task(任务)需要多帧才能完成。例如,大多数动画不会在一帧开始并结束。此外有 conditional(条件)的任务需要一种方法来告诉他们的父任务条件是否正确,以便让父节点确定子节点的执行顺序。这两个问题都可以使用 status(状态)来解决。一个任务有三种不同状态:运行,成功或者失败。在第一个例子中,射击动画的 task 任务只有一个 status 状态,而确定敌人的条件是在 Within Sight任务中返回的,如果返回失败,也就是不在视野中则不会执行到后面的任务!

    究竟该用行为树还是有限状态机?
    Behavior Trees or Finite State Machines

    在什么情况下会需要用行为树(BehaviorTree)而不用有限状态机(FiniteStateMachines)例如 PlayMaker 这种插件?从更高级的逻辑层面来讲,行为树常常用于复杂的人工智能,而有限状态机(FSMs)则用于一般的可视化变成。当然你也可以用有限状态机(FSMs)来写 AI,或者使用行为树(BehaviorTree)来进行可视化编程,工具的使用因人而异!《人工智能开发》《AI Game Development》的作者 Alex J. Champandard 在 2007  12  28 日的一篇博客中提到:有限状态机(FSMs)时代已经结束了的 10 大原因!原文地址:http://aigamedev.com/open/article/fsm-age-is-over/ 。(译者:当然万事无绝对,有限状态机还是有他的用武之地的!因人而异,因游戏而异!)虽然行为树不至于走到这一步,但是可以肯定行为树在 AI 上比状态机有着绝对的优势!

    行为树比有限状态的几个优势:行为树提供了强大的灵活性,非常强大,并且很容易更改行为树结构! 
    让我们先来看第一个优势:灵活性!在使用状态机 FSM 时,你要如何同时执行两个状态呢?你只能去创建两个状态机(FSM)!但是如果你使用行为树的话,你只需要添加一个并行节点(Parallel)即可,所有子节点都将并行执行!使用 Behavior Designer,这些子节点可以是 PlayMaker 的 FSM,并且这些 FSMs 将被并行触发! 
    另一个关于灵活性的例子就是 guard task(监控任务)。比如你有两个不同的 task(任务)一个播放声音,一个播放特效。这两个任务在行为树里是两个不同的分支,所以他们之间互相并不知道对方的状态,有可能同一时间这两个任务被同时执行!你可能不希望这种情况发生。在这种情况下,你可以添加一个 semaphore task(在 Behavior Designer 中被称为 Task Guard 监控任务)这样就可以在行为树中保证当前要么播放音效,要么播放特效!只有当第一个播放完毕,才会播放第二个! 
    行为树另一个有点:行为树的结构很健壮很清晰!这并不是说 FSM 结构并不够健壮不够清晰,只是他们的实现方式不同!在我看来,行为树让 AI 实现比有限制状态机更加方便!行为树能更好的去表达和实现复杂的 AI,而如果使用 FSM 去实现则会很复杂!为了达到同样的效果的 FSM 连接线最终可能开上去就像面条! 
    最后一个行为树优点:方便修改!行为树边的如此受欢迎原因之一就是很容易创建可视化的编辑器!在 FSM 中你如果想改变执行顺序,你必须在状态之间进行切换操作,改变各种连线!而在行为树中你不必这么麻烦!而且添加删除节点也很方便!

    说了这么多,行为树和 FSM 并不一定是互斥的!他们可以相互配合使用已达到更好的效果!

    行为树组件
    Behavior Tree Component
    如下图:


    这个组件记录了你的行为树的结构以及一些 BehaviorDesigner 配置信息!下面的 API 用来启动和停止你的行为树!
    public void EnableBehavior();
    public void DisableBehavior(bool pause = false);
    你可以通过下面的这些方法查找行为树中的相关节点 task 任务
    TaskType FindTask< TaskType >();
    List< TaskType > FindTasks< TaskType >();
    Task FindTaskWithName(string taskName);
    List< Task > FindTasksWithName(string taskName);
    行为树当前的执行状态可以像下面这样获取:
    behaviorTree.ExecutionStatus;
    当行为树运行结束后会有一个状态被返回,返回的接口可能是 Success 成功或者是 Failure 失败,这个结构依赖于行为树中的各个
    子节点 Task 任务的返回值!
    你可以对行为树监听以下事件:
    OnBehaviorStart
    OnBehaviorRestart
    OnBehaviorEnd
    行为树组件包含以下几个属性:
    Behavior Name
    行为树的名称
    Behavior Description
    行为树的描述信息
    External Behavior
    一个外部行为树的资源引用,行为树可以被导出成外部序列化文件(ScriptableObject 文件)单独存储,并被其他行为树引用,或
    者作为子节点任务而使用!方便了行为树的共用!
    Group
    行为树的分组编号,用来将行为树分组!可以用来方便的查找到特定的行为树!
    Start When Enabled
    如果设置为 true,那么当这个行为树组件 enabled 的时候,这个行为树就会被执行!
    Pause When Disabled
    如果设置为 true,那么当这个行为树组件 disabled 的时候,这个行为树就会被暂停!
    Restart When Complete
    如果设置为 true,那么当这个行为树组件执行结束的时候,这个行为树就会被重新执行!
    Reset Values On Restart
    如果设置为 true,那么当这个行为树组件 reset 的时候,这个行为树就会被重新执行!
    Log Task Changes
    当设置为 true 是,这个行为树下只要 task 流程发生变化就会打印一条 log 日志到控制台中!

    用脚本创建一个行为树
    在某些情况下,你可能想要通过脚本在运行时创建一个行为树,而不是直接使用拖拽或者面板操作去创建!例如:如果你已经导出了一个外部行为树,并想通过脚本创建它的话,可以如下这么做:


    在这个例子中公共变量behaviorTree 包含你引用的外部行为树。新创建的行为树在创建时将自动加载所有子节点任务。通过设置 startWhenEnabled 为 false 来阻止行为树在创建后立刻被执行!可以通过 bt.enabledBehavior()来开启行为树!

    行为管理器
    Behavior Manager


    当运行一个行为树的时候,会在场景中自动创建一个名称为 BehaviorManager 的 GameObject,并添BehaviorManage.cs!
    这个脚本用来管理所有场景中的行为树!
    你可以控制行为树的更新类型,以及更新时间等等!"Every Frame"是每帧都更新行为树!"Specify Seconds"定义个一个更新间隔时间!"Manual"是手动调用更新,选择这个后需要通过脚本来调用行为树的更新,例如下面这样:
    BehaviorManager.instance.Tick();
    此外,如果你想让不同的行为树都有各自独立的更新间隔的话,可以这样:
    BehaviorManager.instance.Tick(BehaviorTree);
    Task Execution Type(任务执行类型)允许你指定行为树行为树的执行次数,默认是"No Duplicates"(不复制,不重复)像下图中的这种循环操作
    可以简单的通过这类设置执行次数来实现!


    Repeater Task(重复任务节点)设置成 5 次。如果 Task Execution Type(任务执行类型)被设置为"No Duplicates"(不复制,不重复),那么 Play Soundtask(播放音乐任务节点)则会被每帧执行一次。如果 Task Execution Type(任务执行类型)被设置为 5,那么那么 Play Sound task(播放音乐任务节点)会在每帧被执行 5 次!

    Tasks(任务)
    在整个任务树的最高层的节点我们称之为 Task(任务)。这些 task 任务拥有类似于 MonoBehavior 那样的接口用于实现和扩展,如下:

    // OnAwake is called once when the behavior tree is enabled. Think of it as a constructor
    public virtual void OnAwake();
    // OnStart is called immediately before execution. It is used to setup any variables that need to be reset from the previous run
    public virtual void OnStart();
    // OnUpdate runs the actual task
    public virtual TaskStatus OnUpdate();
    // OnEnd is called after execution on a success or failure.
    public virtual void OnEnd();
    // OnPause is called when the behavior is paused and resumed
    public virtual void OnPause(bool paused);
    // The priority select will need to know this tasks priority of running
    public virtual float GetPriority();
    // OnBehaviorComplete is called after the behavior tree finishes executing
    public virtual void OnBehaviorComplete();
    // OnReset is called by the inspector to reset the public properties
    public virtual void OnReset();
    // Allow OnDrawGizmos to be called from the tasks
    public virtual void OnDrawGizmos();
    // Keep a reference to the behavior that owns this task
    public Behavior Owner;

    task 任务有三个基础的公共属性:name, comment, instant(名称,简介,立刻)。这里的 instant 立刻,并不好容易理解!行为树中,当一个 task 任务返回成功或者失败后,行为树会在同一帧中立刻移动到下一个 task 任务。如果你没有选择 instant 选项,那么在当前 task 任务执行完毕后,都会停留在当前节点中,直到收到了下一个 tick,才会移动到下一个 task 任务!
    下面是执行的顺序的流程图:


    父任务 Parent Tasks
    behavior tree 行为树中的父任务 task 包括:composite(复合),decorator(修饰符)!虽然 Monobehaviour 没有类似的 API,但是并不难去理解这些功能:

    // The maximum number of children a parent task can have. Will usually be 1 or int.MaxValue
    public virtual int MaxChildren();
    // Boolean value to determine if the current task is a parallel task
    public virtual bool CanRunParallelChildren();
    // The index of the currently active child
    public virtual int CurrentChildIndex();
    // Boolean value to determine if the current task can execute
    public virtual bool CanExecute();
    // Apply a decorator to the executed status
    public virtual TaskStatus Decorate(TaskStatus status);
    // Notifies the parent task that the child has been executed and has a status of childStatus
    public virtual void OnChildExecuted(TaskStatus childStatus);
    // Notifies the parent task that the child at index childIndex has been executed and has a status of childStatus
    public virtual void OnChildExecuted(int childIndex, TaskStatus childStatus);
    // Notifies the task that the child has started to run
    public virtual void OnChildStarted();
    // Notifies the parallel task that the child at index childIndex has started to run
    public virtual void OnChildStarted(int childIndex);
    // Some parent tasks need to be able to override the status, such as parallel tasks
    public virtual TaskStatus OverrideStatus(TaskStatus status);
    // The interrupt node will override the status if it has been interrupted.
    public virtual TaskStatus OverrideStatus();
    // Notifies the composite task that an conditional abort has been triggered and the child index should reset
    public virtual void OnConditionalAbort(int childIndex);

    编写自定义的条件任务节点
    Writing a New Conditional Task

    这个主题包含两个部分。第一部分介绍如何编写新的条件任务节点 conditional task,第二个部分介绍如何编写行为任务 action taskconditional task(条件任务节点)用来判断某些变量和条件,而 action task(行为任务节点)则负责执行某些具体的逻辑操作!下面举例来写一个判断是否在视野距离中的条件任务节点(WithinSight)以及一个朝目标移动的(action task)(译者:具体步骤略,直接上最终完整代码)



    编写自定义行为任务节点
    Writing a New Action Task


    最终在编辑器中连接起来后是这个样子!


    调试


    当行为树在执行的过程中,你会看到类似上图的效果,绿色的是真在执行的部分,灰色的是没有执行或者执行过的部分!部分节点的右下角 ,


    或者


    表示这个节点的返回值是成功,还是失败!任务运行时仍然可以通过属性面板来改变数值并查看数值改变后的游戏表现!


    通过鼠标右键点击某个任务节点,可以给这个节点添加一个断掉,这样在运行到这个节点的时候会中断,你可以查看节点的状态和属性等等!如上图;

    当你选中某个任务节点后,可以通过左侧的 Inspector 面板来查看具体的变量,并通过变量掐面的按钮,在设计区域查看变量具体的值!如上图!


    有时候你只希望执行行为树的一部分而不是全部,那么你可以禁用某些节点极其子节点,只需选中某个节点然后选择左上角的 X号即可!


    另外通过打开 Behavior 的 LogTaskchanges 也可以打印行为树的执行顺序,类似于下面的输出

    GameObject - Behavior: Push task Sequence (index 0) at stack index 0
    GameObject - Behavior: Push task Wait (index 1) at stack index 0
    GameObject - Behavior: Pop task Wait (index 1) at stack index 0 with status Success
    GameObject - Behavior: Push task Wait (index 2) at stack index 0
    GameObject - Behavior: Pop task Wait (index 2) at stack index 0 with status Success
    GameObject - Behavior: Pop task Sequence (index 0) at stack index 0 with status Success
    Disabling GameObject – Behavior

    这些消息可以分成以下部分:

    {game object name } – {behavior name}: {task change} {task type} (index {task index}) at stack index {stack index} {optional status}

    条件节点的终止 Conditional Aborts
    Conditional aborts(条件 终止)允许你的行为树动态的改变,而无需使用很多的类似于 打断/执行打断(Interrupt/Perform Interrupt) 等等类似的任务。这个特性类似于虚幻 4 中的观察者中止。大多数的其他行为树工具在处理类似问题的时候都需要重新遍历一次行为树。而这里的 Conditionalaborts (条件终止)可以避免这种重新遍历的情况!以下图为例来说明下它的用法:


    当这个行为树运行的时候,先执行 Conditional 判断,如果返回正确,则到 Wait 节点等待,这里 Wait 节点等待 10 秒!假设在等待过程中 conditional 节点的判断条件发生变化,返回 failure。如果 conditional 的 aborts(打断)被开启了的话,conditional 节点会触发一个打断操作并停止掉 Wait 节点的任务!conditional 节点任务会根据之前的逻辑重新评估并返回是否成功!conditional 节点的aborts 可以被任何 composite 复合节点(上图中的 Sequence 节点)访问到.如下图这样:如果第一个 Int 的判断条件不满足,则会被重新执行一遍 Sequence

    一共有四种中断类型的 abort types: None, Self, Lower Priority, and Both.
    None


    这种是默认的中断类型!
    Self


    这是一种自包含中断类型。也就是会检测此节点下所有条件判断节点,即便是被执行过的节点,如果判断条件不满足则打断当前执行顺序从新回到判断节点判断,并返回判断结果!
    Lower Priority


    当运行到后续节点时,本节点的判断生效了的话则打断当前执行顺序,返回本节点执行!

    Both


    包含了 上面的两个类型!

    下面的示例将使用低优先级的中断类型 Lower Priority:


    在这个例子中左边的 Sequence 的中断类型为 Lower Priority,假设左侧分支返回错误,行为树将跳转到右侧分支!当右侧分支运行时,第一个节点的判断条件变成了 success。这时因为判断结果发生变化并且设置了 Lower Priority,所以会打断当前正在执行 Action 并返回去执行第一个 Action。 译者:如果要自行测试上图效果,建议把 Action 换成 wait 节点,我在测试的时候用的 log 结果 log 太快,这个 LowerPriority测试了很久也没搞明白,原来是我的 Action 太快,导致行为树结束);如下图


    条件在被检测时会有一个图标来标记,表示这个判断节点当前被检测中,当状态变化后会根据打断类型打断行为树当前的执行顺序!上图中左下角的 Int Comparison(整形数值判断)节点,如果判断返回 false 则会打断 wait,并跳转到后一个 Sequence 队列,如果此时判断有变成了有效值则又会跳回来执行第一个 Wait。这是因为第一个 Sequence 选择了 Both 的打断类型!另外有打断的条件节点可以嵌套。例如下图!

    如果按下 Fire1(is Buttondown 监听的是 Fire1),则会跳回到左侧 Sequence 下的 Wait


    Event 事件
    Behavior Designer 中的 Event 事件系统可以让你很容易的使用!你可以通过代码触发一个 event 事件,也可以通过行为树的节点来触发一个事件!
    这些事件可以通过行为树的 SendEvent 节点和 HasRecivedEvent 节点来触发和监听事件!当一个事件要被发送时使用 SendeEvnet 节点。HasRecivedEvent 节点是一个条件节点,当接收到注册的事件后会返回 success。可以通过事件名称的定义来触发和监听一个事件!

    出了通过行为树节点来触发事件,还可以通过代码来触发事件!BehaviorTree.SendEvent 函数就是用来干这个的:

    var behaviorTree = GetComponent< BehaviorTree >();
    behaviorTree.SendEvent< object >("MyEvent", Vector3.zero);

    上面这个例子就是通过代码,将事件" MyEvent"发送到行为树,并带有参数( Vector3.zero),如果行为树中有监听器,则监听器位置会返回 success!

    Task 的引用,任务节点之间的引用!
    在编写一个 Task 任务节点的时候可能需要访问另外一个 Task 任务。例如 TaskA 想访问 TaskB 的某个属性数值!例如下面这样的 TaskA 和 TaskB

    using UnityEngine;
    using BehaviorDesigner.Runtime.Tasks;
    public class TaskA : Action
    {
    public TaskB referencedTask;
    public void OnAwake()
    {
    Debug.Log(referencedTask.SomeFloat);
    }
    }
    TaskB 然后看起来像:
    using UnityEngine;
    using BehaviorDesigner.Runtime.Tasks;
    public class TaskB : Action
    {
    public float SomeFloat;
    }

    将这两个任务添加到行为树编辑器中:


    选中 TaskA,你会在 Inspector 面板中看到变量 referencedTask 他是个 Task 类型,这时你可以选择 Select 按钮进行选择,最终像下图这样


    你可以通过点击"X"号来取消引用关系!这样配置后,在执行 TaskA 的时候就会显示 TaskB 的属性,像下图这样


    Task 的引用也可以是数组引用,像下面这样!

    public class TaskA : Action
    {
    public TaskB[] referencedTasks;
    }

    变量同步器 Variable Synchronizer


    在 GameObject 上挂在脚本,并设置同步的源,以及同步目标,中间的箭头表示同步方向,向右表示上面的变量同步给下面的,向左表示下面的变量同步给上面的!

    Task 任务的可用属性

    HelpURL : web 连接
    [HelpURL("http://www.opsive.com/assets/BehaviorDesigner/documentation.php?id=27")]
    public class Parallel : Composite
    {
    //////////////////////////////////////////////////////////////////////////////
    TaskIcon :任务的图标
    [TaskIcon("Assets/Path/To/{SkinColor}Icon.png")]
    public class MyTask : Action
    {
    //////////////////////////////////////////////////////////////////////////////
    TaskCategory:任务的显示位置(在 Task 任务面板中的显示位置)
    [TaskCategory("Common")]
    public class Seek : Action
    {
    [TaskCategory("RTS/Harvester")]
    public class HarvestGold : Action
    {
    //////////////////////////////////////////////////////////////////////////////
    TaskDescription:功能描述的文本内容,显示在编辑器布局区域的左下角
    [TaskDescription("The sequence task is similar to an \"and\" operation. ..."]
    public class Sequence : Composite
    {
    //////////////////////////////////////////////////////////////////////////////
    LinkedTask:应用其他的 Task 任务
    [LinkedTask]
    public TaskGuard[] linkedTaskGuards = null;
    //////////////////////////////////////////////////////////////////////////////
    InheritedField : 继承属性
    [InheritedField]
    public float moveSpeed;

    Composites (复合)节点
    Sequence(序列)节点


    这个节点是一个"和"的关系,也就是他下面的子节点的执行顺序是一个接着一个的!如果其中一个返回 false。那么后续的子节点不会被执行,这个序列节点返回 false。只有当所有子节点全部完成并返回 success 的时候,这个 Sequence(序列)节点才会返回 success;

    Selector(选择)节点


    这个节点是"或"的关系,也就是他下面的子节点的执行顺序是一个或另一个的!只有所有子节点返回 false 才会返回 false。只要有一个子节点返回 success,那么这个 Selector 节点就会返回 success,后续的节点不会被执行!

    Parallel(并行)节点


    这个节点类似于 Sequence(序列)节点。不同的是,Parallel(并行)节点会在同一时间执行所有子节点而不是一个一个的去执行!

    如果子节点中有任意一个返回 false,则停掉所有子节点并返回 false。只有所有子节点全部返回 success 的时候,才会返回 success。

    Parallel Selector(并行选择)节点


    类似于 Selector(选择)节点,ParallelSelector(并行选择)节点只要有一个子节点返回 success,那么他就会返回 success!不同于 Selector的一点就是 ParalleSelector(并行选择)节点会在同一时间执行下面的所有子节点,如果有一个节点返回 success,则会停止掉其他所有子节点并返回 success。只有当所有子节点全部 false 的时候才会返回 false

    Priority Selector(优先选择)节点


    类似于 Selector(选择)节点,PrioritySelector(并行选择)节点只要有一个子节点返回 success,那么他就会返回 success!不同点在于,子节点的执行顺序不是从左到右的,而是通过优先级来确定的执行顺序!较高的优先级的子节点会被先执行!(译者:优先级在哪里设置的,没有搞清楚,目前测试结果同 Selector 节点,后来还是用我大 Google 搜索到的解决办法!百度就是个垃圾站)需要在 Task 类中覆盖函数,来设置不同的 Priority,原文地址:

    http://forum.unity3d.com/threads/behavior-designer-behavior-trees-for-everyone.227497/page-4
    // The priority select will need to know this tasks priority of running
    public virtual float GetPriority();

    Random Selector(随机选择)节点


    这个节点的特点是:随机的执行子节点,只要有一个子节点返回成功,它就会返回成功,不再执行后续节点。如果所有子节点都返回 false 则它也返回 false!在这个节点的属性面板中有:seed(随机种子)的设置,自行使用!

    Random Sequence(随机序列)节点


    类似于 Sequence(序列)节点,只是他的执行顺序是随机的!只要遇到一个子节点返回 false,RandomSequence(随机序列)就返回错误,直到全部子节点都返回 success,它才会返回 success!在这个节点的属性面板中有:seed(随机种子)的设置,自行使用!

    Selector Evaluator(重复判断选择)节点


    这个节点每帧都会去重新评估子节点的执行状态并选择。它会执行子节点中优先级最低的子节点!每帧都会这么干!如果当前一个高优先级的节点在运行并且下一帧要执行的子节点优先级比较低,那么它会打断高优先级的节点,去执行优先级低的子节点! Selector Evaluator(重复判断选择)节点会从低到高的去遍历执行所有子节点,直到遇到一个返回 success 的!如果所有子节点都返回 false,那么它就返回 false!否则只要有一个返回 success,它就会返回 success!这个节点模拟了条件打断功能,如果子节点没有条件节点的话!

    Conditionals(条件判断)节点
    条件节点的任务是判断游戏的一些属性,比如玩家是否活着,怪物是否在视野距离内!

    Random Probability (随机概率)节点


    通过设置 successProbability 属性来控制返回 success 的几率(默认 0.5,也就是 50%几率)!另外还有 seed 随机种子的设置等!

    Compare Field Value(字段比较)节点


    比较指定的值的字段值。 返回成功如果值是相同的。

    Has Received Event(是否接收到事件)


    (译者:还有很多条件节点,这里就忽略了!)

    Decorators(修饰器)节点


    这种节点的功能是用来包装另一个节点!(只能有一个子节点)。Decorators(修饰节点)将改变节点的行为!例如:修饰节点可以再运行时控制子节点直到返回某个特定状态(success 或者是 false)。后者是对子节点返回结果取反(即:success 返回 falsefalse,返回 success);下面来一一介 BehaviorDesigner 默认自带的几个 Decorator(装饰器节点)

    Conditional Evaluator (条件节点的评估) 装饰节点


    参数设置:
    1:reevaluate :条件节点是否需要每帧都重新评估一次
    2:conditionalTask:要被评估的条件节点,注意:这个节点本身就是个条件节点!
    对设置的条件节点进行评估,如果条件节点返回 success,那么运行子节点并返回子节点的运行结果!如果条件节点没有返回 success那么子节点不会被运行,并且立刻返回 failure!条件节点只会在开始运行的时候被评估一次!

    Interrupt(打断)装饰节点


    如果打断节点被触发,则打断下面的所有子节点任务的执行!打断命令可以被 Perform interruption(执行打断)节点发起!打断节点在收到打断命令前,不会打断他下面的子节点的执行状态!如果子节点执行完毕还没有收到打断命令,则直接返回子节点的执行结果!例如下图这样:


    Inverter(取反)装饰节点


    子节点的任务完成后返回值,在这个节点会被取反并传递到上一级中!

    Repeater(重复/循环)装饰节点


    有三个属性设置:执行次数,是否一直重复,运行直到返回错误!


    Return Failure (返回失败)装饰节点


    只要子节点当前的状态不是 running,也就是子节点执行结果无论是 success 还是 failure,都返回 failure!如果子节点状态是 running的话则返回 running!

    Return Success(返回正确)装饰节点


    只要子节点当前的状态不是 running,也就是子节点执行结果无论是 success 还是 failure,都返回 success!如果子节点状态是 running的话则返回 running!

    Task Guard(任务守卫)装饰节点


    类似于多线程互斥操作中使用的 Lock 标记,为了避免公共数据被多次引用!下图以外部行为树为例进行演示!这里使用并行触发两个外部行为树,如果不加上 TaskGuard,那么两边都会去并行执行外部行为树,现在加上 TaskGuard 后同一时间只能执行一个,而另一个要等待执行完毕才能执行


    上图 TaskGard 配置如下图:


    Until Failure(直到失败)装饰节点


    直到子节点返回 failure,否则一直循环执行子节点, 如下图:


    Until Success(直到成功)装饰节点

    直到子节点返回 success,否则一直循环执行子节点!


    转自:http://www.jianshu.com/p/64b5fe01fb1c

    展开全文
  • Yii2基本概念之——行为(Behavior)

    千次阅读 2018-03-02 23:25:17
    使用行为(behavior)可以在不修改现有类的情况下,对类的功能进行扩充。通过将行为绑定到一个类,可以使得类具有行为本身所具有的属性和方法,就好像是类本来就具有的这些属性和功能一样。 好的代码设计,必须要同时...

    使用行为(behavior)可以在不修改现有类的情况下,对类的功能进行扩充。通过将行为绑定到一个类,可以使得类具有行为本身所具有的属性和方法,就好像是类本来就具有的这些属性和功能一样。

    好的代码设计,必须要同时满足可复用性、可维护性和可扩展性。设计原则中有一条非常重要的一条:类应该对扩展开放,对修改关闭。改变原有代码往往会带来潜在风险,因此我们尽量减少修改的行为。我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可以搭配新的行为。如果能实现这样的目标,有什么好处呢?这样的设计具有弹性,可以应对改变,可以接收新的功能来应对改变的需求。

    Yii的行为就是这样一类对象,当一个对象(继承了Component的)想要扩展功能,又不想改变原有代码时,那么你完全可以用行为去实现这些新功能,然后绑定到该对象上——完全是符合“开闭原则”的。
    Yii的行为都需要继承自yii\base\Behavior,而能接受行为绑定从而扩充自身功能的只能是yii\base\Component的子类,只继承BaseObject基类没有继承Component的不能享受此“待遇”。因此,行为是组件才有的功能。行为和事件结合起来使用,还可以定义组件在何种事件进行何种反馈。因此行为有如下两个作用:
    1. 将属性和方法注入到一个component里面,被访问时和别的属性或者方法访问无异(行为的附加
    2. 响应component中触发的事件,以便对某一事件作出反应(行为的绑定和触发,是对事件的应用)

    定义行为

    行为必须继承自yii\base\Behavior,定义一个行为仿照下面进行:

    class MyBehavior extends \yii\base\Behavior
    {
        public $prop1;
        private $_prop2;
        private $_prop3;
    
        //绑定事件和处理器,从而扩展类的功能表现,这里体现了“行为”字面意义
        public function events()
        {
    
        }
    
        //行为的只读属性
        public function getProp2()
        {
            return $this->_prop2;
        }
    
        //行为的只写属性
        public function setProp3($prop3)
        {
            $this->_prop3 = $prop3;
        }
    
        //行为的方法
        public function foo()
        {
            return 'foo';
        }
    
        protected function bar()
        {
            return 'bar';
        }
    
    }

    接下来,将行为附加到对象上,从而扩充对象的功能:

    $user = new User();
    //$user对像附加行为,扩充功能
    $user->attachBehavior('myBehavior', new MyBehavior());
    //获取prop2属性
    $user->prop2;
    //给只读属性赋值会报错
    $user->prop2 = 3;
    //给只写属性prop3赋值
    $user->prop3 = 2;
    //操作可读-可写属性prop1
    $user->prop1 = 1;
    $var = $user->prop1;
    
    // 使用方法foo
    $user->foo();
    // 不可访问,这里会抛出'Unknown Method'异常
    $user->bar();

    当然MyBehavior()完全可以支持依赖注入,从而在运行时决定这些属性的值。

    从上面可以看出,$user对象使用其MyBehavior的属性和方法来几乎毫不费劲,就像自己拥有这些属性和方法一样。但是,我们并没有给User类中添加任何一行代码,因此这个扩展做得真是悄无声息啊!

    行为的附加

    行为的附加或者绑定,通常是由Component来发起。有两种方式可以将一个Behavior绑定到一个 yii\base\Component 。 一种是静态附加行为,另一种是动态附加行为。静态附加在实践中用得比较多一些,因为一般情况下,在你的代码没跑起来之前,一个类应当具有何种行为是确定的。 动态附加主要是提供了更灵活的方式,上面即是行为的动态附加,但实际使用中并不多见。

    静态附加

    class User extends ActiveRecord
    {
        const MY_EVENT = 'my_event';
        public function behaviors()
        {
            return [           
    
                // 匿名行为,只有行为类名
                MyBehavior::className(),
    
                // 命名行为,只有行为类名
                'myBehavior2' => MyBehavior::className(),
    
                // 匿名行为,配置数组
                [
                    'class' => MyBehavior::className(),
                    'prop1' => 'value1',
                    'prop2' => 'value2',
                ],
    
                // 命名行为,配置数组
                'myBehavior4' => [
                    'class' => MyBehavior::className(),
                    'prop1' => 'value1',
                    'prop2' => 'value2',
                ]
            ];
        }
    }

    上面的数组响应的键就是行为的名称,这种行为成为命名行为,没有指定名称的就成为匿名行为

    还有一个静态的绑定办法,就是通过配置文件来绑定:

    [
        'class' => User::className(),
        'as myBehavior2' => MyBehavior::className(),
        'as myBehavior3' => [
            'class' => MyBehavior::className(),
            'prop1' => 'value1',
            'prop3' => 'value3',
        ],
    ]

    通过这个配置文件获取的User对象的实例,依然被附加了MyBehavior行为。

    动态附加

    要动态附加行为,在对应组件里调用 yii\base\Component::attachBehavior() 方法即可,如:

    use app\components\MyBehavior;
    // 附加行为——对象
    $user->attachBehavior('myBehavior1', new MyBehavior);
    // 附加行为——类名
    $user->attachBehavior('myBehavior2', MyBehavior::className());
    // 附加行为——配置数组
    $user->attachBehavior('myBehavior3', [
        'class' => MyBehavior::className(),
        'prop1' => 'value1',
        'prop2' => 'value2',
    ]);

    也可以通过yii\base\Component::attachBehaviors()同时附加多个行为:

    $myBehavior = new MyBehavior();
    $user->attachBehaviors([
        'myBehavior1'=> $myBehavior,
        [
            'class' => MyBehavior2::className(),
            'prop1' => 'value1',
            'prop3' => 'value2',
        ],
        new MyBehavior3()
    ]);

    附加多个行为,那么组件就获得了所有这些行为的属性和方法。

    不管是静态附加还是动态附加,命名行为都可以通过yii\base\Component::getBehavior($name)获取出来,匿名行为不可以单独获取出来,但是可以通过Component::getBehaviors()一次全部获取出来。

    行为附加的原理

    在Component内部,事件是通过私有属性$_event保存事件及其处理器,和事件类似,行为是通过私有属性$_behavior来保存的:

    private $_events = [];
    private $_behaviors;

    $_behaviors的数据结构:

    $_behaviors 的数据结构

    上图中前面两个是命名行为,后面两个是匿名行为。数组的每个元素值都是Behavior的子类实例。

    行为附加涉及到四个方法:

    Component::behaviors()
    Component::ensureBehaviors()
    Component::attachBehaviorInternal()
    Behavior::attach()

    Component::behaviors()用于供子类覆写,比如:

    public function behaviors()
    {
        return [
            'timeStamp' => [
                'class' => TimeBehavior::className(),
                'create' => 'create_at',
                'update' => 'update_at',
            ],
        ];
    }

    yii\base\Component::ensureBehaviors()方法经常出现,它的作用是将各种动态的和静态的方式附加的行为变成标准格式(参看$_behaviors的数据结构):

     public function ensureBehaviors()
     {
         if ($this->_behaviors === null) {
             $this->_behaviors = [];
             // behaviors()方法由Component的子类重写
             foreach ($this->behaviors() as $name => $behavior) {
                 $this->attachBehaviorInternal($name, $behavior);
             }
         }
     }

    接下来的第三个出场的attachBehaviorInternal(),我们看看是何方神圣:

     private function attachBehaviorInternal($name, $behavior)
     {
         //如果是配置数组,那就将其创建出来再说
         if (!($behavior instanceof Behavior)) {
             $behavior = Yii::createObject($behavior);
         }
         if (is_int($name)) { // 匿名行为
             //先是行为本身和component绑定
             $behavior->attach($this);
             //将行为放进$_behaviors数组,没有键值的是匿名行为
             $this->_behaviors[] = $behavior;
         } else {  //命名行为
             if (isset($this->_behaviors[$name])) {
                 //命名行为需要保证唯一性
                 $this->_behaviors[$name]->detach();
             }         
             $behavior->attach($this);
             //命名行为,键值就是行为名称
             $this->_behaviors[$name] = $behavior;
         }
    
         return $behavior;
     }

    Yii中以Internal开头或者结尾的,一般是私有方法,往往都是命门所在,如果要看源码,这些都是核心逻辑实现的地方。

    最后一个出场的是Behavior::attach(),Behavior有一个属性$owner,指向是拥有它的组件,就是行为的拥有者。组件和行为是一个相互绑定、相互持有的过程。组件在$_behavior持有行为的同时,行为也在$owner中持有组件。因此,不管是行为的附加还是解除都是双方的事情,不是一方能说了算的。

    public function attach($owner)
    {
        //Behavior的$owner指向的是行为的所有者
        $this->owner = $owner;
        //让行为的所有者$owner绑定用户在Behavior::events()中所定义的事件和处理器
        foreach ($this->events() as $event => $handler) {
            $owner->on($event, is_string($handler) ? [$this, $handler] : $handler);
        }
    }

    行为的解除

    有附加当然有解除,命名行为可以被单个解除,使用方法Component::detachBehavior($name),匿名行为不可以单独解除,但是可使用detachBehaviors()方法解除所有的行为。

    //解除命名行为
    $user->detachBehavior('myBehavior1');
    //解除所有行为
    $user->detachBehaviors();

    这上面两种方法,都会调用到 yii\base\Behavior::detach() ,其代码如下:

    public function detachBehavior($name)
    {
        $this->ensureBehaviors();
        if (isset($this->_behaviors[$name])) {
            $behavior = $this->_behaviors[$name];
            //1.将行为从$owner的$_behaviors中删除
            unset($this->_behaviors[$name]);
            //2.解除$owner的所有事件和其处理器
            $behavior->detach();
            return $behavior;
        }
    
        return null;
    }

    $behavior->detach()是这样的:

    public function detach()
    {
        if ($this->owner) {
            //解绑$owner所有事件和其事件处理器
            foreach ($this->events() as $event => $handler) {
                $this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler);
            }
            //$owner重新置为null,表示没有任何拥有者
            $this->owner = null;
        }
    }

    行为所要响应的事件

    行为与事件结合后,可以在不对类作修改的情况下,补充类在事件触发后的各种不同反应。因此,只需要重载 yii\base\Behavior::events() 方法,表示这个行为将对类的何种事件进行何种反馈即可:

    class MyBehavior extends Behavior
    {
        public $attr;
    
        public function events() //覆写events方法
        {
            return [
                ActiveRecord::EVENT_BEFORE_INSERT => 'beforeInsert', //将事件和事件处理器绑定
                User::MY_EVENT => [$object, 'methodName'],//自己定义的事件
            ];
        }
    
        //$event可以带来三个信息,事件名,触发此事件的对象(类或者实例),附加的数据
        public function beforeInsert($event) 
        { 
            $model = $this->owner;//访问已附件的组件
            // Use $model->attr
        }
    
        public function methodName($event) 
        {
            $owner = $this->owner;//行为的拥有者
            $sender = $event->sender//触发此事件的类或者实例
            $data = $event->data;//触发事件时传递的参数
    
            // Use $model->attr
        }
    }

    events()方法返回一个关联数组,键是事件名,值是要响应的事件处理器。事件处理器可以是一下四种形式:

    • 此行为中的方法methodName,等效为[$this, 'methodName']
    • 对象的方法:[$object, 'methodName']
    • 类的静态方法:['Page', 'methodName']
    • 闭包:function ($event) { ... }

    这些方法中都会传递事件$event过来,通过$event你可以获得事件名,触发此事件的对象(类或者实例),附加的数据信息。详见《Yii2基本概念之——事件(Event)》。

    行为响应事件的实例

    Yii费了那么大劲,主要就是为了将行为中的事件handler绑定到类中去。因为在编程中用的最多的,也就是Component对各种事件的响应。通过行为注入,可以在不修改现有类的代码的情况下更改、扩展类对于事件的响应和支持。使用这个技巧,可以玩出很酷的花样出来。
    比如,Yii自带的 yii\behaviors\AttributeBehavior 类,定义了在一个 ActiveRecord 对象的某些事件发生时, 自动对某些字段进行修改的行为。它有一个很常用的子类 yii\behaviors\TimeStampBehavior 用于将指定的字段设置为一个当前的时间戳。现在以它为例子说明行为的运用。
    在 yii\behaviors\AttributeBehavior::event() 中,代码如下:

     public function events()
     {
         return array_fill_keys(
             array_keys($this->attributes),
             'evaluateAttributes'
         );
     }

    代码很容易看懂,无需详述。

    而在yii\behaviors\TimeStampBehavior::init()中有代码:

    public function init()
    {
        parent::init();
    
        if (empty($this->attributes)) {
            //重点看这里
            $this->attributes = [
                BaseActiveRecord::EVENT_BEFORE_INSERT => [$this->createdAtAttribute, $this->updatedAtAttribute],
                BaseActiveRecord::EVENT_BEFORE_UPDATE => $this->updatedAtAttribute,
            ];
        }
    }

    上面的这个方法是初始化$this->attributes这个数组。结合前面的两个方法,返回的$event数组应该是这样的:

    
    return [
        BaseActiveRecord::EVENT_BEFORE_INSERT => 'evaluateAttributes',
        BaseActiveRecord::EVENT_BEFORE_UPDATE => 'evaluateAttributes',
    ];

    这里的意思是BaseActiveRecord::EVENT_BEFORE_INSERTBaseActiveRecord::EVENT_BEFORE_UPDATE都响应处理器evaluateAttributes。看看其关键部分:

    public function evaluateAttributes($event)
    {
        ...
    
        if (!empty($this->attributes[$event->name])) {       
            $attributes = (array) $this->attributes[$event->name];
            //这里默认返回默认的时间戳time()
            $value = $this->getValue($event);
            foreach ($attributes as $attribute) {            
                if (is_string($attribute)) {
                    if ($this->preserveNonEmptyValues && !empty($this->owner->$attribute)) {
                        continue;
                    }
                    //将其赋值给$owner的字段
                    $this->owner->$attribute = $value;
                }
            }
        }
    }

    使用时,只需要在ActiveRecord里面重载behaviors()方法:

    public function behaviors()
    {
        return [
            [
                'class' => TimestampBehavior::className(),
                'attributes' => [
                    ActiveRecord::EVENT_BEFORE_INSERT => 'created_at',
                    ActiveRecord::EVENT_BEFORE_UPDATE => 'updated_at',
                ]
            ],
        ];
    }
    

    因此,当EVENT_BEFORE_INSERT事件触发,
    这样,你在插入记录时created_atupdated_at会自动更新,而在修改时updated_at会更新。

    行为的属性和方法注入原理

    通过以上各个例子,组件附加了行为之后,就获得了行为的属性和方法。那么,这是如何实现的呢?归根结底主要通过__set(),__get(),__call()这些魔术方法来实现的。属性的注入靠的是__set(),__get(),而方法的注入是靠__call()

    属性的注入

    Component持有一个数组$_behavior,里面都是Behavior子类,而Behavior继承自Yii最基础的BaseObject。在《Yii2基本概念之——属性(property)》中我们介绍了属性的概念,因此Behavior也是可以运用属性的。

    Component的可读属性,我们看看Component的getter函数:

    public function __get($name)
    {
        $getter = 'get' . $name;
        //这是自己的可写属性
        if (method_exists($this, $getter)) {        
            return $this->$getter();
        }
    
        /**下面是比BaseObject多出来的部分**/
        $this->ensureBehaviors();
        //依次检查各个行为中的可读属性
        foreach ($this->_behaviors as $behavior) {
            if ($behavior->canGetProperty($name)) {
                return $behavior->$name;
            }
        }
        ...
    }

    Component的可写属性,我们看看Component的setter函数:

    public function __set($name, $value)
    {
        $setter = 'set' . $name;
        //自己的可写属性
        if (method_exists($this, $setter)) {        
            $this->$setter($value);
            return;
        } elseif (strncmp($name, 'on ', 3) === 0) {         
            $this->on(trim(substr($name, 3)), $value);
    
            return;
        } elseif (strncmp($name, 'as ', 3) === 0) {        
            $name = trim(substr($name, 3));
            $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value));
    
            return;
        }
    
        $this->ensureBehaviors();
        //依次检查各个行为中是否有可写属性$name
        foreach ($this->_behaviors as $behavior) {
            if ($behavior->canSetProperty($name)) {
                $behavior->$name = $value;
                return;
            }
        }
        ...
    }

    对于setter函数,略微复杂,检查顺序依次是:

    • 自己的setter函数,也即是自己的可写属性
    • 如果$name是’on xyz’形式,则xyz作为事件,$value作为handler,将其绑定
    • 如果$name是’as xyz’形式,则xyz作为行为名字,$value作为行为,将其附加
    • 依次检查各个行为中是否有可写属性$name,返回第一个;如果没有则抛出异常

    因此,Component的可读属性就是本身的可读属性加上所有行为的可读属性;而可写属性就是本身的可写属性加上所有行为的可写属性

    方法的注入

    同属性的注入类似,方法的注入也是自身的方法加上所有行为的方法:

    public function __call($name, $params)
        {
            $this->ensureBehaviors();
            //遍历所有行为的方法
            foreach ($this->_behaviors as $object) {
                if ($object->hasMethod($name)) {
                    return call_user_func_array([$object, $name], $params);
                }
            }
            ...

    这里以为最终调用的是call_user_func_array()的函数,所以只有行为的public 方法才能被注入组件中。
    除了属性的读和写,还有对属性的判断(isset)和注销(unset),分别通过对魔术方法__isset__unset的重载来实现,这里就不多赘述了。

    结语

    属性,事件和行为是Yii的基础功能,它们使得Yii成为一个变化无穷、魅力无穷的框架。然而,框架不能做PHP本身都做不到的事情,它酷炫的功能无非是PHP自身的面向对象特性(重载,魔术方法,成员变量/函数可见性)和一些数据结构,外加巧妙的算法来实现的。因此“解剖”的目的就在于,解开这次神秘面纱,搞清楚内在逻辑,最终使得自己的编程能力得到切实的提高。

    展开全文
  • CSS中behavior属性语法的使用介绍

    千次阅读 2016-12-09 17:07:57
    讨论一下CSS中behavior属性语法的使用,在进行CSS网页布局的时候,我们经遇到刷新要保留表单里内容的时候,习惯的做法使用cookie,但是那样做实在是很麻烦,CSS中的behavior就为我们很好的解决了这个问题。...
  • 自定义Behavior

    2018-03-23 18:14:11
    Behavior简介CoordinatorLayout是个很牛的布局容器,使用它可以很方面的实现很多效果,比如支付宝首页的折叠效果,知乎首页等等,Behavior是它的一个内部抽象类,声明Behavior属性的View可以和他它依赖的兄弟元素...
  • CSS可以实现,只是你的认知不足,你可能都没有见过scroll-behavior这个标签。 scroll-behavior为一个滚动框指定滚动行为。 默认auto,立即滚动;smooth ,平稳丝滑的滚动; html{scroll-behavior:smooth} 在不考虑...
  • Android Behavior

    千次阅读 2017-01-18 14:50:12
    Behavior是Android新出的Design库里新增的布局概念。Behavior只有是CoordinatorLayout的直接子View才有意义。可以为任何View添加一个BehaviorBehavior是一系列回调。让你有机会以非侵入的为View添加动态的依赖...
  • android Behavior讲解

    千次阅读 2017-04-22 11:31:19
    1.前言在前一篇的文章中大家是否看到这么一行代码:app:layout_behavior="@string/appbar_scrolling_view_behavior"当时我们不知道这是干嘛的,直接照用就行了,后来发现这玩意是一个类!而且我们还可以自定义!所以...
  • 手把手教你使用Unity的Behavior Designer

    万次阅读 多人点赞 2020-09-27 09:31:19
    什么是行为树 如果了解过状态机,会知道在行为树之前,在实现AI用得比较多的技术是状态机,状态机理解起来是比较简单的,即一个状态过渡到另一个状态,通过判断将角色的状态改变即可,如果学习过Unity的Mecanim动画...
  • 针对 CoordinatorLayout 及 Behavior 的一次细节较真

    万次阅读 多人点赞 2018-03-30 17:29:10
    我认真不是为了输赢,我就是认真。– 罗永浩 我一直对 Material Design 很感兴趣,每次在官网上阅读它的相关文档时,我总会有更进一步的体会。当然,Material Design 并不是仅仅针对 Android 而言的,它其实是一套...
  • Android Behavior之相关解析

    千次阅读 2017-10-25 17:33:18
    第一个简单的自定义Behavior在Android 5.0 的时候推出了CoordinatorLayout控件,该控件从翻译上来说称之为 协调性布局,我的理解是,对于他下面的子控件的布局,大小,滚动等等一系列的东西,由每一个子控
  • 源码看CoordinatorLayout.Behavior原理

    万次阅读 多人点赞 2015-12-23 16:21:00
    在上一篇博客CoordinatorLayout高级用法-自定义Behavior中,我们介绍了如何去自定义一个CoordinatorLayout的Behavior,通过文章也可以看出Behavior在CoordinatorLayout中地位是相当高的,那么今天我们就来接着上篇...
  • Material Design系列,自定义Behavior实现Android知乎首页

    万次阅读 多人点赞 2016-08-14 18:59:33
    Material Design系列,自定义自定义Behavior实现Android知乎首页,用Behavior实现知乎首页上滑或者下滑时FAB和TAB导航的显示和隐藏,留给用户更多的位置来显示内容。
  • CoordinatorLayout+Behavior讲解

    万次阅读 多人点赞 2017-04-06 15:25:41
    文章开始前奉送上代码,方便大家对照学习1.前言CoordinatorLayout是在Google I/O 15上,谷歌发布了一个新的 support library中的控件,它是support library中最重要的控件之一,所以大家要掌握它!...
  • CoordinatorLayout高级用法-自定义Behavior

    万次阅读 多人点赞 2015-12-14 22:23:41
    在新的support design中,CoordinatorLayout可以说是最重要的一个控件了,CoordinatorLayout给我们带来了一种新的事件的处理方式——behavior,你是不是还记得我们在使用CoordinatorLayout的时候,一些子view需要一...
  • Behavior

    2018-01-17 11:25:29
    Behavior是Android的Design库里新增的布局概念。 Behavior只有是CoordinatorLayout的直接子View才有意义。可以为任何View添加一个BehaviorBehavior是一系列回调。让你有机会以非侵入的为View添加动态的依赖布局...
  • 深入理解CoordinatorLayout.Behavior

    千次阅读 2016-08-22 10:12:27
    一、Behavior是什么?为什么要用Behavior? 二、怎么使用Behavior? 三、从源码角度看为什么要这么使用Behavior? 一、Behavior是什么?为什么要用Behavior?CoordinatorLayout是android support design推出的...
  • 引言如果说前面提到的TextInputLayout、SnackBar的应用还不是很常见的话,那么今天提到的FloatingActionButton绝对是一个随处可见的Material Design控件了,无论是我们常用的知乎、印象笔记或者是可爱的谷歌全家桶...
  • 写作思路: ...CoordinatorLayout Behavior 简介 怎样自定义 Behavior 仿知乎效果 自定义 Behavior 的实现 自定义 Behavior 两种方法的 对比 FloatActionButton 自定义 Behavior 效果的实现 题外话
1 2 3 4 5 ... 20
收藏数 182,387
精华内容 72,954
关键字:

behavior