Android程序中的一个个界面都是由View组成的,下面让我们一起来了解一下View吧!
View 的分类
如上图所示,系统的View
大体分为两类,即View
与ViewGroup
,分别对应了控件与布局。View是所有控件的基类,而ViewGroup则可以理解为View的组合,它可以包含很多View和ViewGroup,而它所包含的ViewGroup又可以包含View和ViewGroup,形成一个View树
。
View 的重要方法
View 的工作流程
一个View显示在屏幕上,经过了measure,layout,draw三个过程。其中,measure用来测量View的宽和高,layout用来确定View的位置,draw则用来绘制View。
MeasureSpec
MeasureSpec : View的一个内部类,其封装的一个View的规格尺寸,包括viewl的宽和高等信息。它的作用是在Measure流程中,系统会将view的LayoutParams等信息,根据父容器所施加的规则转转换成对应的MeasureSpec,然后在onMeasure方法中根据这个MearsureSpec来确定view的宽和高。
其中定义了三个常量,
三个常量都是32位的int值,高2位代表了SpecMode,低30位则代表SpecSize,前者代表测量模式,后者指的是测量大小。SpecMode有三种模式,
- UNSPECIFIED 未指定模式:view想多大就有多大,富容器不做限制,一般用于系统内部的测量listview
- AT_MOST 最大模式:对应于wrap_content属性,子view的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值
- EXACTLY 精确模式:对应于match_parent属性和具体的数值,父容器测量出view所需要的大小,也就是specsize的值
继承自View的控件,在默认情况下wrap_content与match_parent的显示效果是一样的,原因可从View的源码中(onMeasure())看到。
// 实用程序返回默认大小。如果MeasureSpec未施加任何约束,则使用提供的尺寸。
// 如果测量允许,将变大。
// 参数:size–此视图的默认大小
// measureSpec–父级施加的约束
// 返回:此视图应具有的大小
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:
//默认在最大模式和精确模式中均返回SpecSize
//导致wrap_content与match_parent属性效果相同
result = specSize;
break;
}
return result;
}
因此如果要实现自定义View的wrap_content,则要重写onMeasure(),并对自定义View的wrap_content属性进行处理(具体处理方法可参考:)。
Measure
measure 用来测量View的宽和高,View的measure流程测量自己,而ViewGroup的measure流程除了要完成自己的测量,还要遍历地调用子元素的measure方法。分别对应View的onMeasure方法,ViewGroup的masureChildren方法。
Layout
layout的作用是确定元素的位置。ViewGroup中的layout方法用来确定子元素的位置,View中的layout方法则用来确定自身的位置。
Draw
View的draw方法主要做了以下的工作:
绘制背景;保存当前canvas层;绘制View内容;绘制子View;绘制View的褪色边缘;绘制装饰
坐标系
Android 系统中有两种坐标系,分别为Android 坐标系和 View 坐标系。
Android 坐标系
在Android中,以屏幕左上角的顶点为 Android 坐标系的原点,从该原点向右、向下为正方向。
View 坐标系
View坐标系可以一图说明。
根据此图,可以得到很多结论,如:
View的宽度:width = getRight() - getLeft() = getWidth()
View的高度:height = getBottom() - getTop() = getHeight()
点击相关
MotionEvent 在用户交互中作用重大,其内部提供了很多事件常量,比如常用的ACTION_UP
、ACTION_DOWN
和ACTION_MOVE
。此外,MotionEvent 也提供了获取点击焦点坐标的方法。
getX()
:获取点击事件距离控件左边的距离,即视图坐标
getY()
:获取点击事件距离控件顶边的距离,即视图坐标
getRawX()
:获取点击事件距离屏幕左边的距离,即绝对坐标
getRawY()
:获取点击事件距离屏幕顶边的距离,即绝对坐标
View 的滑动处理
当点击事件传到View时,系统会记下触摸点的坐标,手指移动是系统记下移动后触摸的坐标并计算出偏移量,并通过偏移量来修改View的坐标。
实现View滑动涉及许多方法,这里简单介绍六种。
layout()
View进行绘制的时候会调用onLayout()来设置显示的位置,在滑动时,可通过在onTouchEvent()中的获取触摸点坐标,再计算得出滑动后的控件左边,再调用layout方法重新对屏幕进行布局,从而达到移动View的效果。
public boolean onTouchEvent(MotionEvent event){
...
//获取手指触摸点的横坐标与纵坐标
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
layout(getLeft() + offserX, getTop() + offsetY,
getRight() + offsetX, getBottom() + offsetY);
break;
...
}
}
offsetLeftAndRight() 与 offsetTopAndBottom()
这两个方法与layout方法思想相同,只在最后调用的时候略有区别。
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
break;
LayoutParams
LayoutParams主要保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局参数从而达到改变View位置的效果。
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
动画
可以通过属性动画对控件进行移动,这里暂不叙述,可参考动画相关的文章。
scrollTo scrollBy
二者关系类似于increaseTo 与 increaseBy , 前者 scrollTo(x,y) 代表移动到一个具体的坐标点,而后者 scrllBy(dx, dy) 表示移动的增量。
注意,传入的偏移量应为实际的相反数,因为实际移动的是手机屏幕在画布上的位置。
参考文章:
https://juejin.cn/post/7023992403983859720
《Android 进阶之光》
https://www.cnblogs.com/wjtaigwh/p/6593580.html