Android——CoordinatorLayout之Behavior入门学习(上)

学习资料:

  • dodo_lihao同学:CoordinatorLayout使用系列
  • r17171709 同学:CoordinatorLayout——小试牛刀

最近一直在看Java的知识,在看到上面两位同学的博客。CoordinatorLayout只是简单使用过一次,也学习了解一下。

十分感谢两位同学 :)


1. 简单使用

在之前也了解过一点怎么使用CoordinatorLayout,并写了一篇简单入门使用的博客 CoordinatorLayout、Tablayout、Toolbar简单组合使用

CoordinatorLayout作为一个中间桥梁性质的布局,协调着内部的childView。之前对CoordinatorLayout有点误解,以为需要配合AppBarLayout才有一些比较炫酷的特效,大错特错,BehaviorCoordinatorLayout能够有协调作用以及能支持各种炫酷特效的的关键因素


dodo_lihao同学的思路很好,博客中也把他自己收集的学习资料整理了出来,就直接看着他的博客,跟着他的思路来学习的,感觉他系列博客中写的案例很容易表现出Behavior的特点,所以思路和代码照搬的dodo_lihao同学的,效果图也就一样了,有点剽窃成果的感觉,哈哈

运行效果

依赖控件:红色的MoveView
绑定控件:蓝色的TextView

MoevView受手指的控制,手指怎么移动就怎么移动;而绑定的TextView则是由MoveView通过Behavior来控制


1.1 布局文件中使用

布局文件:


    
    

    


最关键的地方就在于app:layout_behavior,利用这个属性来确定的绑定的目标childView

指定绑定目标有3种方式:

  1. xml布局通过app:layout_behavior
  2. Java代码中,child.getLayoutParams().setBehavior()来指定
  3. 在目标childView类上,通过@DefaultBehavior来指定

1.2 MoveView

MoveView就是一个继承TextView的很简单的自定义View

public class MoveView extends TextView {
    private float lastX, lastY;

    public MoveView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        float x = event.getRawX();
        float y = event.getRawY();
        if (action == MotionEvent.ACTION_MOVE) {
            CoordinatorLayout.MarginLayoutParams layoutParams = (CoordinatorLayout.MarginLayoutParams) getLayoutParams();
            //计算当前的左上角坐标
            float left = layoutParams.leftMargin + x - lastX;
            float top = layoutParams.topMargin + y - lastY;
            //设置坐标
            layoutParams.leftMargin = (int) left;
            layoutParams.topMargin = (int) top;
            setLayoutParams(layoutParams);
        }
        lastX = x;
        lastY = y;
        return true;
    }
}

主要就是重写onTouchEvent()来使MoveView可以根据手指滑动在屏幕改变位置


1.3 一个简单的自定义Behavior

LearnBehavior代码:

public class LearnBehavior extends CoordinatorLayout.Behavior {
    private int width, height;

    public LearnBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        DisplayMetrics display = context.getResources().getDisplayMetrics();
        width = display.widthPixels;
        height = display.heightPixels;
    }

    /**
     * 绑定
     *
     * @param parent     CoordinatorLayout
     * @param child      使用Behavior的childView,绑定对象
     * @param dependency 依赖的childView
     * @return true 绑定
     */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, TextView child, View dependency) {
        return dependency instanceof MoveView;
    }

    /**
     * 依赖的childView 发生改变时
     */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, TextView child, View dependency) {
        int top = dependency.getTop();
        int left = dependency.getLeft();

        int x = width - left - child.getWidth();
        int y = height - top - child.getHeight();
        Log.e("x,y", "--->" + x + "-->" + y);
        setPosition(child, x, y);
        return true;
    }

    /**
     * 设置坐标
     */
    private void setPosition(View v, int x, int y) {
        CoordinatorLayout.MarginLayoutParams layoutParams = (CoordinatorLayout.MarginLayoutParams) v.getLayoutParams();
        layoutParams.leftMargin = x;
        layoutParams.topMargin = y;
        layoutParams.width = y / 2;
        v.setLayoutParams(layoutParams);
    }
}

CoordinatorLayout.Behavior这里使用泛型将绑定的childView限制为了TextView,可以根据实际需求来指定类型,也可以直接指定为View

注意:
当在布局文件中使用了Behavior后,Behavior代码中确定的交互行为便直接奏效,初始化第一次加载CoordinatorLayout时,使用了BehaviorChildView受到onDependentViewChanged()方法的影响,第一次加载的位置也会受到影响,导致和布局文件中指定的位置不相同

