View的杂谈

A view occupies a rectangular area on the screen and is responsible for drawing and event handling

View占据了屏幕上一个矩形区域,并负责他的绘制和事件监听

强烈推荐 中文世界最好的自定义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
  • 后32位,测量的大小
MeasureSpec = 父容器的规则+View的LayoutParams

如果父View是match_parent,子View是match_parent,则子View的MeasureSpec为AT_MOST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 					    			    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

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:
//getSuggestedMinimumWidth()
result = size;
break;
case MeasureSpec.AT_MOST:
//子View为wrap_content时为父view的size
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

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

六步骤

  1. Draw the background
  2. If necessary, save the canvas’ layers to prepare for fading
  3. Draw view’s content (onDraw)
  4. Draw children
  5. If necessary, draw the fading edges and restore layers
  6. 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
2
3
4
5
6
7
8
9
10
11
//init()
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);

//int参数是以毫秒为单位的时间
velocityTracker.computeCurrentVelocity(int);
velocityTracker.getXVelocity();

//destroy()
velocityTracker.clear();
velocityTracker.recycle();

GestureDetector

辅助检测用户的手势动作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
GestureDetector gestureDetector = new GestureDetector(new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return false;
}

@Override
public void onShowPress(MotionEvent e) {

}

@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}

@Override
public void onLongPress(MotionEvent e) {

}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
});

//add this to onTouchEvent
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
}

Region

如果想要监听一个区域,而不是View的矩形区域的点击事件,该怎么办?

传统的setOnClickListener只是监听整个View的矩形区域,想要监听View的特定区域需要使用Region判断点击事件。

比如说下面有一个圆形的自定义View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class RoundImageView extends ImageView {

private Region mClickRegion;

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
mRadius = Math.min(w, h) / 2;

Path clickPath = new Path();
clickPath.addCircle(mRadius, mRadius, mRadius, Path.Direction.CW);
//设定clickRegion,将其与View绑定
mClickRegion.setPath(clickPath, new Region(0, 0, w, h));
}

@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
//判断交互事件是否在Region中
return mClickRegion.contains(x, y) && super.onTouchEvent(event);
}
}

滑动

实现方法

  • scrollTo/By
  • offset()
  • ObjectAnimator
  • 改变MarginLayoutParams
  • layout
  • ObjectAnimatior
  • Scroller
  • ViewDraggerHelper

Scroller

原理是通过不断的computeScroll 不断的重新绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//init()
Scroller mScroller = new Scroller(context);

//invoke
mScroller.startScroll(getScrollX(),getScrollY(),-50,-50);
invalidate();

//系统在draw()方法中调用此方法
//但因为其不会自动调用,所以需要invalidate()>draw()>computeScroll()
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
//true means has not finished
((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}

ViewDraggerHelper

  • 基本可以实现不同的滑动、拖放需求
  • 写在ViewGroup中
  • 内部也是使用Scroller来实现的,所以也需重写computeScroll()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
mViewDraggerHelper = ViewDragHelper.create(this,callback);

@Override
public boolean onInterceptTouchEvent(MotionEvent ev){
return mViewDraggerHelper.shouldInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event){
mViewDragHelper.processTouchEvent(event);
return true;
}

@Override
public void computeScroll(){
if(mViewDragHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(this);
}
}

private ViewDragHelper.Callback callback = new ViewDragHelper.Callback(){

@Override
public boolean tryCaptureView(View child , int pointerId){
//指定哪一个子View可以被移动
return child == XXXView;
}

//返回值为0,表示在该方向上不发生滑动
@Override
public int clampViewPositionVertical(View child,int top,int dy){
return top;
}

@Override
public int clampViewPositinoHorizontal(View child,int left,int dx){
return left;
}

//结束拖动后调用
@Override
public void onViewReleased(View releasedChild,float xvel,float yvel){
super.onViewReleased(releasedChild,xvel,yvel);
if(mMainView.getLeft()<500){
mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
ViewCompat.postInvalidateOnAnimation(this);
}
}
}

滑动冲突

  • 父控件进行拦截
    • 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
2
3
4
5
static final int PFLAG_FOCUSED = 0x00000002;
int mPrivateFlags;
public boolean hasFocus() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0;
}

这样只需要极少量的mFlags,和所有View公用的static bitmask,就可以指示出极多的属性值,是一个非常好的思路。

1
2
3
4
public static final int MASK = 0x00000006;
public static final int FLAG_A = 0x00000000;
public static final int FLAG_B = 0x00000002;
public static final int FLAG_C = 0x00000004;

归类

对于聚簇的Listener,封装成一个实体

1
2
3
4
5
6
7
8
9
10
11
static class ListenerInfo {
protected OnCreateContextMenuListener mOnCreateContextMenuListener;

************

private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;

OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;
}

ListenerInfo mListenerInfo;

StringBuilder

  1. 指定大小,因为内部是使用char[]存储,默认大小为16,变长时候和ArrayList一样的策略,申请新char[]并逐个复制,能抠一点性能是一点。
  2. 相比String的+操作,虽然编译器有优化,但是在循环语句中还是会生成多个对象

方法中使用临时变量

保持原子性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Returns the width of the vertical scrollbar.
*
* @return The width in pixels of the vertical scrollbar or 0 if there
* is no vertical scrollbar.
*/
public int getVerticalScrollbarWidth() {
ScrollabilityCache cache = mScrollCache;
if (cache != null) {
ScrollBarDrawable scrollBar = cache.scrollBar;
if (scrollBar != null) {
int size = scrollBar.getSize(true);
if (size <= 0) {
size = cache.scrollBarSize;
}
return size;
}
return 0;
}
return 0
}

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

0%