Android UI 绘制面试题

1.1 Android 补间动画和属性动画的区别?

特性 补间动画 属性动画
view 动画 支持 支持
非view动画 不支持 支持
可扩展性和灵活性
view属性是否变化 无变化 发生变化
复杂动画能力 局限 良好
场景应用范围 一般 满足大部分应用场景

1.2 Window和DecorView是什么?

DecorView又是如何和Window建立联系的?Window 是 WindowManager 最顶层的视图,它负责背景(窗口背景)、Title之类的标准的UI元素,Window 是一个抽象类,整个Android系统中, PhoneWindow是 Window 的唯一实现类。至于 DecorView,它是一个顶级 View,内部会包含一个竖直方向的LinearLayout,这个LinearLayout 有上下两部分,分为 titlebar 和 contentParent 两个子元素,contentParent 的 id 是 content,而我们自定义的 Activity 的布局就是 contentParent 里面的一个子元素。View 层的所有事件都要先经过 DecorView 后才传递给我们的 View。DecorView 是 Window 的一个变量,即 DecorView 作为一切视图的根布局,被 Window 所持有,我们自定义的View 会被添加到 DecorView ,而DecorView 又会被添加到 Window 中加载和渲染显示。

简单内部层次结构图:

1.3 简述一下 Android 中 UI 的刷新机制?

界面刷新的本质流程

  1. 通过ViewRootImpl的scheduleTraversals()进行界面的三大流程。
  2. 调用到scheduleTraversals()时不会立即执行,而是将该操作保存到待执行队列中。并给底层的刷新信号注册监听。
  3. 当VSYNC信号到来时,会从待执行队列中取出对应的scheduleTraversals()操作,并将其加入到主线程的消息队列中。
  4. 主线程从消息队列中取出并执行三大流程: onMeasure()-onLayout()-onDraw()

同步屏障的作用

  1. 同步屏障用于阻塞住所有的同步消息(底层VSYNC的回调onVsync方法提交的消息是异步消息)
  2. 用于保证界面刷新功能的performTraversals()的优先执行。

同步屏障的原理?

  1. 主线程的Looper会一直循环调用MessageQueue的next方法并且取出队列头部的Message执行,遇到同步屏障(一种特殊消息)后会去寻找异步消息执行。如果没有找到异步消息就会一直阻塞下去,除非将同步屏障取出,否则永远不会执行同步消息。
  2. 界面刷新操作是异步消息,具有最高优先级
  3. 我们发送的消息是同步消息,再多耗时操作也不会影响UI的刷新操作

1.4 FrameLayout, LinearLayout, RelativeLayout 哪个效率高, 为什么?

对于比较三者的效率那肯定是要在相同布局条件下比较绘制的流畅度及绘制过程,在这里流畅度不好表达,并且受其他外部因素干扰比较多,比如CPU、GPU等等,我说下在绘制过程中的比较,

1、FrameLayout是从上到下的一个堆叠的方式布局的,那当然是绘制速度最快,只需要将本身绘制出来即可,但是由于它的绘制方式导致在复杂场景中直接是不能使用的,所以工作效率来说FrameLayout仅使用于单一场景

2、LinearLayout 在两个方向上绘制的布局,在工作中使用也比较多,绘制的时候只需要按照指定的方向绘制,绘制效率比FrameLayout要慢,但使用场景比较多

3、RelativeLayout 它的每个子控件都是需要相对的其他控件来计算,按照View树的绘制流程、在不同的分支上要进行计算相对应的位置,绘制效率最低,但是一般工作中的布局使用较多,所以说这三者之间效率分开来讲个有优势、不足,那一起来讲也是有优势、不足,所以不能绝对的区分三者的效率,好马用好铵 拿需求来说

1.5 谈谈Android的事件分发机制?

当点击的时候,会先调用顶级viewgroup的dispatchTouchEvent,如果顶级的viewgroup拦截了此事件(onInterceptTouchEvent返回true),则此事件序列由顶级viewgroup处理。

如果顶级viewgroup设置setOnTouchListener,则会回调接口中的onTouch,此时顶级的viewgroup中的onTouchEvent不再回调,如果不设置setOnTouchListener则onTouchEvent会回调。如果顶级viewgroup设置setOnClickListener,则会回调接口中的onClick。

如果顶级viewgroup不拦截事件,事件就会向下传递给他的子view,然后子view就会调用它的dispatchTouchEvent方法。

1.6 谈谈自定义View的流程?

1 安卓View的绘制流程(比较简单,想要深入的可以去看源码)
2 安卓自定义View的绘制步骤自定义View是一个老生常谈的问题,对于一个Android开发者来说是必须掌握的知识点,也是Android开发进阶的必经之路。要想安卓理解自定义View的流程,首先我们要了解View的绘制流程。分析之前,我们先来看底下面这张图:

View的绘制流程

DecorView是一个应用窗口的根容器,它本质上是一个FrameLayout。DecorView有唯一一个子View,它是一个垂直LinearLayout,包含两个子元素,一个是TitleView(ActionBar的容器),另一个是ContentView(窗口内容的容器)。关于ContentView,它是一个FrameLayout(android.R.id.content),我们平常用的setContentView就是设置它的子View。上图还表达了每个Activity都与一个Window(具体来说是PhoneWindow)相关联,用户界面则由Window所承载。

ViewRoot

在介绍View的绘制前,首先我们需要知道是谁负责执行View绘制的整个流程。实际上,View的绘制是由ViewRoot来负责的。每个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,这种关联关系是由WindowManager来维护的。那么decorView与ViewRoot的关联关系是在什么时候建立的呢?答案是Activity启动时,ActivityThread.handleResumeActivity()方法中建立了它们两者的关联关系。这里我们不具体分析它们建立关联的时机与方式,感兴趣的同学可以参考相关源码。下面我们直入主题,分析一下ViewRoot是如何完成View的绘制的。

View绘制的起点

当建立好了decorView与ViewRoot的关联后,ViewRoot类的requestLayout()方法会被调用,以完成应用程序用户界面的初次布局。实际被调用的是ViewRootImpl类的requestLayout()方法,这个方法的源码如下:

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            // 检查发起布局请求的线程是否为主线程
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

上面的方法中调用了scheduleTraversals()方法来调度一次完成的绘制流程,该方法会向主线程发送一个“遍历”消息,最终会导致ViewRootImpl的performTraversals()方法被调用。下面,我们以performTraversals()为起点,来分析View的整个绘制流程。

三个阶段

View的整个绘制流程可以分为以下三个阶段:

  • measure: 判断是否需要重新计算View的大小,需要的话则计算;
  • layout: 判断是否需要重新计算View的位置,需要的话则计算;
  • draw: 判断是否需要重新绘制View,需要的话则重绘制。

这三个子阶段可以用下图来描述:

measure阶段

此阶段的目的是计算出控件树中的各个控件要显示其内容的话,需要多大尺寸。起点是ViewRootImpl的measureHierarchy()方法,这个方法的源码如下:

    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
                                     final Resources res, final int desiredWindowWidth,
                                     final int desiredWindowHeight) {
        // 传入的desiredWindowXxx为窗口尺寸
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        boolean windowSizeMayChange = false;
        //. . .
        boolean goodMeasure = false;
        if (!goodMeasure) {
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                windowSizeMayChange = true;
            }
        }
        return windowSizeMayChange;
    }