官方有好几个非常好的学习资料,例如:
android.support.design.widget.AppBarLayout$ScrollingViewBehavior

一个依赖AppBarLayout后,处理滑动事件的Behavior,对Behavior中的属性及方法有了大概了解后,可以学习具体细节的设计和优化


2. Behavior 行为

直译就是行为的意思

源码中的注释:

    /**
     * Interaction behavior plugin for child views of {@link CoordinatorLayout}.
     * 用于CoordinatorLayout中ChildView交互的行为的插件
     *
     * A Behavior implements one or more interactions that a user can take on a child view.
     *一个ChildView可以实现一个或者多个Behavior
     *
     * These interactions may include drags, swipes, flings, or any other gestures.
     *交互的行为包括点击,拖动,滑动或者一些其他的收拾操作
     *
     * @param  The View type that this Behavior operates on
     * 泛型就是指定使用当前Behavior的ChildView类型
     */
    public static abstract class Behavior {
        ...
        方法省略
        ...
    }

需要注意的是Behavior可以几乎包括所有的交互行为,配合ViewDragHelper应该能够实现出一些很炫酷的交互效果


2.1 常用的方法

构造方法有两个:

默认:public Behavior() {}

布局:public Behavior(Context context, AttributeSet attrs) {  }

两个构造方法也比较容易理解,一个是默认的空参的构造方法,一个是带有布局属性AttributeSet的方法,有了这个构造方法,可以直接在布局文件中使用


根据Behavior的特性,可以将内部的方法分以下类:

  • 测量与布局:
测量:public boolean onMeasureChild(){}
布局:public boolean onLayoutChild(){}
  • 特定状态:
//当Behavior添加到参数实例时,回调
public void onAttachedToLayoutParams(){}

//当Behavior与参数实例分离时,回调
public void onDetachedFromLayoutParams(){}

//当Behavior关联的对象想要定位到特定的矩形时,回调
public boolean onRequestChildRectangleOnScreen(){}

//当一个ChildView设置为回避属性时,回调
public boolean getInsetDodgeRect(){}

//当窗口发生改变时,回调
public WindowInsetsCompat onApplyWindowInsets(){}

//需要保存临时状态信息,回调
public Parcelable onSaveInstanceState(){}

//需要恢复临时状态信息,回调
public void onRestoreInstanceState(){}

//作用未知
public int getScrimColor(){}
 
//作用未知
public float getScrimOpacity(){}
  • 确定依赖与绑定对象:
//根据参数来确定依赖与绑定对象
public boolean layoutDependsOn(){}
  • 当依赖对象发生改变时:
//当依赖对象发生改变,包括位置,大小,颜色,进行回调
public boolean onDependentViewChanged(){}

//当依赖对象被移除时,进行回调
public void onDependentViewRemoved(){}
  • 事件相关:
//拦截事件,在CoordinatorLayout把事件分发到childView之前
public boolean onInterceptTouchEvent(){}

//消费事件
public boolean onTouchEvent(){}
  • 嵌套滑动:
//CoordinatorLayout中的滑动嵌套childView开始启动一次嵌套滚动时,回调
public boolean onStartNestedScroll(){}

//嵌套滑动结束时,回调
public void onStopNestedScroll(){}

//当一次嵌套滑动被CoordiantorLayout识别并确定时,进行回调
public void onNestedScrollAccepted(){}

//嵌套滚动正在进行中并且绑定目标childView已经开始滚动或者被CoordinatorLayout接受后试图滚动
public void onNestedScroll(){}

//嵌套滚动正在准备更新进度,并且是在绑定目标childView已经出现滚动距离之前,回调
public void onNestedPreScroll(){}

//当嵌套滚动的childView正在开始fling或者一个动作确认为fling
public boolean onNestedFling(){}

//当滑动嵌套childView检测到适当的条件,马上开始一次fling事件前回调
public boolean onNestedPreFling(){}

暂时就这么分,分类并不算合理,也无所谓,目的是以后自己回头来看时,能比较清晰能快速定位方法是干嘛的


3. 事件相关

需求:CoordinatorLayout内有一个可以点击的TextView,长按之后,可以拖动,此时蓝色的TextView要依然可以点击

运行效果

布局代码:

布局代码中,并没有添加Behaivor,一旦添加了,在加载布局之时,Behaivor便开始作用于依赖目标childView



    



Activity代码:

