自定义View应该明白的基础知识

自定义View应该明白的基础知识

  • 自定义View应该明白的基础知识
  • 认识Android坐标系
    • 获取相对于父坐标的距离
    • MotionEvent中 event 获取的坐标
  • 自定义View绘制流程
  • 关于自定义View的构造函数
  • 测量View的大小
    • MeasureSpec 中的3种测量方式
    • 确定View的大小
  • 确定子View的位置
  • 绘制View onDraw
  • View事件分发
    • 事件分发机制
    • View中的OnTouch事件
    • View的滑动冲突
  • View的滑动
    • ScrollTo和ScrollBy
    • 使用属性动画
    • 设置LayoutParams的值
  • View的工作原理
    • MeasureSpec 测量规则
      • SpecMode 由三类
    • 3个流程 mesure layout draw
      • Measure 过程
      • Layout过程
      • Draw过程
  • 参考

认识Android坐标系

自定义View应该明白的基础知识_第1张图片

Android的坐标系是从左上角开始,向左为X正方向,向下为Y正方向,与普通的坐标系有些区别。

获取相对于父坐标的距离

getTop();       //获取子View左上角距父View顶部的距离
getLeft();      //获取子View左上角距父View左侧的距离
getBottom();    //获取子View右下角距父View顶部的距离
getRight();     //获取子View右下角距父View左侧的距离

Android3.0 之后还添加了 x,y,translationX,translationY 四个值,这四个值也是相对于父View的,默认 translationX和translationY 等于0,并且存在以下关系:

x = left + translationX
y = top + translationY

当View发生平移时,left 和 top 不会改变,改变的是 x,y,translationX,translationY 四个值

自定义View应该明白的基础知识_第2张图片

MotionEvent中 event 获取的坐标

event.getX();       //触摸点相对于其所在组件坐标系的坐标
event.getY();
event.getRawX();    //触摸点相对于屏幕默认坐标系的坐标
event.getRawY();

自定义View绘制流程

自定义View应该明白的基础知识_第3张图片

关于自定义View的构造函数

public void SloopView(Context context) {}
public void SloopView(Context context, AttributeSet attrs) {}
public void SloopView(Context context, AttributeSet attrs, int defStyleAttr) {}
public void SloopView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {}

构造函数是以上四种,最常用的是带一个参数何两个参数的。调用的时机为:

SloopView view = new SloopView(this); // 调用一个参数的构造函数
  //调用两个参数的构造函数
  <com.sloop.study.SloopView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

如果使用了自定义属性,则需要定义3个参数的构造函数,这里省略。

测量View的大小

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthsize = MeasureSpec.getSize(widthMeasureSpec);      //取出宽度的确切数值
    int widthmode = MeasureSpec.getMode(widthMeasureSpec);      //取出宽度的测量模式
    int heightsize = MeasureSpec.getSize(heightMeasureSpec);    //取出高度的确切数值
    int heightmode = MeasureSpec.getMode(heightMeasureSpec);    //取出高度的测量模式
}

MeasureSpec 中的3种测量方式

模式 二进制数值 描述
UNSPECIFIED 00 默认值,父控件没有给子view任何限制,子View可以设置为任意大小。
EXACTLY 01 表示父控件已经确切的指定了子View的大小。
AT_MOST 10 表示子View具体大小没有尺寸限制,但是存在上限,上限一般为父View大小。

注意:
如果对View的宽高进行修改了,不要调用super.onMeasure(widthMeasureSpec,heightMeasureSpec);要调用setMeasuredDimension(widthsize,heightsize); 这个函数。

确定View的大小

 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
    }

确定子View的位置

child.layout(l, t, r, b);
名称 说明 对应的函数
l View左侧距父View左侧的距离 getLeft();
t View顶部距父View顶部的距离 getTop();
r View右侧距父View左侧的距离 getRight();
b View底部距父View顶部的距离 getBottom();

绘制View( onDraw() )

真正绘制View的部分,当计算玩View的大小,并且通过onLayout确定了它的位置,就可以通过onDraw() 绘制出View。

View事件分发

这样一个View层级:

自定义View应该明白的基础知识_第4张图片

结构如下:

自定义View应该明白的基础知识_第5张图片

事件分发流程:

当有点击事件,首先是Activity捕获到,一直传递到View,如果这一个过程事件都没有被处理,则事件会被反向传播给Activity,如果还没有被处理,则抛弃。

Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View
Activity <- PhoneWindow <- DecorView <- ViewGroup <- ... <- View