上面的代码中调用getRootMeasureSpec()方法来获取根MeasureSpec,这个根MeasureSpec代表了对decorView的宽高的约束信息。具体的内部方法您可以直接再AS进行查看,不再赘述。

layout阶段

layout阶段的基本思想也是由根View开始,递归地完成整个控件树的布局(layout)工作。

View.layout()

我们把对decorView的layout()方法的调用作为布局整个控件树的起点,实际上调用的是View类的layout()方法,源码如下:

    public void layout(int l, int t, int r, int b) {
        // l为本View左边缘与父View左边缘的距离
        // t为本View上边缘与父View上边缘的距离
        // r为本View右边缘与父View左边缘的距离
        // b为本View下边缘与父View上边缘的距离
        // . . .
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            //. . .
        }
        //. . .
    }

这个方法会调用setFrame()方法来设置View的mLeft、mTop、mRight和mBottom四个参数,这四个参数描述了View相对其父View的位置(分别赋值为l, t, r, b),在setFrame()方法中会判断View的位置是否发生了改变,若发生了改变,则需要对子View进行重新布局,对子View的局部是通过onLayout()方法实现了。由于普通View( 非ViewGroup)不含子View,所以View类的onLayout()方法为空。因此接下来,您可以通过源码查看ViewGroup类的onLayout()方法的实现,不再赘述。

draw阶段

对于本阶段的分析,我们以decorView.draw()作为分析的起点,也就是View.draw()方法,它的源码如下:

    public void draw(Canvas canvas) {
        . . .
        // 绘制背景,只有dirtyOpaque为false时才进行绘制,下同
        int saveCount;
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
        . . .
        // 绘制自身内容
        if (!dirtyOpaque) onDraw(canvas);
        // 绘制子View
        dispatchDraw(canvas);
        . . .
        // 绘制滚动条等
        onDrawForeground(canvas);
    }

简单起见,在上面的代码中我们省略了实现滑动时渐变边框效果相关的逻辑。实际上,View类的onDraw()方法为空,因为每个View绘制自身的方式都不尽相同,对于decorView来说,由于它是容器View,所以它本身并没有什么要绘制的。dispatchDraw()方法用于绘制子View,显
然普通View(非ViewGroup)并不能包含子View,所以View类中这个方法的实现为空。

ViewGroup类的dispatchDraw()方法中会依次调用drawChild()方法来绘制子View,drawChild()方法的源码如下:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
     return child.draw(canvas, this, drawingTime);
}

这个方法调用了View.draw(Canvas, ViewGroup,long)方法来对子View进行绘制。在draw(Canvas, ViewGroup, long)方法中,首先对canvas进行了一系列变换,以变换到将要被绘制的View的坐标系下。完成对canvas的变换后,便会调用View.draw(Canvas)方法进行实际的绘制工作,此时传入的canvas为经过变换的,在将被绘制View的坐标系下的canvas。

进入到View.draw(Canvas)方法后,会向之前介绍的一样,执行以下几步:

  • 绘制背景
  • 通过onDraw()绘制自身内容
  • 通过dispatchDraw()绘制子View
  • 绘制滚动条

至此,整个View的绘制流程我们就分析完了。

Android自定义View / ViewGroup的步骤大致如下:

  1. 自定义属性;
  2. 选择和设置构造方法;
  3. 重写onMeasure()方法;
  4. 重写onDraw()方法;
  5. 重写onLayout()方法;
  6. 重写其他事件的方法(滑动监听等);

自定义属性

Android自定义属性主要有定义、使用和获取三个步骤。

定义自定义属性

我们通常将自定义属性定义在/values/attr.xml文件中(attr.xml文件需要自己创建)。
示例代码:




    


可以看到,我们先是定义了一个属性rightPadding,然后又在CustomMenu中引用了这个属性。下面说明一下:首先,我们可以在declare-stylable标签中直接定义属性而不需要引用外部定义好的属性,但是为了属性的重用,我们可以选择上面的这种方法:先定义,后引用;

declare-stylable标签只是为了给自定义属性分类。一个项目中可能又多个自定义控件,但只能有一个attr.xml文件,因此我们需要对不同自定义控件中的自定义属性进行分类,这也是为什么declare-stylable标签中的name属性往往定义成自定义控件的名称;

所谓的在declare-stylable标签中的引用,就是去掉了外部定义的format属性,如果没有去掉format,则会报错;如果外部定义中没有format而在内部引用中又format,也一样会报错。

常用的format类型:

  1. string:字符串类型;
  2. integer:整数类型;
  3. float:浮点型;
  4. dimension:尺寸,后面必须跟dp、dip、px、sp等单位;
  5. Boolean:布尔值;
  6. reference:引用类型,传入的是某一资源的ID,必须以“@”符号开头;
  7. color:颜色,必须是“#”符号开头;
  8. fraction:百分比,必须是“%”符号结尾;
  9. enum:枚举类型

下面对format类型说明几点:
format中可以写多种类型,中间使用“|”符号分割开,表示这几种类型都可以传入这个属性;
enum类型的定义示例如下代码所示:



    
    


    


使用时通过getInt()方法获取到value并判断,根据不同的value进行不同的操作即可。

使用自定义属性

在XML布局文件中使用自定义的属性时,我们需要先定义一个namespace。Android中默认的namespace是android,因此我们通常可以使用“android:xxx”的格式去设置一个控件的某个属性,android这个namespace的定义是在XML文件的头标签中定义的,通常是这样的:

xmlns:android="http://schemas.android.com/apk/res/android"

我们自定义的属性不在这个命名空间下,因此我们需要添加一个命名空间。
自定义属性的命名空间如下:

xmlns:app="http://schemas.android.com/apk/res-auto"

可以看出来,除了将命名空间的名称从android改成app之外,就是将最后的“res/android”改成了“res-auto”。

注意:自定义namespace的名称可以自己定义,不一定非得是app。

获取自定义属性

在自定义View / ViewGroup中,我们可以通过TypedArray获取到自定义的属性。示例代码如下:

    public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.getTheme()
                .obtainStyledAttributes(attrs, R.styleable.CustomMenu, defStyleAttr, 0);
        int indexCount = a.getIndexCount();
        for (int i = 0; i < indexCount; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.CustomMenu_rightPadding:
                    mMenuRightPadding = a.getDimensionPixelSize(attr, 0);
                    break;
            }
        }
        a.recycle();
    }

获取自定义属性的代码通常是在三个参数的构造方法中编写的(具体为什么是三个参数的构造方法,下面的章节中会有解释);

在获取TypedArray对象时就为其绑定了该自定义View的自定义属性集(CustomMenu),通过
getIndexCount()方法获取到自定义属性的数量,通过getIndex()方法获取到某一个属性,最后通过switch语句判断属性并进行相应的操作;

在TypedArray使用结束后,需要调用recycle()方法回收它。

构造方法

当我们定义一个新的类继承了View或ViewGroup时,系统都会提示我们重写它的构造方法。View / ViewGroup中又四个构造方法可以重写,它们分别有一、二、三、四个参数。四个参数的构造方法我们通常用不到,因此这个章节中我们主要介绍一个参数、两个参数和三个参数的构造方法(这里以CustomMenu控件为例)。

一个参数的构造方法

public CustomMenu(Context context) { …… }

这个构造方法只有一个参数Context上下文。当我们在JAVA代码中直接通过new关键在创建这个控件时,就会调用这个方法。

两个参数的构造方法

public CustomMenu(Context context, AttributeSet attrs) { …… }

