Android中的View是应用开发者接触最多的,感觉它的重要性可以匹敌四大组件。这里对View的基础到进阶部分的点作出记录。
View的事件体系
View的事件体系中我会记录一些我不太用或者不太熟悉的基础知识点。
View基础
View位置参数
View的位置由四个顶点的位置决定,对应View的四个属性:top,left,right,bottom。其中top为左上角的纵坐标,left为左上角的横坐标,right为右下角的横坐标,bottom为右下角的纵坐标,其中Android中的坐标体系都很了解,这样确立了一个view的左上角和右下角的两个点,连接成矩形即位View在视图中的位置了。其中引入一张经典的图就一眼就明白了:
其中具体的基础介绍可以查看这系列文章传送
TouchSlop
TouchSlop是系统所能识别的被认为是滑动的最小距离。这是一个常量,和设备有关,在不同的设备上面这个值可能会不同。可以这样来获取它:ViewConfiguration.get(getContext()).getScaledTouchSlop()。在我们处理滑动的时候可以根据这个常量来判断是否发生了滑动事件。
VelocityTracker
VelocityTracker是一个速度追踪的一个类,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。使用方式:在View的onTouchEvent中追踪
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);//把监听事件传递给它
velocityTracker.computeCurrentVelocity(100);//设定监听速度时间单位为100毫秒
float xVelocity = velocityTracker.getXVelocity();
float yVelocity = velocityTracker.getYVelocity();
Log.i(TAG, "onTouchEvent: xVelocity=="+xVelocity+" yVelocity=="+yVelocity);
velocityTracker.clear();//不使用的时候回收掉
velocityTracker.recycle();
注意在不使用的使用调用clear方法和recycler方法进行回收。
计算公式为:速度=(终点位置-开始位置)/时间段
所以当手指在屏幕滑动的时候计算得到的速度值可以为负值。
GestureDetector
手势监测。用于辅助监测用户的单击,滑动,长按,双击等行为。使用方法:获取GestureDetector:
mGestureDetector = new GestureDetector(getBaseContext(),this);
其中第二个参数是一个监听器,那么我们可以在这个接口中回调到我们要处理的事件。
使用GestureDetector:
@Override
public boolean onTouchEvent(MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
在onTouchEvent()中接管touch事件就可以了,这时候当有事件产生的使用会在回调接口中调用。第二个参数提供两个回调接口一个GestureDetector.OnGestureListener,和另外一个OnDoubleTapListener这个接口是对双击事件的一些封装处理。
注意:我们发现在使用上述接口的时候会有很多的回调的方法,有一些我们并不需要,代码看上去不是很好看。打开源码一看在GestureDetector类中有一个两个接口的默认实现SimpleOnGestureListener,我们继承这个类来复写我们实际要处理的方法就可以了,不会出现那么多我们不需要处理的方法,不得不说真的很人性化。。
Scroller
弹性滑动对象,用于辅助实现View的滑动效果。这个类本身无法让View滑动,需要View自己的computeScroll()方法来进行配合滑动。原理很简单:调用Scroller的startScroll方法以后调用invalidate()方法导致View重新绘制,而View重新绘制的时候调用onDraw()方法,这个方法里面有一个空的computeScroll()方法,我们在View中覆盖这个方法中处理Scroller中的滑动过程的时间变化过程,根据这个过程来调用View自己的scrollTo方法来进行View内容的滑动。形成一个在指定时间范围的重复调用。
使用示例:
public void startScroll(int destX){
int startX = getScrollX();
int deta = destX-startX;
Log.i(TAG, "startScroll: startX=="+startX+" deta=="+deta);
mScroller.startScroll(startX,0,deta,0,3000);
invalidate();
}
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
int currentX = mScroller.getCurrX();
Log.i(TAG, "computeScroll: currentX=="+currentX+
" currentY=="+mScroller.getCurrY());
//从左往右开始滑动注意下面为负值
scrollTo(-currentX,mScroller.getCurrY());
postInvalidate();
}
}
注意上面我们的scrollTo在X方向上面的变量为负值,假如我们要让View移动到(300,0)进行水平移动。那么在我们的常规意识里面上面的不应该为负值呀!这里有一个坑,就是scrollTo的这个x参数不是代表它的坐标,而是偏移量的概念。即位要一到(300,0)的偏移量为(0,0)-(300,0)=(-100,0),所以我们要移动到我们想到达的位置为负值。
View的滑动
View的滑动通用的方法大致记录为第一种使用上述的scrollTo和scrollBy方法;第二种通过给View提供平移效果来实现滑动;第三种通过改变View的LayoutParams来进行重新布局。第一种我们上述已经提及,我们记录一下第二种和第三种。
使用动画
直接使用View动画来进行平移操作:
Animation animation = new TranslateAnimation(0,300,0,0);
animation.setDuration(2000);
animation.setFillAfter(true);
mTextView.startAnimation(animation);
这里View动画执行我们移动到(300,0)的位置然后设置View在那个位置显示,但是这个动画处理的是View的内容移到(300,0)的位置,而这个View实际上并没有跑到那个位置,这个概念不好理解。(咋更好的阐述View的内容QAQ)。要熟知的一点是在(300,0)的那个View的内容显示区域View无法响应我们的点击事件,反而在原始的位置如果可以点击,View倒是可以响应我们点击事件。
使用布局参数
使用View的LayoutParams来设置左边距来达到View移动到了指定位置的效果。
使用延时策略
View实现弹性滑动的一种方式可以使用View或者是Handler的延时策略来进行实现。下面示例在1000毫米内完成View30帧的滑动过程:
private static final int MSG_SCROLL_TO = 0x01;
private static final int FRAME_COUNT = 30;
private static final int DELAY_TIME = 33;
private int mCount = 0;
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MSG_SCROLL_TO:
if(mCount < FRAME_COUNT){
int scrollX = (int)(-300*((float)mCount/FRAME_COUNT));
mTextView.scrollTo(scrollX,0);
mCount++;
mHandler.sendEmptyMessageDelayed(MSG_SCROLL_TO,DELAY_TIME);
}
break;
}
}
};
View 的事件分发机制
View的事件分发机制刚开始的时候一直认为这是个很难掌握的点,当静下心来梳理清楚整个过程以后就会有种茅塞顿开的感觉。首先我们要有一个整体流程:Activity接收触屏事件->传递给Window对象->传递给第一个View这里肯定是我们的顶级的ViewGroup->决定往子View传递还是自己来处理。我们重点要了解的是最后一个步骤,并且掌握每一个事件分发的函数的意义和调用的时机。
总体流程图:
事件分发应注意内容:
1. 某个ViewGroup一旦决定拦截,那么这一个事件序列都只能由它来处理,并且它的onInterceptTouchEvent不会再调用。
2. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么同一事件序列中的其他事件都不会交给它处理,并且事件将重新交给它的父元素去处理,即父元素的onTouchEvent会调用。
3. View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就会返回true。
4. 事件传递过程是由内向外的,先给父View再分发给子View。 通过requestDisableInterceptTouchEvent方法可以在子View中干预父View的分发过程,但是ACTION_DOWN事件除外.
源码解析追踪传送
View的工作原理
对于深入的理解了View的原理,每个方法的调用时机以及意义我们才可以自己定制出我们的View。
ViewRoot和DecorView
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的绘制过程是通过ViewRoot来完成的。在ActivityThread中,当Activity创建后会将DecorView添加到Window中,同时创建ViewRootImpl对象,将ViewRootImpl对象和DecorView建立关联。
View的绘制流程是从ViewRoot的实现ViewRootImpl的performTraversals方法开始的,它的源码一个方法太长,但是我们可以看出在ViewRootImpl中大步骤是performMeasure->performLayout->performDraw 在每个方法里面去分发处理子ViewGroup的方法。大致流程图为:
MeasureSpec
看这个单词的意思,我们感觉像是测量规格。MeasureSpec在很大程度上面决定一个View的尺寸规格,因为这个过程还受父容器的影响,因为父View影响View的MeasureSpec的创建过程。在测量中系统会将View的LayoutParams根据父View所施加的规则转换成MeasureSpec,然后根据这个MeasureSpec来测量View的宽高。
MeasureSpec使用一个32位的int值来表示,高两位代表SpecMode,低30位代表测量的SpecSize。SpecMode测量模式有三种类型:
- UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部。
- EXACTLY:父容器已经监测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
- AT_MOST:父容器指定了一个可用的大小SpecSize,View的大小不能大于这个值,具体的值看View的实现。它对于LayoutParams中的wrap_content。
View的工作流程
View的工作流程主要是指measure,layout,draw这三大流程。measure确定View的测量宽高,layout确定View的最终宽高和四个顶点的位置,而draw就是把View绘制到屏幕上面。
measure过程
View的measure过程
View的measure方法是一个final类型的,即我们不可以覆盖它,在这歌方法里面调用了onMeasure()方法。追踪源码最后的测量大小为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;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
根据源码我们得到一个结论:直接继承View的控件需要重写onMeasure()方法并设置wrap_content时的大小,不然在布局中使用wrap_content相当于match_parent。下面提供解决代码:重写onMeasure()方法,自己指定大小。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST &&
heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightMeasureSpec);
} else if (heightMeasureSpec == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthMeasureSpec, mHeight);
}
}
其中mWidth和mHeight的值为View指定wrap_content的时候我们设置的默认想要指定的值。
ViewGroup的measure过程
ViewGroup的measure方法出了测量自己它还要遍历去测量子View的measure方法。和View不同的是它没有重写onMeasure()方法,但是其中有一个measureChildren的方法。ViewGroup本身没有具体的测量过程,它需要子类去进行实现,不和View一样做了一个具体的实现,因为ViewGroup可能的测量策略不同。
layout过程
View的layout过程
源码咋们这里就不贴了,View的layout方法中会调用onLayout方法,注意View的onLayout是一个空实现。
ViewGroup的layout过程
ViewGroup的layout过程更加精简了,调用View的layout方法,而重写了View的onLayout方法为一个抽象的方法,这个待继承ViewGroup的子类去实现。
draw过程
ViewGroup的draw过程实际上是调用子类的draw方法,所以我们理解了View的draw过程就OK,翻开View的draw源码我们会看到醒目的步骤注释,这个就是View的draw过程。代码我们也不贴了。
自定义View
我想自定义View是每个Android开发程序员所要了解并且掌握的。这个是一个大量的积累和联系的过程。我认为想要处理好这个点就是多看然后特别注意要多动手实现。我们在掌握了基础的时候再开始去实战才是进步的最快途径。这里我们就记录几个要注意的点:
- 让View支持wrap_content:我们上面已经说明
- 继承View自定义View要注意padding的代码处理。
- View中如果有线程或动画,需要及时停止。开始和停止的时间为onAttachedToWindow和onDetachedFromWindow。这样可以避免内存泄漏。
参考实际操作自定义View:
一个整合的内容
鸿洋的自定义View系列
对各个自定义View基础知识的讲解