自定义控件:侧滑面板

本篇博客讲解的是自定义View之侧滑面板,应用场景:QQ,知乎,效果图如下
侧滑面板

1. 内容摘要

  • 了解ViewDragHelper 的产生及解决的问题
  • 掌握ViewDragHelper 的使用步骤
  • 掌握属性动画的使用
  • 掌握状态更新及事件回调的用法

2. 实现最简单的拖拽

2.1 实现最简单的拖拽

在创建DragLayout 时,继承FrameLayout,这里需要注意两个问题

为什么不继承ViewGroup,因为继承ViewGroup 需要重写onMeasure()和实现onLayout()方法,自己实现子view 的测量和摆放,在这里我们不需要自己去做测量和摆放,而FrameLayout 已经对这两个方法进行了具体实现,所以继承FrameLayout 更加简单省事

为什么不继承RelativeLayout,因为这里我们只需要层级关系,不需要相对关系,继承RelativeLayout界面效果是一样的,但RelativeLayout 对FrameLayout 多了相对关系的计算,效率会低一些,所以选择继承FrameLayout

public class DragLayout extends FrameLayout {
   
        public DragLayout(Context context) {
            super(context);
        }
        public DragLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
        public DragLayout(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }
    }

2.2 串联构造方法

DragLayout 实例化时需要做一些初始化操作,如果我们定义一个init()方法,则我们需要在三个构造方法中都调用init()方法,这样非常麻烦,我们可以通过串连三个构造方法的方式实现只调用一次init()方法这样无论是代码创建还是布局在xml 中都能调用到我们的初始化代码

public class DragLayout extends FrameLayout {
   
        public DragLayout(Context context) {
            //代码创建时调用
            this(context, null);
        }

        public DragLayout(Context context, AttributeSet attrs) {
            //布局在xml 中,实例化时调用
            this(context, attrs, 0);
        }

        public DragLayout(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            //在这里初始化
        }
    }

2.3 ViewDragHelper 简介

我们要实现拖拽的效果,则需要自己去解析Touch 事件的ACTION_DOWN,ACTION_MOVE,ACTION_UP,相当的麻烦。所以Google 在2013 年的IO 大会上发布了ViewDragHelper 这个类,用来解决滑动拖拽问题,用这个类可以非常简单的实现view 的拖拽

2.4 创建ViewDragHelper

由于eclipse 创建项目时,为我们添加的android-support-v4.jar 没有包含ViewDragHelper,我们需要将最新的android-support-v4.jar 拷贝到libs 下面,然后clean 一下工程。

在这里我们需要关联android-support-v4.jar 的源码,通过配置文件的方法来关联源码

在libs 下面创建一个android-support-v4.jar.properties 的文件
这里写图片描述

android-support-v4.jar.properties 中的内容为src = V4 包源码路径
这里写图片描述

我们只需要在第三个构造方法中实现ViewDragHelper 的实例即可

public DragLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        // 在这里初始化
        // forParent 父类容器
        // sensitivity 敏感度,越大越敏感,1.0f 是默认值
        // Callback 回调事件
        //1.通静态方法创建拖拽辅助类
        mViewDragHelper = ViewDragHelper.create(this, 1.0f, mCallback);
    }

ViewDragHelper 三个参数的创建的方法源码中的mTouchSlop 表示触摸的最小敏感范围,越小越敏感即在界面拖动的瞬间变化量大于mTouchSlop 时才可以成功触发拖拽事件

 public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb){
    final ViewDragHelper helper = create(forParent, cb);
    helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
    return helper;
 }

2.5 触摸事件转交

ViewDragHelper 创建成功了,但它和DragLayout 并没有任何关系,我们需要让它们建立关系

 //2.转交触摸事件
 @Override
 public boolean onInterceptTouchEvent(MotionEvent event) {
    //由ViewDragHelper 判断是否拦截
    return mViewDragHelper.shouldInterceptTouchEvent(event);
 };

重写onInterceptTouchEvent 方法,将触摸事件交给ViewDragHelper 判断是否拦截,这样它们就建立了关系,事件拦截后,还需要对拦截到的事件进行处理,注意返回值必须是true

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        try {
            //由ViewDragHelper 处理拦截的事件
            mViewDragHelper.processTouchEvent(event);
        } catch (Exception e) {}
        //事件已被处理,所以需要返回true
        return true;
    };

2.6 处理回调事件

ViewDragHelper 在处理触摸事件时会通过传入的callback 给我们反馈,通过对回调方法的处理即可实现简单的拖拽

