Android进阶之光读书笔记——第三章:View体系与自定义View

第三章 View体系与自定义View

  • 本章将介绍Android中十分重要的View,在多本书中View是必讲的一节,Android群英传就讲了不少的View的知识,那么在这里我们再去复习一遍吧

3.1 View与ViewGroup

  • View是所有控件的基类,ViewGroup也都是继承View的,View和ViewGroup形成一个View树
  • 常用的TextView、ImageView、继承自View
  • LinearLayout、RelativeLayout继承自ViewGroup
  • 来一张图自己体会:

3.2 坐标系

3.2.1 Android坐标系

  • 屏幕左上方为圆点,向右为x正方向,向下为y正方向,getRawX()和getRawY()获得的是Android坐标系的坐标

3.2.2 View坐标系

  • 直接甩图:

1.View获取自身的宽和高

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

// 然而系统提供的getWidth()和getHeight()的源码就是这么写的

2.View自身的坐标

通过如下方法可以获得View到其父控件(ViewGroup)的距离,记住要获取的都是距离x轴和y轴方向的距离:

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

3.MotionEvent提供的方法

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

3.3 View的滑动

  • 不管哪种滑动方式,其基本思想都是类似的,当点击事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来改变View的坐标
  • 在这里讲6中滑动方法:
  1. layout()
  2. offsetLeftAndRight()与offsetTopAndBottom()
  3. LayoutParams
  4. 动画
  5. scrollTo与scrollBy
  6. Scroller

3.3.1 layout()方法

  • View在进行绘制的时候会调用layout()方法来设置显示的位置,我们可以通过在onTouchEvent()方法的ACTION_MOVE事件中算出偏移量,然后去调用layout()方法来进行重绘,写完后在xml中直接定义就能用了,来写一次吧:
public class CustomView extends View {

    private int lastX;
    private int lastY;

    public CustomView(Context context) {
        super(context);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    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;
                break;
            case MotionEvent.ACTION_MOVE:
                // 计算移动的距离
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                // 调用layout重新设置该控件的位置
                layout(getLeft() + offsetX, getTop() + offsetY, 
                        getRight() + offsetX, getBottom() + offsetY);
                break;
            default:
        }
        return true;
    }
}

3.3.2 offsetLeftAndRight()与offsetTopAndBottom()

这两个方法和layout()方法基本差不多了,调一个方法变为调两个方法

case MotionEvent.ACTION_MOVE:
    // 计算移动的距离
    int offsetX = x - lastX;
    int offsetY = y - lastY;
    // 左右方向来进行偏移
    offsetLeftAndRight(offsetX);
    // 上下方向来进行偏移
    offsetTopAndBottom(offsetY);
    break;

3.3.3 LayoutParams(改变布局参数)

通过改变margin来改变位置,还是替换ACTION_MOVE中的代码:

    // 通过LinearLayuot.LayoutParams
    LinearLayout.LayoutParams layoutParams =(LinearLayout.LayoutParams)getLayoutParams();
    layoutParams.leftMargin = getLeft() + offsetX;
    layoutParams.topMargin = getTop() + offsetY;
    setLayoutParams(layoutParams);

    // 通过ViewGroup.MarginLayoutParams
    ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
    layoutParams.leftMargin = getLeft() + offsetX;
    layoutParams.topMargin = getTop() + offsetY;
    setLayoutParams(layoutParams);

3.3.4 动画

  • 通过View动画来进行移动,在res目录新建anim文件夹并创建translate.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:andorid="http://schemas.android.com/apk/res-auto"
    android:fillAfter="true">
    <translate
        andorid:duration="1000"
        android:fromXDelta="0"
        android:toXDelta="300"/>
</set>
  • 然后直接在代码中调用:
mCustomView.setAnimation(AnimationUtils.loadAnimation(this, R.anim.translate));
  • 我们设置向右平移300px,然后又会回到原来的位置,如果在translate.xml中加入fillAfter="true"(上面的代码中已加),那么平移后就不会移动了
  • View动画不能改变View的位置参数,比如一个button平移后,点击平移后的button并不会响应,点之前的位置才会响应,这个问题,可以通过属性动画来解决,属性动画不仅可以执行动画,还可以改变View的位置参数,使用代码:
ObjectAnimator.ofFloat(mCustomView,"translationX",0,300).setDuration(1000).start();

3.3.5 scrollTo与scrollBy

  • scrollTo(x, y)表示移动到一个具体的坐标点
  • scrollBy(dx, dy)表示移动的增量为dx、dy
  • scrollBy在源码中也是通过调用scrollTo来实现的
  • scrollTo、scrollBy是移动Veiw的内容,在ViewGroup中使用代表移动其所有的子view

ACTION_MOVE中代码改为:

((View)getParent()).scrollBy(-offsetX,-offsetY);
  • 为什么要改为负值呢,这里说一下,比如:scrollBy(50, 50)
  • scrollBy(dx, dy)意味着手机屏幕相对画布移动的距离,所以屏幕向右移动50,向下移动50,结果控件向左上方移动了,那么我们如果想让子控件向右下移动,就得使用负值才能达到想要的效果
  • 移动前:

  • 移动后:

3.3.6 Scroller

  • 在使用scrollTo()与scrollBy()的时候,这个过程是瞬间完成的,所以用户体验不好,我们可以使用Scroller来实现有过渡效果的滑动,Scroller本身不能实现View的滑动,它需要与View的computeScroll()方法配合才能实现弹性滑动效果
public class CustomView extends View {
    
    private Scroller mScroller;
    
    public CustomView(Context context) {
        super(context);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * 绘制的时候draw()方法中会调用该方法,并且只要invalidate(),也就是重绘就会调用该方法
     */
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            ((View)getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

    /**
     * 自定义一个方法,调用Scroller的startScroll()方法,在2000ms内沿X轴平移delta像素
     * @param destX
     * @param destY
     */
    public void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int delta = destX - scrollX;
        mScroller.startScroll(scrollX,0,delta,0,2000);
        invalidate();
    }
}

3.4 属性动画

属性动画出现之前,Android系统提供的动画只有帧动画View动画

1、帧动画其实就是不停的切换图片,还记得我们自己手画的小人书,然后从第一页开始快速拨动,就成了好看的动画

2、View动画应该很熟悉了,它提供了四种动画方式:

  1. AlphaAnimation(透明)、
  2. RotateAnimation(旋转)、
  3. TranslateAnimation(平移)、
  4. ScaleAnimation(缩放)、
  • 并且提供了AnimationSet集合来混合使用多种动画
  • 缺点:只能做普通的动画效果,无法交互,比如某一个元素发生View动画后,其响应事件的位置仍然在动画进行之前的地方
  • 优点:就是效率比较高,使用也方便

3、Android3.0之后,谷歌推出了新的动画框架,Animator框架中使用最多的就是AnimatorSet和ObjectAnimator配合:使用ObjectAnimator进行更精细化强大的属性动画框架基本可以实现所有的动画效果

1.ObjectAnimator

  • ObjectAnimator是属性动画最重要的类,创建其对象通过其静态工厂创建,参数包括一个对象和对象的属性名字,而且这个属性名字必须有get和set方法
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values)

第一个参数是对象,得个参数是对象的属性,第三个参数是属性变化的取值过程

• translationX和translationY:用来沿着X轴或者Y轴进行平移。

• rotation、rotationX、rotationY:用来围绕View的支点进行旋转。

• PrivotX和PrivotY:控制View对象的支点位置,围绕这个支点进行旋转和缩放变换处理。默认该支点 位置就是View对象的中心点。

• alpha:透明度,默认是1(不透明),0代表完全透明。

