[读书笔记] Android进阶之光-View体系

1. View与ViewGroup

View和ViewGroup之间的关系?

1. ViewGroup可以理解为View的组合,它可以包含很多View以及ViewGroup。

2. ViewGroup也继承自View。

2. 坐标系

Android 系统中有两种坐标系,分别为Android坐标系和View坐标系。

2.1 Android坐标系

将屏幕左上角的顶点作为Android坐标系的原点,这个原点向右是X轴正方向,向下是Y轴正方向。

使用方法:getRawX 和 getRawY

2.2 View坐标系

2.2.1 View获取自身的宽和高

getHeigh() 和 getWidth()

2.2.2 View的自身的坐标

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

2.2.3 MotionEvent提供的方法

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

3. View的滑动 

原理: 当点击事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标。以下是6种滑动方法:

3.3.1 layout()方法

绘制View的时候会调用onLayout方法来设置显示的位置。

应用:

1.  在onTouchEvent方法中获取触摸点的坐标。

2. 在ACTION_MOVE事件中计算偏移量,再调用layout方法重新设置这个自定义View的位置。

3. 在每次移动时都会调用layout方法对屏幕重新布局,从而达到移动View的效果。

3.3.2 offsetLeftAndRight() 与 offsetTopAndBottom

这两种方法和layout方法效果差不多,使用方法也差不多。

  • offsetLeftAndRight(offsetX) //对left和right进行偏移
  • offsetTopAndBottom(offsetY) //对top和bottom进行偏移

3.3.3 LayoutParams (改变布局参数)

LayoutParams保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局参数,从而达到改变View位置的效果。

LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.rightMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
复制代码

如果父控件是RelativeLayout ,则要使用RelativeLayout.LayoutParams。

3.3.4 动画

1. View动画:

使用:在res目录中新建anim文件夹并创建translate.xml.最后使用loadAnimation(this,R.anim.translate)

缺点:View动画并不能改变View的位置参数。

2. 属性动画:

ObjectAnimator.ofFloat(mCustomView,"translationX",0,300).setDuration(1000).start();
复制代码

3.3.5 scrollTo 与 scrollBy

scrollTo(x,y): 表示移动到一个具体的坐标点。

scrollBy(dx,dy): 表示移动的增量为dx,dy。

3.3.6 Scroller

Scroller本身是不能实现View的滑动的,它需要与View的computeScroll方法配合才能实现弹性滑动的效果。

系统在绘制View的的时候在draw方法中调用该方法,在这个方法中,我们调用父类的scrollTo方法并通过Scroller来不断获取当前的滑动值。每滑动一小段距离,我们就调用invalidate方法不断的进行重绘。重绘又会调用computeScroll方法,这样就连贯起来了。

4. 属性动画

4.1 ObjectAnimator

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

4.2 ValueAnimator

ValueAnimator不提供任何的动画效果,它更像一个数值发生器,用来产生有一定规律的数字,从而让调用者控制动画的实现过程。

4.3 动画的监听

完整的动画具有start,Repeat,end, cancel这4个过程。

  • onAnimationStart(Animator animation)
  • onAnimationEnd(Animator animation)
  • onAnimationCancel(Animator animation)
  • onAnimationRepeat(Animator animation)

4.4 组合动画 – AnimatorSet

AnimatorSet提供了一个play方法,如果我们向这个方法中传入一个Animator对象,将返回一个AnimatorSet.Builder的实例,这个Builder类AnimatorSet的内部类,采用建造者模式。提供了四种方法:

1. after(Animator anim) : 将现有动画插入到传入的动画之后执行。

2. after(long delay) : 将现有动画延迟指定的毫秒后执行。

3. befor(Animator anim): 将现有动画插入到传入的动画之前执行。

4. with(Animator anim): 将现有动画和传入的动画同时执行。

4.5 组合动画 – PropertyValueHolder

ObjectAnimator.ofPropertyValuesHolder(Object target, PropertyValuesHolder...values)
复制代码

4.6 在XML中使用属性动画

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

AnimatorInflater.loadAnimator(this,R.animator.scale)
复制代码

5. 解析Scroller

Scroller原理:Scroller并不能直接实现View的滑动,它需要配合View的computeScroll()方法。在computeScroll()中不断让View进行重绘,每次重绘都会计算滑动持续的时间,根据这个持续时间就能算出这次View滑动的位置,我们根据每次滑动的位置调用scrollTo()方法进行滑动,这样不断地重复上述过程就形成了弹性滑动。

6. View的事件分发机制

6.1 源码解析Activity的构成

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

6.2 源码解析View的事件分发机制

点击事件有三个重要的方法:

  • dispatchTouchEvent(MotionEvent ev) —用来进行事件的分发;

  • onInterceptTouchEvent(MotionEvent ev) —用来进行事件的拦截, 在dispatchTouchEvent() 中调用,需要注意的是View没有提供该方法;

  • onTouchEvent(MotionEvent ev) —用来处理点击事件, 在dispatchTouchEvent() 方法中进行调用。