public class CoordinatorLayoutLActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_coordinator_layout_l);
        init();
    }

    /**
     * 初始化
     */
    private void init() {
        initTextView(R.id.tv_coordinator_activity, "TextView-->蓝色被点击");
        CoordinatorLayout layout = (CoordinatorLayout) findViewById(R.id.cl_coordinator_activity);
        layout.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ToastUtils.show(CoordinatorLayoutLActivity.this, "CoordinatorLayout被点击");
            }
        });
    }

    /**
     * TextView进行初始化
     */
    private void initTextView(int id, final String str) {
        final TextView tv = (TextView) findViewById(id);
        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ToastUtils.show(CoordinatorLayoutLActivity.this, str);
            }
        });

        /**
         *  长按 ,提示动画效果结束后 ,动态添加 Behavior
         */
        tv.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                //动画提示效果
                animation(tv);
                return true;
            }
        });
    }

    private void animation(final TextView tv) {
        AnimatorSet set = new AnimatorSet();
        set.setInterpolator(new BounceInterpolator());
        set.setDuration(1000);
        set.playTogether(
                ObjectAnimator.ofFloat(tv, "scaleX", 1, 1.5f),
                ObjectAnimator.ofFloat(tv, "scaleY", 1, 1.5f),
                ObjectAnimator.ofFloat(tv, "scaleX", 1.5f, 1),
                ObjectAnimator.ofFloat(tv, "scaleY", 1.5f, 1)
        );
        //动画监听,结束时添加 Behavior
        set.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                addBehavior(tv);
            }
        });
        set.start();
    }

    /**
     * 为TextView添加Behavior
     */
    private void addBehavior(TextView tv) {
        tv.setLongClickable(false);
        CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) tv.getLayoutParams();
        //为TextView设置Behaior
        lp.setBehavior(new LongBehavior());
        ToastUtils.show(CoordinatorLayoutLActivity.this, "可以开始拖动了");
    }
}

代码很简单,都是一眼能看明白的


LongBehavior代码:


    private float lastX, lastY;
    private float moveX, moveY;

    public LongBehavior() {
        Log.e("LongBehavior", "新建");
    }

    public LongBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }


    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, TextView child, MotionEvent ev) {
        int action = ev.getAction();
        boolean isIntercept = false;
        float x = ev.getX();
        float y = ev.getY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                //判断落点是否在TextView范围内
                //若不在 就进行拦截 返回true
                isIntercept = !isInChildView(child, ev);
                if (isIntercept) {
                    Log.e("MotionEvent.ACTION_DOWN", "---->MotionEvent.ACTION_DOWN--->进行拦截");
                } else {
                    Log.e("MotionEvent.ACTION_DOWN", "---->MotionEvent.ACTION_DOWN--->不拦截");
                }
                break;
            case MotionEvent.ACTION_MOVE:
                 //滑动距离大于10
                if (Math.abs(lastX - x) >= 10 || Math.abs(lastY - y) >= 10) {
                    isIntercept = true;
                }
                break;
        }
        return isIntercept;
    }

    /**
     * 判断落点是否在childView范围内
     */
    private boolean isInChildView(TextView child, MotionEvent ev) {
        return ev.getX() >= child.getLeft() && ev.getX() <= child.getRight()
                && ev.getY() >= child.getTop() && ev.getY() <= child.getBottom();
    }

    @Override
    public boolean onTouchEvent(CoordinatorLayout parent, TextView child, MotionEvent ev) {
        //根据是否拦截来执行
        if (onInterceptTouchEvent(parent, child, ev)) {
            int action = ev.getAction();
            float x = ev.getX();
            float y = ev.getY();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    moveX = x;
                    moveY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    //计算偏移量
                    float offsetX = x - moveX;
                    float offsetY = y - moveY;
                    // Log.e("offset", "&&&--" + offsetX + "-->" + offsetY);
                    if (Math.abs(offsetX)>= 10 || Math.abs(offsetY) >= 10) {
                        CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
                        layoutParams.leftMargin = (int) (offsetX);
                        layoutParams.topMargin = (int) (offsetY);
                        child.setLayoutParams(layoutParams);
                    }
                    break;
            }
        }
        return true;
    }
}

当拦截了DOWN事件之后,后续的事件便都由CoordinatorLayout来消费,onTouchEvent返回了True,事件也就终止了,onClik便也接收不到事件了,CoordinatorLayout自身的点击事件不能执行了,


4.最后

嵌套滚动事件,下一篇进行记录学习

本人很菜,有错误请指出

共勉 :)

你可能感兴趣的:(Android——CoordinatorLayout之Behavior入门学习(上))