• x和y:描述View对象在其容器中的最终位置。 

使用ObjectAnimator的时候要操作的属性必须要有get和set方法

2.ValueAnimator

  • ValueAnimator不提供任何动画效果,它更像是一个数值发生器,用来产生有一定规律的数字
  • 通常情况下,在ValueAnimator的AnimatorUpdateListener中监听数值的变化,从而完成动画的变换

3.动画的监听

  • 完整的动画具有start、End、Cancel、Repeat这4个过程
  • 大部分时候只关心onAnimationEnd事件,Android提供了AnimatorListenerAdapter来让我们选择事件来进行监听
ObjectAnimator animator = ObjectAnimator.ofFloat(mScroller,"alpha",1.5f);
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationCancel(Animator animation) {
                super.onAnimationCancel(animation);
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                super.onAnimationRepeat(animation);
            }

            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
            }

            @Override
            public void onAnimationPause(Animator animation) {
                super.onAnimationPause(animation);
            }

            @Override
            public void onAnimationResume(Animator animation) {
                super.onAnimationResume(animation);
            }
        });

4.组合动画——AnimatorSet

  • AnimatorSet类提供了一个play()方法,向此方法中传入一个Animator对象,将会返回一个AnimatorSet.Builder的实例

AnimatorSet.Builder中包括以下4个方法。

  • after(Animator anim):将现有动画插入到传入的动画之后执行。
  • after(long delay):将现有动画延迟指定毫秒后执行。
  • before(Animator anim):将现有动画插入到传入的动画之前执行。
  • with(Animator anim):将现有动画和传入的动画同时执行。 
// 比如有三个动画,那么可以这样执行
AnimatorSet set = new AnimatorSet();
set.play(animator1).with(animator2).after(animator3);

5.组合动画——PropertyValuesHolder

  • 使用此类只能是多个动画一起执行,关键代码:
ObjectAnimator objectAnimator = ObjectAnimator.ofPrepertyValuesHolder(mCustomView, 
            valuesHolder1, valuesHolder2,valuessHolder3);
objectAnimator.setDuration(2000).start();

6.在XML中使用属性动画

  • 和View动画一样,属性动画也可以直接写在xml文件当中
  • 然后在代码中直接引用xml动画即可
Animator animator = AnimatorInflater.loadAnimator(this, R.animator.scale);
animator.setTarget(view);
animator.start();

3.5 解析Scroller

这个不写了,一些源码,想看的可以看看,大体说一下

  • Scroller三个构造方法,一般用第一个
  • startScroll()做了一些前期数据准备,然后调用invalidate()方法进行View的重绘,View的重绘会调用View的draw()方法,draw()方法又会调用View的computeScroll()方法,我们去重写computeScroll()方法
  • 通过Scroller来获取当前的ScrollX和ScrollY,然后调用ScrollTo()来进行View的滑动,接着调用invalidate()来让View进行重绘,重绘就会再次调用computeScroll()方法来实现View的滑动,这样通过不断的移动一个小的距离来连贯的实现平滑移动的效果,如何获取当前位置的ScrollX和ScrollY呢,那就是在scrollTo()方法前会调用Scroller的computeScrollOffset()方法

3.6 View的事件分发机制

3.6.1 源码解析Activity的构成

  • 点击事件用MotionEvent来表示,当一个点击事件产生后,事件最先传递给Activity
  • Activity的setContentView()方法大家很熟悉了吧,此方法中调用了getWindow().setContentView()方法
  • getWindow()得到的是PhoneWindow,PhoneWindow中的setContentView方法中又调用了installDecor()方法
  • 而installDecor()方法又调用了generateDecor()方法来返回一个DecorView(意为:装饰、布景),调用了generateLayout()方法来根据不同的情况加载不同的布局给layoutResource