这个构造方法有两个参数:Context上下文和AttributeSet属性集。当我们需要在自定义控件中获取属性时,就默认调用这个构造方法。AttributeSet对象就是这个控件中定义的所有属性。

我们可以通过AttributeSet对象的getAttributeCount()方法获取属性的个数,通过getAttributeName()方法获取到某条属性的名称,通过getAttributeValue()方法获取到某条属性的值。
注意:不管有没有使用自定义属性,都会默认调用这个构造方法,“使用了自定义属性就会默认调用三个参数的构造方法”的说法是错误的。

三个参数的构造方法

public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) { …… }

这个构造方法中有三个参数:Context上下文、AttributeSet属性集和defStyleAttr自定义属性的引用。这个构造方法不会默认调用,必须要手动调用,这个构造方法和两个参数的构造方法的唯一区别就是这个构造方法给我们默认传入了一个默认属性集。

defStyleAttr指向的是自定义属性的标签中定义的自定义属性集,我们在创建TypedArray对象时需要用到defStyleAttr。

三个构造方法的整合

一般情况下,我们会将这三个构造方法串联起来,即层层调用,让最终的业务处理都集中在三个参数的构造方法。我们让一参的构造方法引用两参的构造方法,两参的构造方法引用三参的构造方法。示例代码如下:

public CustomMenu(Context context) {
    this(context, null);
}
public CustomMenu(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}
public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
// 业务代码
}

这样一来,就可以保证无论使用什么方式创建这个控件,最终都会到三个参数的构造方法中处理,减少了重复代码。

onMeasure()

onMeasure()方法中主要负责测量,决定控件本身或其子控件所占的宽高。我们可以通过onMeasure()方法提供的参数widthMeasureSpec和heightMeasureSpec来分别获取控件宽度和高度的测量模式和测量值(测量 = 测量模式 + 测量值)。

widthMeasureSpec和heightMeasureSpec虽然只是int类型的值,但它们是通过MeasureSpec类进行了编码处理的,其中封装了测量模式和测量值,因此我们可以分别通过MeasureSpec.getMode(xMeasureSpec)和MeasureSpec. getSize(xMeasureSpec)来获取到控件或其子View的测量模式和测量值。

测量模式分为以下三种情况:

  1. EXACTLY:当宽高值设置为具体值时使用,如100DIP、match_parent等,此时取出的size是精确的尺寸;
  2. AT_MOST:当宽高值设置为wrap_content时使用,此时取出的size是控件最大可获得的空间;
  3. UNSPECIFIED:当没有指定宽高值时使用(很少见)。

onMeasure()方法中常用的方法:

  1. getChildCount():获取子View的数量;
  2. getChildAt(i):获取第i个子控件;
  3. subView.getLayoutParams().width/height:设置或获取子控件的宽或高;
  4. measureChild(child, widthMeasureSpec, heightMeasureSpec):测量子View的宽高;
  5. child.getMeasuredHeight/width():执行完measureChild()方法后就可以通过这种方式获取子View的宽高值;
  6. getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;
  7. setMeasuredDimension(width, height):重新设置控件的宽高。如果写了这句代码,就需要删除“super.onMeasure(widthMeasureSpec,heightMeasureSpec);”这行代码。

注意:onMeasure()方法可能被调用多次,这是因为控件中的内容或子View可能对分配给自己的空间“不满意”,因此向父空间申请重新分配空间。

onDraw()

onDraw()方法负责绘制,即如果我们希望得到的效果在Android原生控件中没有现成的支持,那么我们就需要自己绘制我们的自定义控件的显示效果。

要学习onDraw()方法,我们就需要学习在onDraw()方法中使用最多的两个类:Paint和Canvas。

注意:每次触摸了自定义View/ViewGroup时都会触发onDraw()方法。

Paint类

Paint画笔对象,这个类中包含了如何绘制几何图形、文字和位图的样式和颜色信息,指定了如何绘制文本和图形。画笔对象右很多设置方法,大体上可以分为两类:一类与图形绘制有关,一类与文本绘制有关。

Paint类中有如下方法:

1、图形绘制:

  1. setArgb(int a, int r, int g, int b):设置绘制的颜色,a表示透明度,r、g、b表示颜色值;
  2. setAlpha(int a):设置绘制的图形的透明度;
  3. setColor(int color):设置绘制的颜色;
  4. setAntiAlias(boolean a):设置是否使用抗锯齿功能,抗锯齿功能会消耗较大资源,绘制图形的速度会减慢;
  5. setDither(boolean b):设置是否使用图像抖动处理,会使图像颜色更加平滑饱满,更加清晰;
  6. setFileterBitmap(Boolean b):设置是否在动画中滤掉Bitmap的优化,可以加快显示速度;
  7. setMaskFilter(MaskFilter mf):设置MaskFilter来实现滤镜的效果;
  8. setColorFilter(ColorFilter cf):设置颜色过滤器,可以在绘制颜色时实现不同颜色的变换效果;
  9. setPathEffect(PathEffect pe):设置绘制的路径的效果;
  10. setShader(Shader s):设置Shader绘制各种渐变效果;
  11. setShadowLayer(float r, int x, int y, int c):在图形下面设置阴影层,r为阴影角度,x和y为阴影在x轴和y轴上的距离,c为阴影的颜色;
  12. setStyle(Paint.Style s):设置画笔的样式:FILL实心;STROKE空心;FILL_OR_STROKE同时实心与空心;
  13. setStrokeCap(Paint.Cap c):当设置画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式;
  14. setStrokeJoin(Paint.Join j):设置绘制时各图形的结合方式;
  15. setStrokeWidth(float w):当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度;
  16. setXfermode(Xfermode m):设置图形重叠时的处理方式;

2、文本绘制:

  1. setTextAlign(Path.Align a):设置绘制的文本的对齐方式;
  2. setTextScaleX(float s):设置文本在X轴的缩放比例,可以实现文字的拉伸效果;
  3. setTextSize(float s):设置字号;
  4. setTextSkewX(float s):设置斜体文字,s是文字倾斜度;
  5. setTypeFace(TypeFace tf):设置字体风格,包括粗体、斜体等;
  6. setUnderlineText(boolean b):设置绘制的文本是否带有下划线效果;
  7. setStrikeThruText(boolean b):设置绘制的文本是否带有删除线效果;
  8. setFakeBoldText(boolean b):模拟实现粗体文字,如果设置在小字体上效果会非常差;
  9. setSubpixelText(boolean b):如果设置为true则有助于文本在LCD屏幕上显示效果;

3、其他方法:

  1. getTextBounds(String t, int s, int e, Rect b):将页面中t文本从s下标开始到e下标结束的所有字符所占的区域宽高封装到b这个矩形中;
  2. clearShadowLayer():清除阴影层;
  3. measureText(String t, int s, int e):返回t文本中从s下标开始到e下标结束的所有字符所占的宽度;
  4. reset():重置画笔为默认值。

这里需要就几个方法解释一下:

1、setPathEffect(PathEffect pe),设置绘制的路径的效果,常见的有以下几种可选方案:

  1. CornerPathEffect:可以用圆角来代替尖锐的角;
  2. DathPathEffect:虚线,由短线和点组成;
  3. DiscretePathEffect:荆棘状的线条;
  4. PathDashPathEffect:定义一种新的形状并将其作为原始路径的轮廓标记;
  5. SumPathEffect:在一条路径中顺序添加参数中的效果;
  6. ComposePathEffect:将两种效果组合起来,先使用第一种效果,在此基础上应用第二种效果。