//3.处理回调事件
    ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
        @Override
        //返回值决定了child 是否可以被拖拽
        public boolean tryCaptureView(View child, int pointerId) {
            //child 被用户拖拽的孩子
            //pointerId 多点触摸的手指id
            return true;
        }
        @Override
        //修正子view 水平方向上的位置,此时还没有真正的移动,返回值决定view 将移动到的位置
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            //left 建议移动到的位置
            return left;
        }
    };

2.7 DragLayout 布局到xml 中

给左面板和主面板设置不同的背景颜色便于拖拽时观察效果,运行工程,即可实现简单的拖拽

<com.example.draglayout.widget.DragLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/bg">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#66ff0000">
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#00ff00">
    </LinearLayout>

</com.example.draglayout.widget.DragLayout>

3. 限定拖拽范围

现在左面板和主面板可以任意拖动,本节要实现左面板不动,拖动时,主面板在一定范围内拖动

3.1 OnFinishInflate()介绍

onFinishInflate()在控件inflate 完成时会被调用,可以在这个方法中查找子控件

  • 可以通过findViewById()的方式查找子控件
  • 可以通过子view 索引的方式查找子控件

这里采用第二种方式

@Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        //增强代码的健壮性
        if(getChildCount() < 2){
            //必须有两个子view
            throw new IllegalStateException("Your viewgroup must have two children.");
        }
        if(!(getChildAt(0)instanceofViewGroup)||!(getChildAt(1)instanceof ViewGroup)){
            //子view 必须是viewgroup 的子类
            throw new IllegalStateException("The child must an instance of viewgroup.");
        }
        mLeftContent = getChildAt(0);
        mMainContent = getChildAt(1);
    };

3.2 获取控件宽高

在onMeasure()方法中可以获取到控件的宽高,也可以在onSizeChanged()方法中去获取宽高,onMeasure()方法调用后会检测宽高值有没有变化,有变化才调用onSizeChanged()方法,无变化则不调用,所以onSizeChanged()调用的次数比onMeasure()少,在这里我们在onSizeChanged()方法中去获取宽高,同时计算出拖拽范围为宽度的60%

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
        //拖拽的范围
        mRange = (int) (mWidth * 0.6f);
        System.out.println("mWidth:"+mWidth+" mHeight:"+mHeight +" mRange:"+mRange);
    }

3.3 限定主面板的拖动范围

对callback 中的其它几个方法进行重写

 //3.处理回调事件
    ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
        @Override
        //返回值决定了child 是否可以被拖拽
        public boolean tryCaptureView(View child, int pointerId) {
            //child 被用户拖拽的孩子
            //pointerId 多点触摸的手指id
            return true;
        }
        @Override
        public int getViewHorizontalDragRange(View child) {
            return super.getViewHorizontalDragRange(child);
        }
        @Override
        //修正子view 水平方向上的位置,此时还没有真正的移动,返回值决定view 将移动到的位置
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            //left 建议移动到位置
            return left;
        }
        @Override
        public void onViewPositionChanged(View changedView, int left, int top,
                                          int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
        }
    };

回调方法中的getViewHorizontalDragRange(View child)方法返回拖拽的范围,但不会真正限定这个范围,只要返回一个大于零的值即可。

在ViewDragHelper 源码中,computeSettleDuration()会调用这个返回值来计算动画执行的时长,checkTouchSlop()方法会调用这个返回值检查左面板,主面板是否可以被滑动,所以需要返回一个大于0的值才能实现拖动。

如果返回值为0,左面板,主面板中不能有子view 或子view 没有对touch 事件做处理,最后触摸还是会交给ViewDragHelper 处理,所以也能实现拖动

 @Override
    //返回拖拽的范围,返回一个大于零的值,计算动画执行的时长,水平方向是否可以被滑开
    public int getViewVerticalDragRange(View child) {
        //computeSettleDuration 计算动画执行的时长
        //checkTouchSlop 检查是否可以被滑动(没有孩子处理触摸事件,最后返回给DragLayout 处理)
        return mRange;
    }

限定主面板的拖拽范围,当建议的值left 小于0 时,让left 等于0,大于mRange 时等于mRange,然后再将left 返回


    @Override
    // 修正子view 水平方向上的位置,此时还没有真正的移动,返回值决定view 将移动到的位置
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        // child 被用户拖拽的孩子
        // left 建议移动到位置
        // dx 新的位置与旧的位置的差值
        int oldLeft = mMainContent.getLeft();
        System.out.println("clamp: left:" + left + " oldLeft:"<
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值