如果在View1 上发生touch事件,会依次调用下面的方法,红色是正向传播,绿色是回传:

自定义View应该明白的基础知识_第6张图片

事件分发机制

当一个点击事件发生时,一般会经历下面3个重要的方法:

  1. dispatchTouchEvent(MotionEvent ev)
  2. onInterceptTouchEvent(MotionEvent ev)
  3. onTouchEvent(MotionEvent ev)

dispatchTouchEvent:用来进行事件分发,如果事件能传递给当前View,这个方法一定会被执行

onInterceptTouchEvent:在上述方法内部调用,用来判断是否拦截某个事件

onTouchEvent:在 dispatchTouchEvent 中调用,用于处理点击事件

三者的调用如下伪代码:
自定义View应该明白的基础知识_第7张图片

View中的OnTouch事件

onTouchListener 的优先级比 onTouch 高,如果 listener中返回 true,表示已经处理,就不会将事件继续传递给 onTouch,这样做的好处是方便外界处理点击事件。

View的滑动冲突

这里先省略,详细请看《Android开发艺术探索》

View的滑动

一般有以下几种常用的方法:
1. scrollTo和scrollBy
2. 通过动画(平移动画,属性动画两种)
3. 改变View的LayoutParams使得View重新布局

ScrollTo和ScrollBy

  1. 如果要使View向右下方向滑动,那么 传入的 x,y的值应该为负数。
  2. 移动时,只有View的内容移动,但是View本身不移动

使用属性动画

mLauncher1.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    float x = mLauncher1.getX();
    float y = mLauncher1.getY();
    ObjectAnimator.ofFloat(mLauncher1, "translationX", x, x + 100).setDuration(100).start();
    ObjectAnimator.ofFloat(mLauncher1, "translationY", y, y + 100).setDuration(100).start();
  }
});

设置LayoutParams的值

需要注意的是,这种情况会引起View的重新绘制,效率要低一些

mlauncher2.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mlauncher2.getLayoutParams();
    params.leftMargin += 100;
    params.topMargin += 100;
    mlauncher2.requestLayout();
  }
});

View的工作原理

MeasureSpec 测量规则

MeasureSpec是一个int型的值, 由两个值 Mode(高2位) 和 Size(低30位)组成。

子View的 MeasureSpec由 父容器的MeasureSpec和子View的LayoutParams共同决定

SpecMode 由三类

UNSPECIFIED:父容器不对View由任何的限制,一般系统中用,不用关注。
EXACTLY:父容器已经测量出View所需的大小,View的大小由 SpecSize所指定,它对对应于LayoutParams中的match_parent
AT_MOST:父容器指定一个可用的大小 SpecSize给View,对应 wrap_content

3个流程 mesure, layout, draw

自定义View应该明白的基础知识_第8张图片

Measure 过程

分为View的Measure过程和ViewGroup的Measure过程,ViewGroup的其实就是计算自己的,并且调用所有子View的Measure方法。

如何获取View的宽高:
1. 如果要获取某个View的宽高,在onCcreate,onResume,onStart这些方法中都是不能获取的。因为Activity和View的Measure不是同步执行的。可以在 onWindowFocusChanged 这个方法中获取,表示View的初始化已经完毕,会调用这个方法。但是它会被多吃调用,失去焦点和得到焦点都会被调用依次。

  1. view.post(runnable) 中获取
  2. ViewTreeObserver,onGlobalLayoutListener中可以,但是当View树状态改变时都会被调用。
  3. view.measure( measureSpecWidth, measureSpecHeight) 手动对View的measure中获得。根据LayoutParams获得,但是这种情况比较复杂,分为下面几种:
    • match_parent:基本无法获得
    • 具体值:width = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY)
    • wrap_content:width = MeasureSpec.makeMeasureSpec((1<<30)-1, MeasureSpec.AT_MOST)

Layout过程

测量自己应该在父容器中的位置

一般在onMeasure 中可以获取View的最终大小,但是极端情况也可能不正确。例如 onLayout中调用:super.layout(l,t,r+100,b+100),就会导致view的宽高都加100

Draw过程

一般分为下面4个步骤:
1. background.draw(canvas) 画背景
2. onDraw 画自己
3. dispatchDraw 画children
4. onDrawScrollBar 画装饰

参考

《Android开发艺术探索》
自定义View合集

你可能感兴趣的:(Android,进阶)