2、setXfermode(Xfermode m),设置图形重叠时的处理方式,关于Xfermode的多种效果,我们可以参考下面一张图:

在使用的时候,我们需要通过paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XXX))来设置,XXX是上图中的某种模式对应的常量参数,如DST_OUT。

这16中情况的具体解释如下:

  1. PorterDuff.Mode.CLEAR:所绘制不会提交到画布上。
  2. PorterDuff.Mode.SRC:显示上层绘制图片
  3. PorterDuff.Mode.DST:显示下层绘制图片
  4. PorterDuff.Mode.SRC_OVER:正常绘制显示,上下层绘制叠盖。
  5. PorterDuff.Mode.DST_OVER:上下层都显示。下层居上显示。
  6. PorterDuff.Mode.SRC_IN:取两层绘制交集。显示上层。
  7. PorterDuff.Mode.DST_IN:取两层绘制交集。显示下层。
  8. PorterDuff.Mode.SRC_OUT:上层绘制非交集部分。
  9. PorterDuff.Mode.DST_OUT:取下层绘制非交集部分。
  10. PorterDuff.Mode.SRC_ATOP:取下层非交集部分与上层交集部分
  11. PorterDuff.Mode.DST_ATOP:取上层非交集部分与下层交集部分
  12. PorterDuff.Mode.XOR:异或:去除两图层交集部分
  13. PorterDuff.Mode.DARKEN:取两图层全部区域,交集部分颜色加深
  14. PorterDuff.Mode.LIGHTEN:取两图层全部,点亮交集部分颜色
  15. PorterDuff.Mode.MULTIPLY:取两图层交集部分叠加后颜色
  16. PorterDuff.Mode.SCREEN:取两图层全部区域,交集部分变为透明色

Canvas类

Canvas即画布,其上可以使用Paint画笔对象绘制很多东西。

Canvas**对象中可以绘制:

  1. drawArc():绘制圆弧;
  2. drawBitmap():绘制Bitmap图像;
  3. drawCircle():绘制圆圈;
  4. drawLine():绘制线条;
  5. drawOval():绘制椭圆;
  6. drawPath():绘制Path路径;
  7. drawPicture():绘制Picture图片;
  8. drawRect():绘制矩形;
  9. drawRoundRect():绘制圆角矩形;
  10. drawText():绘制文本;
  11. drawVertices():绘制顶点;

Canvas**对象的其他方法:

  1. canvas.save():把当前绘制的图像保存起来,让后续的操作相当于是在一个新图层上绘制;
  2. canvas.restore():把当前画布调整到上一个save()之前的状态;
  3. canvas.translate(dx, dy):把当前画布的原点移到(dx, dy)点,后续操作都以(dx, dy)点作为参照;
  4. canvas.scale(x, y):将当前画布在水平方向上缩放x倍,竖直方向上缩放y倍;
  5. canvas.rotate(angle):将当前画布顺时针旋转angle度。

onLayout()

onLayout()方法负责布局,大多数情况是在自定义ViewGroup中才会重写,主要用来确定子View在这个布局空间中的摆放位置。

onLayout(boolean changed, int l, int t, int r, int b)方法有5个参数,其中changed表示这个控件是否有了新的尺寸或位置;l、t、r、b分别表示这个View相对于父布局的左/上/右/下方的位置。

以下是onLayout()方法中常用的方法:

  1. getChildCount():获取子View的数量;
  2. getChildAt(i):获取第i个子View
  3. getWidth/Height():获取onMeasure()中返回的宽度和高度的测量值;
  4. child.getLayoutParams():获取到子View的LayoutParams对象;
  5. child.getMeasuredWidth/Height():获取onMeasure()方法中测量的子View的宽度和高度值;
  6. getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;
  7. child.layout(l, t, r, b):设置子View布局的上下左右边的坐标。

其他方法

generateLayoutParams()

generateLayoutParams()方法用在自定义ViewGroup中,用来指明子控件之间的关系,即与当前的ViewGroup对应的LayoutParams。我们只需要在方法中返回一个我们想要使用的LayoutParams类型的对象即可。

在generateLayoutParams()方法中需要传入一个AttributeSet对象作为参数,这个对象是这个ViewGroup的属性集,系统根据这个ViewGroup的属性集来定义子View的布局规则,供子View使用。

例如,在自定义流式布局中,我们只需要关心子控件之间的间隔关系,因此我们需要在
generateLayoutParams()方法中返回一个newMarginLayoutParams()即可。

onTouchEvent()

onTouchEvent()方法用来监测用户手指操作。我们通过方法中MotionEvent参数对象的getAction()方法来实时获取用户的手势,有UP、DOWN和MOVE三个枚举值,分别表示用于手指抬起、按下和滑动的动作。每当用户有操作时,就会回掉onTouchEvent()方法。

onScrollChanged()

如果我们的自定义View / ViewGroup是继承自ScrollView / HorizontalScrollView等可以滚动的控件,就可以通过重写onScrollChanged()方法来监听控件的滚动事件。

这个方法中有四个参数:l和t分别表示当前滑动到的点在水平和竖直方向上的坐标;oldl和oldt分别表示上次滑动到的点在水平和竖直方向上的坐标。我们可以通过这四个值对滑动进行处理,如添加属性动画等。

invalidate()

invalidate()方法的作用是请求View树进行重绘,即draw()方法,如果视图的大小发生了变化,还会调用layout()方法。

一般会引起invalidate()操作的函数如下:

  1. 直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身;
  2. 调用setSelection()方法,请求重新draw(),但只会绘制调用者本身;
  3. 调用setVisibility()方法,会间接调用invalidate()方法,继而绘制该View;
  4. 调用setEnabled()方法,请求重新draw(),但不会重新绘制任何视图,包括调用者本身。

postInvalidate()

功能与invalidate()方法相同,只是postInvalidate()方法是异步请求重绘视图。

requestLayout()

requestLayout()方法只是对View树进行重新布局layout过程(包括measure()过程和layout()过程),不会调用draw()过程,即不会重新绘制任何视图,包括该调用者本身。

requestFocus()

请求View树的draw()过程,但只会绘制需要重绘的视图,即哪个View或ViewGroup调用了这个方法,就重绘哪个视图。

总结

最后,让我们来总览一下自定义View / ViewGroup时调用的各种函数的顺序,如下图所示:

1.7 针对RecyclerView你做了哪些优化?

1 onBindViewHolder
这个方法含义应该都知道是绑定数据,并且是在UI线程,所以要尽量在这个方法中少做一些业务处理

2 数据优化
采用android Support 包下的DIffUtil集合工具类结合RV分页加载会更加友好,节省性能

3 item优化
减少item的View的层级,(pps:当然推荐把一个item自定义成一个View,如果有能力的话),如果item的高度固定的话可以设置setHasFixedSize(true),避免requestLayout浪费资源

4 使用RecycledViewPool
RecycledViewPool是对item进行缓存的,item相同的不同RV可以才使用这种方式进行性能提升

5 Prefetch预取
这是在RV25.1.0及以上添加的新功能,预取详情

6 资源回收
通过重写RecyclerView.onViewRecycled(holder)来合理的回收资源。

1.8 谈谈如何优化ListView?

  • ViewHolder什么的持有View
  • 预加载/懒加载数据什么的
  • 大招:用RecyclerView替换ListView
  • 绝招:直接删除控件

