变身大佬的重要一环,就是自定义View!(1)

  1. 测量阶段(measure)

  2. 布局阶段(layout)

  3. 绘制阶段(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)自己的期望尺寸。

具体流程如下:

  1. 运行前,开发者在xml中写入对ViewGroup和ViewGroup子View的尺寸要求layout_xxx

  2. ViewGroup在自己的onMeasure()方法中,根据开发者在xml中写的对ViewGroup子View的尺寸要求、自己的父View(ViewGroup的父View) 对自己的尺寸要求和自己的可用空间计算出自己对子View的尺寸要求,并调用每个子View的measure()将ViewGroup对子View的尺寸要求传入,测量子View尺寸

  3. ViewGroup在子View计算出期望尺寸之后(在ViewGroup的onMeasure()方法中,ViewGroup递归调用每个子View的measure()方法,子View在自己的onMeasure()方法中会通过调用setMeasuredDimension()方法告知父View(ViewGroup)自己的期望尺寸),得出子View的实际尺寸和位置,并暂时保存计算结果,以便布局阶段使用

  4. 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步骤

  1. 自定义属性的声明与获取

  2. 重写测量阶段相关方法(onMeasure())

  3. 重写布局阶段相关方法(onLayout()(仅ViewGroup需要重写))

  4. 重写绘制阶段相关方法(onDraw()绘制主体、dispatchDraw()绘制子View和onDrawForeground() 绘制前景)

  5. onTouchEvent()

  6. 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,大致的流程都是一样的:

  1. 自定义属性的声明与获取

  2. 重写测量阶段相关方法(onMeasure())

  3. 重写布局阶段相关方法(onLayout()(仅 ViewGroup 需要重写))

  4. 重写绘制阶段相关方法(onDraw() 绘制主体、dispatchDraw() 绘制子 View 和 onDrawForeground() 绘制前景)

  5. onTouchEvent()

  6. 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移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

【Android 详细知识点思维脑图(技能树)】

image

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

、实战项目、讲解视频,并且会持续更新!**

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

【Android 详细知识点思维脑图(技能树)】

[外链图片转存中…(img-Kgr2JZ75-1712385136097)]

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

[外链图片转存中…(img-yfVwOgeF-1712385136097)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值