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

Posted DakerYi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义View应该明白的基础知识相关的知识,希望对你有一定的参考价值。

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

认识android坐标系

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 四个值

MotionEvent中 event 获取的坐标

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

自定义View绘制流程

关于自定义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种测量方式

模式二进制数值描述
UNSPECIFIED00默认值,父控件没有给子view任何限制,子View可以设置为任意大小。
EXACTLY01表示父控件已经确切的指定了子View的大小。
AT_MOST10表示子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);
名称说明对应的函数
lView左侧距父View左侧的距离getLeft();
tView顶部距父View顶部的距离getTop();
rView右侧距父View左侧的距离getRight();
bView底部距父View顶部的距离getBottom();

绘制View( onDraw() )

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

View事件分发

这样一个View层级:

结构如下:

事件分发流程:

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

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

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

事件分发机制

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

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

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

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

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

三者的调用如下伪代码:

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

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合集

以上是关于自定义View应该明白的基础知识的主要内容,如果未能解决你的问题,请参考以下文章

自定义View入门-绘制基础

自定义View_1_关于View,ViewGroup的测量和绘制流程

Android自定义View 基础篇

Android自定义View 基础篇

自定义控件知识储备-LayoutParams的那些事

自定义控件知识储备-LayoutParams的那些事