1.9 谈谈自定义LayoutManager的流程?

  1. 确定Itemview的LayoutParams generateDefaultLayoutParams
  2. 确定所有itemview在recyclerview的位置,并且回收和复用itemview onLayoutChildren
  3. 添加滑动canScrollVertically

1.10 什么是 RemoteViews?使用场景有哪些?

RemoteViews
RemoteViews翻译过来就是远程视图.顾名思义,RemoteViews不是当前进程的View,是属于SystemServer进程.应用程序与RemoteViews之间依赖Binder实现了进程间通信.

用法
通常是在通知栏

    //1.创建RemoteViews实例
    RemoteViews mRemoteViews = new RemoteViews("com.example.remoteviewdemo", R.layout.remoteview_layout);
    //2.构建一个打开Activity的PendingIntent
    Intent intent = new Intent(MainActivity.this, MainActivity.class);
    PendingIntent mPendingIntent = PendingIntent.getActivity(MainActivity.this, 0,
            intent, PendingIntent.FLAG_UPDATE_CURRENT);
    //3.创建一个Notification
    mNotification = new Notification.Builder(this)
    .setSmallIcon(R.drawable.ic_launcher)
    .setContentIntent(mPendingIntent)
    .setContent(mRemoteViews)
    .build();

    //4.获取NotificationManager
    manager =(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

    Button button1 = (Button) findViewById(R.id.button1);
    button1.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick (View v){
        //弹出通知
        manager.notify(1, mNotification);
        }
    });

1.11 谈一谈获取View宽高的几种方法?

  1. OnGlobalLayoutListener获取
  2. OnPreDrawListener获取
  3. OnLayoutChangeListener获取
  4. 重写View的onSizeChanged()
  5. 使用View.post()方法

1.12 谈一谈插值器和估值器?

1、插值器,根据时间(动画时常)流逝的百分比来计算属性变化的百分比。系统默认的有匀速,加减速,减速插值器。

2、估值器,通过上面插值器得到的百分比计算出具体变化的值。系统默认的有整型,浮点型,颜色估值器

3、自定义只需要重写他们的evaluate方法就可以了。

1.13 getDimension、getDimensionPixelOffset 和 getDimensionPixelSize 三者的区别?

相同点

单位为dp/sp时,都会乘以density,单位为px则不乘

不同点

1、getDimension返回的是float值
2、getDimensionPixelSize,返回的是int值,float转成int时,四舍五入
3、getDimensionPixelOffset,返回的是int值,float转int时,向下取整(即忽略小数值)

1.14 请谈谈源码中StaticLayout的用法和应用场景?