总的来说,一个Activity包含一个PhoneWindow对象,PhoneWindow又将DecorView作为整个窗口的根布局,而这个根布局又将屏幕划分为两个区域,一个是TitleView,另一个是ContentView,来个图吧,清晰明了的:

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

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

  1. dispatchTouchEvent(MotionEvent ev)——用来进行事件的分发,dispatch意为发出、派遣
  2. onInterceptTouchEvent(MotionEvent ev)——用来进行事件的拦截,在dispatchTouchEvent()中调用,View没有提供该方法,intercept意为拦截、阻止
  3. onTouchEvent(MotionEvent ev)——用来处理点击事件,在dispatchTouchEvent()中调用

1.View的事件分发机制

当点击事件发生时,事件先传递给Activity,这会调用Activity的dispatchTouchEvent()方法,当然具体的事件处理工作都是交给了PhoneWindow,PhoneWindow再把事件处理工作交给DecorView,然后DecorView再交给根ViewGroup

分析根ViewGroup的dispatchTouchEvent()方法:

  1. 简单说几句吧,mFirstTouchTarget==null时,代表拦截了事件,如果此时拦截了事件,如果这时触发DOWN事件,则会执行onInterceptTouchEvent(),如果是MOVE或UP事件则不执行onInterceptTouchEvent(),直接设置intercepted = true,onInterceptTouchEvent()并不是每次都会调用的
  2. 如果想要ViewGroup拦截事件,可以重写onInterceptTouchEvent()方法并返回true
  3. 如果有子View,则调用子View的dispatchTouchEvent(event)方法,如果没有子View则调用super方法
  4. 如果onTouchListener不为null并且onTouch方法返回true,表示事件被消费,就不会执行onTouchEvent(),否则就会去执行

2.点击事件分发的传递规则

书中用了一个倚天屠龙记的例子,就用这个例子来说吧,我们缩小范围为两个角色

  • 我们分为武当掌门(根ViewGroup)、武当弟子(底层View)
  • 当有敌人来犯(点击事件),先汇报给掌门,如果掌门决定亲自出马,直接解决(onInterceptTouchEvent()返回true,交给onTouchEvent()处理),掌门不想亲自出马,将任务交给武当小弟(onInterceptTouchEvent()返回false),事件由上而下传递返回值的规则如下:为true拦截,不传递;为false不拦截,继续传递
  • 如果武当小弟把敌人解决了(onTouchEvent()返回true,直接消耗处理掉了),如果武当小弟打不过敌人(onTouchEvent()返回false),还是让掌门的来打敌人吧(传递给父类的onTouchEvent()来处理,父类消耗处理掉了,父类的onTouchEvent()返回true),事件由下而上传递返回值的规则如下:为true,处理,为false,不处理,向上传递

3.7 View的工作流程

measure、layout、draw:测量、确定位置、绘制

3.7.1 View的工作流程入口

1.DecorView被加载到Window中

  • 当我们调用Activity的startActivity方法时,最终调用的是ActivityThread的handleLaunchActivity方法来创建Activity的
  • 在handleLaunchActivity中调用performLaunchActivity方法来创建Activity,在这里会调用onCreate()方法,从而完成对DecorView的创建,接着调用handleResumeActivity()方法,此方法中的performResumeActivity方法中会调用Activity的onResume()方法,接着handleResumeActivity()方法中会得到DecorView、WindowManager,而在WIndowManagerImpl中则会调用addView()方法,此addView()方法又调用了WindowManagerGlobal的addView方法,在此addView()方法中创建了ViewRootImpl实例,该实例通过setView方法将DecorView作为参数传进去,这样就把DecorView加载到了Window中,但是界面还是什么都不会显示出来,因为View的工作流程还没走完,还需要经过measure、layout、draw才会把view绘制出来

2.ViewRootImpl的PerformTraveals方法

  • 在此方法中执行了3个方法,分别是performMeasure、performLayout和performDraw,在其方法内部又会分别调用View的measure、layout、draw方法

