自定义View
自定义view
定义
View是所有控件的基类:Button、Textview View是一种界面层空间的抽象,VIew与ViewGroup代表了控件和控件组 这意味着View本身可以是单个控件也可以是由多个控件组成的一个控件组,这种结构就形成了一种View树的结构 类似于前端的DOM树
View的层级结构
Object-View-TextView-TestButton
- View的位置参数
VIew的位置主要是由四个顶点的位置来决定的:top(左上角纵坐标)、left(左上角横坐标)、right(右下角横坐标)、bottom(右下角纵坐标)(这都是一种相对坐标)
VIew的宽高和坐标关系:
- width=right-left height=bottom-top
利用Left=getLeft(); Right=getRight(); Top=getTop; Bottom=getBottom().
- 从Android3.0开始,View新增了X,Y,translationXhetranslationY
XY是左上角的图标 translationX和TranslationY是对于父容器的偏移量,默认值为0
View也提供了set和get方法给这两个参数
换算关系如下
X=left+translationX
y=top+translationY
View在平移的过程中 top和left是表示原始左上角的位置信息,值并不会发生改变
MotionEvent和TouchSlop
- MotionEvent
在手指接触屏幕所产生的一系列事件中,典型的事件类型有如下几种
ACTION_DOWN:手指刚接触屏幕
ACTION_MOVE:手指在屏幕上移动;
ACTION_UP:手机从屏幕上松开的一瞬间
一般会有这两种情况:
- 手指点击屏幕后再松开:DOWN->UP
- 手指点击屏幕滑动一会后松开:DOWN->MOVE->….->MOVE->UP
通过MotionEvent对象来获得点击事件发生的X和Y坐标。
getX/getY/getRawX/getRawY
getX和getY是返回当前View左上角的X和Y坐标。而getRawX和getRawY是返回手机屏幕最上角的X和Y坐标
- TouchSlop
定义:系统所能识别出的被认为是滑动的最小距离
当滑动的距离小于这个值的时候,系统就不任务进行了一次滑动。所以说TouchSlop是一个常量
我们通过ViewConfiguration.get(getComtext()).getScaledTouchSlop()来获得
VelocityTracker、GestureDestector和Scroller
- VelocityTracker:速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度
方法:VIew的onTouchEvent方法VelocityTracker.obtain();来追踪事件的速度
此时我们需要去添加事件从而进行追踪当前事件的速度:velocityTracker.addMovement(event);//添加这个事件
我们再先计算速度,再去获取速度(都是这个流程)
计算速度:velocityTracker.computeCurrentVelocity(1000)//在1000ms中经过的像素数 从左往右为正,从右往左为负
然后再去get
最后当我们不需要VelocityTracker这个对象的时候去:
velocityTracker.clear();
velocityTracker.recycle();
- GestureDetector:手势检测,辅助检测用户的单机、滑动、长按、双击
首先创建一个GestureDetector对象,这个对象实现接口OnGetureListener,还可以去实现OnDoubleTapListener监听双击行为
1 | GestureDetector mgestureDetector =new GestureDetector(this) |
在待监听的View的onTouchEvent方法中添加
1 | boolean consume=mGestureDetector.onTouchEvent(event); |
至此,就可以去实现监听和接口的方法了
- Scroller:弹性滑动对象,实现View的弹性滑动
具体3.2中讲解
View的滑动(第一种View的滑动效果实现)
- scrollTo/scrollBy:View专门实现滑动的功能所提供的方法。
scrollTo是一种绝对滑动 scrollBy是一种在To的基础上的相对滑动
如果从左向右滑动,那么mScrollX为负值,反之为正值;如果从上往下滑动,那么mScrollY为负值,反之为正值。
动画的使用(第二种实现View的滑动效果)
- 定义:使用动画来移动View,而不是平移(滑动)来移动View
- 分类
- 传统动画:
- 属性动画:采用开源库
- 注意:VIew的动画只是对View的一个影像做的操作,没有真正的改变View的位置参数,宽与高
- 具体在第五章《Android开发艺术探索》讲解
改变布局参数(第三中实现View的滑动效果)
通过改变布局参数从而来实现VIew的滑动
两种办法:
- 设置一个空的View来“挤”需要滑动的组件(1)
- 重新设置滑动组件的marginLeft参数(2)
(2)来实现
1 | MarginLayoutParams params=(MarginLayoutParams)mButton1.getLayoutParams(); |
滑动方式的对比
- scrollTo/scrollBy:View的原生方法,专门用于View的滑动,方便,不影响点击事件,但是只能滑动View本身不能滑动VIew本身
- 动画:采用属性动画的办法是一种比较好的办法,几乎没有缺点。复杂效果必须要通过动画来实现,如果无交互需求的话可以采用动画,操作简单
- 改变布局:使用麻烦,主要适用有交互性的View
弹性滑动
- 思想:将一次大的滑动分成若干次小的滑动并在一段时间内完成,
方法一:使用Scroller
当我们new了一个新的Scroller对象时,我们调用startScrooller方法时,我们只是保存了几个我们传递的参数
,这表示我们并不是利用Scroller的start来实现一种滑动的。我们调用invalidate方法,证明:invalidate方法导致view重绘,在View的draw方法中又回去调用computeScroll方法,computeScroll方法在View中是一个空实现
所以需要自己去实现,应为这个方法才会去实现弹性的滑动
在computerScroll方法中,返回值是一个布尔,如果返回的是对的,证明这个View滑动还没有结束。反之结束
方法二:通过动画
使用ObjectAnimator.ofFloat(yargetView,“translationx”,0,100)
(100).start;
可以让一个View内容在100ms内向左移动100像素
方法三:通过延迟策略
采用Handler或者View的机制
View的事件分发机制
- 点击事件的传递规则
MotionEvent
点击事件的传递过程是由三个组成dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent
dispatchTouchEvent(MotionEvent ev)表示传递给当前View的onTouchEvent和下级的dispatchTouchEvent方法的影响 布尔类型
表示是否消耗当前事件
onInterceptTouchEvent(MotionEvent event)在方法中内部调用,用于判断是否拦截某个事件,如果View拦截了某个事件,那么在同一个事件序列当中,此方法不再去调用,返回结果表示是否拦截当前事件 布尔类型
onTouchEvent方法在DispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件 布尔类型 如果不消耗,则在同一个事件序列中,当前View无法在接收到事件
“对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。”
摘录来自: 任玉刚. “Android开发艺术探索。” Apple Books.
上述我们说明了如何去拦截,下面我们来说说怎么处理
当一个View设置了OnTouchListener
在OnTouchListener中的onTouch方法被回调,然后这个方法的返回值是false则调用,如果是true则不会调用
优先级排列:OnTouchListener>OnTouchEvent>OnClickListener
- 点击事件的传递顺序:Activity->Window->View
当View接受到事件后,就会按照事件分发机制去处理
但如果OnTouchEvent返回的结果是false则会往上传递(父类的OnTouchEvent)最后如果解决不了上传至Activity的OnTouchEvent
- 整个传递机制
- 以Down开始,中间无数的Move,以Up结束
- 一个事件序列只能被一个View拦截且消耗
- 某个View一旦决定拦截,那么这个事件序列都只能由他来处理
- 如果某个View一旦开始处理事件,如果没有消耗ACTION_DOWN事件,就同一时间序列的其他事件都不会再交于他来处理
事件分发的源码解析
操作流程
当点击事件MotionEvent发生操作的时候,
我们将事件传递给当前的Activity,
再由Activity的DispatchTouchEvent来进行事件派发。
Activity的内部Window来完成,会将事件传递给Decorview
decorview一般是当前的底层容器(setContentView的参数)
通过1
Activity.getWindow.getDecorView()
获得
源码解析
Activity的dispatchTouchEvent
1
2
3
4
5
6
7
8
9public boolean dispatchTouchEvent(MotionEvent ev){
if(ev.getAction()==MotionEvent.ACTION_DOWN){
onUserInteraction();
}
if(getWindow().superDispatchTouchEvent(ev)){
return true;
}
return onTouchEvent(ev);
}返回true证明有人处理,那就循环结束
如果返回false,证明没人处理,就调用Activity的onTouchEventWindow->ViewGroup
首先明确Window是一个抽象类,Window的superDispatchTouchEvent(MotionEvent event)也是一个抽象的方法
所以先要找到实现类
Window的实现类是PhoneWindow
所以我们查看PhoneWindow的superDisptachTouchEvent方法1
2
3public boolean superDispatchTouchEvent(MotionEvent event){
return mdecor.superDispatchTouchEvent(event);
}PhoneWindow直接将事件传递给了DecorView
1
2
3
4
5
6
7
8
9private final class DecorView extends FrameLayout implements RootViewSur-faceTaker
private DecorView mdecor;
public final View getDecorView(){
if(mDecor==null){
installDecor();
}
return mDecor;
}在此时已经传递到了顶级的View
也就是ViewGroup,根View,通过传递给顶级的View来进行一个处理(就是setContentView所设置的View)顶级View对事件的分发机制
在View中的时间处理如下:
如果顶级View的拦截事件onInterceptTouchEvent返回true,则事件由ViewGroup来处理,如果ViewGroup的mOntouchListener被设置,onTouch会调用,否则是onTouchEvent被调用,如果都提供的话,onTouch会屏幕onTouchEvent,在onTouchEvent中,如果设置了mOnClickListener,则onClick会被调用,如果ViewGroup不拦截事件,则会传递到子View上
子View的dispatchTouchEvent会被调用。接下来都一样