构造方法:

    public StaticLayout(CharSequence source,
                        int bufstart,
                        int bufend,
                        TextPaint paint,
                        int outerwidth,
                        Alignment align,
                        float spacingmult,
                        float spacingadd,
                        boolean includepad,
                        TextUtils.TruncateAt ellipsize,
                        int ellipsizedWidth) {
        this(source, bufstart, bufend, paint, outerwidth, align,
                TextDirectionHeuristics.FIRSTSTRONG_LTR, spacingmult, 
                spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
    }

说明参数的作用:
CharSequence source 需要分行的字符串
int bufstart 需要分行的字符串从第几的位置开始
int bufend 需要分行的字符串到哪里结束
TextPaint paint 画笔对象
int outerwidth layout的宽度,超出时换行
Alignment align layout的对其方式,有ALIGN_CENTER, ALIGN_NORMAL, ALIGN_OPPOSITE三种
float spacingmult 相对行间距,相对字体大小,1.5f表示行间距为1.5倍的字体高度。
float spacingadd 在基础行距上添加多少
boolean includepad,TextUtils.TruncateAt ellipsize 从什么位置开始省略
int ellipsizedWidth 超过多少开始省略

1.15 有用过ConstraintLayout吗?它有哪些特点?

属性图

1.16 关于LayoutInflater,它是如何通过inflate 方法获取到具体View的?

系统通过LayoutInflater.from创建出布局构造器,inflate方法中,最后会掉用createViewFromTag 这里他会去判断两个参数 factory2 和factory 如果都会空就会系统自己去创建view, 并且通过一个xml解析器,获取标签名字,然后判断是

1.17 谈一谈Fragment懒加载?

重写setUserVisibleHint()

1.18 谈谈RecyclerView的缓存机制?

scrap viewCache recyclerPool

  • scrap 是当前展示的缓存, 在onlayout时候 缓存
  • viewCache 是屏幕外看不见的缓存, 可以吧viewCache设置大点,空间换时间 避免一段距离内快速滑动卡顿

以上两种缓存是不走 createView和 onbind

  • recyclerPool 比较特殊他是会走 onbind的,他可以被多个recyclerView共享内部的item,实际用途是:多个RecyclerView之间共享item,应用在垂直RecyclerView内嵌水平RecyclerView,或者ViewPager中多个RecyclerView

1.19 请谈谈View.inflate和LayoutInflater.inflate的区别?

  1. 实际上没有区别,View.inflate实际上是对LayoutInflater.inflate做了一层包装,在功能上,LayoutInflate功能更加强大。
  2. View.inflate实际上最终调用的还是LayoutInflater.inflate(@LayoutRes int resource, @nullable ViewGroup root)三个参数的方法,这里如果传入的root如果不为空,那么解析出来的View会被添加到这个ViewGroup当中去。
  3. 而LayoutInflater.inflate方法则可以指定当前View是否需要添加到ViewGroup中去。

总结一下:

  • 如果root为null,attachToRoot将失去作用,设置任何值都没有意义。
  • 如果root不为null,attachToRoot设为true,则会给加载的布局文件的指定一个父布局,即root。
  • 如果root不为null,attachToRoot设为false,则会将布局文件最外层的所有layout属性进行设- 置,当该view被添加到父view当中时,这些layout属性会自动生效。
  • 在不设置attachToRoot参数的情况下,如果root不为null,attachToRoot参数默认为true。

不管调用的几个参数的方法,最终都会调用如下方法:

    /**
     * Inflate a new view hierarchy from the
     * specified XML node. Throws
     * {@link InflateException} if there is
     * an error.
     * 

* Important *    For performance * reasons, view inflation relies heavily * on pre-processing of XML files * that is done at build time. Therefore, * it is not currently possible to * use LayoutInflater with an * XmlPullParser over a plain XML file at * runtime. * * @param parser XML dom node * containing the description of the view * hierarchy. * @param root Optional view to * be the parent of the generated hierarchy (if * * attachToRoot is true), or else * simply an object that * provides a set of * LayoutParams values for root of the returned * hierarchy (if * attachToRoot is false.) * @param attachToRoot Whether the * inflated hierarchy should be attached to * the root * parameter? If false, root is only used to * create the * correct subclass * of LayoutParams for the root view in the XML. * @return The root View of the inflated * hierarchy. If root was supplied and * attachToRoot is true, this is root; * otherwise it is the root of * the inflated XML file. */ public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate"); final Context inflaterContext = mContext; final AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context) mConstructorArgs[0]; mConstructorArgs[0] = inflaterContext; //最终返回的View View result = root; try { // Look for the root node. int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } if (type != XmlPullParser.START_TAG) { throw new InflateException(parser.getPositionDescription() + ": No start tag found !"); } final String name = parser.getName(); if (DEBUG) { System.out.println("************************** "); System.out.println("Creating root view: " + name); System.out.println("************************** "); } if (TAG_MERGE.equals(name)) { if (root == null || !attachToRoot) { throw new InflateException(" can be used only with a valid" + "ViewGroup root and attachToRoot = true"); } rInflate(parser, root, inflaterContext, attrs, false); } else { // Temp is the root view that was found in the xml final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; //root不为空,并且 attachToRoot为false时则给当前View设置 LayoutParams if (root != null) { if (DEBUG) { System.out.println("Creating params from root:" + root); } // Create layout params that match root, if supplied params = root.generateLayoutParams(attrs); if (!attachToRoot) { // Set the layout params for temp if we are not // attaching. (If we are, we use addView, below) temp.setLayoutParams(params); } } if (DEBUG) { System.out.println("----- > start inflating children"); } // Inflate all children under temp against its context. rInflateChildren(parser, temp, attrs, true); if (DEBUG) { System.out.println("----- > done inflating children"); } // We are supposed to attach all the views we found ( int temp) // to root. Do that now. //如果root不为空,并且attachToRoot为ture,那么将解析出来当View添加到当前到root当中,最后返回root if (root != null && attachToRoot) { root.addView(temp, params); } // Decide whether to return the root that was passed in or the // top view found in xml. //如果root等于空,那么将解析完的布局赋值给result最后返回, 大部分用的都是这个。 if (root == null || !attachToRoot) { result = temp; } } } catch (XmlPullParserException e) { final InflateException ie = new InflateException(e.getMessage(), e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } catch (Exception e) { final InflateException ie = new InflateException(parser.getPositionDescription() + ": " + e.getMessage(), e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } finally { // Don't retain static reference on context. mConstructorArgs[0] = lastContext; mConstructorArgs[1] = null; Trace.traceEnd(Trace.TRACE_TAG_VIEW); } return result; } }

1.20 请谈谈invalidate()和postInvalidate()方法的区别和应用场景?

  1. invalidate()用来重绘UI,需要在UI线程调用。
  2. postInvalidate()也是用来重新绘制UI,它可以在UI线程调用,也可以在子线程中调用postInvalidate()方法内部通过Handler发送了一个消息将线程切回到UI线程通知重新绘制,并不是说postInvalidate()可以在子线程更新UI,本质上还是在UI线程发生重绘,只不过我们使用postInvalidate()它内部会帮我们切换线程
    /**
     * 

Cause an invalidate to happen on a * subsequent cycle through the event loop. * Use this to invalidate the View from a * non-UI thread.

* *

This method can be invoked from * outside of the UI thread * only when this View is attached to a * window.

* * @see #invalidate() * @see #postInvalidateDelayed(long) */ public void postInvalidate() { postInvalidateDelayed(0); } /** *

Cause an invalidate to happen on a * subsequent cycle through the event * loop. Waits for the specified amount * of time.

* *

This method can be invoked from * outside of the UI thread * only when this View is attached to a * window.

* * @param delayMilliseconds the duration * in milliseconds to delay the * invalidation by * @see #invalidate() * @see #postInvalidate() */ public void postInvalidateDelayed(long delayMilliseconds) { // We try only with the AttachInfo because there's no point in invalidating // if we are not attached to our window final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds); } } public void dispatchInvalidateDelayed(View view, long delayMilliseconds) { Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view); mHandler.sendMessageDelayed(msg, delayMilliseconds); }

1.21 谈一谈自定义View和自定义ViewGroup?

onMeasure()
onDraw()
dispatchDraw()
onLayout()
onKeyDown()

1.22 谈一谈SurfaceView与TextureView的使用场景和用法?

1、频繁绘制和对帧率要求比较高的需求,比如拍照、视频和游戏等
2、SurfaceView有独立的绘图表面,可以在子线程中进行绘制,缺点是不能够执行平移、缩放、旋转、透明渐变操作,TextureView的出现就是为了解决这些问题
3、SurfaceView的使用方法,大概是获取SurfaceHolder对象,监听surface创建,更新,销毁,创建一个新的线程,并在其中绘制并提交
4、TextureView并没有独立的绘图表面,在使用过程中,需要添加监听surfaceTexture是否可用,再做相应的处理

1.23 谈一谈RecyclerView.Adapter的几种刷新方式有何不同?

  • 刷新全部可见的item,notifyDataSetChanged()
  • 刷新指定item,notifyItemChanged(int)
  • 从指定位置开始刷新指定个item,notifyItemRangeChanged(int,int)
  • 插入、移动、删除一个并自动刷新,notifyItemInserted(int)、notifyItemMoved(int)、notifyItemRemoved(int)
  • 局部刷新,notifyItemChanged(int, Object)

1.24 谈谈你对Window和WindowManager的理解?

  • Window:抽象类,窗体容器.创建DecorView.
  • PhoneWindow:Window实现类.
  • AppCompatDelegateImpl:AppCompatDeleGate的实现类.在构造方法中传入了Window.该类是Activity中方法的代理实现类.如:setContentView()...
  • WindowManager:接口类.同时实现了ViewManager.定义了大量Window的状态值
  • WindowManagerImpl:WindowManager的接口实现类.但具体的方法实现交给了WindowManagerGlobal.
  • WindowManagerGlobal:真正的WindowManager接口方法的处理类.如:创建ViewRootImpl等..
  • Window/WindowManager均在Activity的attach中完成
    final void attach(Context context, ActivityThread aThread,
                      Instrumentation instr, IBinder token, int ident,
                      Application application, Intent intent, ActivityInfo info,
                      CharSequence title, Activity parent, String id,
                      NonConfigurationInstances lastNonConfigurationInstances,
                      Configuration config, String referrer, IVoiceInteractor voiceInteractor,
                      Window window, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);
        mFragments.attachHost(null, parent);
    }
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().
setPrivateFactory(this);
if(info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED){
    mWindow.setSoftInputMode(info.softInputMode);
}
if(info.uiOptions !=0) {
    mWindow.setUiOptions(info.uiOptions);
}
mUiThread = Thread.currentThread();
......
mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(), (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if(mParent != null) {
    mWindow.setContainer(mParent.getWindow());
}
mWindowManager = mWindow.getWindowManager();
......

1.25 谈一谈Activity,View,Window三者的关系?

在Activity中调用attach,创建Window;
创建的Window是其子类PhoneWindow,在attach中创建PhoneWindow;
在Activity中调用setContentView (R.layout.xx);
其实就是调用getWindow.setContentView()创建parentView;
将指定的R.layout.xx布局进行填充调用ViewGroup;
调用ViewGroup先移除removeAllview();在进行添加新的View -- addview。

1.26 有了解过WindowInsets吗?它有哪些应用?

ViewRootImpl在performTraversals时会调dispatchApplyInsets,内调DecorView的dispatchApplyWindowInsets,进行WindowInsets的分发。

1.27 Android中View几种常见位移方式的区别?

  • setTranslationX/Y
  • scrollBy/scrollTo
  • offsetTopAndBottom/offsetLeftAndRight
  • 动画
  • margin
  • layout

这些位移的区别

1.28 为什么ViewPager嵌套ViewPager,内部的ViewPager滚动没有被拦截?

被外部的ViewPager拦截了,需要做滑动冲突处理。重写子View的 dispatchTouchEvent方法,在子View需要拦截的时候进行拦截,否则交给父View处理。


1.29 请谈谈Fragment的生命周期?

1,onAttach:Fragment 和 Activity 关联时调用,且调用一次。在回调中可以将参数 content 转换为 Activity保存下来,避免后期频繁获取 Activity。
2,onCreate:和 Activity 的 onCreate 类似
3,onCreateView:准备绘制 Fragment 界面时调用,返回值为根视图,注意使用 inflater 构建 View时 一定要将attachToRoot 指明为 false。
4,onActivityCreated:Activity 的onCreated 执行完时调用
5,onStart:可见时调用,前提是 Activity 已经 started
6,onResume:交互式调用,前提是 Activity 已经resumed
7,onPause:不可交互时调用
8,onStop:不可见时调用
9,onDestroyView:移除 Fragment 相关视图时调用
10,onDestroy:清除 Fragment 状态是调用
11,onDetach:和 Activity 解除关联时调用从生命周期可以看出,他们是两两对应的,如 onAttach 和 onDetach ,onCreate 和 onDestory , onCreateView 和 onDestroyView等

Fragment 在 ViewPager中的生命周期

ViewPager 有一个预加载机制,他会默认加载旁边的页面,也就是说在显示第一个页面的时候 旁边的页面已经加载完成了。这个可以设置,但不能为0,但是有些需求则不需要这个效果,这时候就可以使用懒加载了:懒加载的实现
1,当 ViewPager 可以左右滑动时,他左右两边的 Fragment 已经加载完成,这就是预加载机制
2,当 Fragment 不处于 ViewPager 左右两边时,就会执行 onPause,onStop,OnDestroyView 方法。

Fragment 之间传递数据方法

1,使用 bundle,有些数据需要被序列化
2,接口回调
3,在创建的时候通过构造直接传入
4,使用 EventBus 等

单 Activity 多 Fragment 的优点,Fragment 的优缺点

Fragment 比 activity 占用更少的资源,特别在中低端手机,Fragment 的响应速度非常快,如丝般的顺滑,更容易控制每个场景的生命周期和状态

优缺点:非常流畅,节省资源,灵活性高,Fragment 必须赖于Acctivity,而且 Fragment 的生命周期直接受所在的 Activity 影响。

1.30 请谈谈什么是同步屏障?

handler.getLooper().getQueue().postSyncBarrier()加入同步屏障后,Message.obtain()获取一个target为null的msg,并根据当前时间将该msg插入到链表中。在Looper.loop()循环取消息中 Message msg = queue.next(); target为空时,取链表中的异步消息。

通过setAsynchronous(true)来指定为异步消息应用场景:ViewRootImpl scheduleTraversals中加入同步屏障 并在view的绘制流程中post异步消息,保证view的绘制消息优先执行

1.31 谈一谈ViewDragHelper的工作原理?

ViewDragHelper类,是用来处理View边界拖动相关的类,比如我们这里要用的例子—侧滑拖动关闭页面(类似微信),该功能很明显是要处理在View上的触摸事件,记录触摸点、计算距离、滚动动画、状态回调等,如果我们自己手动实现自然会很麻烦还可能出错,而这个类会帮助我们大大简化工作量。
该类是在Support包中提供,所以不会有系统适配问题,下面我们就来看看他的原理和使用吧。

1. 初始化

    private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
        ...
        mParentView = forParent;//BaseView
        mCallback = cb;//callback
        final ViewConfiguration vc = ViewConfiguration.get(context);
        final float density = context.getResources().getDisplayMetrics().density;
        mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);//边界拖动距离范围
        mTouchSlop = vc.getScaledTouchSlop();//拖动距离阈值
        mScroller = new OverScroller(context, sInterpolator);//滚动器
    }
  • mParentView是指基于哪个View进行触摸处理
  • mCallback是触摸处理的各个阶段的回调
  • mEdgeSize是指在边界多少距离内算作拖动,默认为20dp
  • mTouchSlop指滑动多少距离算作拖动,用的系统默认值
  • mScroller是View滚动的Scroller对象,用于处理释触摸放后,View的滚动行为,比如滚动回原始位置或者滚动出屏幕

