安卓开发笔记 之 View 视图
基本概念
- 每个 Activity 都有一个 Window(接口),通常是由 PhoneWindow 类来实现的。 PhoneWindow 将 DecorView 作为整个应用窗口的根 View,DecorView 将屏幕分成两部分:TitleView 和 ContentView。所有 view 的操作、绘制都由 ViewRootImpl 完成。
- setContentView() 做了什么?
- ViewGroup extends View;
- view 的位置参数:top、left、right、bottom,分别对应 View 的左上角和右下角相对于父容器的横纵坐标值。
- 从 Android 3.0 开始,view 增加了 x、y、translationX、translationY 四个参数,这几个参数也是相对于父容器的坐标。x 和 y 是左上角的坐标,而 translationX 和 translationY 是 view 左上角相对于父容器的偏移量,默认值都是 0。x = left + translationX ;y = top + translationY
- MotionEvent 主要有
ACTION_UP
、ACTION_DOWN
、ACTION_MOVE
、ACTION_CANCEL
等。- 点击一次,事件序列是
ACTION_DOWN
->ACTION_UP
; - 点击后滑动一会再离开,事件序列是
ACTION_DOWN -> ACTION_MOVE -> ACTION_MOVE -> … -> ACTION_UP
; ACTION_CANCEL
触发时机:子 view 接受到了ACTION_DOWN
事件,父容器拦截了ACTION_MOVE、ACTION_UP
之后,子 view 会收到ACTION_CANCEL
- 如果 view 不处理
ACTION_DOWN
则后面的事件都不会再给它 ACTION_DOWN
会让 ViewGroup 重置滑动标记(requestDisallowInterceptTouchEvent)
- 点击一次,事件序列是
- getRawX 和 getRawY 是相对于手机屏幕左上角的 x 和 y 坐标。
绘制流程
ViewRootImpl.performTravserals() 负责对所有 view 递归绘制
1、onMeasure()
- 测量模式 MeasureSpec 是一个 32 位的 int 值,其中高 2 位是测量的模式,低 30 位是测量的大小 (使用位运算是为了提高效率和节省空间)
EXACTLY
:精确值模式,属性设置为精确数值或者 match_parent 时AT_MOST
:最大值模式,属性设置为 wrap_content 时UNSPECIFIED
:不指定大小测量模式,父容器不对大小限制,通常情况下用于系统内部多次 Measure 或在绘制自定义 View 时才会用到,表示一种测量的状态- View 类默认的 onMeasure() 方法只支持 EXACTLY 模式,所以如果在自定义 View 的时候不重写 onMeasure 方法的话,就只能使用 EXACTLY 模式。自定义 View 可以响应你指定的具体的宽高值或者是
match_parent
属性, 但是如果要让自定义 View 支持wrap_content
属性的话,那么就必须要重写 onMeasure 方法来指定wrap_content
时 view 的大小。 - 对于 DecorView,它的 MeasureSpec 由 window 尺寸和自己的 LayoutParams 决定
- 对于普通 View,它的 MeasureSpec 由父容器的 MeasureSpec 和自己的 LayoutParams 决定
2、onLayout()
- 它的作用是 ViewGroup 确定子元素的位置,onLayout() --> layout()
- 通过 serFrame() 确定 view 四个顶点的位置(mLeft、mRight、mTop、mBittom)
getWidth 和 getMeasureWidth 区别
- 在 view 默认实现中二者值相等
- getWidth/getHeight 形成在 layout 过程中,也就是二者的赋值时机不同
- 改写 layout 方法可以让 二者不同
LinearLayout 和 RelativeLayout 有何不同?
- RelativeLayout 需要对其子 View 进行两次 measure 过程。而 LinearLayout 则只需一次 measure 过程,所以显然会快于 RelativeLayout,但是如果 LinearLayout 中有 weight 属性,则也需要进行两次 measure
- RelativeLayout 的子 View 如果高度和 RelativeLayout 不同,会引发效率问题,可以使用 padding 代替 margin 以优化此问题
- 参考
3、onDraw()
- 绘制背景
- 绘制内容
- 绘制子元素
- 绘制滚动条
自定义 view
- 继承 view 重写 onDraw 方法需要自己支持 wrap_content,并且 padding 也要自己处理(onMeasure、onLayout)。继承特定的 View 例如 TextView 不需要考虑。
- 尽量不要在 View 中使用 Handler,因为 view 内部本身已经提供了 post 系列的方法,完全可以替代 Handler 的作用。
- view 中如果有线程或者动画,需要在 onDetachedFromWindow 方法中及时停止。
- 处理好 view 的滑动冲突情况。
事件分发
这是段伪代码,能说明整个流程
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
public boolean dispatchTouchEvent(MotionEvent ev)
属于 View 的方法, 父容器、子 view 都有 ,用来进行事件的分发。如果事件能够传递给当前 view,那么此方法一定会被调用,返回结果受当前 view 的 onTouchEvent 和下级 view 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。public boolean onInterceptTouchEvent(MotionEvent event)
在 dispatchTouchEvent 方法内部调用,用来判断是否拦截某个事件,**只有 ViewGroup 才有。**如果当前 view 拦截了某个事件,那么在同一个事件序列当中,此方法不会再被调用,返回结果表示是否拦截当前事件。
若返回值为 True 事件会传递到自己的 onTouchEvent();
若返回值为 False 传递到子 view 的 dispatchTouchEvent()。public boolean onTouchEvent(MotionEvent event)
在 dispatchTouchEvent 方法内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前 view 无法再次接收到事件。
若返回值为 True,事件由自己处理,后续事件序列让其处理;
若返回值为 False,自己不消耗事件, 向上返回让父容器的 onTouchEvent 接受处理,一直到 Activity 的 onTouchEvent- 优先级:OnTouch > onTouchEvent > OnClick > OnLongClick
- 某个 view 一旦开始处理事件,如果它不消耗
ACTION_DOWN
事件,那么同一事件序列的其他事件都不会再交给它来处理,并且**后续事件将重新交给它的父容器去处理(父容器的 onTouchEvent 方法);**如果它消耗ACTION_DOWN
事件,但是不消耗其他类型事件,那么这个点击事件会消失,父容器的 onTouchEvent 方法不会被调用,当前 view 依然可以收到后续的事件,但是这些事件最后都会传递给 Activity 处理。 - ViewGroup 默认不拦截任何事件,因为它的 onInterceptTouchEvent 方法默认返回 false
- View 的 enable 属性不影响 onTouchEvent 的默认返回值。哪怕一个 view 是 disable 状态,只要它的 clickable 或者 longClickable 有一个是 true,那么它的 onTouchEvent 就会返回 true。
- ViewGroup 的 dispatchTouchEvent 方法中有一个标志位
FLAG_DISALLOW_INTERCEPT
,这个标志位就是通过子 view 调用 requestDisallowInterceptTouchEvent 方法来设置的,一旦设置为 true,那么 ViewGroup 不会拦截该事件。
由此可见,整个事件分发是由 Acitivity 开始的向下递归过程
滑动
- 通过 view 本身提供的 scrollTo 和 scrollBy 方法:操作简单,适合对 view 内容的滑动;
- 通过动画给 view 施加平移效果来实现滑动:操作简单,适用于没有交互的 view 和实现复杂的动画效果;
- 通过改变 view 的 LayoutParams 使得 view 重新布局从而实现滑动:操作稍微复杂,适用于有交互的 view。
scrollTo 和 scrollBy 方法只能改变 view 内容的位置而不能改变 view 在布局中的位置。 scrollBy 是基于当前位置的相对滑动,而 scrollTo 是基于所传参数的绝对滑动。通过 View 的 getScrollX 和 getScrollY 方法可以得到滑动的距离。
Scroller
Scroller 的工作原理:Scroller 本身并不能实现 view 的滑动,它需要配合 view 的 computeScroll 方法才能完成弹性滑动的效果,它不断地让 view 重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔 Scroller 就可以得出 view 的当前的滑动位置,知道了滑动位置就可以通过 scrollTo 方法来完成 view 的滑动。就这样,view 的每一次重绘都会导致 view 进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动。
使用:
- 初始化 Scroller 对象,一般在 view 初始化的时候同时初始化 scroller;
- 重写 view 的 computeScroll 方法,computeScroll 方法是不会自动调用的,只能通过 invalidate->draw->computeScroll 来间接调用,实现循环获取 scrollX 和 scrollY 的目的,当移动过程结束之后,Scroller.computeScrollOffset 方法会返回 false,从而中断循环;
- 调用 Scroller.startScroll 方法,将起始位置、偏移量以及移动时间(可选) 作为参数传递给 startScroll 方法。
private void ininView(Context context) {
setBackgroundColor(Color.BLUE);
// 初始化 Scroller
mScroller = new Scroller(context);
}
@Override
public void computeScroll() {
super.computeScroll();
// 判断 Scroller 是否执行完毕
if (mScroller.computeScrollOffset()) {
((View) getParent()).scrollTo( mScroller.getCurrX(), mScroller.getCurrY());
// 通过重绘来不断调用 computeScroll
invalidate();//很重要
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = (int) event.getX();
lastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
((View) getParent()).scrollBy(-offsetX, -offsetY);
break;
case MotionEvent.ACTION_UP:
// 手指离开时,执行滑动过程
View viewGroup = ((View) getParent());
mScroller.startScroll( viewGroup.getScrollX(), viewGroup.getScrollY(),
-viewGroup.getScrollX(), -viewGroup.getScrollY());
invalidate();//很重要
break;
}
return true;
}
滑动冲突
分两类:
- 外部滑动方向与内部滑动方向一致
- 外部滑动方向与内部滑动方向不一致
1、外部拦截法
重写父容器的 onInterceptTouchEvent 方法,根据适当条件来决定 return true/false
,其他均不需要做修改。
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (父容器需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
2、内部拦截法
父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器来处理。 需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作
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 = y - mLastY;
if (当前 view 需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
参考
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: Android 类初始化与方法调用
下一篇: MyBatis 介绍和使用
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论