其实关于自定义控件,我个人认为是安卓中最为重点也是最难得一个知识点,为什么这么说呢?
因为自定义控件 需要掌握view的绘制流程 事件的传递过程 以及paint和canvas的大量api,这都是需要不停地去熟悉他,才能逐渐掌握自定义控件,这是一个过程,需要大家常去学习!
自定义控件一般分为三种 组合控件 继承控件 纯粹自定义控件,前两种不多说了,主要去介绍第三种,关于事件的分发流程以及绘制流程以后会开贴说,今天介绍一个安卓原生提供的工具ViewDragHelper!
下面开始以一个侧拉滑动面板为例进行讲解,因为教自定义控件,会造成大篇文字去介绍,所以只放代码在代码中解释,关键流程会进行说明,不做太多介绍,毕竟时间有限
首先 自定义控件一般继承view或者ViewGroup,我们这里是需要对子控件进行一个摆放肯定是要继承viewgroup,但是我不推荐直接继承 viewgroup,我建议大家没有特殊要求的话就去继承FrameLayout,这样就迷人实现了onMeasure和onLayout方法
以往我们如果要对面面板进行拖拽是不是要去onTounchEvent里面去实现?获取按下位置位置-->获取移动的位置-->获取移动的距离-->然后不停的去绘制?
用了安卓提供的这个工具,就很方便了,我个人认为最大的优势就是逻辑很简洁,每个方法去实现一个固定的功能!
1 首先我们要在自定义控件中对ViewDragHelper进行初始化
public class DrawerLayout extends FrameLayout { private ViewDragHelper mViewDragHelper; public DrawerLayout(Context context) { this(context,null); } public DrawerLayout(Context context, AttributeSet attrs) { this(context, attrs,0); } public DrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //被拖拽控件的父控件 敏感度,根据单位时间内拖拽的距离来确定是否滑动 回调 mViewDragHelper = ViewDragHelper.create(this, 1, callback); } ViewDragHelper.Callback callback = new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child, int pointerId) { return false; } }; }
2 重写几个方法,都加了注释方便看懂,大部分都是代码以及我的注释
package com.xu.heh.view; import android.content.Context; import android.support.v4.view.ViewCompat; import android.support.v4.widget.ViewDragHelper; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; /** * 创建者 * 创建时间 2017/3/13 23:20 * 描述 ${TODO} * <p> * 更新者 $Author$ * 更新时间 $Date$ * 更新描述 ${TODO} */ public class DrawerLayout extends FrameLayout { private ViewDragHelper mViewDragHelper; private int mMeasuredHeight;//空间高度 private int mMeasuredWidth;//控件宽度 private int mRange;//偏移限定 private View mChildLeft; private View mChildContext; public DrawerLayout(Context context) { this(context,null); } public DrawerLayout(Context context, AttributeSet attrs) { super(context, attrs,0); } public DrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mViewDragHelper = ViewDragHelper.create(this, 1, callback); } ViewDragHelper.Callback callback = new ViewDragHelper.Callback() { //重写事件回调 @Override//child 触摸到的子控件 //pointerId多点触摸手指id public boolean tryCaptureView(View child, int pointerId) { //尝试捕获控件,返回值决定了子空间是否可以被拖拽,不管触摸哪个子控件我都返回true让他都可以被拖拽 //所以可以根据child决定哪个可以被拖拽那个不可以,根据返回值决定 //在这里指定某个子childe才可以被拖动,返回ture,但是我们不这么写,因为我们要实现伴随动画,这么写就无法实现伴随动画 //return child==mChildContext; //这里的伴随动画指的是拖拽任意面板,另一个面板会随之移动,所以我们返回true; return true; } //如果想让子控件可以动起来,就要实现这个方法,前期的准备都是保证子view可以被拖动 //此方法是为了修正控件水平方向的位置 //注意这个方法运行时空间还未发生位移,只有这个方法运行完了控件才会发生位移 @Override public int clampViewPositionHorizontal(View child, int left, int dx) { //chile 被拖拽的子空间 //left 当前框架建议移动到的位置 //dx将要发生的水平偏移量 int leftold = child.getLeft(); Log.d("DrawerLayout", "leftold:" + leftold+" left: "+left+ " dx: "+dx); //这里要注意leftold=left+dx if(child==mChildContext){//只有当我们华东的是指定的子childe才会限定他的范围在mRange内 //我们限定左边距 if(left<0){ return 0; }else if(left>mRange){ return mRange; } } return left;//这里我们返回left,系统建议值,返回什么值就是距离左边的值 //记住了这个left是我们触摸时的经过一系列触摸事件的解析,判断每个move移动的偏移量传递给这个方法 //当然你要不要这个left值,就要看你了,如果要我么就返回出去,不要就进行修改, // 所以当我们要对偏移做限定其实就是限定这个left,比如我们限定left>200就不再移动 //各位明白我的意思么?就是限定left的范围 } //如果某个可拖动的子childe中正好有一个按钮或其他空间就会抢夺事件,导致无法滑动 @Override//这个方法设置视图水平方向的拖拽范围,不会影响真正的拖拽范围 public int getViewHorizontalDragRange(View child) { return mRange; //这个return 默认是返回0,我们设置一个值.首先要明白,当我们在按钮按下时到底是点击事件还是滑动事件有一部分是取决于这个值 //>0,当前控件才不受子控件(子控件的子控件)的触摸事件影响,也是用来计算动画执行时长,就是所谓的平滑 } //当控件位置发生变化时被调用 @Override//多用于伴随动画 状态更新 事件回调- public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { //changedView 位置发生变化的子控件 left最新水平方向位置 top最新竖直方向位置 dx 刚刚发生的水平偏移量 dy 刚刚发生的竖直偏移量 //固定主菜单移动左边面板 if(changedView==mChildLeft){ mChildLeft.layout(0,0,0+mMeasuredWidth,0+mMeasuredHeight); //把左菜单的水平偏移量传递给主面板 int newLeft=mChildContext.getLeft()+dx; if(left<0){ newLeft=0; }else if(left>mRange){ newLeft = mRange; } mChildContext.layout(newLeft,0,newLeft+mMeasuredWidth,0+mMeasuredHeight); //只要有控件被移动就执行伴随动画 dispatchViewEvent(); } } //当我们拉开面板,速度如果没有到达一个值,没有满足条件那么要么关闭要么打开 //当控件被释放时执行(松手) @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild, xvel, yvel); //这是松手时水平方向的速度 像素每秒 向右为正 if(xvel<=100&&mChildContext.getLeft()>mRange*0.5f){ //满足条件松手时就打开面板 open(true); }else if(xvel>100){ open(true); }else{ close(); } } /** STATE_IDLE 0空闲 * STATE_DRAGGING 1 拖拽中 * STATE_SETTLING 2 */ @Override//拖拽状态变化的时候调用 public void onViewDragStateChanged(int state) { //state 最新的状态 } }; //执行伴随动画,这里就可以设置子控件大小 位移 颜色等伴随动画 private void dispatchViewEvent() { float percent = mChildContext.getLeft()*1.0f/mRange;//0.0-->1.0时间轴概念 执行动画,动画是随着滑动的距离与最大滑动距离的比值 mChildLeft.setScaleX(0.5f+0.5f*percent);//希望是从0.5-1.0 mChildLeft.setScaleY(0.5f+0.5f*percent);//希望是从0.5-1.0 } private void close() { int finalLeft=0; mChildContext.layout(0,0,finalLeft+mMeasuredWidth,0+mMeasuredHeight); } private void open() { open(true); } //打开时是否平滑打开 private void open(boolean b) { int finalLeft=mRange; if(b){ if( mViewDragHelper.smoothSlideViewTo(mChildContext,finalLeft,0)){//触发平滑动画 //此时动画还没有移动到指定位置,需要引发界面重绘 ViewCompat.postInvalidateOnAnimation(this);//可以用invalidate(),但建议用这个方法可以兼容低版本,让整个ViewGroup根据动画重绘界面 // 这里会引发drawChild-->draw-->computeScroll } }else{ mChildContext.layout(finalLeft,0,finalLeft+mMeasuredWidth,0+mMeasuredHeight); } } //维持动画继续,其实就是一直在计算,比如我打算如何重绘,但是我仅仅是打算还没有行动,而这里就是帮助你计算出真正重回的参数(可以这么理解) @Override public void computeScroll() { super.computeScroll(); //每次重绘界面,都会判断当前位置是否到达指定位置,如果没有就继续计算,直到到达了指定位置(有点像递归吧?) if(mViewDragHelper.continueSettling(true)){ ViewCompat.postInvalidateOnAnimation(this);//很像递归有木有,进行一个死循环,直到满了条件continueSettling这个方法就会返回false } } //xml初始化完毕会调用 @Override protected void onFinishInflate() { super.onFinishInflate(); //在这里获取子view,这样只会调用一次,如果放到其他地方就会降低代码的效率不停的重复获取子child mChildLeft = getChildAt(0); mChildContext = getChildAt(1); } //关联触摸事件,触摸事件种弗雷对子类进行事件拦截的方法,这里我们交给mViewDragHelper即可 //mViewDragHelper会判断是否拦截事件,比如我们左右滑动就会进行拦截,上下滑动就不进行拦截继续向下分发 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mViewDragHelper.shouldInterceptTouchEvent(ev); } //消费事件,一般是事件分发给子控件,子控件不消费这里才会消费,这里返回true就会消费事件,返回false //只接收down事件,其他事件继续向上传递,以后我会介绍事件的传递过程 @Override public boolean onTouchEvent(MotionEvent event) { //如果不用工具,在这里我们要自己去计算,有了工具就可以很方便的进行 //交给ViewDragHelper处理触摸事件 mViewDragHelper.processTouchEvent(event); return true;//消费事件 } //经过onMesure之后会调用的方法 //可以用来发现空间的宽高变化了,此方法被执行 @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mMeasuredHeight = getMeasuredHeight(); mMeasuredWidth = getMeasuredWidth(); mRange = (int)(mMeasuredWidth * 0.6f); //这里既然获取道德宽高,而我们要做的就是获取宽度的40%,宽度的40%来限定left的偏移宽度 } }
做起来虽然很多api不常用,但是多用用就好了,用的时候万一忘记了,可以回头来看我的代码,都有注释方便易懂,每个方法是做什么的,用在哪里都有清晰的注释