2. 拦截事件处理

该类提供了boolean shouldInterceptTouchEvent(MotionEvent)方法,通常我们需要这么写:

override fun onInterceptTouchEvent(ev: MotionEvent?) =
dragHelper?.shouldInterceptTouchEvent(ev) ?: super.onInterceptTouchEvent(ev)

该方法用于处理mParentView是否拦截此次事件

    public boolean shouldInterceptTouchEvent(MotionEvent ev) {
        ...
        switch (action) {
            ...
            case MotionEvent.ACTION_MOVE: {
                if (mInitialMotionX == null || mInitialMotionY == null) break;
                // First to cross a touch slop over a draggable view wins. Also report edge drags.
                final int pointerCount = ev.getPointerCount();
                for (int i = 0; i < pointerCount; i++) {
                    final int pointerId = ev.getPointerId(i);
                    // If pointer is invalid then skip the ACTION_MOVE.
                    if (!isValidPointerForActionMove(pointerId)) continue;
                    final float x = ev.getX(i);
                    final float y = ev.getY(i);
                    final float dx = x - mInitialMotionX[pointerId];
                    final float dy = y - mInitialMotionY[pointerId];
                    final View toCapture = findTopChildUnder((int) x, (int) y);
                    final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
                    ...
                    //判断pointer的拖动边界
                    reportNewEdgeDrags(dx, dy, pointerId);
                    ...
                }
                saveLastMotion(ev);
                break;
            }
            ...
        }
        return mDragState == STATE_DRAGGING;
    }

拦截事件的前提是mDragState为STATE_DRAGGING,也就是正在拖动状态下才会拦截,那么什么时候会变为拖动状态呢?当ACTION_MOVE时,调用reportNewEdgeDrags方法:

    private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
        int dragsStarted = 0;
        //判断是否在Left边缘进行滑动
        if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) {
            dragsStarted |= EDGE_LEFT;
        }
        if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) {
            dragsStarted |= EDGE_TOP;
        }
        ...
        if (dragsStarted != 0) {
            mEdgeDragsInProgress[pointerId] |= dragsStarted;
          //回调拖动的边
            mCallback.onEdgeDragStarted(dragsStarted, pointerId);
        }
    }

    private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) {
        final float absDelta = Math.abs(delta);
        final float absODelta = Math.abs(odelta);
        //是否支持edge的拖动以及是否满足拖动距离的阈值
        if ((mInitialEdgesTouched[pointerId] & edge) != edge || 
                (mTrackingEdges & edge) == 0 || 
                (mEdgeDragsLocked[pointerId] & edge) == edge || 
                (mEdgeDragsInProgress[pointerId] & edge) == edge || 
                (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) {
            return false;
        }
        if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) {
            mEdgeDragsLocked[pointerId] |= edge;
            return false;
        }
        return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop;
    }

可以看到,当ACTION_MOVE时,会尝试找到pointer对应的拖动边界,这个边界可以由我们来制定,比如侧滑关闭页面是从左侧开始的,所以我们可以调用setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT)来设置只支持左侧滑动。而一旦有滚动发生,就会回调callback的onEdgeDragStarted方法,交由我们做如下操作:

    override fun onEdgeDragStarted(edgeFlags: Int, pointerId: Int) {
        super.onEdgeDragStarted(edgeFlags, pointerId)
        dragHelper?.captureChildView(getChildAt(0), pointerId)
    }
    ...
    我们调用了ViewDragHelper的captureChildView方法:
    ...
    public void captureChildView(View childView, int activePointerId) {
        mCapturedView = childView;//记录拖动view
        mActivePointerId = activePointerId;
        mCallback.onViewCaptured(childView, activePointerId);
        setDragState(STATE_DRAGGING);//设置状态为开始拖动
    }

