Material Design - Behavior

作者: Arnold_J | 来源:发表于2017-11-02 16:35 被阅读117次

    关键字:Behavior、材料设计
    项目地址:AboutMaterialDesign


    大多数情况下,我们的项目只需要用到材料设计的 Toolbar DrawerLayout TabLayout 等等现成的控件就可以了,使用方便,也允许我们有不同程度的自定义优化。但是一旦业务对于界面的交互有更明确的要求,上面的控件就变得不再那么够用。材料设计为我们提供了协调者布局 CoordinatorLayout 用于联系所有子控件,使得子控件之间在保持相对独立的同时,又能相互影响,适时变化。

    而这一切除了要感谢 CoordinatorLayout 的出现,还要记得 Google 工程师在 Behavior 上的付出。


    学习 Behavior 就是学习一点:根据监听得到合适的时机去修改目标视图的状态。
    带着这样的学习目的,我们去查看相关代码,会容易很多。

    根据文档,我们可以知道,Behavior 是 CoordinatorLayout 的一个内部类。

    Interaction behavior plugin for child views of {@link CoordinatorLayout}.
    A Behavior implements one or more interactions that a user can take on a child view.
    These interactions may include drags, swipes, flings, or any other gestures.
    

    一、使用自带的 Behavior

    由于大会影响,翻不了墙,因此只拣了我记得的 BottomSheetBehaviorSwipeDismissBehavior 来写。

    1) BottomSheetBehavior
    GIF.gif

    BottomSheetBehavior 一共有五个状态,上面的效果为 BottomSheetBehavior 对应的三个状态:

        /**
         * The bottom sheet is dragging.
         */
        public static final int STATE_DRAGGING = 1;
    
        /**
         * The bottom sheet is settling.
         */
        public static final int STATE_SETTLING = 2;
    
        /**
         * The bottom sheet is expanded.
         */
        public static final int STATE_EXPANDED = 3;
    
        /**
         * The bottom sheet is collapsed.
         */
        public static final int STATE_COLLAPSED = 4;
    
        /**
         * The bottom sheet is hidden.
         */
        public static final int STATE_HIDDEN = 5;
    

    实现上面的效果很简单,只需要在以 CoordinatorLayout 为根节点的布局文件中增加一个容器放置弹框的内容,然后在容器根节点下新增属性:

    app:behavior_hideable="true"
    app:behavior_peekHeight="100dp"
    app:layout_behavior="@string/bottom_sheet_behavior"
    

    其中

    • layout_behavior 用于指定容器指定的 Behavior ,在使用自定义 Behavior 的时候,也可以通过这个方法,将视图和 Behavior 关联
    • behavior_peekHeight 用于指定视图折叠时显示的高度
    • behavior_hideable 用于指定视图是否可以完全隐藏
    <android.support.design.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/cl"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        >
     
        <android.support.v4.widget.NestedScrollView
            android:id="@+id/bottom_sheet"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:behavior_hideable="true"
            app:behavior_peekHeight="50dp"
            app:layout_behavior="@string/bottom_sheet_behavior"
            >
              <!-- NestedScrollView里设置你的底部表长什么样的-->
        </android.support.v4.widget.NestedScrollView>
    </android.support.design.widget.CoordinatorLayout>
    

    代码中需要注意这样几个方法:

    //1.动态设置 Behavior
    BottomSheetBehavior behavior = new BottomSheetBehavior();
    behavior.setPeekHeight(100);
    behavior.setHideable(true);
    behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
    behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
        @Override
        public void onStateChanged(@NonNull View bottomSheet, int newState) {
            //状态变化时调用
        }
    
        @Override
        public void onSlide(@NonNull View bottomSheet, float slideOffset) {
            //滑动时调用,可以根据 slideOffset 配置动画效果
        }
    });
    
    //2.获取 xml 中设置的 behavior 并设置相关属性
    View bottomSheet = findViewById(R.id.bottom_sheet);
    if (bottomSheet != null) {
        behavior = BottomSheetBehavior.from(bottomSheet);
        behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
    }
    
    //3.获取当前状态
    int state = behavior.getState();
    
    2) SwipeDismissBehavior

    在 xml 布局文件中,利用联想并没有跳出这个 Behavior。但是在 Java 代码中,我们还是可以找到这个类的。

    SwipeDismissBehavior<View> behavior = new SwipeDismissBehavior<>();
    behavior.setSwipeDirection(SWIPE_DIRECTION_START_TO_END);
    //behavior.setDragDismissDistance(0.5f);//
    //behavior.setStartAlphaSwipeDistance(0.1f);//设置拖动的有效距离,即开始透明消失的距离
    //behavior.setEndAlphaSwipeDistance(0.7f);//完全消失时候的距离
    //behavior.setSensitivity(0.5f);//敏感度
    behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
        @Override
        public void onDismiss(View view) {
            Log.d(TAG, "onDismiss: ");
        }
    
        @Override
        public void onDragStateChanged(int state) {
            Log.d(TAG, "onDragStateChanged: ");
        }
    });
    ((CoordinatorLayout.LayoutParams) mTextView.getLayoutParams()).setBehavior(behavior);
    
    GIF.gif

    二、自定义简单 Behavior

    下面贴出大部分可重写方法的代码:

    public class TestBehavior extends CoordinatorLayout.Behavior<View> {
        private static final String TAG = "TestBehavior";
    
        public TestBehavior() {
            super();
            Log.i(TAG, "TestBehavior(1): ");
        }
    
        public TestBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
            Log.i(TAG, "TestBehavior(2): ");
        }
    
        //--------------------[布局事件 start]-----------------------------------------------
        @Override
        public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {
            super.onAttachedToLayoutParams(params);
            Log.i(TAG, "onAttachedToLayoutParams: ");
        }
    
        @Override
        public void onDetachedFromLayoutParams() {
            super.onDetachedFromLayoutParams();
            Log.i(TAG, "onDetachedFromLayoutParams: ");
        }
    
        @Override
        public boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
            Log.i(TAG, "onMeasureChild: ");
            return super.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
        }
    
        @Override
        public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
            Log.i(TAG, "onLayoutChild: ");
            return super.onLayoutChild(parent, child, layoutDirection);
        }
        //--------------------[布局事件 end]-----------------------------------------------
        
        //--------------------[触摸事件 start]-----------------------------------------------
        @Override
        public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) {
            Log.i(TAG, "onInterceptTouchEvent: ");
            return super.onInterceptTouchEvent(parent, child, ev);
        }
    
        @Override
        public boolean onTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) {
            Log.i(TAG, "onTouchEvent: ");
            return super.onTouchEvent(parent, child, ev);
        }
        //--------------------[触摸事件 end]-----------------------------------------------
    
        //--------------------[状态改变事件 start]-----------------------------------------------
        @Override
        public int getScrimColor(CoordinatorLayout parent, View child) {
            Log.i(TAG, "getScrimColor: ");
            return super.getScrimColor(parent, child);
        }
    
        @Override
        public float getScrimOpacity(CoordinatorLayout parent, View child) {
            Log.i(TAG, "getScrimOpacity: ");
            return super.getScrimOpacity(parent, child);
        }
    
        @Override
        public boolean blocksInteractionBelow(CoordinatorLayout parent, View child) {
            Log.i(TAG, "blocksInteractionBelow: ");
            return super.blocksInteractionBelow(parent, child);
        }
    
        @Override
        public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
            Log.i(TAG, "layoutDependsOn: dependency=="+dependency.getClass().getSimpleName());
            return super.layoutDependsOn(parent, child, dependency);
        }
    
        @Override
        public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
            Log.i(TAG, "onDependentViewChanged: dependency=="+dependency.getClass().getSimpleName());
            return super.onDependentViewChanged(parent, child, dependency);
        }
    
        @Override
        public void onDependentViewRemoved(CoordinatorLayout parent, View child, View dependency) {
            Log.i(TAG, "onDependentViewRemoved: dependency=="+dependency.getClass().getSimpleName());
            super.onDependentViewRemoved(parent, child, dependency);
        }
        //--------------------[状态改变事件 end]-----------------------------------------------
    
        //--------------------[滑动事件 start]-----------------------------------------------
        @Override
        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
            Log.i(TAG, "onStartNestedScroll: ");
            return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
        }
    
        @Override
        public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
            Log.i(TAG, "onNestedScrollAccepted: ");
            super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
        }
    
        @Override
        public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
            Log.i(TAG, "onStopNestedScroll: ");
            super.onStopNestedScroll(coordinatorLayout, child, target);
        }
    
        @Override
        public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
            Log.i(TAG, "onNestedScroll: ");
            super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
        }
    
        @Override
        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
            Log.i(TAG, "onNestedPreScroll: ");
            super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        }
    
        @Override
        public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY, boolean consumed) {
            Log.i(TAG, "onNestedFling: ");
            return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
        }
    
        @Override
        public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
            Log.i(TAG, "onNestedPreFling: ");
            return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
        }
        //--------------------[滑动事件 end]-----------------------------------------------
    }
    

    而我们的需求通常集中在这几个方法中:

    //监听状态的改变 返回是否调用 onDependentViewChanged 方法
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        Log.i(TAG, "layoutDependsOn: dependency=="+dependency.getClass().getSimpleName());
        return super.layoutDependsOn(parent, child, dependency);
    }
    
    //监听状态的改变 反馈方法
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        Log.i(TAG, "onDependentViewChanged: dependency=="+dependency.getClass().getSimpleName());
        return super.onDependentViewChanged(parent, child, dependency);
    }
    
    //监听滑动,返回 true 时调用 onNestedScroll 等一系列方法
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild,
                                       View target, int nestedScrollAxes) {
        return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }
    
    /**
     *  正在滚动 反馈方法
     *
     * 这里我们只关心y轴方向的滑动,所以简单测试了dyConsumed、dyUnconsumed
     * dyConsumed > 0 && dyUnconsumed == 0 上滑中
     * dyConsumed == 0 && dyUnconsumed > 0 到边界了还在上滑
     *
     * dyConsumed < 0 && dyUnconsumed == 0 下滑中
     * dyConsumed == 0 && dyUnconsumed < 0 到边界了还在下滑
     *
     * @param coordinatorLayout     parent
     * @param child                 child
     * @param target                dependency
     * @param dxConsumed            x轴滑动的距离
     * @param dyConsumed            y轴滑动的距离
     * @param dxUnconsumed          到顶之后滑动的距离
     * @param dyUnconsumed          到顶之后滑动的距离
     */
    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        Log.i(TAG, String.format("onNestedScroll: dxConsumed==%d,dyConsumed==%d,dxUnConsumed==%d,dyUnConsumed==%d", dxConsumed,dyConsumed,dxUnconsumed,dyUnconsumed));
    }
    

    实现效果:视图 child 随 dependency 的改变而改变

    GIF.gif

    这个效果是拖拽 dependency 的时候,目标视图会按照预设的规则变化位置。

    思路:

    • 自定义一个 Behavior 重写 layoutDependsOn 和 onDependentViewChanged 方法
    • 在 layoutDependsOn 中,判断该 dependency 是否为我们指定的视图
    • 在 onDependentViewChanged 设置目标视图的改变规则

    下面贴上测试 Behavior 代码:

    public class TestDragBehavior extends CoordinatorLayout.Behavior<View> {
        private static final String TAG = "TestDragBehavior";
        private int width;
        private int height;
    
        /**
         * 这个构造方法必须重载,因为在CoordinatorLayout里利用反射去获取Behavior的时候就是拿的这个构造
         */
        public TestDragBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
            width  = context.getResources().getDisplayMetrics().widthPixels;
            height  = context.getResources().getDisplayMetrics().heightPixels;
        }
    
        /**
         * 是否调用 onDependentViewChanged
         * @param parent        父容器
         * @param child         子视图
         * @param dependency    依赖视图
         * @return              是否依赖
         */
        @Override
        public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
            return dependency instanceof DragText;
        }
    
        /**
         * 在这里我们定义 child 要执行的具体动作
         * @return  child是否要执行相应动作
         */
        @Override
        public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    
            int top = dependency.getTop();
            int left = dependency.getLeft();
    
            int x = width - left - child.getWidth();
            int y = height - top - child.getHeight();
    
            setPosition(child, x, y);
    
            return true;
        }
    
        private void setPosition(View child, int x, int y) {
            CoordinatorLayout.MarginLayoutParams layoutParams = (CoordinatorLayout.MarginLayoutParams) child.getLayoutParams();
            layoutParams.leftMargin = x;
            layoutParams.topMargin = y;
            child.setLayoutParams(layoutParams);
        }
    
    }
    

    实现效果:视图 child 随 dependency 滚动状态的改变而改变

    GIF.gif

    下面贴上测试 Behavior 代码:

    public class TestScrollBehavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
        private static final String TAG = "TestScrollBehavior";
        
        public TestScrollBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        /**
         * 监听滚动,返回是否调用 onNestedScroll 等监听代码
         * @param coordinatorLayout
         * @param child
         * @param directTargetChild
         * @param target
         * @param nestedScrollAxes
         * @return
         */
        @Override
        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild,
                                           View target, int nestedScrollAxes) {
            return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
        }
    
        /**
         * 正在滚动
         *
         * 这里我们只关心y轴方向的滑动,所以简单测试了dyConsumed、dyUnconsumed
         * dyConsumed > 0 && dyUnconsumed == 0 上滑中
         * dyConsumed == 0 && dyUnconsumed > 0 到边界了还在上滑
         *
         * dyConsumed < 0 && dyUnconsumed == 0 下滑中
         * dyConsumed == 0 && dyUnconsumed < 0 到边界了还在下滑
         *
         * @param coordinatorLayout     parent
         * @param child                 child
         * @param target                dependency
         * @param dxConsumed            x轴滑动的距离
         * @param dyConsumed            y轴滑动的距离
         * @param dxUnconsumed          到顶之后滑动的距离
         * @param dyUnconsumed          到顶之后滑动的距离
         */
        @Override
        public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
            Log.i(TAG, String.format("onNestedScroll: dxConsumed==%d,dyConsumed==%d,dxUnConsumed==%d,dyUnConsumed==%d", dxConsumed,dyConsumed,dxUnconsumed,dyUnconsumed));
    
            if (((dyConsumed > 0 && dyUnconsumed == 0)
                    || (dyConsumed == 0 && dyUnconsumed > 0))
                    && child.getVisibility() != View.INVISIBLE) {// 上滑隐藏
                child.setVisibility(View.INVISIBLE);
            } else if (((dyConsumed < 0 && dyUnconsumed == 0)
                    || (dyConsumed == 0 && dyUnconsumed < 0))
                    && child.getVisibility() != View.VISIBLE ) {//下滑显示
                child.setVisibility(View.VISIBLE);
            }
    
        }
    }
    

    实现效果:滑动改变 top 布局位置

    GIF.gif
    /**
     * Created by Arno on 2017/10/24.
     *
     * 实现上滑滚出,下滑跟进的效果
     */
    public class TestScrollBehavior2 extends CoordinatorLayout.Behavior<View>{
        private static final String TAG = "TestScrollBehavior2";
        private int offset = 0;
        /**
         * 必须重写这个方法
         * @param context
         * @param attrs
         */
        public TestScrollBehavior2(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
            return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
        }
    
        @Override
        public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
            return true;
        }
    
        @Override
        public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
            int temp = offset;
            int top = offset - dyConsumed;
    
            //上滑
            if (dyConsumed > 0) {
                temp = Math.max(top,-child.getHeight());
            }
    
            //下滑
            if (dyConsumed < 0) {
                temp = Math.min(top,0);
            }
    
            child.offsetTopAndBottom(temp - offset);
            offset = temp;
        }
    
    }
    

    注意
    上面大部分的点,需要自行测试,这里放上代码中获取 Behavior 的方法(还找了一小会它藏哪里去了),根据源码可以看到,Behavior 被存储在 CoordinatorLayout.LayoutParams 中。

    CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) bottomSheet.getLayoutParams()).getBehavior();
    

    以上,谢谢观赏

    相关文章

      网友评论

        本文标题:Material Design - Behavior

        本文链接:https://www.haomeiwen.com/subject/uziuyxtx.html