6.2.1 View的事件分发机制

当点击事件产生后,事件首先会传递给当前的Activity,这会调用Activity的dispatchTouchEvent()方法,当然具体的事件处理工作都是交由Activity中的PhoneWindow来完成的,然后PhoneWindow再把事件处理工作交给DecorView,之后再由DecorView将事件处理工作交给根ViewGroup。

一个完整的事件序列是以DOWN开始,以UP结束的。如果当前ViewGrou拦截该事件,则会将调用onInterceptTouchEvent(ev),此后的一个事件序列均由这个ViewGroup处理不再执行onInterceptTouchEvent(ev)方法,并将intercepted = true。

所以, onInterceptTouchEvent() 方法并不是每次事件都会调用的,默认返回false。

如果当前ViewGroup未拦截该事件,则将这一系列事件交由子View处理。

FLAG_DISALLOW_INTERCEPT 标志位:它主要是禁止ViewGroup 拦截除了DOWN之外的事件, 一般通 过子View的requestDisallowInterceptTouchEvent来设置。

View的dispatchTouchEvent

若onTouchListener 不为null并且onTouch方法返回true,事件就被消费,就不会执行onTouchEvent(ev)。否则就会执行onTouchEvent(ev).在onTouchEvent中,只要View的clickable和LONG_CLICKABLE 有一个为true,那么onTouchEvent()就会返回true消耗掉事件。

接着在ACTION_UP中调用performClick方法,如果设置了点击事件,则会调用onClick方法。

onTouchListener的接口的优先级是要高于onTouchEvent的,假若onTouchListener中的onTouch方法返回true,表示此次事件已经被消费了,那onTouchEvent是接收不到消息的。

如果给一个Button设置一个onTouchListener并且重写onTouch方法,返回值为true,此时的Button的点击事件还处理吗?

答案是:
是得不到处理的。
由于Button的performClick是利用onTouchEvent实现,假若onTouchEvent没有被调用到,那么Button的Click事件也无法响应。

总结: 

 1. onTouchListener的onTouch方法优先级比onTouchEvent高,会先触发。 

 2. 假如onTouch方法返回false会接着触发onTouchEvent,反之onTouchEvent方法不会被调用。 

3.  内置诸如click事件的实现等等都基于onTouchEvent,假如onTouch返回true,这些事件将不会被触发。 

(回去要动手试一下,不然理解困难)

事件是一系列的,如果DOWN在哪里被消费,那后续事件都是在哪里被消费。

6.2.2 点击事件分发的传递规则

由上而下:

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

由下而上:

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

7. View的工作流程

measure:用来测量View的宽和高;

layout:用来确定View的位置;

draw:用来绘制View;

7.1 View的工作流程入口

(1) DecorView如何被加载到Window中?

7.2 理解MeasureSpec

MeasureSpec的作用:

MeasureSpec是View的内部类,其封装了一个View的规格尺寸,包括View的宽和高的信息,它的作用是,在Measure流程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure方法中根据这个MeasureSpec来确定View的宽和高。

测量模式specMode:

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

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

对于DecorView来说,它的MeasureSpec由自身的LayoutParams和窗口的尺寸决定,这一点和普通的View是不同的。                                                                                                                                                                          

7.3 View的measure流程

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

(1) View的measure流程

测量过程:

onMeasure()->setMeasuredDimension()->setMesuredDimensionRaw()

getDefaultSize作用:

public static int getDefaultSize(int size, int measureSpec) {
    //size表示的是View想要的尺寸信息,比如最小宽度或最小高度
    int result = size;
    //从measureSpec中解析出specMode信息
    int specMode = MeasureSpec.getMode(measureSpec);
    //从measureSpec中解析出specSize信息,不要将specSize与上面的size变量搞混
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    //如果mode是UNSPECIFIED,表示View的父ViewGroup没有给View在尺寸上设置限制条件
    case MeasureSpec.UNSPECIFIED:
        //此处当mode是UNSPECIFIED时,View就直接用自己想要的尺寸size作为量算的结果
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        //此处mode是AT_MOST或EXACTLY时,View就用其父ViewGroup指定的尺寸作为量算的结果        result = specSize;      
        break;
    }
    return result;
}
   
这里的result就是通过下面这个函数获得的。
protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : 
                max(mMinWidth, mBackground.getMinimumWidth());
}
可以看到mBackground  == null 为没有设置背景,那么返回mMinWidth ,也就是android:minWidth 这个属性所指定的值,这个值可以是0 ;如果View 设置了背景,则返回mMinWidth 与背景的最小宽度这两者的最大值。

getSuggestedMinimumWidth() 的返回值就是View 在UNSPECIFIED 情况下的测量宽。
复制代码

根据不同的SpecMode值来返回 不同的result值,也就是SpecSize。在AT_MOST和EXACTLY模式下,都返回SpecSize这个值,即View在这两种模式下的测量宽和高都直接取决于SpecSize。也就是说, 对于一 个直接继承自View的自定义View来说, 它的wrap_content 和 match_parent 属性的效果是一样的。 

