A view occupies a rectangular area on the screen and is responsible for drawing and event handling
View占据了屏幕上一个矩形区域,并负责他的绘制和事件监听
强烈推荐 中文世界最好的自定义View教程
要点:
- 矩形区域
- 绘制和事件监听
绘制流程
主要是按照以下流程
- measure
- layout
- draw
有一篇详细的文章介绍深入理解Android之View的绘制流程
onMeasure
Called to determine the size requirements for this view and all of its children
- onMeasure()方法用于测量视图的大小
- measure()是final的,但是onMeasure()却是可以进行重写的
- View系统的绘制流程会从ViewRoot的performTraversals()方法中开始,在其内部调用View的measure方法。measure()接收两个参数,widthMeasureSpec和heightMeasureSpec
- 一般调用setMeasuredDimension()
- RelativeLayout会让子View调用两次onMeasure,LinearLayout在有weight时,也会调用2次,其他情况一次
- 尽量使用padding代替margin,因为RelativeLayout的子View如果高度和RelativeLayout不同,会引发效率问题。
MeasureSpec
is used by views to tell their parents how they want to be measured and positioned.
MeasureSpecs are used to push requirements down the tree from parent to
child.
MeasureSpec是一个32位int类型
- 前2位,测量的模式
- EXACTLY
- 具体数值
- match_parent
- AT_MOST
- wrap_content
- UNSPECIFIED
- EXACTLY
- 后32位,测量的大小
MeasureSpec = 父容器的规则+View的LayoutParams
如果父View是match_parent,子View是match_parent,则子View的MeasureSpec为AT_MOST
1 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); |
onLayout
Called when this view should assign a size and position to all of its children
- layout()方法中,首先会调用setFrame()方法来判断视图的大小是否发生过变化,以确定是否有必要对当前的视图进行重绘。如果需要重绘则调用onLayout方法。
- onLayout()方法View中是空方法,ViewGroup中为抽象方法,每个ViewGroup的子类必须重写这个方法。
- 一般由ViewGroup重写,调用childView的layout方法
measuredWidth和width的区别
measuredWidth
These dimensions define how big a view wants to be within its parent (see Layout for more details.
测量后的值,是View期望分配的值,是onMeasure中setMeasuredDimension设置的值
在measure()过程结束后就可以得到
width
These dimensions define the actual size of the view on screen, at drawing time and
after layout. These values may, but do not have to, be different from the
measured width and height.
layout之后才能获取到的值,表示画面在屏幕上的真是宽高,未必和measuredWidth相同
比如自定义View的策略就是layout(0,0,50,50),而不管measuredDiemnsion
onSizeChange
Called when the size of this view has changed
- 此处的size不一定与getMeasuredSize()一致,需要看layout的策略
- 如果自定义View需要持有mWidth,mHeight,一般在此获得
Draw
六步骤
- Draw the background
- If necessary, save the canvas’ layers to prepare for fading
- Draw view’s content (onDraw)
- Draw children
- If necessary, draw the fading edges and restore layers
- Draw decorations (scrollbars for instance)
onDraw
Called when the view should render its content
一般自定义View在此方法中通过canvas进行绘制
交互事件
事件拦截机制
- ViewGroup
- dispatchTouchEvent
- onInterceptTouchEvent
- onTouchEvent
- View
- diapatchTouchEvent
- onTouchEvent
onTouchEvent
Called when a touch screen motion event occurs
注意如果在其中生成点击事件,调用onClickListener,最好使用performClick(),保持系统内部一致性
分类
- ACTION_DOWN
- ACTION_MOVE,ACTION_UP发生的前提是曾经发生了ACTION_DOWN,如果没有消费ACTION_DOWN,系统不会捕获ACTION_MOVE和ACTION_UP。onTouchEvent的事件回传到父控件 只会发生在ACTION_DOWN中。ACTION_MOVE和ACTION_UP会跟随ACTION_DOWN进行处理
- ACTION_UP
- ACTION_MOVE
- ACTION_CANCEL
- ACTION_CANCEL是收到事件前驱后,后续事件被父空间拦截的情况下产生的
- ACTION_OUTSIDE
- ACTION_CANCEL
- ACTION_POINTER_DOWN
- ACTION_POINTER_UP
一些辅助类
TouchSlop
系统能识别出的最小滑动距离
1 | ViewConfiguratin.get(this).getScaledTouchSlop(); |
Velocity Tracker
常用来得到滑动速度
1 | //init() |
GestureDetector
辅助检测用户的手势动作
1 | GestureDetector gestureDetector = new GestureDetector(new GestureDetector.OnGestureListener() { |
Region
如果想要监听一个区域,而不是View的矩形区域的点击事件,该怎么办?
传统的setOnClickListener只是监听整个View的矩形区域,想要监听View的特定区域需要使用Region判断点击事件。
比如说下面有一个圆形的自定义View
1 | public class RoundImageView extends ImageView { |
滑动
实现方法
- scrollTo/By
- offset()
- ObjectAnimator
- 改变MarginLayoutParams
- layout
- ObjectAnimatior
- Scroller
- ViewDraggerHelper
Scroller
原理是通过不断的computeScroll 不断的重新绘制
1 | //init() |
ViewDraggerHelper
- 基本可以实现不同的滑动、拖放需求
- 写在ViewGroup中
- 内部也是使用Scroller来实现的,所以也需重写computeScroll()
1 | mViewDraggerHelper = ViewDragHelper.create(this,callback); |
滑动冲突
- 父控件进行拦截
- dispatchTouchEvent()
- onInterceptTouchEvent()
- 子控件进行拦截
- getParent().requestDisallowInterceptTouchEvent(true);
- 一般在actiondown中判断是否拦截,传入true,在up中传入false取消拦截
四大构造方法
- View(Context)
- 代码中构造
- View(Context,AttributeSet)
- style默认是使用Context的主题
- View(Context,AttributeSet,int)
- View(Context,AttributeSet,int,int)
- 归根结底XML构造方法都会调用到此方法
- 这里面有TypedArray的标准读取方法,自定义View的模板可以直接模仿此处
一些杂谈
BitMask
一个有趣的地方,因为View有很多属性,比如clickable、focusable等等,可能高达几十上百个。
这里并没有采用简单的boolean值,而是采用一个全局的int变量和对应的静态BITMASK进行判断,
比如:
1 | static final int PFLAG_FOCUSED = 0x00000002; |
这样只需要极少量的mFlags,和所有View公用的static bitmask,就可以指示出极多的属性值,是一个非常好的思路。
1 | public static final int MASK = 0x00000006; |
归类
对于聚簇的Listener,封装成一个实体
1 | static class ListenerInfo { |
StringBuilder
- 指定大小,因为内部是使用char[]存储,默认大小为16,变长时候和ArrayList一样的策略,申请新char[]并逐个复制,能抠一点性能是一点。
- 相比String的+操作,虽然编译器有优化,但是在循环语句中还是会生成多个对象
方法中使用临时变量
保持原子性
1 | /** |
padding和margin
Even though a view can define a padding, it does not provide any support for
margins. However, view groups provide such a support.
某些情况下发现使用margin不奏效,不妨试试看使用padding