-
测量阶段(measure)
-
布局阶段(layout)
-
绘制阶段(draw)
自定义ViewGroup测量阶段
同自定义View一样,在自定义ViewGroup的测量阶段会执行两个方法:
-
measure()
-
onMeasure()
measure():调度方法,主要做一些前置和优化工作,并最终会调用onMeasure()方法执行实际的测量工作;
onMeasure() :实际执行测量任务的方法,与自定义View不同,在自定义ViewGroup的 onMeasure() 方法中,ViewGroup会递归调用子View的measure()方法,并通过measure()将ViewGroup对子View的尺寸要求(ViewGroup会根据开发者对子View的尺寸要求、自己的父View(ViewGroup的父View) 对自己的尺寸要求和自己的可用空间计算出自己对子View的尺寸要求)传入,对子View进行测量,并把测量结果临时保存,以便在布局阶段使用。测量出子View的实际尺寸之后,ViewGroup会根据子View的实际尺寸计算出自己的期望尺寸,并通过setMeasuredDimension()方法告知父View(ViewGroup 的父 View)自己的期望尺寸。
具体流程如下:
-
运行前,开发者在xml中写入对ViewGroup和ViewGroup子View的尺寸要求layout_xxx
-
ViewGroup在自己的onMeasure()方法中,根据开发者在xml中写的对ViewGroup子View的尺寸要求、自己的父View(ViewGroup的父View) 对自己的尺寸要求和自己的可用空间计算出自己对子View的尺寸要求,并调用每个子View的measure()将ViewGroup对子View的尺寸要求传入,测量子View尺寸
-
ViewGroup在子View计算出期望尺寸之后(在ViewGroup的onMeasure()方法中,ViewGroup递归调用每个子View的measure()方法,子View在自己的onMeasure()方法中会通过调用setMeasuredDimension()方法告知父View(ViewGroup)自己的期望尺寸),得出子View的实际尺寸和位置,并暂时保存计算结果,以便布局阶段使用
-
ViewGroup根据子View的尺寸和位置计算自己的期望尺寸,并通过setMeasuredDimension()方法告知父View自己的期望尺寸。如果想要做的更好,可以在「ViewGroup根据子View的尺寸和位置计算出自己的期望尺寸」之后,再结合ViewGroup的父View对ViewGroup的尺寸要求进行修正(通过resolveSize()方法),这样得出的ViewGroup的期望尺寸更符合ViewGroup的父View对ViewGroup的尺寸要求
自定义ViewGroup布局阶段
同自定义 View 一样,在自定义 ViewGroup 的布局阶段会执行两个方法:
-
layout()
-
onLayout()
layout():保存ViewGroup的实际尺寸。调用setFrame()方法保存ViewGroup的实际尺寸,调用onSizeChanged()通知开发者ViewGroup的尺寸更改了,并最终会调用onLayout()方法让子View布局;
onLayout():ViewGroup会递归调用每个子View的layout()方法,把测量阶段计算出的子View的实际尺寸和位置传给子View,让子View保存自己的实际尺寸和位置。
自定义ViewGroup绘制阶段
同自定义View一样,在自定义ViewGroup的绘制阶段会执行一个方法——draw()。draw()是绘制阶段的总调度方法,在其中会调用绘制背景的方法 drawBackground()、绘制主体的方法onDraw()、绘制子View的方法 dispatchDraw()和绘制前景的方法onDrawForeground():
draw():绘制阶段的总调度方法,在其中会调用绘制背景的方法drawBackground()、绘制主体的方法onDraw()、绘制子View的方法dispatchDraw()和绘制前景的方法onDrawForeground();
在ViewGroup中,你也可以重写绘制主体的方法onDraw()、绘制子View的方法dispatchDraw()和绘制前景的方法onDrawForeground()。但大多数情况下,自定义ViewGroup是不需要重写任何绘制方法的。因为通常情况下,ViewGroup的角色是容器,一个透明的容器,它只是用来盛放子View的。
自定义ViewGroup布局、绘制流程时序图
[外链图片转存失败(img-KbVwyBSB-1568966046699)(https://upload-images.jianshu.io/upload_images/15679108-557dc3089e745399?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
自定义View步骤
-
自定义属性的声明与获取
-
重写测量阶段相关方法(onMeasure())
-
重写布局阶段相关方法(onLayout()(仅ViewGroup需要重写))
-
重写绘制阶段相关方法(onDraw()绘制主体、dispatchDraw()绘制子View和onDrawForeground() 绘制前景)
-
onTouchEvent()
-
onInterceptTouchEvent()(仅ViewGroup有此方法)
/ 实战演练 /
自定义View
自定义View的绘制内容
自定义View,它的内容是「三个半径不同、颜色不同的同心圆」,效果图如下:
[外链图片转存失败(img-hJZJpB3W-1568966046700)(https://upload-images.jianshu.io/upload_images/15679108-814de781e72fc876?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
自定义属性的声明与获取
//在 xml 中自定义 View 属性
<?xml version="1.0" encoding="utf-8"?>//在 View 构造函数中获取自定义 View 属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mRadius = typedArray.getDimension(R.styleable.CircleView_circle_radius, getResources().getDimension(R.dimen.avatar_size));
mOuterCircleColor = typedArray.getColor(R.styleable.CircleView_outer_circle_color, getResources().getColor(R.color.purple_500));
mMiddleCircleColor = typedArray.getColor(R.styleable.CircleView_middle_circle_color, getResources().getColor(R.color.purple_500));
mInnerCircleColor = typedArray.getColor(R.styleable.CircleView_inner_circle_color, getResources().getColor(R.color.purple_500));
typedArray.recycle();
重写测量阶段相关方法(onMeasure())
由于不需要自定义View的尺寸,所以,不用重写该方法。
重写布局阶段相关方法(onLayout()(仅 ViewGroup 需要重写))
由于没有子View需要布局,所以,不用重写该方法。
重写绘制阶段相关方法(onDraw()绘制主体、dispatchDraw()绘制子View和onDrawForeground()绘制前景)
//重写 onDraw() 方法,自定义 View 内容
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(mOuterCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
mPaint.setColor(mMiddleCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius * 2/3, mPaint);
mPaint.setColor(mInnerCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius/3, mPaint);
}
onTouchEvent()
由于View不需要和用户交互,所以,不用重写该方法。
onInterceptTouchEvent()(仅ViewGroup有此方法)
ViewGroup的方法。
最终效果如下:
[外链图片转存失败(img-RZuN19Li-1568966046701)(https://upload-images.jianshu.io/upload_images/15679108-723a48f644bda93f?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
此时,即使你在xml中将CircleView的宽、高声明为「match_parent」,你会发现最终的显示效果都是一样的。
主要原因是:默认情况下,View的onMeasure()方法在通过setMeasuredDimension()告知父View自己的期望尺寸时,会调用getDefaultSize()方法。在getDefaultSize()方法中,又会调用getSuggestedMinimumWidth()和getSuggestedMinimumHeight()获取建议的最小宽度和最小高度,并根据最小尺寸和父View对自己的尺寸要求进行修正。
最主要的是,在getDefaultSize()方法中修正的时候,会将MeasureSpec.AT_MOST和MeasureSpec.EXACTLY一视同仁,直接返回父View对View的尺寸要求:
//1. 默认 onMeasure 的处理
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
//2. getSuggestedMinimumWidth()
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
//3. getSuggestedMinimumHeight()
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
//4. getDefaultSize()
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
//MeasureSpec.AT_MOST、MeasureSpec.EXACTLY 一视同仁
result = specSize;
break;
}
return result;
}
正是因为在getDefaultSize()方法中处理的时候,将MeasureSpec.AT_MOST和MeasureSpec.EXACTLY一视同仁,所以才有了上面「在xml中应用CircleView的时候,无论将CircleView的尺寸设置为match_parent还是wrap_content效果都一样」的现象。
具体分析如下:
[外链图片转存失败(img-4Nh2OBKk-1568966046701)(https://upload-images.jianshu.io/upload_images/15679108-b6e7ff9d9e721219?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
注:
上表中,「View的父View对View的尺寸要求」是View的父View根据「开发者对子View的尺寸要求」、「自己的父View(View的父View的父View) 对自己的尺寸要求」和「自己的可用空间」计算出自己对子View的尺寸要求。
另外,由执行结果可知,上表中的specSize实际上等于View的尺寸:
2019-08-13 17:28:26.855 16024-16024/com.smart.a03_view_custom_view_example E/TAG: Width(getWidth()): 1080 Height(getHeight()): 1584
自定义View的尺寸和绘制内容
自定义View,它的内容是「三个半径不同、颜色不同的同心圆」,效果图如下:
[外链图片转存失败(img-tmsAYLZ9-1568966046702)(https://upload-images.jianshu.io/upload_images/15679108-1fca9fff597111a7?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
自定义属性的声明与获取
//在 xml 中自定义 View 属性
<?xml version="1.0" encoding="utf-8"?>//在 View 构造函数中获取自定义 View 属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mRadius = typedArray.getDimension(R.styleable.CircleView_circle_radius, getResources().getDimension(R.dimen.avatar_size));
mOuterCircleColor = typedArray.getColor(R.styleable.CircleView_outer_circle_color, getResources().getColor(R.color.purple_500));
mMiddleCircleColor = typedArray.getColor(R.styleable.CircleView_middle_circle_color, getResources().getColor(R.color.purple_500));
mInnerCircleColor = typedArray.getColor(R.styleable.CircleView_inner_circle_color, getResources().getColor(R.color.purple_500));
typedArray.recycle();
重写测量阶段相关方法(onMeasure())
//onMeasure()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//2.1 根据 View 特点或业务需求计算出 View 的尺寸
mWidth = (int)(mRadius * 2);
mHeight = (int)(mRadius * 2);
//2.2 通过 resolveSize() 方法修正结果
mWidth = resolveSize(mWidth, widthMeasureSpec);
mHeight = resolveSize(mHeight, heightMeasureSpec);
//2.3 通过 setMeasuredDimension() 保存 View 的期望尺寸(通过 setMeasuredDimension() 告知父 View 的期望尺寸)
setMeasuredDimension(mWidth, mHeight);
}
重写布局阶段相关方法(onLayout()(仅ViewGroup需要重写))
由于没有子View需要布局,所以,不用重写该方法。
重写绘制阶段相关方法(onDraw()绘制主体、dispatchDraw()绘制子View和onDrawForeground()绘制前景)
// 重写 onDraw() 方法,自定义 View 内容
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(mOuterCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
mPaint.setColor(mMiddleCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius * 2/3, mPaint);
mPaint.setColor(mInnerCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius/3, mPaint);
}
onTouchEvent()
由于View不需要和用户交互,所以,不用重写该方法。
onInterceptTouchEvent()(仅 ViewGroup 有此方法)
ViewGroup的方法。
最终效果如下:
[外链图片转存失败(img-T2oJ2eAG-1568966046703)(https://upload-images.jianshu.io/upload_images/15679108-9a3cbbd7b592b6c5?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
当在xml中将MeasuredCircleView的宽、高声明为「match_parent」时,显示效果跟CircleView显示效果一样。
[外链图片转存失败(img-Nrcx6co4-1568966046704)(https://upload-images.jianshu.io/upload_images/15679108-34ab98bbf38b14cd?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
但是,当在xml中将MeasuredCircleView的宽、高声明为「wrap_content」时,显示效果是下面这个样子:
[外链图片转存失败(img-jcbrokSS-1568966046704)(https://upload-images.jianshu.io/upload_images/15679108-921cc7047283c959?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
其实,也很好理解:
[外链图片转存失败(img-9Qfmlyi2-1568966046705)(https://upload-images.jianshu.io/upload_images/15679108-6714ffe0d3fb6854?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
自定义ViewGroup
自定义ViewGroup,标签布局,效果图如下:
[外链图片转存失败(img-JhiJuU2L-1568966046705)(https://upload-images.jianshu.io/upload_images/15679108-bbfb4bfd501feb50?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
无论是自定义View还是自定义ViewGroup,大致的流程都是一样的:
-
自定义属性的声明与获取
-
重写测量阶段相关方法(onMeasure())
-
重写布局阶段相关方法(onLayout()(仅 ViewGroup 需要重写))
-
重写绘制阶段相关方法(onDraw() 绘制主体、dispatchDraw() 绘制子 View 和 onDrawForeground() 绘制前景)
-
onTouchEvent()
-
onInterceptTouchEvent()(仅 ViewGroup 有此方法)
只不过,大多数情况下,ViewGroup不需要「自定义属性」和「重写绘制阶段相关方法」,但有些时候还是需要的,如,开发者想在ViewGroup的所有子View上方绘制一些内容,就可以通过重写ViewGroup的onDrawForeground()来实现。
自定义属性的声明与获取
在自定义ViewGroup中「自定义属性的声明与获取」的方法与在自定义View中「自定义属性的声明与获取」的方法一样,且因为大多数情况下,在自定义ViewGroup中是不需要自定义属性的,所以,在这里就不自定义属性了。
重写测量阶段相关方法(onMeasure())
//2. 重写测量阶段相关方法(onMeasure());
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//2.1 解析 ViewGroup 的父 View 对 ViewGroup 的尺寸要求
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(widthMeasureSpec);
//2.2 ViewGroup 根据「开发者在 xml 中写的对 ViewGroup 子 View 的尺寸要求」、「自己的父 View(ViewGroup 的父 View)对自己的尺寸要求」和
//「自己的可用空间」计算出自己对子 View 的尺寸要求,并将该尺寸要求通过子 View 的 measure() 方法传给子 View,让子 View 测量自己(View)的期望尺寸
//具体代码查看原文…
//2.3 ViewGroup 暂时保存子 View 的尺寸,以便布局阶段和绘制阶段使用
Rect childBound;
if(mChildrenBounds.size() <= i){
childBound = new Rect();
mChildrenBounds.add(childBound);
}else{
childBound = mChildrenBounds.get(i);
}
//此处不能用 child.getxxx() 获取子 View 的尺寸值,因为子 View 只是量了尺寸,还没有布局,这些值都是 0
// childBound.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
childBound.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(), heightUsed + child.getMeasuredHeight());
lineWidthUsed += child.getMeasuredWidth() + mItemSpace;
widthUsed = Math.max(lineWidthUsed, widthUsed);
lineHeight = Math.max(lineHeight, child.getMeasuredHeight());
}
//2.4 ViewGroup 将「根据子 View 的实际尺寸计算出的自己(ViewGroup)的尺寸」结合「自己父 View 对自己的尺寸要求」进行修正,并通
//过 setMeasuredDimension() 方法告知父 View 自己的期望尺寸
int measuredWidth = resolveSize(widthUsed, widthMeasureSpec);
int measuredHeight = resolveSize((heightUsed + lineHeight + getPaddingBottom()), heightMeasureSpec);
setMeasuredDimension(measuredWidth, measuredHeight);
}
//重写generateLayoutParams()
//2.2.1 在自定义 ViewGroup 中调用 measureChildWithMargins() 方法计算 ViewGroup 对子 View 的尺寸要求时,
//必须在 ViewGroup 中重写 generateLayoutParams() 方法,因为 measureChildWithMargins() 方法中用到了 MarginLayoutParams,
//如果不重写 generateLayoutParams() 方法,那调用 measureChildWithMargins() 方法时,MarginLayoutParams 就为 null,
//所以在自定义 ViewGroup 中调用 measureChildWithMargins() 方法时,必须重写 generateLayoutParams() 方法。
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/2f4db881a183fa16d36f294e3b7961c6.jpeg)
总结
【Android 详细知识点思维脑图(技能树)】
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
、实战项目、讲解视频,并且会持续更新!**
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/2f4db881a183fa16d36f294e3b7961c6.jpeg)
总结
【Android 详细知识点思维脑图(技能树)】
[外链图片转存中…(img-Kgr2JZ75-1712385136097)]
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
[外链图片转存中…(img-yfVwOgeF-1712385136097)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。