此时,我们就记录了拖动的View,并将状态置为拖动,那么在下次ACTION_MOVE的时候,该mParentView就会拦截事件,交由自己的onTouchEvent方法处理拖动了!

3.拖动事件处理

该类提供了void processTouchEvent(MotionEvent)方法,通常我们需要这么写:

override fun onTouchEvent(event: MotionEvent?): Boolean {
    dragHelper?.processTouchEvent(event)//交由ViewDragHelper处理
    return true
}

该方法用于处理mParentView拦截事件后的拖动处理:

    public void processTouchEvent(MotionEvent ev) {
        ...
        switch (action) {
        ...
            case MotionEvent.ACTION_MOVE: {
                if (mDragState == STATE_DRAGGING) {
                    // If pointer is invalid then skip the ACTION_MOVE.
                    if (!isValidPointerForActionMove(mActivePointerId)) break;
                    final int index = ev.findPointerIndex(mActivePointerId);
                    final float x = ev.getX(index);
                    final float y = ev.getY(index);
                    //计算距离上次的拖动距离
                    final int idx = (int) (x - mLastMotionX[mActivePointerId]);
                    final int idy = (int) (y - mLastMotionY[mActivePointerId]);
                    dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);//处理拖动
                    saveLastMotion(ev);//记录当前触摸点
                }
                ...
                break;
            }
            ...
            case MotionEvent.ACTION_UP: {
                if (mDragState == STATE_DRAGGING) {
                    releaseViewForPointerUp();//释放拖动view
                }
                cancel();
                break;
            }
            ...
        }
    }

(1)拖动
ACTION_MOVE时,会计算出pointer距离上次的位移,然后计算出capturedView的目标位置,进行拖动处理

    private void dragTo(int left, int top, int dx, int dy) {
        int clampedX = left;
        int clampedY = top;
        final int oldLeft = mCapturedView.getLeft();
        final int oldTop = mCapturedView.getTop();
        if (dx != 0) {
            clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);//通过callback获取真正的移动值
            ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);//进行位移
        }
        if (dy != 0) {
            clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
            ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
        }
        if (dx != 0 || dy != 0) {
            final int clampedDx = clampedX - oldLeft;
            final int clampedDy = clampedY - oldTop;
            mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, clampedDx, clampedDy);//callback回调移动后的位置
        }
    }

通过callback的clampViewPositionHorizontal方法决定实际移动的水平距离,通常都是返回left值,即拖动了多少就移动多少

通过callback的onViewPositionChanged方法,可以对View拖动后的新位置做一些处理,如:

    override fun onViewPositionChanged(changedView:View?, left: Int, top: Int, dx: Int, dy: Int) {
        super.onViewPositionChanged(changedView, left, top, dx, dy)
        //当新的left位置到达width时,即滑动除了界面,关闭页面
        if (left >= width && context is Activity && !context.isFinishing) {
            context.finish()
        }
    }

(2)释放
而ACTION_UP动作时,要释放拖动View

    private void releaseViewForPointerUp() {
        ...
        dispatchViewReleased(xvel, yvel);
    }

    private void dispatchViewReleased(float xvel, float yvel) {
        mReleaseInProgress = true;
        mCallback.onViewReleased(mCapturedView, xvel, yvel);//callback回调释放
        mReleaseInProgress = false;
        if (mDragState == STATE_DRAGGING) {
            // onViewReleased didn't call a method that would have changed this. Go idle.
            setDragState(STATE_IDLE);//重置状态
        }
    }

通常在callback的onViewReleased方法中,我们可以判断当前释放点的位置,从而决定是要回弹页面还是滑出屏幕:

    override fun onViewReleased(releasedChild: View?, xvel: Float, yvel: Float) {
        super.onViewReleased(releasedChild, xvel, yvel)
        //滑动速度到达一定值时直接关闭
        if (xvel >= 300) {//滑动页面到屏幕外,关闭页面
            dragHelper?.settleCapturedViewAt(width, 0)
        } else {//回弹页面
            dragHelper?.settleCapturedViewAt(0, 0)
        }
        //刷新,开始关闭或重置动画
        invalidate()
    }

如滑动速度大于300时,我们调用settleCapturedViewAt方法将页面滚动出屏幕,否则调用该方法进行回弹
(3)滚动
ViewDragHelper的settleCapturedViewAt(left,top)方法,用于将capturedView滚动到left,top的位置

    public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
        return forceSettleCapturedViewAt(finalLeft, finalTop,
                (int) mVelocityTracker.getXVelocity(mActivePointerId),
                (int) mVelocityTracker.getYVelocity(mActivePointerId));
    }

    private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
        //当前位置
        final int startLeft = mCapturedView.getLeft();
        final int startTop = mCapturedView.getTop();
        //偏移量
        final int dx = finalLeft - startLeft;
        final int dy = finalTop - startTop;
        ...     
        final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
        //使用Scroller对象开始滚动
        mScroller.startScroll(startLeft, startTop, dx, dy, duration);
        //重置状态为滚动
        setDragState(STATE_SETTLING);
        return true;
    }

其内部使用的是Scroller对象:是View的滚动机制,其回调是View的computeScroll()方法,在其内部通过Scroller对象的computeScrollOffset方法判断是否滚动完毕,如仍需滚动,需要调用invalidate方法进行刷新ViewDragHelper据此提供了一个类似的方法continueSettling,需要在computeScroll中调用,判断是否需要invalidate

    public boolean continueSettling(boolean deferCallbacks) {
        if (mDragState == STATE_SETTLING) {
        //是否滚动结束
            boolean keepGoing = mScroller.computeScrollOffset();
            //当前滚动值
            final int x = mScroller.getCurrX();
            final int y = mScroller.getCurrY();
            //偏移量
            final int dx = x - mCapturedView.getLeft();
            final int dy = y - mCapturedView.getTop();
            //便宜操作
            if (dx != 0) {
                ViewCompat.offsetLeftAndRight(mCapturedView, dx);
            }
            if (dy != 0) {
                ViewCompat.offsetTopAndBottom(mCapturedView, dy);
            }
            //回调
            if (dx != 0 || dy != 0) {
                mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy);
            }
            //滚动结束状态
            if (!keepGoing) {
                if (deferCallbacks) {
                    mParentView.post(mSetIdleRunnable);
                } else {
                    setDragState(STATE_IDLE);
                }
            }
        }
        return mDragState == STATE_SETTLING;
    }

在我们的View中:

    override fun computeScroll() {
        super.computeScroll()
        if (dragHelper?.continueSettling(true) == true) {
            invalidate()
        }
    }

1.32 谈一谈屏幕刷新机制?

屏幕刷新频率和绘制频率

  • cpu 负责 measure layout draw => displayList
  • gpu 负责 display => 位图
  • 每个16ms会发送一次垂直同步信号 vsync
  • 每次信号发送的时候都会从gpu的buffer中取出渲染好的位图 显示在屏幕上
  • 同时如果有需要 还会进行下一次的 cpu计算,计算好后放入buffer中

如果计算时间超过了两次vsync之间的时间 即16ms 则vsync信号会把 上一次gpu buffer中的信息展示出来 这时候就是卡顿

另外如果页面没有变化 屏幕还是一样会去buffer中取出上一次的刷新,只不过cpu不再去计算而已

你可能感兴趣的:(Android UI 绘制面试题)