-
Notifications
You must be signed in to change notification settings - Fork 783
2019-04-28:谈谈自定义View的流程? #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
文章很长,希望你能看下去,主要从两方面说起:
自定义 要想安卓理解自定义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阶段此阶段的目的是计算出控件树中的各个控件要显示其内容的话,需要多大尺寸。起点是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)方法后,会向之前介绍的一样,执行以下几步:
至此,整个View的绘制流程我们就分析完了。 Android自定义View / ViewGroup的步骤大致如下:
自定义属性Android自定义属性主要有定义、使用和获取三个步骤。 定义自定义属性参考:<https://blog.csdn.net/zhcswlp0625/article/details/54958220> 我们通常将自定义属性定义在/values/attr.xml文件中(attr.xml文件需要自己创建)。 先来看一段示例代码: <?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="rightPadding" format="dimension" />
<declare-styleable name="CustomMenu">
<attr name="rightPadding" />
</declare-styleable>
</resources> 可以看到,我们先是定义了一个属性rightPadding,然后又在CustomMenu中引用了这个属性。下面说明一下:
常用的format类型:
下面对format类型说明几点:
使用自定义属性在XML布局文件中使用自定义的属性时,我们需要先定义一个namespace。Android中默认的namespace是android,因此我们通常可以使用“android:xxx”的格式去设置一个控件的某个属性,android这个namespace的定义是在XML文件的头标签中定义的,通常是这样的:
我们自定义的属性不在这个命名空间下,因此我们需要添加一个命名空间。 自定义属性的命名空间如下:
可以看出来,除了将命名空间的名称从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();
}
构造方法当我们定义一个新的类继承了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的测量模式和测量值。 测量模式分为以下三种情况:
onMeasure()方法中常用的方法:
**注意:**onMeasure()方法可能被调用多次,这是因为控件中的内容或子View可能对分配给自己的空间“不满意”,因此向父空间申请重新分配空间。 onDraw()onDraw()方法负责绘制,即如果我们希望得到的效果在Android原生控件中没有现成的支持,那么我们就需要自己绘制我们的自定义控件的显示效果。 要学习onDraw()方法,我们就需要学习在onDraw()方法中使用最多的两个类:Paint和Canvas。 **注意:**每次触摸了自定义View/ViewGroup时都会触发onDraw()方法。 Paint类Paint画笔对象,这个类中包含了如何绘制几何图形、文字和位图的样式和颜色信息,指定了如何绘制文本和图形。画笔对象右很多设置方法,大体上可以分为两类:一类与图形绘制有关,一类与文本绘制有关。 Paint类中有如下方法: 1、图形绘制:
2、文本绘制:
3、其他方法:
这里需要就几个方法解释一下: 1、setPathEffect(PathEffect pe):设置绘制的路径的效果: 常见的有以下几种可选方案:
2、setXfermode(Xfermode m):设置图形重叠时的处理方式: 关于Xfermode的多种效果,我们可以参考下面一张图: 在使用的时候,我们需要通过paint. setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XXX))来设置,XXX是上图中的某种模式对应的常量参数,如DST_OUT。 这16中情况的具体解释如下:
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****对象的其他方法:
onLayout()onLayout()方法负责布局,大多数情况是在自定义ViewGroup中才会重写,主要用来确定子View在这个布局空间中的摆放位置。 onLayout(boolean changed, int l, int t, int r, int b)方法有5个参数,其中changed表示这个控件是否有了新的尺寸或位置;l、t、r、b分别表示这个View相对于父布局的左/上/右/下方的位置。 以下是onLayout()方法中常用的方法:
其他方法generateLayoutParams()generateLayoutParams()方法用在自定义ViewGroup中,用来指明子控件之间的关系,即与当前的ViewGroup对应的LayoutParams。我们只需要在方法中返回一个我们想要使用的LayoutParams类型的对象即可。 在generateLayoutParams()方法中需要传入一个AttributeSet对象作为参数,这个对象是这个ViewGroup的属性集,系统根据这个ViewGroup的属性集来定义子View的布局规则,供子View使用。 例如,在自定义流式布局中,我们只需要关心子控件之间的间隔关系,因此我们需要在generateLayoutParams()方法中返回一个new MarginLayoutParams()即可。 onTouchEvent()onTouchEvent()方法用来监测用户手指操作。我们通过方法中MotionEvent参数对象的getAction()方法来实时获取用户的手势,有UP、DOWN和MOVE三个枚举值,分别表示用于手指抬起、按下和滑动的动作。每当用户有操作时,就会回掉onTouchEvent()方法。 onScrollChanged()如果我们的自定义View / ViewGroup是继承自ScrollView / HorizontalScrollView等可以滚动的控件,就可以通过重写onScrollChanged()方法来监听控件的滚动事件。 这个方法中有四个参数:l和t分别表示当前滑动到的点在水平和竖直方向上的坐标;oldl和oldt分别表示上次滑动到的点在水平和竖直方向上的坐标。我们可以通过这四个值对滑动进行处理,如添加属性动画等。 invalidate()invalidate()方法的作用是请求View树进行重绘,即draw()方法,如果视图的大小发生了变化,还会调用layout()方法。 **一般会引起invalidate()**操作的函数如下:
postInvalidate()功能与invalidate()方法相同,只是postInvalidate()方法是异步请求重绘视图。 requestLayout()requestLayout()方法只是对View树进行重新布局layout过程(包括measure()过程和layout()过程),不会调用draw()过程,即不会重新绘制任何视图,包括该调用者本身。 requestFocus()请求View树的draw()过程,但只会绘制需要重绘的视图,即哪个View或ViewGroup调用了这个方法,就重绘哪个视图。 总结最后,让我们来总览一下自定义View / ViewGroup时调用的各种函数的顺序,如下图所示: 在这些方法中:
文章参考: 《深入理解Android(卷三)》 |
|
1l已经讲得很详细了 |
1.继承view,重写构造方法,为什么要重写构造方法,因为需要拿到父类的属性指 |
谈谈,个人在实际项目的自定义View过程 1,拿到需求,判断是否能通过拆分为 简单的View 实现、是否能够隔离事件分发等难题 2,通过步骤1,筛选到只需要绘制简单的View样式 |
补充: |
1,定义一个类 继承View |
No description provided.
The text was updated successfully, but these errors were encountered: