View体系和自定义View:

目录

View和ViewGroup:

坐标系:

1.Android坐标系:

2.View坐标系:

2.1:View获取自身的宽和高:

2.2:View自身的坐标:

2.3:MotionEvent提供的方法:

View的滑动:

1.layout方法:

2.offsetLeftAndRight()与offsetTopAndBottom():

3.LayoutParams(改变布局参数):

4.动画:

5.scrollTo与scrollBy:

6.Scroller:

6.1:解析Scroller:

属性动画:

1.ObjectAnimator:

2.ValueAnimator:

3.动画的监听:

4.组合动画(AnimatorSet):

5.组合动画(PropertyValuesHolder):

6.XML中使用属性动画:

View的事件分发机制:

1.解析Activity的构成:

2.解析View的事件分发机制:

2.1:从上往下:

2.2:从下往上:

2.3:图:

View的工作流程:

1.View的工作流程入口:

1.1:DecorView被加载到Window中:

1.2:ViewRootImpl的performTraversals方法:

2. 理解 MeasureSpec:

3.View的measure流程:

3.1:View的measure流程:

3.2:ViewGroup的measure流程:

4.View的layout流程:

5.View的draw流程:

自定义View:

1.继承系统控件的自定义View:

2.继承View的自定义View:

3.自定义属性:


View和ViewGroup:

对于一个APP来说,与用户的交互,将内容展示给用户,即是十分重要的,也是十分必要的,而这些就是通过一个个View通过扩展实现的。View就像现实世界的原子一样,是实现界面展示和交互的最小颗粒。

View是Android所有控件的基类,我们平常使用的TextView和ImageView都是继承于View,而常用的布局控件LinearLayout继承于ViewGroup。ViewGroup又是什么呢? ViewGroup 可以理解为 View 的组合,它可以包含很多 View以及ViewGroup,而它包含的ViewGroup又可以包含View和ViewGroup。依此类推,形成一个View树。需要注意的是ViewGroup也继承自View, ViewGroup作为View或者 ViewGroup这些组件的容器,派生了多种布局控件子类,比如LinearLayout、RelativeLayout等。一般来说,开发Andrord应用的用户界面都不会直接使用View和ViewGroup,而是使用这两大基类的派生类。

坐标系:

Android系统中有两种坐标系,分别是Android坐标系View坐标系

1.Android坐标系:

在Android中,将屏幕左上角的顶点作为Android坐标系的原点,这个原点向右是X轴正方向,向下是Y轴正方向。另外在触控事件中,使用getRawY方法和getRawX方法获取的坐标也是Android坐标系的坐标。

2.View坐标系:

除了Android坐标系,还有一个View坐标系。它与Android坐标系并不冲突,两者是共同存在的,它们一起来帮助开发者更好的控制View。

2.1:View获取自身的宽和高:

width=getRight()-getLeft();
height=getBottom()-getTop();

当然这么做有些麻烦,因为系统已经向我们提供了获取宽和高的方法,但是这些方法都是基于上面的逻辑的。

//高
​public final int getHeight(){
        return mBottom-mTop;
}

//宽
public final int getWidth(){
        return mRight-mLeft;
}

2.2:View自身的坐标:

通过如下方法可以获得View到其父控件(ViewGroup)的距离。

  • getTop():获取View自身顶边到其父布局顶边的距离。
  • getLeft():获取View自身左边到其父布局左边的距离。
  • getRight():获取 View自身右边到其父布局左边的距离。
  • getBottom():获取View自身底边到其父布局顶边的距离。

2.3:MotionEvent提供的方法:

上图中间的那个圆点,假设就是我们触摸的点。我们知道无论是View还是ViewGroup最终的点击事件都会由onTouchEvent(MotionEvent event)方法来处理。MotionEvent 在用户交互中作用重大,其内部提供了很多事件常量,比如:我们常用的ACTION_DOWN,ACTION_UP和ACTION_MOVE。此外,MotionEvent也提供了获取焦点坐标的各种方法:

  • getX():获取点击事件距离控件左边的距离,即视图坐标。
  • getY():获取点击事件距离控件顶边的距离,即视图坐标。
  • getRawX():获取点击事件距离整个屏幕左边的距离,即绝对坐标,
  • getRawY():获取点击事件距离整个屏幕顶边的距离,即绝对坐标。

View的滑动:

View的滑动是Android实现自定义控件的基础,同时在开发中我们也难免会遇到View的事动处理,其实不管是哪种滑动方式,其基本思想都是类似的,当点击事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并且算出偏移量,并通过偏移量来修改View的坐标,实现 View滑动有很多种方法,在这里主要讲解6种滑动方法,分别是:

  1. layout()
  2. offsetLeftAndRight()与offsetTopAndBottom()
  3. LayouParams
  4. 动画
  5. scrollTo与scrollBy
  6. Scroller

1.layout方法:

绘制View的时候会调用onLayout方法来设置显示的位置,因此我们同样也可以通过修改View的left, top,right,bottom这4种属性来控制View的坐标。首先在自定义一个View的onTouchEvent方法中获取触摸点的坐标,接下来我们在ACTION_MOVE事件中计算偏移量,再调用layout方法重新放置这个自定义View的位置,在每次移动时都会调用layout方法对屏幕重新布局,从而达到移动View的效果。

public boolean onTouchEvent(MotionEvent event){
    //获取手指触摸点的横坐标和纵坐标
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()){
        
        //手指初次触摸到屏幕时
        case MotionEvent.ACTION_DOWN;
            lastX= x;
            lastY =y;

        //滑动时触发
        case MotionEvent.ACTION_MOVE:
        //计算移动的距离
            int offsetX= x - lastX;
            int offsetY = y - lastY;

           //调用1ayout方法来重新放置它的位置
           layout (getLeft() + offsetX, getTop() + offsetY,
                   getRight() + offsetx,getBottom() + offsetY);
           break;
    }
return true
}

2.offsetLeftAndRight()与offsetTopAndBottom():

这两种方法和layout方法的效果差不多,使用方式也差不多。只需要把ACTION_MOVE中的代码替换为:

        //滑动时触发
        case MotionEvent.ACTION_MOVE:
        //计算移动的距离
            int offsetX= x - lastX;
            int offsetY = y - lastY;
            
           //对left和right进行偏移
           offsetLeftAndRight(offsetX);
           //对top和bottom进行偏移
           offsetTopAndBottom(offsetY);
           break;


3.LayoutParams(改变布局参数):

LayoutParams主要保存了一个View的布局参数,因此可以通过LayoutParams来改变View的布局参数,从而达到改变View位置的效果。假设父控件是LinearLayout,那么可以把ACTION_MOVE替换为:

case MotionEvent_MOVE:
    LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)getLayoutParams();

    layoutParams.leftMargin=getLeft()+offsetX;
    layoutParams.topMargin=getTop()+offsetY;
    setLayoutParams(layoutParams);

4.动画:

也可以采用动画来移动,在res目录中新建文件夹并且创建translate.xml。

  <translate
        android:duration="800"
        android:fromYDelta="0"
        android:toYDelta="100">
  </translate>

其中:

  • fromXDelta :属性为动画起始时 X坐标上的位置  
  • toXDelta:属性为动画结束时 X坐标上的位置  
  • fromYDelta:属性为动画起始时 Y坐标上的位置  
  • toYDelta:属性为动画结束时 Y坐标上的位置 

接着在代码中调用:

mCustomView.setAnimation(AnimationUtils.loadAnimation(this,R.anim.translate));

但是这样在移动之后,又会回到原来的位置,可以在xml中加上fillAfter="true"解决。但是View动画并不能改变View的位置参数,也就是说在平移之后,点击平移后的位置并不会触发点击事件,而在平移之前的位置点击会触发点击事件。使用属性动画可以解决,因为它不仅仅可以移动,还可以改变View的位置参数。

5.scrollTo与scrollBy:

scrollTo(x,y)表示移动到一个具体的坐标点,而scrollBy(dx,dy)则表示移动的增量为dx,dy。其中scrollBy最终也是要调用scrollTo的。源码如下:

public void scrollTo(int x,int y){
    if (mScrollX!=x || mScrollY!=y){
        int oldX=mScrollX;
        int oldY=mScrollY;
        mScrollX=x;
        mScrollY=y;
        invalidateParentCaches();
        onScrollChanged(mScrollX,mScrollY,oldX,oldY);
        if(!awakenSrcollBars()){
            postInvalidateOnAnimation();
        }
    }
}


public void scrollBy(int x,int y){
        scrollTo(mScrollX+x,mScrollY+y)
    }

在View中使用移动的是View的内容,在ViewGroup中使用是移动的其所有子View的内容。在ACTION_MOVE将代码替换为:

((View)getParent()).scrollBy(-offsetX,-offsetY);

就可以实现随手指移动的效果,这里的偏移量是负值,才能达到自己想要的效果。

6.Scroller:

使用scrollTo与scrollBy方法时这个过程是瞬间完成的,而Scroller可以实现有过渡效果的滑动,这个过程是有一定的时间间隔的。Scroller本身是不能实现View的滑动的,它是与View的computeScroll方法配合才能实现弹性滑动的效果。使用方式为:

//实现View平滑向右400

//初始化Scroller
public MyView(Context context,AttributeSet attrs){
        super(context,arrts);
        mScroller = new Scroller(context);
    }

//重写computeScroll方法
@Override
    public void computeScroll(){
        super.computeScroll();
        if (mScroller.computeScrollOffset()){
            ((View)getParent())scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            invalidate();
        }
    }

//在View中封装一个smoothScrollTo方法,调用startScroll方法
public void smoothScrollTo(int destX,int destY){
        int scrollX=getScrollX();
        int delta=destX-scrollX;
        mScroller.startScroll(scrollX,0,delta,0,2000);
        invalidate();
    }

//客户端调用
mMyview.smoothScrollTo(-400,0);

也就是说:

  • 初始化一个Scroller
  • 重写computeScroll方法,系统会在绘制View的时候在draw方法中调用它,在这个方法中,通过调用父类的ScrollTo方法并且通过Scroller来不断的获取当前的滑动值,每次滑动一段距离,就调用invalidate方法进行重绘,而重绘又会调用computeScroll方法,这样实现不断移动一段距离实现平滑移动的效果。

6.1:解析Scroller:

首先要想使用Scroller,必须先调用new Scroller()。先来看看Scroller的构造方法。它有三个构造方法,通常情况下,我们都使用Scroller(Context context,Interpolator interpolator)这个构造方法。有两个构造方法需要传进去一个插值器。如果不传,则使用默认的插值器。

插值器:简单来说,Interpolator负责控制动画变化的速率,即确定了 动画效果变化的模式,使得基本的动画效果能够以匀速、加速、减速、抛物线速率等各种速率变化。

之后就是 Scroller的startScroller方法了:在startScroll方法中并没有调用类似开启滑动的方法,而是保存了传进来的各种参数:startx和starty表示滑动开始的起点,ds和dy表示滑动的距离,duration则表示滑动持续的时间,所startScroll方法只是用来做前期准备的,并不能使View进行滑动。关键是我们在startScroll方法后调用了invalidate方法,这个方法会导致View的重绘,而View的重绘会调用View的draw方法,draw方法又会调用View的computeScroll方法。

//重写computeScroll方法
@Override
    public void computeScroll(){
        super.computeScroll();
        if (mScroller.computeScrollOffset()){
            ((View)getParent())scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            invalidate();
        }
    }

我们在computeScroll方法中通过Scroller来获取当前的scrollX和serallY,然后调用scrollTo方法进行 View 的滑动,接着调用 invalidate方法来让 View进行重绘,重绘就会调用computeScroll方法来实现View的滑动。这样通过不断地移动一个小的距离并连贯起来就实现了平滑移动的效果。但是在Scroller中如何获取当前位置的scrollX和scrollY呢?即是调用scrolITo方法前会调用Scroller的computeScrollOffset方法。 

这里首先计算动画持续的时间 timePassed。如果动画持续的时间小于我们设置的滑动持续时间mDuration,则执行switch语句。因为在startScroll方法中的mMode值为SCROLL_MODE。所拟执行分支语句SCROLL_MODE,然后根据插值器(Intsrpolator)来计算出该时间段内移动的距离,赋值给mCurX和mCurY,这样我们就能通过Scroller来获取当前的scrollX和scrollY了。另外,computeScrollOffset的返回值如果为 true,则表示滑动未结束;computeScrollOffset的返回值如果为 false,则表示滑动结束。

属性动画:

在属性动画出现之前,Android系统提供的动画只有帧动画和View动画,View动画我们都了解,它提供了AlphaAnirmation,RotateAnimation,TranslateAnimation和ScaleAnimation这4种动画方式,并提供了AnimationSet动画集合来混合使用多种动画。随着Adroid 3.0属性动画的推出,View动画不再风光。相比于属性动画,View动画的一个非常大的缺陷凸显——其不具有交互性。当某个元素发生View动画后,其响应事件的位置依然在动画进行前的地方。所以View动画只能做普通的动画效果,要避免涉及交互操作,但是它的优点也非常明显:效率比较高,使用也方便。由于Android 3.0之前的版本已有的动画框架Animation存在一些局限性,也就是动画改变的只是显示,但View的位置没有发生变化,Wew移动后并不能响应事件,因此在Android 3.0中,谷歌公司就推出了新的动画框架,帮助开发者实现更加丰富的动画效果。在Animator框架中使用最多的就是AnimatorSet 和ObjectAnimator配合:使用ObjectAnimator进行更精细化的控制,控制一个对象和一个属性值;而使用多ObjectAnimator组合到AnimatorSet形成一个动画。属性动画通过调用属性的get方法,set方法来真实地控制一个View的属性值,因此,强大的属性动画框架基本可以实现所有的动画效果。

1.ObjectAnimator:

ObjectAnimator是属性动画最重要的类,创建一个ObjectAnimator 只需通过其静态工厂类直接返回一个ObjectAnimator 对象参数包括一个对象和对象的属性名字,但这个属性必须有get方法和set方法,其内部会通过Java反射机制来调用set方法修改对象的属性值。例如:实现平移动画:

ObjectAnimator mObjectAnimator=ObjectAnimator.ofFloat(view,"translationX",200);

mObjectAnimator.setDuration(300);
mObjectAnimator.start();

通过ObjectAnimator的静态方法,创建一个ObjectAnimator对象,查看ObjectAnimator的静志方法ofFloat():

它的第一个参数是要操作的Object;第二个参数是要操作的属性:最后一个参数是一个可变的float类型数组,需要传进去该属性变化的取值过程。这里设置了一个参数,变化到200。与View动画一样,也可以给属性动画设置显示时长、插值器等属性。

下面是一些可以直接使用的属性动画的属性值:

  • translationX和translationY:用来沿着X轴或者Y轴进行平移。
  • rotation、rotationX、rotationY:用来围绕View的支点进行旋转。
  • PrivotX和PrivotY:控制View对象的支点位置,围绕这个支点进行旋转和缩放变换处理。默认该支点位置就是View对象的中心点。
  • alpha:透明度,默认值是1(不透明),0代表完全透明。
  • x和y:描述View对象在其容器中的最终位置。

需要注意的是,在使用ObjectAnimator的时候,要操作的属性必须要有get方法和set方法,不然ObjectAnimator就无法生效。如果一个属性没有get方注set方法,也可以通过自定义属性类或包装类来间接地给这个属性增加 get方法和set方法。现在来看看如何通过包装类的方法给一个属性增加 get方法和set方法。

//包装类
private static class MyView{

       private View mTarget;
       private MyView(View mTarget){
           this.mTarget=mTarget;
       }

       public int getWidth(){
          return mTarget.getLayoutParams().width;
       }

       public void setwidth(int width){
           mTarget.getLayoutParams().width=width;
           mTarget.requestLayout();
      }

//调用方
MyView m=new MyView(button);
objectAnimator.ofInt(m,"width",500).setDuration(500).start();

2.ValueAnimator:

ValueAnimator不提供任何的动画效果,它更像是一个数值发生器,用来产生一定规律的数字,从而让调用者控制动画的实现过程通常情况下,在ValueAnimator的AnimatorUpdateListener中监听数值的变化,从而完成动画的变换。

//创建ValueAnimator并且启动
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 200);
valueAnimator.setDuration(2000);
valueAnimator.start();

//监听数值变化
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){
       @Override
       public void onAnimationUpdate(ValueAnimator animation) {
       int value = (int) animation.getAnimatedValue();
       Log.e(TAG, "value:" + value);
       }
});

3.动画的监听:

在动画执行的过程中,还可以为动画添加监听事件。完整的动画具有start,repeat,end,cancel这4个过程。

//添加监听​
ObjectAnimator mObjectAnimator=ObjectAnimator.ofFloat(view,"translationX",200);

mObjectAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                //动画开始的时候调用
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                //画结束的时候调用
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                //动画被取消的时候调用
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                //动画重复执行的时候调用

            }
        });

​

4.组合动画(AnimatorSet):

AnimatorSet类提供了一个play方法,如果我们向这个方法中传入一个Animator对象(ValueAnimator或者ObjectAnimator),将返回一个AnimatorSet.Builder的实例。这个Builder类是AnimatorSet的内部类,从源码中可以看出,Bulider类采用了建造者模式,每次调用方法都返回Builder自身用于继续构建。AnimatorSet.Builder中包括以下4个方法:

  • after(Animator anim):将现有动画插入到传入的动画之后执行。
  • after(long delay):将现有动画延迟指定的毫秒后执行。
  • before(Animator anim):将现有动画插入到传入的动画之前执行。
  • with(Animator anim):将现有动画和传入的动画同时执行。

AnimatorSet正是通过这几种方法来控制动画播放顺序的。

5.组合动画(PropertyValuesHolder):

除了上面的AnimatorSet类,还可以使用PropertyValuesHolder类来实现组合动画。不过这个组合动画没有AnimatorSet类所实现的组合动画复杂。使用PropertyValuesHolder类只能做到多个动画一起执行。当然我们还得结合objectAnimator.ofPropertyValuesHolder方法来使用,其第一个参数是动画的目标对象,之后的参数是实例,可以有多个这样的实例。

6.XML中使用属性动画:

和View动画一样,属性动画也可以直接写在XML文件中。在res文件中新建animator文件,在里面新建一个XML文件。

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android=""
    android:duration="3000"
    android:propertyName="alpha"
    android:repeatCount="-1"
    android:repeatMode="reverse"
    android:valueFrom="1.0"
    android:valueTo="2.0"
    android:valueType="floatType" />

之后,再引用XML文件:

Animator animator = AnimatorInflater.loadAnimator(this, R.animator.scale);
animator.setTarget(View);
animator.start();

View的事件分发机制:

1.解析Activity的构成:

点击事件用MotionEvent来表示,当一个点击事件产生后,事件最先传递给 Activity。所以首先要了解一下activity的构成。当我们写activity的时候,会调用setContentView方法来加载布局。setContentView方法调用了getWindow().setContentView方法,而getWindow方法返回的是mWindow,也就是PhoneWindow。PhoneWindow继承于抽象类Window,它的setContentView方法有一个installDecor方法,该方法通过generateDecor方法获取了一个DecorView。这个DecorView是Activity的根View,也是PhoneWindow的内部类。而installDecor方法中还有一个generateLayout方法根据不同的情况给layoutResource加载不同的布局。

一个Activity包含一个Window对象,该对象是由PhoneWindow来实现的。PhoneWindow将DecorView作为整个应用窗口的根View,这个DecorView又将屏幕划分为两个区域:一个区域是TitleView,另一个区域是ContentView,而我们平常做应用所写的布局正是展示在ContentView中的。

2.解析View的事件分发机制:

当我们点击屏幕时,就产生了点击事件,这个事件被封装成了一个类:MotionEvent。而当个MotionEvent产生后,系统就会将这个MotionEvent传递给View的层级,MotionEvent在View中的层级传递过程就是点击事件分发。在了解了什么是事件分发后,我们还需要了解事件分发的3个重要方法。点击事件有3个重要的方法,它们分别是:

  • dispatchTouchEvent(MotionEvent ev)——用来进行事件的分发。
  • onInterceptTouchEvent(MotionEvent ev)——用来进行事件的拦截,在dispatchTouchEvent方法中调用,需要注意的是View没有提供该方法。
  •  onTouchEvent(MotionEvent ev)——用来处理点击事件,在dispatchTouchEvent方法中进行调用。

2.1:从上往下:

当点击事件产生后,事件首先会传递给当前的Activity,这会调用 Activity的dispatchTouchEvent方法。当然,具体的事件处理工作都是交由Activity中的PhoneWindow来完。然后 PhoneWindow 再把事件处理工作交给DecorView,之后再由DecorView将事件处理工作交给根 ViewGroup。一般在事件传递中只考虑 ViewGroup的onInterceptTouchEvent方法,因为一般情况下我们不会重写dispatchTouchEvent方法。对于根ViewGroup,点击事件首先传递给它的dispatchTouchEvent方法。如果该ViewGroup的onlnterceptTouchEvent方法返回true,则表示它要拦截这个事件,这个事件就会交结它的 on TouchEvent方法处理;如果onlnterceptTouchEvent方法返回 false,则表示它不拦截这个事件,这个事件就会交给它的子元素的dispatchTouchEvent 来处理,如此反复下去。如果传还给底层的View,该View是没有子View的,这时就会调用View的dispatchTouchEvent方法,一般情况下最终会调用View的onTouchEvent方法。

事件由上而下传递返回值的规则如下:如果onInterceptTouchEvent方法返回true,则拦截,不继续向下传递:如果onInterceptTouchEvent方法返回false,则不拦截,继续向下传递。

2.2:从下往上:

点击事件由下而上的传递过程:当点击事件传给底层的View 时,如果其onTouchEvent方法返回 true,则事件由底层的View消耗并处理:如果onTouchEvent方法返回false,则表示该 View不做处理,并传递给父View的onTouchEvent方法处理:如果父View的onTouchEvent方法仍旧返回false,则继续传递给该父View的父View处理,如此反复下去。

2.3:图:

View的工作流程:

View的工作流程,指的就是measure、 layout和draw。其中,measure用来测量View的高和宽,layout用来确定View的位置,draw则用来绘制View。

1.View的工作流程入口:

Activity的构成的最后讲到了DecorView的创建以及它加载的资源,这个时候DecorView的内容还无法显示,因为它还没有被加载到Window中。

1.1:DecorView被加载到Window中:

当DecorView创建完毕,要加载到Window中时,首先需要先了解一下Activity的创建过程。当我们调用Activity的 startActivity方法时,最终是调用ActivityThread的handleLaunchActivity方法来创建Activity的。

handleLaunchActivity方法中的performLaunchActivity方法用于创建Activity,在这里面会调用Activity的onCreate方法,从而完成DecorView的创建。

而handleLaunchActivity方法中的performResumeActivity方法中会调用Activity的onResume方法。接着得到了DecorVicw。然后得到了WindowManager,WindowManager是一个接口并且继承了接口ViewManager。接着调用WindowManager的addView方法,WindowManager的实现类是 WindowManagerlmpl,所以实际调用的是WindowManagerImpl的addView方法。

WindowManagerImpl的addView方法中,又调用了WindowManagerGlobal的addView方法,该方法创建了ViewRootImpl实例,并且调用了ViewRootImpl的setView方法,将DecorView传进去,这样DecorView就加载到了Window中了。

当然,这个时候界面仍然不会显示出什么来,因为view的工作流程还没有执行完,还需要经过measure、 layout和draw才会把view绘制出来。 

1.2:ViewRootImpl的performTraversals方法:

DecorView加载到Window中是通过ViewRootImpl的setView方法进行的,而ViewRootImpl的还有一个performTraversals方法使得ViewTree开启了View的工作流程。performTraversals方法主要执行了3个方法,分别是performMeasure、perfomLayout和performDraw,在其方法的内部又会分别调用View的measure、layout和draw方法。需要注意的是,在performMeasure方法中需要传入两个参数,分别是childWidthMeasureSpec和childHeightMeasureSpec

2. 理解 MeasureSpec:

MeasureSpec是View的内部类,其封装了一个View的规格尺寸,包括View的宽和高的信息。它的作用是,在Measure流程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure方法中根据这个MeasureSpec来确定View的宽和高。而且从MeasureSpec的常量可以看出,它代表了32位的int值,其中高2位代表了specMode(测量模式),低30位则代表了specSize(测量大小)。specMode有3种模式:

  • UNSPECIFIED:未指定模式,View想多大就多大,父容器不做限制,一般用于系统内部的测量。
  • AT_MOST:最大模式,对应于wrap_content属性,子View的最终大小是父View指定的specSize值,并且子View的大小不能大于这个值。
  • EXACTLY:精确模式,对应于match_parent属性和具体的数值,父容器测量出View所需要的大小,也就是specSize的值。

对于每一个View,都持有一个MeasureSpec,而该MeasureSpec则保存了该View的尺寸规格。在View的测量流程中,通过makeMeasureSpec来保存宽和高的信息。通过getMode或getSize到模式和宽、高。MeasureSpec是受自身LayoutParams和父容器的 MeasureSpec 共同影响的。

作为顶层View的DecorView来说,其并没有父容器,那么它的MeasureSpec 是如何得来的呢?答案是:ViewRootlmpl的performTraversals方法中调用了getRootMeasureSpec方法,getRootMcasureSpec方法的第一个参数windowSize指的是窗口的尺寸,所以对于DecorView来说,它的MeasureSpec由自身的LayoutParams和窗口的尺寸决定的,这也是和普通的View不一样的地方。接着往下看,就会看到根据自身的LayoutParams来得到不同的MeasureSpec。

3.View的measure流程:

measure用来测量 View的宽和高。它的流程分为View的measure流程ViewGroup的measure 流程;只不过ViewGroup的 measure 流程除了要完成自己的测量,还要遍历地调用子元素的measure方法。

3.1:View的measure流程:

performMeasure方法内部会调用View的measure方法之后,measure方法在完成一些逻辑之后会调用View的onMeasure方法。该方法调用了setMeasuredDimension方法,setMeasuredDimension方法的两个参数都为调用getDefaultSize方法的返回值。setMeasuredDimension方法是用来设置View的宽和高的。

getDefaultSize方法中根据不同的specMode值来返回不同的result值,也就是specSize。在AT_MOST和EXACTLY模式下,都返回specSize这个值,即View在这两种模式下的测量宽和高直接取决于specSize。也就是说,对于一个直接继承自View的自定义View来说,它的wrap_content和 match_parent 属性的效果是一样的。因此如果要实现自定义View的wrap_content,则要重写onMeasure方法,并对自定义View的wrap_content属性进行处理。而在UNSPECIFIED模式下返回的是getDefaultSize方法的第一个参数 size的值,size的值从onMeasure方法来看是通过 getSuggestedMinimumWidth 方法或者 getSuggestedMinimumHeight方法得到的。

3.2:ViewGroup的measure流程:

对于ViewGroup,它不只要测量自身,还要遍历地调用子元素的measure方法。ViewGroup定义了measureChildren方法,该方法遍历子元素并调用measureChild方法。measureChild方法首先调用getLayoutParams方法获取子元素的LayoutParams属性,然后通过getChildMeasureSpec方法获取子元素的MeasureSpec并且调用子元素的measure方法进行测量。

4.View的layout流程:

Layout方法的作用是确定元素的位置。viewGroup中的layout方法用来确定子元素的位置,view中的layout方法则用来确定自身的位置。来看看View的layout方法:layout方法中的四个参数L,T,R,B分别是view从左,上,右,下相对于其父容器的距离。接着看setFrame方法用传进来的L,T,R,B四个参数分别初始化mLeft,mTop,mRight,mBottom这4个值,这样就确定了该View在父容器中的位置。然后再调用onLayout方法,该方法是一个空方法,与onMeasure方法类似,确定位置时根据不同的控件有不同的实现,所以在View和ViewGroup中都没有实现onLayout方法。

5.View的draw流程:

View的draw流程很简单,下面先来看看View的draw方法,如下所示:

  1. 如果需要,则绘制背景。绘制背景调用的是View的drawBackground方法
  2. 保存当前canvas层。
  3. 绘制View的内容。绘制内容调用的是View的onDraw方法,这个方法是一个空方法,需要自己实现。
  4. 绘制子View。调用的是dispatchDraw方法,这个方法也是一个空方法,需要自己实现。
  5. 如果需要,则绘制View的褪色边缘,这类似于阴影效果。
  6. 绘制装饰,比如滚动条。调用的是View的onDrawForeground方法绘制装饰。
  7. 其中第2步和第5可以跳过。

自定义View:

需要注意的是,凡事都要有度,自定义View毕竟不是规范的控件,如果设计不好、不考虑性能,则反而会适得其反;另外,其适配起来可能也会产生问题。如果能用系统控件的情况还是应尽量用系统控件。自定义View分为三大类,第一种是自定义View,第二种是自定义ViewGroup,第三种是自定义组合控件。其中,自定义View又分为继承系统控件(比如 TextView)和继承View两种情况。自定义ViewGroup也分为继承ViewGroup和继承系统特定的ViewGroup(比如RelativeLayout)两种情况。

1.继承系统控件的自定义View:

这种自定义View在系统控件的基础上进行拓展,一般是添加新的功能或者修改显示的效果,通常在onDraw方法中进行处理。比如自定义View,继承于TextView,并且加上一条横线。

public class InvalidTextView extends TextView {
    private Paint mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
    public InvalidTextView(Context context)
    {
        super(context);
        initDraw();

    }
    public InvalidTextView(Context context, AttributeSet attrs)
    {
        super(context,attrs);
        initDraw();

    }
    public InvalidTextView(Context context,AttributeSet attrs,int defStyleAttr)
    {
        super(context,attrs,defStyleAttr);
        initDraw();

    }

//初始化paint
    private void initDraw(){
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth((float) 10.0);
    }

//重写onDraw方法加上横线
    @Override
    protected void onDraw(Canvas canvas){
       super.onDraw(canvas);
       int width=getWidth();
       int height=getHeight();
       canvas.drawLine(0,height/2,width,height/2,mPaint);
    }

}

2.继承View的自定义View:

与上面的继承系统控件的自定义View不同,继承View的自定义View实现起来要稍微复杂一些。其不只是要实现onDraw方法,而且在实现过程中还要考虑到wrap_content 属性以及padding属性的设置;为了方便配置自己的自定义View,还会对外提供自定义的属性。另外,如果要改变触控的逻辑,还要重写onTouchEvent()等触控事件的方法。这里是继承View画一个正方形,流程为:

  • 继承View,并且做一些初始化的操作。
  • 重写onDraw方法,并且对padding属性进行处理。
  • 重写onMeasure方法,并且对wrap_content属性进行处理。

3.自定义属性:

Android系统中的控件以Android开头的都是系统自带的属性,为了方便配置自定义view的属性,我们也可以自定义属性。首先在values目录下创建attrs.xml文件

<declare-styleable name = "名称">
     <attr name = "属性" format = "格式" />
</declare-styleable>

这个XML文件定义了一个名称为XX的自定义属性组合,其中有一个属性XX,格式为XX。然后在View的构造方法中获取自定义的属性集,通过context的obtainStyledAttributes方法获取一个TypedArray,然后调用TypedArray的get+某个格式的方法提取属性,获取资源后还要调用recycle方法及时释放。这样就可以在XML文件中引用该属性了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mo@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值