3.7.2 理解MeasureSpec

意为:计量单位

在onMeasure中根据这个MeasureSpec来确定View的宽和高

从MeasureSpec的常量可以看出,它代表了32位的int值,其中高2位代表了SpecMode,低30位则代表 SpecSize。SpecMode指的是测量模式,SpecSize指的是测量大小。SpecMode有3种模式,如下所示:

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

每一个View都持有一个MeasureSpec,作为顶层View的DecorView,它的MeasureSpec由自身的LayoutParams来得到不同的MeasureSpec

3.7.3 View的measure流程

1.View的measure流程

  • onMeasure()方法调用setMeasureDimension(),用来设置宽高
  • 去了解getDefaultSize()方法、getSuggustedMinimumWidth()方法

2.ViewGroup的measure流程

。。。

3.7.4 View的layout流程

  • ViewGroup中的layout方法用来确定子元素的位置,View中的layout方法则用来确定自身的位置
  • layout()方法中的4个参数l、t、r、b分别是view从左、上、右、下相对于父容器的距离
  • setFrame()方法中传进来的4个参数可以确定该View在父容器中的位置,调用setFrame()方法后,就会调用onLayout()方法

3.7.5 View的draw流程

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

官方注释清楚地说明了每一步的做法,它们分别是:

  1. 如果需要,则绘制背景。
  2. 保存当前canvas层。
  3. 绘制View的内容。
  4. 绘制子View。
  5. 如果需要,则绘制View的褪色边缘,这类似于阴影效果。
  6. 绘制装饰,比如滚动条。

其中第2步和第5步可以跳过,所以这里不做分析,重点分析其他步骤。 

步骤1:绘制背景

  • 绘制背景调用了View的drawBackground方法

步骤3:绘制View的内容

  • 调用了View的onDraw()方法,空实现,自定义View的时候自己去实现

步骤4:绘制子View

  • 调用了dispatchDraw()方法,也是一个空实现,ViewGroup中此方法对子类进行了遍历,并调用drawChild方法

步骤6:绘制装饰

  • 绘制装饰的方法为View的onDrawForeground()方法

3.8 自定义View

3.8.1 继承系统空间的自定义View

  • 这种自定义控件一般是添加新的功能或者修改显示的效果,一般在draw()方法中进行处理

3.8.2 继承View的自定义View

  • 不光要实现onDraw()方法,还得考虑wrap_content和padding等属性的设置,改变触控的逻辑,就要重写onTouchEvent()等方法

3.8.3 自定义组合控件

  • 比如TopBar或者固定样式的Dialog
  • 写xml布局文件,然后填充布局,设置必要的方法,比如setTitle()或者点击事件等,在attrs.xml中添加自定义属性

3.8.4 自定义ViewGroup

1.继承ViewGroup

  • 2.对wrap_content属性进行处理,padding、margin

3.实现onLayout

  • 如果子元素不是GONE,就将它放到合适的位置上

4.处理滑动冲突

  • 这个自定义ViewGroup为水平滑动,如果里面有一个ListView会导致滑动冲突,如果检测到是水平滑动,就让父View进行拦截,具体实现就是重写onInterceptTouchEvent方法,判断如果是水平滑动就返回true来拦截

5.弹性滑动到其他页面

  • 在刚进入onTouchEvent()方法中就得到点击事件的坐标,判断滑动距离是否大于1/2然后来进行对应的滑动

6.快速滑动到其他页面

  • 类似,如果速度的绝对值大于50,就被认为快速滑动,去切换到下一个界面

7.再次触摸屏幕阻止页面

  • 在onInterceptTouchEvent()方法中执行对应的操作

8.应用HorizontalView

  • 。。。

3.9 本章小结

这章的内容挺多挺丰富的,是很重要的一章,好好读会有收获的

猜你喜欢

转载自blog.csdn.net/pengbo6665631/article/details/82794873
今日推荐