因此如果 要实现自定义 View 的wrap_content, 则要重写onMeasure方法, 并对自定义View的wrap_content属性进行处理。

(2) ViewGroup的measure流程

ViewGroup中没有定义onMeasure()方法,但却定义了measureChildren()方法。

遍历子元素并调用measureChild方法, 调用child.getLayoutParams()方法来获得子元素的LayoutParams属性,获取子元素的MeasureSpec调用子元素的measure()方法进行测量。

看下getChildMeasureSpec()方法写了什么?

 /**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension
     * @return a MeasureSpec integer for the child
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
复制代码

很显然,这是根据父容器的MeasureSpec模式再结合子元素的LayoutParams属性来得出子元素的MeasureSpec属性。有一点需要注意的是,如果父容器的MeasureSpec属性为AT_MOST,子元素的LayoutParams属性为WRAP_CONTENT,那根据上面的代码,我们会发现子元素的属性也为AT_MOST,它的SpecSize值为父容器的SpecSize减去Padding值。换句话说,这和子元素设置LayoutParams属性为MATCH_PARENT效果一样的。为了解决这个问题,需要在LayoutParams为WRAP_CONTENT时指定一下默认的宽和高。

7.4 View的layout流程

layout方法的作用是确定元素的位置。ViewGroup中的layout方法用来确定子元素的位置,View中的layout方法则用来确定自身的位置。

layout方法的4个参数l,t,r,b分别是View从左,上,右,下相对于其父容器的距离。

layout->onLayout  ,onLayout方法是个空方法,这和onMeasure方法类似。确定位置时根据不同的控件有不同的实现,所以在View和ViewGroup中均没有实现onLayout方法。

以LinearLayout中的onLayout为例:

其中layoutVertical()会遍历子元素并调用setChildFrame方法。

在setChildFrame方法中调用子元素的layout方法来确定自己的位置。

7.5 View的draw流程

1. 如果需要,则绘制背景。 

 2. 保存当前的canvas层。 

 3. 绘制View的内容。 

 4. 绘制子View。 

 5. 如果需要,则绘制View的褪色边缘,这类似于阴影效果。 

 6. 绘制装饰比如。滚动条。

一. 绘制背景调用了View的drawBackground(Canvas canvas)。

二. 绘制View的内容调用了View的onDraw(Canvas canvas)方法。

三. 绘制子View调用了dispatchDraw**(Canvas canvas)**方法

ViewGroup重写了这个方法,在dispatchDraw方法中对子类View进行遍历,并调用drawChild方法,这里又调用了View的draw方法。这里会判断是否有缓存,没有就正常显示,有就显示缓存。

四.绘制装饰用onDrawForeground方法,很明显这个方法用于绘制ScrollBar以及其他的装饰,并将它绘制在视图内容的最上层。

8. 自定义View

8.1 继承系统控件的自定义View

继承系统的View,重写onDraw方法即可。

8.2 继承View的自定义View

8.2.1 简单实现继承View的自定义View

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

8.2.1 对padding属性进行处理

使用getPaddingLeft(),getPaddingRight()来获取padding。

8.2.3 对wrap_content 属性进行处理

给自定义View设置默认的宽和高:

**8.2.4 自定义属性
**

8.3 自定义组合控件

1. 继承RelativeLayout相关组件

2. 可以使用findViewById

3. 亦可自定义属性

4. 自定义属性需要添加

schemas: xmlns: app=”http: //schemas.android.com/apk/res-auto”命名空间

8.4 自定义ViewGroup

1. 需重写onLayout()

2. 对wrap_content属性进行处理

3. 根据有无子元素进行宽高的重置

4. 重写onlayout方法。

如果子元素不是GONE,则调用子元素的layout方法,将其放到合适的位置是那个。

8.4.4 处理滑动冲突

这个自定义ViewGroup为水平滑动,如果里面是ListView,ListView则为垂直滑动,这样会导致滑动的冲突。解决的方法就是,如果我们检测到的滑动方向是水平的话,就让父View进行拦截,确保父View用来进行View的滑动切换。

8.4.5 弹性滑动到其他页面

在onTouchEvent方法里需要进行滑动切换页面,需要用到Scroller。

在刚进入onTouchEvent方法时就得到点击事件的坐标,在MotionEvent.ACTION_MOVE中用scrollBy方法来处理HorizontalView控件随手指滑动的效果。如果宽度大于1/2,调用Scroller来进行滑动。

8.4.6 快速滑动到其他页面

需要用到VelocityTracker来测试滑动速度。

使用方法:

  • tracker = VelocityTracker.obtain();
  • tracker.computeCurrentVelocity(1000);//多少秒内
  • tracker.getXVelocity();
  • tracker.clear()

8.4.7 再次触摸屏幕阻止页面继续滑动

可调用Scroller.isFinished()判断Scroller是否在滑动中,如果在滑动中,则调用Scroller.abortAnimation终止滑动。

仅供自己学习使用。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享