很早就入手了《Android开发艺术探索》这本书,但是一直尘封着,没有看过,最近不是很忙,抽出时间,细细研究一番,感觉是对自己开发知识的梳理,查缺不漏。打算根据书中内容,另外加入自己的一些梳理,整理这一篇文章,对自己是个总结,对他人希望能提供一点点帮助。
这篇文章整体根据以下目录展开叙述:
- View的位置参数;
- MotionEvent ,TouchSlop ,VelocityTracker,GestureDetector;
- View的滑动;
- View的弹性滑动;
- 事件分发机制;
- 滑动冲突解决
- View的工作原理
- 自定义View
一:View的位置参数
1:getLeft(),getTop(),getRight(),getBottom()
View相对于父布局原始的位置参数;View发生移动,这几个参数不发生变化,仅仅表示View相对父布局原始的位置参数;
2:getX(),getY()
View左上角相对父布局水平方向和竖直方向的位置参数,如果View发生位置移动,这两个参数发生变化;
3:getTranslationX(),getTranslationY()
View左上角相对父布局的位置偏移量;如果View发生位置移动,这两个参数发生变化;
4: 三者之间的关系如下:
width= right- left
height = bottom - top
x = left + translationX
y = top + translationY
5:获取屏幕尺寸
//Activity中获取屏幕尺寸
DisplayMetrics mDisPlay = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(mDisPlay);
int screenHeight = mDisPlay.heightPixels;
int screenWidth=mDisPlay.widthPixels;
Log.d(TAG, "onWindowFocusChanged: 屏幕尺寸:宽"+screenWidth+"高:"+screenHeight);
//非Activity(View)中获取屏幕尺寸
width=getResources().getDisplayMetrics().widthPixels;
6:获取应用程序App显示区域(屏幕尺寸高度-状态栏高度)
//可编辑区域宽高
Rect mRect=new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(mRect);
7:获取布局部分的尺寸(App显示区域高度基础上-标题栏高度)
//View布局区域宽高等尺寸获取
Rect rect = new Rect();
getWindow().findViewById(Window.ID_ANDROID_CONTENT).getDrawingRect(rect);
备注:
1:5~7是获取手机相关区域尺寸的方法,这几个方法在onCreate(),onResume()中都无法正常获取到数据,建议在onWindowFocusChanged ()中调用。原因参考文章浅谈View的绘制流程
2:获取屏幕位置信息参考Android应用坐标系统全面详解
二:MotionEvent ,TouchSlop ,VelocityTracker,GestureDetector;
1:MotionEvent包含了用户按下,滑动,抬起,等一系列动作信息,即在手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种:
- ACTION_DOWN一手指刚接触屏幕
- ACTION_MOVE一手指在屏幕上移动
- ACTION_UP —手机从屏幕上松开的一瞬间
备注:一个MotionEvent中往往包含一个action_down和一个action_up,两者中间包含无数个action_move;
另外补充一下,获取用户触摸点水平和竖直方向位置坐标的方法。区别查看代码注释:
@Override
public boolean onTouchEvent(MotionEvent event) {
//表示动作执行点相对手机屏幕的绝对位置
Float screanX= event.getRawX();
Float screanY= event.getRawY();
//表示动作执行点相对控件自身的相对位置信息
Float X= event.getX();
Float Y= event.getY();
return super.onTouchEvent(event);
}
2: TouchSlop 表示系统所能识别的最小移动距离,用于判断用户滑动距离是否有效;
touchSlop= ViewConfiguration.get(mContext).getScaledTouchSlop();
3:VelocityTracker用于测算用户水平或者竖直方向的滑动速度;
@Override
public boolean onTouchEvent(MotionEvent event) {
/**
* 作用:根据水平和竖直方向的滑动速度对比大小得知滑动方向
*/
//初始化
VelocityTracker mVelocityTracker=VelocityTracker.obtain();
//将事件添加进去(可以理解为告诉mVelocityTracker移动距离)
mVelocityTracker.addMovement(event);
//设定测速的单位时间
mVelocityTracker.computeCurrentVelocity(1000);
//获取水平方向的移动速度
float velocityX = mVelocityTracker.getXVelocity();
//获取竖直方向的移动速度
float velocityY= mVelocityTracker.getYVelocity();
》》》》//不用之后务必进行回收,释放资源 《《《《
mVelocityTracker.clear();
mVelocityTracker.recycle();
return super.onTouchEvent(event);
}
4:GestureDetector 手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。具体流程如下:
- 创建一个GestureDetector对象并实现OnGestureListener接口,根据需要我们还可以实现OnDoubleTapListener从而能够监听双击行为。
//创建对象并实现onGestureListener监听
GestureDetector mGestureDetector = new GestureDetector(new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
//手指触摸屏幕瞬间,由一个ACTION_down触发
//这个地方默认返回false;
//手动改成返回true,否则其他监听无法触发(我测试中是这样)
return true;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
//单击事件监听
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//滑动监听
return false;
}
@Override
public void onLongPress(MotionEvent e) {
//长按监听
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//飞快滑动监听
return false;
}
});
//解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);
- 接管onTouchEvent事件:
@Override
public boolean onTouchEvent(MotionEvent event) {
int mAction = event.getAction();
if(mAction==MotionEvent.ACTION_UP){
startScroll(0,-mScroller.getFinalY());
return super.onTouchEvent(event);
}else{
//接管事件
return mGestureDetecter.onTouchEvent(event);
}
}
-
根据具体业务需要实现相应监听
a.jpg
三 View的滑动方法
1:View.scrollTo()/View.scrollBy()
- View.scrollTo();
基于所传递参数的绝对滑动,即相对View的原始位置进行移动。假如用户重复调用该方法,只会重复移动,不会在已经移动的距离基础上,再移动相应的距离; - View.scrollBy();
它实现了基于当前位置的相对滑动,也就是,用户重复调用该方法,会再已经移动的距离基础上,继续移动相应距离。
备注:scrollBy和scrollTo都是只能移动View的内容(对于textView 文字就是其内容,对于ViewGroup子View就是其内容),不能移动View的位置信息。
另外要重点理解View内部的两个属性mScrollX和mScrollY的改变规则,这两个属性可以通过getScrollX和getScrollY方法分别得到。可以这样理解:mSrollX表示滑动过程中View左侧边沿距离View内容左边沿的水平距离,mSrollY表示滑动过程中View上边沿距离View内容上边沿的竖直距离,两种随着左右上下滑动,缩小变大。并且当View左边缘在Veiw内容左边缘的右边时,mScrolX为正值,反之为负值;当View上边缘在View内容上边缘的下边时,mScrollY为正值,反之为负值。换句话说,如果从左向右滑动,那么mScrollX负值,反之为正值:如果从上往下滑动,那么mScrollY为负值,反之为正值。
b.jpg
2: 使用动画实现View的滑动
这个不是这篇文章重点考虑的,所以略过,想看的同学请参考:
补间动画写的比较全面
知乎上关于android动画的问题,里面有很多源码解读;
关于android属性动画的介绍,写的非常基础全面;
android关于属性动画的优化处理部分
3:改变View的位置参数实现位置移动
这个比较好理解了,比如我们想把一个Button向右平移100px,我们只需要将这个Bution的LayoutParams里的marginLeft参数的值增加100px即可,是不是很简单呢?还有一种情形,view的默认宽度为0,当我们需要向右移动Button时,只需要重新设置空View的宽度即可,就自动被挤向右边,即实现了向右平移的效果。如何重新设置一个View 的LayoutParams呢?很简单,如下所示:
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) testButton.getLayoutParams();
layoutParams.width +=100;
layoutParams.leftMargin +=100;
testButton.requestLayout();
//或者testButton.setLayoutParams(layoutParams);
总结
- 移动之后需要接受类似点击事件以及其他位置信息的场景:建议使用属性动画或者改变View的位置参数两种方法实现位置移动。
- 移动之后不需要获取View的位置信息变化或者点击事件的场景:三种方法都可以使用
四: View的弹性滑动
上面提到的View的滑动三种方法除了动画外,另外两种方法滑动都是瞬间完成的,没有过渡效果,用户体验相当不友好。所以这里介绍几种能实现弹性滑动的方法:
1:借助动画(忽略,不详述)
2:使用延时策略(这个方法,我大致看了看,比较麻烦,不想展开详述,想详细查看的请自行百度<使用延时策略实现View的弹性滑动>或者查看原著)
3: 借助工具类Scroller实现弹性滑动
这个方法是我想重点展开详述的,借助Scroller实现弹性滑动的代码如下:
public MyHeadScroll(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
/**
* 1 初始化mRcroller
*/
mScroller=new Scroller(context);
}
private void startScroll(int x,int y){
Log.d(TAG, "startScroll: "+y);
/**
* 2 设置滑动距离数据
*/
mScroller.startScroll(mScroller.getFinalX(),mScroller.getFinalY(),x,y);
/**
* 3 重绘,会调用触发computeScroll()
*/
invalidate();
}
@Override
//这个方法是个空实现,需要重写,自己写滑动逻辑
public void computeScroll() {
//判断滑动是否完整,没成就继续滑动,继续重绘,继续调用computeScroll,类似递归,知道移动完整
if(mScroller.computeScrollOffset()){
/**
*4 真正滑动的地方,nnd原来也是借助scrollTo或者scrollBy
*/
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
super.computeScroll();
}
上面代码可以说是模板,借助scroller实现弹性滑动基本上都这样写(不懂的地方看注释),大致流程如下:
1:初始化mScroller,并设置滑动数据,重绘;
2:手动实现computScroll()方法,自己实现滑动逻辑;
3:切记,两个重绘的地方,缺一不可;
补充
1:startScroll(int startX, int startY, int dx, int dy, int duration):
指定起点(startX,startY),从起点平滑变化(dx,dy),耗时duration,通常用于:知道起点与需要改变的距离的平滑滚动等。
2:fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY):
惯性滑动。 给定一个初始速度(velocityX,velocityY),该方法内部会根据这个速度去计算需要滑动的距离以及需要耗费的时间。通常用于:界面的惯性滑动等。
关于View的一些基本知识,包括滑动的实现,基本上讲完了。另外关于scrollTo/scrollBy和Scroller的源码解析和实现原理,我就不展开详述了,推荐参考:
站在源码的肩膀上全解Scroller工作机制
看到这里,相信大家对View基础知识基本掌握了,下面要讲的几个模块,是View的重点部分,也是进阶自定义View所必须要掌握的,喝口茶,咱们继续聊。
五 事件分发机制
1:概述
所谓的事件分发其实就是对MotionEvent事件的分发过程。即当一个MotionEvent事件产生之后,系统需要将这个事件传递给一个具体的View进行处理相应事件,这个传递的过程就是事件分发的过程。
2:涉及到的主要方法
事件分发的过程是由三个很重要的方法来完成的,这三个方法分别是:
- public boolean dispatchTouchEvent(MotionEvent ev)
是事件分发的开始,返回值代表是否将事件继续分发下去;
用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,
返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
- public boolean onInterceptTouchEvent(MotionEvent ev)
这个方法是ViewGroup所独有的方法,表示父控件是否拦截事件,返回值也代表当前父控件是否对事件进行拦截。
如果拦截,就交由该父控件的onTouchEvent()方法消耗处理事件;
如果不拦截,就交由子布局的dispatchTouchEvent(),继续传递事件;
在上述dispatchTouchEvent(MotionEvent ev)方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,
那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件
- public boolean onTouchEvent(MotionEvent event)
表示是否消耗处理事件。
如果不处理,事件就交由上一级父控件的onTouchEvent进行处理;
如果处理,那么MotionEvent中一系列的其他事件都交由当前控件进行处理;
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当
前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
三个方法之间的关系可以用一段伪代码来表示:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
3:事件传递流程(见图)
事件分发机制.pngc.png
整个一个事件分发的流程,我直接扔上两张图,感觉这样不光是对读者,更是对自己的不负责,下面我就根据最近看的书,理解到的东西,纯白话试着写写这个流程:
1:研究对象 dispatchTouchEvent()
用户点击屏幕,MotionEvent.Action_Down事件产生,开始所谓的事件传递。最开始触发的是Activity的dispatchTouchEvent()方法。如果手动重写dispatchTouchEvent():
1.1 修改 返回false/true,无法调用下面方法, 事件就在这里消耗,不往下传递
1.2 返回super ,正常调用下面 方法,事件开始传递;
查看源码得知,返回super,调用源码中以下方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//做屏保功能的时候可能用到
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
继续: 该方法中会调用getWindow().superDispatchTouchEvent(ev)。根据发现:getWindow()获取的window对象,查阅源码得知,PhoneWindow是window唯一实现类,所以直接查看PhoneWindow类中的superDispatchTouchEvent(ev)方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
继续 mDecor是DecorView的对象,代表可编辑布局区域的最外层布局,实际是FrameLayout,也就是一个ViewGroup,代码如下:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
继续 跟进去查看,就直接进入ViewGroup的dispatchTouchEvent()方法了,这里源码不贴了(本来想白话说这个过程的,结果为了说清楚过程还是贴出部分源码,请原谅我的无能)。到这里,事件已经由activity传递到了ViewGroup.dispatchTouchEvent。
2:研究对象ViewGroup.dispatchTouchEvent
2.1:开发者不重写该方法,类似下面2.2.3:
2.2:开发者重写该方法,手动改动返回结果,会出现以下几种可能:
2.2.1: 如果修改这个方法返回false,等同于贴出的第一段源码中的,getWindow().superDispatchTouchEvent(ev)返回false,那这样直接调用activity.onTouchEvent(ev),方法消耗事件;
2.2.2: 如果修改这个方法返回true,就不用调用activity.onTouchEvent(ev)方法,事件直接在这里消耗了;
2.2.3: 如果不修改直接返回super,其实是走进源码中,下面的伪代码可以说明源码这一块的逻辑:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//会询问是否拦截当前事件,如果拦截
if(onInterceptTouchEvent(ev)){
//r如果拦截,就调用自己的onTouchEvent();
return onTouchEvent(ev);
}else{
//如果不拦截
/**
* 1:对子View进行遍历,查找能够处理事件的View
* 2;调用该子View的dispatchTouchEvent方法;
* 由此,事件传递到了View的dispatchTouchEvent
* 如果View.dispatchTouchEvent()返回true,事件直接消耗
* 如果返回false,直接调用父控件的onTouchEvent()方法消耗处理事件
*/
if(View.dispatchTouchEvent()){
return true;
}else{
return onTouchEvent(ev);
}
}
}
由此,事件的传递已经从ViewGroup传递到了View.dispatchTouchEvent();
3:研究对象 View.dispatchTouchEvent方法;
3.1:如果开发者不重写该方法,类似下面3.2.3;
3.2:如果开发者手动重写该方法:
3.2.1:修改返回值为true,事件到此传递结束,直接消耗;
3.2.2:修改返回值为false;查看上面伪代码得知,事件直接交付给父控件的onTouchEvent()进行消耗;
3.2.3:不修改,直接返回super;还是得进源码看看去:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
源码很多,我删除了一些无关紧要,留下关于这块逻辑的部分:
如果子View有设置过onTouchListener监听,并且监听中onTouch(event)返回true,就不会调用onTouchEvent(ev)方法;由此可以得出一条结论:回调顺序是先是:onTouch,再是onTouchEvent();也就是setOnTouchListener()方法优先级高于onTouchEvent()方法,这样设计的目的是有利于外界监听用户点击滑动事件;
延伸一点对于优先级来说:
onTouchListener()<<onTouch()<<onTouchEvent()<<performClick()<<onClickListener
4:研究对象onTouchEvent();
关于onTouchEvent()这个方法就不贴代码了
4.1 每个View的onTouchEvent()方法都是默认返回true的,即默认消耗事件的,除非View的clickble()和longClickble()同时为false(enable属性不影响View是否消耗事件)。对于不同的View的longClickble()默认都是false的,对于clickble()属性要区别不同的View来说,对于可以点击的View类似Bottom的clickble()默认为true;不可点击的View类似TextView的clickble()属性默认为false。(通常我们设置onClickListener和onLongClickListener都是默认开启对应属性的);
4.2 onTouchEvent()方法中在Action_Up中会调用performClick(),performClick()中会调用日常开发中用的最多的setOnClickListener()。简单来说:自己重写onTouchEvent方法之后,注意对performClick()的处理,否则点击事件监听无效;
事件分发大致流程基本上分析完了,建议读者自己对比流程图,对比上述过程,最后再加上源码,自己好好理解。感觉上述过程写的不是很清楚,虽然整个事件分发机制,自己大致流程弄清楚了,但是想写出来。写清楚,还是不容易的。
小结
1:某个控件一旦处理了MotionEvent中的Action_down事件之后,那么后续的事件(Action_move/Action_up)正常来说,都是交由该控件处理,如果该控件是ViewGroup的话,onInterceptTouchEvent()只会调用一次,不会重复调用(一旦处理事件之后,后续不会再调用)因此对于ViewGroup,不建议在onInterceptTouchEvent()方法中处理过多逻辑(特别是处理Action_move/Action_up事件),因为不是每次都调用;
2:事件传递过程是由外到内的。简单理解就是:事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterptTouchEvent方法可以再子元素中干预元素的事件分发过程,但是ACTION_DOWN除外;
六 滑动冲突解决
1:常见滑动冲突
- 滑动方向不一致造成的冲突(ViewPager中嵌套ListView)
- 滑动方向一致造成的冲突(竖直方向的scrollView嵌套多个竖直方向的ListView)
- 上述两种的综合
2:处理原则
- 对于滑动方向不一致造成的冲突,通常解决办法是通过代码判断用户滑动方向(通过水平和竖直方向的滑动距离大小比较判断滑动方向),如果是左右滑动,就交给ViewPager,如果是上下滑动就交给ListView;
- 对于滑动方向一致造成的冲突,这个场景只能根据业务逻辑进行活动冲突的解决。
3:解决办法
3.1:外部拦截
结合上一节的事件分发机制可知,通过重写父控件的onInterceptTouchEvent()方法,可以拦截事件继续传递。也就是:需要外层控件滑动的时候,进行拦截,需要内层控件滑动的时候,不进行拦截。标准的伪代码(看注释)如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
//这里一定要返回false,如果返回true,事件就不会继续
//传递,onInterceptTouchEvent也不会重复调用
//父容器必须返回false,即不拦截ACTION_DOWN事件,
//这是因为一旦父容器拦截了ACTION_DOWN,
//那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if("父容器的点击/滑动事件"){
intercepted = true;
}else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
//最后是ACTION_UP事件,这里必须要返回false,
//因为ACTION_UP事件本身没有太多意义考虑一种情况,假设事件交由子元素处理,如果父容器在ACTION_UP时返回了true,
//会导致子元素无法接收到ACTION_UP事件,这个时候子元素中的onClick事件就无法触发,
intercepted = false;
break;
}
mLastXIntercept = x;
mLastYIntercept = x;
return intercepted;
}
3.2: 内部拦截
父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素要消耗此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截法稍显复杂。伪代码如下,我们需要重写子元素的dispatchTouchEvent方法:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = x - mLastY;
if("父容器的点击/滑动事件"){
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
除了子元素需要处理之外,父元素默认也要拦截除ACTION_DOWN之外的其他事件,这样当子元素调用getParent().requestDisallowInterceptTouchEvent(true)方法时,父元素才能继续拦截所需要的事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if(action == MotionEvent.ACTION_DOWN){
return false;
}else {
return true;
}
}
小结
两种方法对比,都能解决问题,但是第一种方法简单明了,所以。。。你懂的。。。
七 View的工作原理
1:初识ViewRoot和DecorView
- ViewRoot对应于ViewRootImpl类,他是连接WindowManager和DecorView的纽带,View的三大流程都是通过ViewRoot来完成的。
- 顶级View DecorView,一般情况下他内部会包含一个竖直方向的LinearLayout,这里面有上下两部分,上面是标题栏,下面是内容,在Activity中,我们可用通过setContentView设置布局文件就是放在内容里,而内容栏的id为content,因此我們可以理解为实际上是在setView,那如何得到content呢?你可以ViewGroup content = findviewbyid(android.R.id.content),如何得到我们设置的View呢:content.getChildAt(0),同时,通过源码我们可用知道,DeaorView其实就是一个FrameLayout,View层事件都先经过DecorView,然后传递给View。
- 在ActivityThread中,当Activity被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立联系。从而完成DecorView的三大绘制流程。
- View的绘制流程从ViewRoot的perfromTraversals方法开始,他经过measure,layout和draw三个过程才能将View画出来,,其中measure测量,layout确定view在容器的位置,draw开始绘制在屏幕上,perfromTraversals的大致流程,可以看图
1539225875(1).png
2:理解MeasureSpec
-
MeasureSpec代表一个32位int值,高两位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某个测量模式下的规格大小。
-
SpecMode有三种分别是:
EXACTLY:父容器已经检测出View所需要的精度大小,这个时候View的最终大小就是SpecSize所指定的值,它对应于LayoutParams中的match_parent,和具体的数值这两种模式;
AT_MOST:父容器指定了一个可用大小,即SpecSize,view的大小不能大于这个值,具体是什么值要看不同view的具体实现,它对应于LayoutParams中wrap_content;
UNSPECIFIED:父容器不对View有任何的限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态 -
子View的尺寸宽高大小是由自己的MeasureSpec直接决定的,但是子View的MeasureSpec形成过程是子View的LayoutParams和父View的限制共同决定的.也就是说:在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec。
3:View的工作流程
View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制,其中measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点的位置,而draww则将View绘制到屏幕上
3.1:Measure过程
3.1.1:View的Measure
- View 的 measure过程由其measure方法来完成,measure方法是一个final类型的方法,这就意味着子类不能重写此方法,在View的measure方法中去调用View的onMesure方法,因此只需要看onMeasure的实现即可,View的onMesure方法如下所示:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
- setMeasuredDimension会设置View宽/高的测量值,因此我们只需要看getDefaultSize方法(看注释)即可。
*
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//这里两种模式并成一种模式进行处理,可以联想到,如果自定View,
//使用wrap_content属性,即case MeasureSpec.AT_MOST,
//会按照case MeasureSpec.EXACTLY,进行处理,这往往不是我们想要的
//所以自定义View直接继承View,针对wrap_content,我们需要自己进行处理,这个文章最后补充中会提到;
//突然想问一句:既然如此源码为啥要这样写呢???????
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
- 看AT_MOST和EXACTLY这两种情况,简单的理解,其实getDefaultSize返回的大小就是mesourSpec中的specSize,而这个specSize就是view的大小,这里多次提到测量后的大小,是因为View最终的大小,是在layout阶段的,所以这里必须要加以区分,但是几乎所有情况下的View的测量大小和最终大小是相等的。 至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize的第一个参数是size,即宽高分别为getSuggestedMinimumWidth和getSuggestedMinimumHeight()这两个方法的返回值:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
- 简单来说就是:看控件是否设置背景属性,如果设置了背景属性,比较控件宽高最小值mMinWidth(没有设置的话,默认为0)和背景属性(Drawable中图片有宽高,shapeDrawable无宽高,这里不详细展开,请自行百度)宽高谁大就返回谁;如果没有设置背景属性,就直接返回宽高的最小值mMinWidth;
总结
- onMeasure中View测量自己的高度;
- 方法调用流程:measure()>>>onMeasure()>>>setMeasureDimension()>>>getDefaultSizi()>>>getSuggestedMinimumWidth();
3.1.2:ViewGroup的Measure
对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素再重复去执行3.1.1过程。和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但是它提供了一个叫measureChildren:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
//遍历对每个子View进行测量
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
//这个方法在自定义ViewGroup中使用到
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
//在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的
//MeasureSpec。
//这两个过程就是获取子View的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//拿到子View的MeasureSpec,调用子View的测量方法。之后就重复3.1.1了
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现。那是因为不同的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同.这里就不举例子详细说明了,总结以下大致流程:
- ViewGroup遍历所有的子View,获取各子View的MeasureSpec,调用子View的测量Measure()方法进行测量;
- 根据自个ViewGroup各自的特性综合各自子View的测量结果,测量自身的宽高尺寸;
3.2:layout过程
Layout的作用是ViewGroup用来确定自己的位置,当ViewGroup的位置被确认之后,他的layout就会去遍历所有子元素并且调用onLayout方法,在layout方法中onLayou又被调用,layout的过程和measure过程相比就要简单很多了,layout方法确定了View本身的位置,而onLayout方法则会确定所有子元素的位置,先看View的layout方法
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
layout的方法的大致流程如下
首先会通过一个setFrame方法来设定父View的四个顶点的位置,即初始化mLeft,mTop,mRight,mBottom这四个值,View的四个顶点一旦确定,那么父View在其父容器的位置也就确定了,接下来会调用onLayout方法,这个方法的用途是父View确定其子元素的位置。
3.3:draw过程
3.3.1:draw是比較简单的,他的作用是将View绘制到屏幕上面,View的绘制过程由如下几个步骤
- 绘制背景
- 绘制自己
- 绘制children
- 绘制装饰
3.3.2:setwilINotDraw()
作用:决定onDraw()方法是否会执行。
无论对于View还是ViewGroup而言,对于自定义而言,需要调用onDraw()方法进行绘制东西的话,就要在构造方法中明确设置setwilINotDraw(false);不需要调用onDraw()方法的话,也要在构造方法中明确设置setwilINotDraw(true),系统会进行相应优化。
小结
到这里View的工作原理三大流程measure,layout,draw.基本上分析完了,下面补充一点东西,日常开发中,获取控件宽高的方法。
4:获取控件宽高的方法
4.1: Activity/View#onWindowFocusChanged
View已经初始化完毕了,宽/高已经准备好了,这个时候去获取宽/高是没问题的。需要注意的是,onWindowFocusChanged会被调用多次,当Activity的窗口得到焦点和失去焦点时均会被调用一次。具体来说,当Activity继续执行和暂停执行时,onWindowFocusChanged均会被调用,如果频繁地进行onResume和onPause,那么onWindowFocusChanged也会被频繁地调用,典型代码如下:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
//获取屏幕尺寸
DisplayMetrics mDisPlay = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(mDisPlay);
int screenHeight = mDisPlay.heightPixels;
int screenWidth=mDisPlay.widthPixels;
Log.d(TAG, "onWindowFocusChanged: 屏幕尺寸:宽"+screenWidth+"高:"+screenHeight);
}
4.2: view.post(runnable)
通过post可以将一个runnable投递到消息队列,然后等到Lopper调用runnable的时候,View也就初始化好了,典型代码如下:
@Override
protected void onStart() {
super.onStart();
mTextView.post(new Runnable() {
@Override
public void run() {
int width = mTextView.getMeasuredWidth();
int height = mTextView.getMeasuredHeight();
}
});
}
4.3: ViewTreeObserver
使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当View树的状态发生改变或者View树内部的View的可见性发生改变,onGlobalLayout方法就会回调,因此这是获取View的宽高一个很好的例子,需要注意的是,伴随着View树状态的改变,这个方法也会被调用多次,典型代码如下
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = mTextView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mTextView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = mTextView.getMeasuredWidth();
int height = mTextView.getMeasuredHeight();
}
});
}
4.4: view.measure(int widthMeasureSpec , int heightMeasureSpec)
通过手动测量View的宽高,这种方法比较复杂,这里要分情况来处理,根据View的LayoutParams来处理
- match_parent
直接放弃,无法测量出具体的宽高,根据View的测量过程,构造这种measureSpec需要知道parentSize,即父容器的剩下空间,而这个时候我们无法知道parentSize的大小,所以理论上我们不可能测量出View的大小 - 比如宽高都是100dp,那我们可以这样:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
mTextView.measure(widthMeasureSpec,heightMeasureSpec);
- wrap_content
可以这样测量:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
mTextView.measure(widthMeasureSpec,heightMeasureSpec);
八:自定义View
1:自定义View须知:
- 让View支持wrap_content
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
} else if (eightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
- 如果有有必要,让你的View支持padding
这是因为如果你不处理下的话,那么该属性是不会生效的,在ViewGroup也是一样 - 尽量不要在View中使用Handler
View本身就有一系列的post方法,建议使用这View.post()代替Handler; - View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
不停止这个线程或者动画,容易导致内存溢出的,所以你要在一个合适的机会销毁这些资源,在Activity有生命周期,而在View中,当View被remove的时候,onDetachedFromWindow会被调用,,和此方法对应的是onAttachedToWindow - View带有滑动嵌套时,需要处理好滑动冲突
2: 自定义View例子
自己手动撸了一遍书中自定义View的例子,将自己的理解加入到注释中。有助于理解代码。
public class MyViewPagerX extends ViewGroup {
private Scroller mScroller;
private VelocityTracker mTracker;
private Context context;
private int mLastX;
private int mLastY;
private int currentIndex;
public MyViewPagerX(Context context) {
super(context);
initView();
this.context=context;
}
public MyViewPagerX(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
this.context=context;
}
public MyViewPagerX(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
this.context=context;
}
private void initView(){
if(mScroller==null){
mScroller=new Scroller(context);
mTracker=VelocityTracker.obtain();
}
}
/**
* 测量四步部曲:
* 1:获取父容器宽高测量值以及测量模式
* 2:拿着父容器的测量widthMeasureSpec,heightMeasureSpec测量子View:measureChildren(widthMeasureSpec,heightMeasureSpec);
* 3;支持wrap_content
* 4:测量自己:setMeasuredDimension(measureWidth,measureHeight);
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measureWidthMode=MeasureSpec.getMode(widthMeasureSpec);
int measureWidth=MeasureSpec.getSize(widthMeasureSpec);
int measureHeightMode=MeasureSpec.getMode(heightMeasureSpec);
int measureHeight=MeasureSpec.getSize(heightMeasureSpec);
int childCount=getChildCount();
//测量子View
measureChildren(widthMeasureSpec,heightMeasureSpec);
//支持wrap_content
View childView = getChildAt(0);
if(childCount==0){
setMeasuredDimension(0,0);
}else if(measureWidthMode==MeasureSpec.AT_MOST&&measureHeightMode==MeasureSpec.AT_MOST){
measureWidth=childView.getMeasuredWidth()*childCount;
measureHeight=childView.getMeasuredHeight();
}else if(measureHeightMode==MeasureSpec.AT_MOST){
measureHeight=childView.getMeasuredHeight();
}else if(measureWidthMode==MeasureSpec.AT_MOST){
measureWidth=childView.getMeasuredWidth()*childCount;
}
//测量自己
setMeasuredDimension(measureWidth,measureHeight);
}
/**
* 布局:layout确定自己的位置
* onLayout确定子View的位置
* 1:遍历子View,分别获取各个子View的left,right,top,bottom等值
* 2:进行布局:childView.layout();
*
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if(changed){
int childCount = getChildCount();
for (int i = 0; i <childCount ; i++) {
View childView=getChildAt(i);
//严谨一点判断一下当前View有没有被Gone掉
if(childView.getVisibility()!=GONE){
int childWidth = childView.getMeasuredWidth();
int childHeight=childView.getMeasuredHeight();
childView.layout(childWidth*(i-1),0,childWidth+i,childHeight);
}
}
}
}
/**
* 借助scroller弹性滑动方法:
* 1:初始化scroller对象mScroller;
* 2:设置滑动距离:mScroller.startScroll(mScroller.getFinalX(),mScroller.getFinalY(),x,y);
* 3:重绘
* 4:重写computeScroll(),自己实现滑动逻辑
* 5:重绘
* @param x
* @param y
*/
private void startSmoothTo(int x,int y){
mScroller.startScroll(mScroller.getFinalX(),mScroller.getFinalY(),x,y);
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
@Override
protected void onDetachedFromWindow() {
mTracker.recycle();
super.onDetachedFromWindow();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean isIntercepted=false;
int x= (int) ev.getX();
int y= (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
isIntercepted=false;
if(!mScroller.isFinished()){
//如果没有滑动结束比方说:处理用户手指快速滑动并离开屏幕然后点击屏幕也就是
// “快速滑动”这一过程未结束,重新进来ACTION_DOWN事件对其进行拦截
mScroller.abortAnimation();
isIntercepted=true;
}
break;
case MotionEvent.ACTION_UP:
//如果这里返回true的话,内部控件无法接收ACTION_UP事件
//考虑perfomOnClick()在Action_up中执行,所以
//内部控件无法相应onClick事件
isIntercepted=false;
break;
case MotionEvent.ACTION_HOVER_MOVE:
if(Math.abs(x-mLastX)>Math.abs(y-mLastY)){
isIntercepted=true;
}else{
isIntercepted=false;
}
break;
}
mLastY=y;
mLastX=x;
return isIntercepted;
}
/**1:ACTION_DOWN中:不做任何处理,见注释
* 思考:主要是action_move和action_up中的逻辑:
* action_up中等同于滑动动作结束,需要判断当前滑动距离应该显示哪个页面
* action_move中不需要考虑那么多,滑动没结束,继续滑动就行
*
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
int x= (int) event.getX();
int y= (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//如果没有滑动结束比方说:处理用户手指快速滑动并离开屏幕然后点击屏幕也就是
// “快速滑动”这一过程未结束,重新进来ACTION_DOWN事件:不做任何处理
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_UP:
int mScrollX = getScrollX();
mTracker.addMovement(event);
mTracker.computeCurrentVelocity(1000);
float mXVelocity = mTracker.getXVelocity();
//如果滑动速度大于50:
//滑动速度>0证明从左向右滑动,currentIndex应该-1;
//滑动速度<0证明从右向左滑动,currentIndex应该+1;
//如果滑动速度小于50
//自己举例子试试,说不明白;
// 然后 对currentIndex 进行大于0处理;
//获取滑动差距,继续代码滑动
//特别说明getScrollX()表示:控件左边距到控件内容左边距的偏移距离
if(Math.abs(mXVelocity)>50){
currentIndex=mXVelocity>0?currentIndex-1:currentIndex+1;
}else{
currentIndex=(mScrollX+getWidth()/2)/getWidth();
}
currentIndex=Math.max(0,Math.min(currentIndex,getChildCount()-1));
startSmoothTo(currentIndex*getWidth()-mScrollX,0);
mTracker.clear();
break;
case MotionEvent.ACTION_MOVE:
//考虑scrollBy和scrollTo滑动方向和预期相反,所以是mLastX-x,不是x-mLastX;
scrollBy(mLastX-x,0);
break;
}
mLastX=x;
mLastY=y;
return true;
}
}
网友评论