参考链接
View和ViewGroup
View是Android所有控件的基类,同时ViewGroup也是继承自View。ViewGroup作为View或者ViewGroup这些组件的容器,派生了 多种布局控件子类,比如LinearLayout、RelativeLayout等
Android坐标系
Android视图坐标系
View获取自身宽高
- getHeight():获取View自身高度
- getWidth():获取View自身宽度
MotionEvent提供的方法
我们看上图那个深蓝色的点,假设就是我们触摸的点,我们知道无论是View还是ViewGroup,最终的点击事件都会由onTouchEvent(MotionEvent event)方法来处理,MotionEvent也提供了各种获取焦点坐标的方法:
- getX():获取点击事件距离控件左边的距离,即视图坐标
- getY():获取点击事件距离控件顶边的距离,即视图坐标
- getRawX():获取点击事件距离整个屏幕左边距离,即绝对坐标
- getRawY():获取点击事件距离整个屏幕顶边的的距离,即绝对坐标
View滑动的六种方法
layout(int l, int t, int r, int b)
通过修改View的left、top、right、bottom这四种属性来控制View的坐标。
传进来里面的四个参数分别是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方法来重新放置它的位置
layout(getLeft()+offsetX, getTop()+offsetY,
getRight()+offsetX , getBottom()+offsetY);
break;
}
return true;
}
offsetLeftAndRight()与offsetTopAndBottom()
将ACTION_MOVE中的代码替换成如下代码:
case MotionEvent.ACTION_MOVE:
//计算移动的距离
int offsetX = x - lastX;
int offsetY = y - lastY;
//对left和right进行偏移
offsetLeftAndRight(offsetX);
//对top和bottom进行偏移
offsetTopAndBottom(offsetY);
break;
LayoutParams(改变布局参数)
LayoutParams主要保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局的参数从而达到了改变View的位置的效果。同样的我们将ACTION_MOVE中的代码替换成如下代码:
LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
因为父控件是LinearLayout,所以我们用了LinearLayout.LayoutParams,如果父控件是RelativeLayout则要使用RelativeLayout.LayoutParams。除了使用布局的LayoutParams外,我们还可以用ViewGroup.MarginLayoutParams来实现:
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
scollTo与scollBy
scollTo(x,y)表示移动到一个具体的坐标点
scollBy(dx,dy)则表示移动的增量为dx、dy。
其中scollBy最终也是要调用scollTo的。scollTo、scollBy移动的是View的内容。
如果在ViewGroup中使用则是移动他所有的子View/子控件
Scroller
我们用scollTo/scollBy方法来进行滑动时,这个过程是瞬间完成的,所以用户体验不大好。这里我们可以使用Scroller来实现有过度效果的滑动。
Activity的构成
一个Activity包含一个window对象,这个对象是由PhoneWindow来实现的,PhoneWindow将DecorView做为整个应用窗口的根View,而这个DecorView又将屏幕划分为两个区域一个是TitleView一个是ContentView,而我们平常做应用所写的布局正是展示在ContentView中的。
- Activity 并不负责视图控制,它只是控制生命周期和处理事件。真正控制视图的
是 Window。一个 Activity 包含了一个 Window,Window 才是真正代表一个窗口。
Activity 就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来
与 Window、以及 View 进行交互。 - Window是视图的承载器,内部持有一个 DecorView,而这个DecorView才是 view的根布局。Window 是一个抽象类,实际在 Activity 中持有的是其子类PhoneWindow。PhoneWindow 中有个内部类 DecorView,通过创建 DecorView 来加载 Activity 中设置的布局 R.layout.activity_main。Window 通过WindowManager 将 DecorView 加载其中,并将 DecorView 交给 ViewRoot,进行视图绘制以及其他交互。
Window 负责窗口管理(实际是子类 PhoneWindow),窗口的绘制和渲染交给 DecorView完成; - DecorView 是 FrameLayout 的子类,它可以被认为是 Android 视图树的根节点视
图。DecorView 作为顶级 View,一般情况下它内部包含一个竖直方向的
LinearLayout,在这个 LinearLayout 里面有上下三个部分,上面是个 ViewStub,延
迟加载的视图(应该是设置 ActionBar,根据 Theme 设置),中间的是标题栏(根 据 Theme 设置,有的布局没有),下面的是内容栏。
MeasureSpec
参考-Android开发艺术探索
MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配。
MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一 起才能决定View的MeasureSpec,从而进一步决定View的宽/高。对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的 LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽/高。
MeasureSpec和LayoutParams的对应关系
前面已经提到,对于普通View,其MeasureSpec由父容器的 MeasureSpec和自身的LayoutParams来共同决定,那么针对不同的父容器和View本身不同的LayoutParams, View就可以有多种MeasureSpec。这里简单说一下,当View采用固定宽/高的时候,不管父容器的 MeasureSpec是什么,View的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小。当View的 宽/高是match_parent时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩 余空间;如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。当 View的宽/高是wrap_content时,不管父容器的模式是精准还是最大化,View的模式总是最大化并且大小不 能超过父容器的剩余空间。可能读者会发现,在我们的分析中漏掉了UNSPECIFIED模式,那是因为这个 模式主要用于系统内部多次Measure的情形,一般来说,我们不需要关注此模式。
通过上图可以看出,只要提供父容器的MeasureSpec和子元素的LayoutParams,就可以快速地确定出 子元素的MeasureSpec了,有了MeasureSpec就可以进一步确定出子元素测量后的大小了。
自定义view学习
刘望舒-Android View体系Android自定义View全解自定义View,有这一篇就够了
函数 | 作用 | 相关方法 |
measure() | 测量View的宽高 | measure(),setMeasuredDimension(),onMeasure() |
layout() | 计算当前View以及子View的位置 | layout(),onLayout(),setFrame() |
draw() | 视图的绘制工作 | draw(),onDraw() |
测量模式 | 表示意思 |
UNSPECIFIED | 未指定模式,父容器没有对当前View有任何限制,当前View可以任意取尺寸 |
EXACTLY | 精确模式,对应于match_parent属性和具体的数值,父容器测量出View所需要的大小,也就是specSize的值。 |
AT_MOST | 最大模式,对应于wrap_comtent属性,只要尺寸不超过父控件允许的最大尺寸就行。 |
而上面的测量模式跟我们的引用自定义View时在xml中设置wrap_content、match_parent以及写成固定的尺寸有什么对应关系呢?
match_parent
—>EXACTLY
。怎么理解呢?match_parent就是要利用父布局给我们提供的所有剩余空间,而父View剩余空间是确定的,也就是这个测量模式的整数里面存放的尺寸。
wrap_content
—>AT_MOST
。怎么理解:就是我们想要将大小设置为包裹我们的view内容,那么尺寸大小就是父布局给我们作为参考的尺寸,只要不超过这个尺寸就可以啦,具体尺寸就根据我们的需求去设定。
固定尺寸(如100dp)
—>EXACTLY
。用户自己指定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主啦。
private int getMySize(int defaultSize, int measureSpec) {
int mySize = defaultSize;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.UNSPECIFIED: {//如果没有指定大小,就设置为默认大小
mySize = defaultSize;
break;
}
case MeasureSpec.AT_MOST: {//如果测量模式是最大取值为size
//我们将大小取最大值,你也可以取其他值
mySize = size;
break;
}
case MeasureSpec.EXACTLY: {//如果是固定的大小,那就不要去改变它
mySize = size;
break;
}
}
return mySize;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMySize(100, widthMeasureSpec);
int height = getMySize(100, heightMeasureSpec);
if (width < height) {
height = width;
} else {
width = height;
}
setMeasuredDimension(width, height);
}
- draw()步骤
public void draw(Canvas canvas) {
...
//步骤1:绘制View的背景
drawBackground(canvas);
...
//步骤2:如果需要的话,保存canvas的涂层,为fading做准备
saveCount = canvas.getSaveCount();
...
canvas.saveLayer(left,top,right, top + length, null, flags);
...
//步骤3:绘制View的内容
onDraw(canvas);
...
//步骤4:绘制View的子View
dispatchDraw(canvas)
...
//步骤5:如果需要的话,绘制View的fading边缘并恢复图层
canvas.drawRect(left, top, right, top +length, p);
...
canvas.restoreToCount(saveCount);
...
//步骤6: 绘制View的装饰(例如滚动条)
onDrawScrollBars(canvas)
...
Android 代码动态布局 LayoutParams 使用
Android 代码动态布局 LayoutParams 使用LayoutParams 的作用是:子控件告诉父控件,自己要如何布局。
inflate方法
public View inflate(int resource, ViewGroup root, boolean attachToRoot)
- resource,要添加的 Layout
- root,父布局
- attachToRoot是否调用addview添加到父布局
- 如果 root 为 null,attachToRoot 将失去作用,设置任何值都没有意义。
- 如果 root 不为 null,attachToRoot 设为 true,则会给加载的布局文件的指定一个父布局,即 root。
- 如果 root 不为 null,attachToRoot 设为 false,则会将布局文件最外层的所有 layout 属性进行设置,当该 view 被添加到父 view 当中时,这些 layout属性会自动生效。
- 在不设置 attachToRoot 参数的情况下,如果 root 不为 null,attachToRoot 参数默认为 true。
layout_width 和 layout_height
它们其实是用于设置 View 在布局中的大小的,也就是说,首先 View 必须存在于一个布局中,layout_width 和 layout_height 才能生效。 平时在 Activity中指定布局文件的时候,最外层的那个布局是可以指定大小的呀,layout_width和 layout_height 都是有作用的。确实,这主要是因为,在 setContentView() 方法中,Android 会自动在布局文件的最外层再嵌套一个 FrameLayout,所以layout_width 和 layout_height 属性才会有效果。
如何自定义控件
- 1.自定义属性的声明和获取
- 2.分析需要的自定义属性 在res/values/attrs.xml定义声明 在layout文件中进行使用 在View的构造方法中进行获取
- 3.测量onMeasure 布局onLayout(ViewGroup)
- 4.绘制onDraw
- 5.处理点击事件
状态的恢复与保存
Android 布局优化之 include、merge、ViewStub
include标签 include 就是为了解决重复定义相同布局的问题。include 使用注意
1.一个xml布局文件有多个include标签需要设置ID,才能找到相应子View的控件,否则只能找到第一个include的layout布局,以及该布局的控件 2.include标签如果使用layout xx属性,会覆盖被include的xml文件根节点对应的layoutxx属性,建议在include标签调用的布局设置好宽高位置,防止不必要的bug 3.include 添加id,会覆盖被include的xml文件根节点ID,这里建议include和被include覆盖的xml文件根节点设置同名的ID,不然有可能会报空指针异常 4.如果要在include标签下使用RelativeLayout,如layout_margin等其他属性,记得要同时设置layout_width和 layout_height,不然其它属性会没反应
merge 标签 merge标签主要用于辅助 include标签,在使用 include后可能导致布局嵌套过多,多余的 layout 节点或导致解析变慢(可通过 hierarchy viewer 工具查看布局的嵌套情况)。官方文档说明:merge 用于消除视图层次结构中的冗余视图,例如根布局是Linearlayout,那么我们又 include 一个 LinerLayout 布局就没意义了,反而会减慢 UI 加载速度。merge 官方文档
merge 标签使用注意
1.因为merge标签井不是View,所以在通过LayoutInflate.inflate()方法渲染的时候,第二个参数必须指定一个父容器,且第三个参数必须为true,也就是必须为merge下的视图指定一个父亲节点. 2.因为merge不是View,所以对merge标签设置的所有属性都是无效的. 3.注意如果include的layout用了merge,调用include的根布局也使用了merge标签,那么就失去布局的属性了 4. merge标签必须使用在根布局 5.ViewStub标签中的layout布局不能使用merge标签
ViewStub 标签 ViewStub 标签最大的优点是当你需要时才会加载,使用它并不会影响 UI 初始化时的性能.各种不常用的布局像进度条、显示错误消息等可以使用 ViewStub标签,以减少内存使用量,加快渲染速度.ViewStub 是一个不可见的,实际上是把宽高设置为 0 的 View.效果有点类似普通的 view.setVisible(),但性能体验提高不少
ViewStub 标签使用注意点
- ViewStub标签不支持merge标签
- ViewStub的inflate只能被调用一次,第二次调用会抛出异常,setVisibility可以被调用多次,但不建议这么做(ViewStub 调用过后,可能被GC掉,再调用setVisibility()会报异常)
- 为ViewStub赋值的android∶layout_XX属性会替换待加载布局文件的根节点对应的属性
Space 组件
在 ConstraintLayout 出来前,我们写布局都会使用到大量的 margin 或padding,但是这种方式可读性会很差,加一个布局嵌套又会损耗性能鉴于这种情况,我们可以使用 space,使用方式和 View一样,不过主要用来占位置,不会有任何显示效果
Android 优化之路(一)布局优化
自定义View的分类
- 继承View重写onDraw方法 这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要 静态或者动态地显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写onDraw方法。 采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。
- 继承ViewGroup派生特殊的Layout 这种方法主要用于实现自定义的布局,即除了LinearLayout、RelativeLayout、FrameLayout这几种系统 的布局之外,我们重新定义一种新布局,当某种效果看起来很像几种View组合在一起的时候,可以采用这 种方法来实现。采用这种方式稍微复杂一些,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。
- 继承特定的View(比如TextView) 这种方法比较常见,一般是用于扩展某种已有的View的功能,比如TextView,这种方法比较容易实 现。这种方法不需要自己支持wrap_content和padding等。
- 继承特定的ViewGroup(比如LinearLayout) 这种方法也比较常见,当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法来实 现。采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程。需要注意这种方法和方法2的区 别,一般来说方法2能实现的效果方法4也都能实现,两者的主要差别在于方法2更接近View的底层。
自定义View须知
- 让View支持wrap_content 这是因为直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,那么当外界在布局中使用wrap_content时就无法达到预期的效果。
解决方案:在onMeasure中添加对wrap_content情况的判断,为wrap_content可以设置一个默认大小。
@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//wrap_content时设置默认大小为200
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200,200);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200,heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize,200);
}
}
- 如果有必要,让你的View支持padding 这是因为直接继承View的控件,如果不在draw方法中处理padding,那么padding属性是无法起作用 的。另外,直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其 造成的影响,不然将导致padding和子元素的margin失效。
解决方案:针对padding的问题,也很简单,只要在绘制的时候考虑一下padding即可,因此我们需要对 onDraw稍微做一下修改,修改后的代码如下所示。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingLeft();
final int paddingTop = getPaddingLeft();
final int paddingBottom = getPaddingLeft();
int width = getWidth() -paddingLeft -paddingRight;
int height = getHeight() -paddingTop -paddingBottom;
int radius = Math.min(width,height) / 2;
canvas.drawCircle(paddingLeft + width / 2,paddingTop + height/2,radius, mPaint);
}
- 尽量不要在View中使用Handler,没必要 这是因为View内部本身就提供了post系列的方法,完全可以替代Handler的作用,当然除非你很明确地 要使用Handler来发送消息。
- View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow 这一条也很好理解,如果有线程或者动画需要停止时,那么onDetachedFromWindow是一个很好的时 机。当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调 用 , 和 此 方 法 对 应 的 是 onAttachedToWindow , 当 包 含 此 View 的 Activity 启 动 时 , View 的 onAttachedToWindow方法会被调用。同时,当View变得不可见时我们也需要停止线程和动画,如果不及 时处理这种问题,有可能会造成内存泄漏。
- View带有滑动嵌套情形时,需要处理好滑动冲突