阿里面试官: 自定义View跟绘制流程相关知识点?

class Practice02BeforeOnDrawView : AppCompatTextView {

internal var paint = Paint(Paint.ANTI_ALIAS_FLAG)

internal var bounds = RectF()

constructor(context: Context) : super(context) {}

constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {}

constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}

init {

paint.color = Color.parseColor(“#FFC107”)

}

override fun onDraw(canvas: Canvas) {

// 把下面的绘制代码移到 super.onDraw() 的上面,就可以让原主体内容盖住你的绘制代码了

// (或者你也可以把 super.onDraw() 移到这段代码的下面)

val layout = layout

bounds.left = layout.getLineLeft(1)

bounds.right = layout.getLineRight(1)

bounds.top = layout.getLineTop(1).toFloat()

bounds.bottom = layout.getLineBottom(1).toFloat()

//绘制方形背景

canvas.drawRect(bounds, paint)

super.onDraw(canvas)

}

}

这里会涉及到画笔Paint()、画布canvas、路径Path、绘画顺序等的一些知识点,后面再详细说明

  • 直接继承View

这种就是类似TextView等,不需要去轮训子View只需要根据自己的需求重写onMeasure()onLayout()onDraw()等方法便可以,要注意点就是记得Padding等值要记得加入运算

private int getCalculateSize(int defaultSize, int measureSpec) {

int finallSize = defaultSize;

int mode = MeasureSpec.getMode(measureSpec);

int size = MeasureSpec.getSize(measureSpec);

// 根据模式对

switch (mode) {

case MeasureSpec.EXACTLY: {

break;

}

case MeasureSpec.AT_MOST: {

break;

}

case MeasureSpec.UNSPECIFIED: {

break;

}

}

return finallSize;

}

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

int width = getCalculateSize(120, widthMeasureSpec);

int height = getCalculateSize(120, heightMeasureSpec);

setMeasuredDimension(width, height);

}

//画一个圆

@Override

protected void onDraw(Canvas canvas) {

//调用父View的onDraw函数,因为View这个类帮我们实现了一些基本的而绘制功能,比如绘制背景颜色、背景图片等

super.onDraw(canvas);

int r = getMeasuredWidth() / 2;

//圆心的横坐标为当前的View的左边起始位置+半径

int centerX = getLeft() + r;

//圆心的纵坐标为当前的View的顶部起始位置+半径

int centerY = getTop() + r;

Paint paint = new Paint();

paint.setColor(Color.RED);

canvas.drawCircle(centerX, centerY, r, paint);

}

  • 直接继承ViewGroup

类似实现LinearLayout等,可以去看那一下LinearLayout的实现 基本的你可能要重写onMeasure()onLayout()onDraw()方法,这块很多问题要处理包括轮训childView的测量值以及模式进行大小逻辑计算等,这个篇幅过大后期加多个文章写详细的

这里写个简单的需求,模仿LinearLayout的垂直布局

class CustomViewGroup :ViewGroup{

constructor(context:Context):super(context)

constructor(context: Context,attrs:AttributeSet):super(context,attrs){

//可获取自定义的属性等

}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

super.onMeasure(widthMeasureSpec, heightMeasureSpec)

//将所有的子View进行测量,这会触发每个子View的onMeasure函数

measureChildren(widthMeasureSpec, heightMeasureSpec)

val widthMode = MeasureSpec.getMode(widthMeasureSpec)

val widthSize = MeasureSpec.getSize(widthMeasureSpec)

val heightMode = MeasureSpec.getMode(heightMeasureSpec)

val heightSize = MeasureSpec.getSize(heightMeasureSpec)

val childCount = childCount

if (childCount == 0) {

//没有子View的情况

setMeasuredDimension(0, 0)

} else {

//如果宽高都是包裹内容

if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {

//我们将高度设置为所有子View的高度相加,宽度设为子View中最大的宽度

val height = getTotalHeight()

val width = getMaxChildWidth()

setMeasuredDimension(width, height)

} else if (heightMode == MeasureSpec.AT_MOST) {

//如果只有高度是包裹内容

//宽度设置为ViewGroup自己的测量宽度,高度设置为所有子View的高度总和

setMeasuredDimension(widthSize, getTotalHeight())

} else if (widthMode == MeasureSpec.AT_MOST) {//如果只有宽度是包裹内容

//宽度设置为子View中宽度最大的值,高度设置为ViewGroup自己的测量值

setMeasuredDimension(getMaxChildWidth(), heightSize)

}

}

/***

  • 获取子View中宽度最大的值

*/

private fun getMaxChildWidth(): Int {

val childCount = childCount

var maxWidth = 0

for (i in 0 until childCount) {

val childView = getChildAt(i)

if (childView.measuredWidth > maxWidth)

maxWidth = childView.measuredWidth

}

return maxWidth

}

/***

  • 将所有子View的高度相加

*/

private fun getTotalHeight(): Int {

val childCount = childCount

var height = 0

for (i in 0 until childCount) {

val childView = getChildAt(i)

height += childView.measuredHeight

}

return height

}

}

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {

val count = childCount

var currentHeight = t

for (i in 0 until count) {

val child = getChildAt(i)

val h = child.measuredHeight

val w = child.measuredWidth

//摆放子view

child.layout(l, currentHeight, l + w, currentHeight + h)

currentHeight += h

}

}

}

主要两点 先 measureChildren()轮训遍历子View获取宽高,并根据测量模式逻辑计算最后所有的控件的所需宽高,最后setMeasuredDimension()保存一下 ###四、 View的绘制流程相关 最基本的三个相关函数 measure() ->layout()->draw()

五、onMeasure()相关的知识点

1. MeasureSpec

MeasureSpecView的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。 MeasureSpec 的数据是int类型,有32位。 高两位表示模式,后面30位表示大小size。则MeasureSpec = mode+size 三种模式分别为:EXACTLY,AT_MOST,UNSPECIFIED

EXACTLY: (match_parent或者 精确数据值)精确模式,对应的数值就是MeasureSpec当中的size

AT_MOST😦wrap_content)最大值模式,View的尺寸有一个最大值,View不超过MeasureSpec当中的Size

UNSPECIFIED:(一般系统使用)无限制模式,View设置多大就给他多大

//获取测量模式

val widthMode = MeasureSpec.getMode(widthMeasureSpec)

//获取测量大小

val widthSize = MeasureSpec.getSize(widthMeasureSpec)

//通过Mode和Size构造MeasureSpec

val measureSpec = MeasureSpec.makeMeasureSpec(size, mode);

2. View #onMeasure()源码

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}

protected int getSuggestedMinimumWidth() {

return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());

}

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {

boolean optical = isLayoutModeOptical(this);

if (optical != isLayoutModeOptical(mParent)) {

Insets insets = getOpticalInsets();

int opticalWidth = insets.left + insets.right;

int opticalHeight = insets.top + insets.bottom;

measuredWidth += optical ? opticalWidth : -opticalWidth;

measuredHeight += optical ? opticalHeight : -opticalHeight;

}

setMeasuredDimensionRaw(measuredWidth, measuredHeight);

}

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:

result = specSize;

break;

}

return result;

}

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {

mMeasuredWidth = measuredWidth;

mMeasuredHeight = measuredHeight;

mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;

}

  • setMeasuredDimension(int measuredWidth, int measuredHeight) :用来设置View的宽高,在我们自定义View保存宽高也会要用到。

  • getSuggestedMinimumWidth():当View没有设置背景时,默认大小就是mMinWidth,这个值对应Android:minWidth属性,如果没有设置时默认为0. 如果有设置背景,则默认大小为mMinWidthmBackground.getMinimumWidth()当中的较大值。

  • getDefaultSize(int size, int measureSpec):用来获取View默认的宽高,在**getDefaultSize()**中对MeasureSpec.AT_MOST,MeasureSpec.EXACTLY两个的处理是一样的,我们自定义View的时候 要对两种模式进行处理。

3. ViewGroup中并没有measure()也没有onMeasure()

因为ViewGroup除了测量自身的宽高,还需要测量各个子View的宽高,不同的布局测量方式不同 (例如 LinearLayoutRelativeLayout等布局),所以直接交由继承者根据自己的需要去复写。但是里面因为子View的测量是相对固定的,所以里面已经提供了基本的measureChildren()以及measureChild()来帮助我们对子View进行测量 这个可以看一下我另一篇文章:LinearLayout # onMeasure()LinearLayout onMeasure源码阅读

六、onLayout()相关

  1. View.java的onLayout方法是空实现:因为子View的位置,是由其父控件的onLayout方法来确定的。
  1. onLayout(int l, int t, int r, int b)中的参数l、t、r、b都是相对于其父 控件的位置。
  1. 自身的mLeft, mTop, mRight, mBottom都是相对于父控件的位置。
1. Android坐标系

2. 内部View坐标系跟点击坐标

3. 看一下View#layout(int l, int t, int r, int b)源码

public void layout(int l, int t, int r, int b) {

if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {

onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);

mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;

}

int oldL = mLeft;

int oldT = mTop;

int oldB = mBottom;

int oldR = mRight;

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);

// …省略其它部分

}

private boolean setOpticalFrame(int left, int top, int right, int bottom) {

Insets parentInsets = mParent instanceof View ?

((View) mParent).getOpticalInsets() : Insets.NONE;

Insets childInsets = getOpticalInsets();

return setFrame(

left + parentInsets.left - childInsets.left,

top + parentInsets.top - childInsets.top,

right + parentInsets.left + childInsets.right,

bottom + parentInsets.top + childInsets.bottom);

}

protected boolean setFrame(int left, int top, int right, int bottom) {

boolean changed = false;

// …省略其它部分

if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {

changed = true;

int drawn = mPrivateFlags & PFLAG_DRAWN;

int oldWidth = mRight - mLeft;

int oldHeight = mBottom - mTop;

int newWidth = right - left;

int newHeight = bottom - top;

boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

invalidate(sizeChanged);

mLeft = left;

mTop = top;

mRight = right;

mBottom = bottom;

mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

mPrivateFlags |= PFLAG_HAS_BOUNDS;

if (sizeChanged) {

sizeChange(newWidth, newHeight, oldWidth, oldHeight);

}

if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {

mPrivateFlags |= PFLAG_DRAWN;

invalidate(sizeChanged);

invalidateParentCaches();

}

mPrivateFlags |= drawn;

mBackgroundSizeChanged = true;

mDefaultFocusHighlightSizeChanged = true;

if (mForegroundInfo != null) {

mForegroundInfo.mBoundsChanged = true;

}

notifySubtreeAccessibilityStateChangedIfNeeded();

}

return changed;

}

四个参数l、t、r、b分别代表View的左、上、右、下四个边界相对于其父View的距离。 在调用onLayout(changed, l, t, r, b);之前都会调用到setFrame()确定View在父容器当中的位置,赋值给mLeft,mTop,mRight,mBottom。 在ViewGroup#onLayout()View#onLayout()都是空实现,交给继承者根据自身需求去定位

部分零散知识点:

  • getMeasureWidth()getWidth() getMeasureWidth()返回的是mMeasuredWidth,而该值是在setMeasureDimension()中的setMeasureDimensionRaw()中设置的。因此onMeasure()后的所有方法都能获取到这个值。 getWidth返回的是mRight-mLeft,这两个值,是在layout()中的setFrame()中设置的. getMeasureWidthAndState中有一句: This should be used during measurement and layout calculations only. Use {@link #getWidth()} to see how wide a view is after layout.

总结:只有在测量过程中和布局计算时,才用getMeasuredWidth()。在layout之后,用getWidth()来获取宽度

七、draw()绘画过程

/*

  • Draw traversal performs several drawing steps which must be executed

  • in the appropriate order:

  •  1\. Draw the background
    
  •  2\. If necessary, save the canvas' layers to prepare for fading
    
  •  3\. Draw view's content
    
  •  4\. Draw children
    
  •  5\. If necessary, draw the fading edges and restore layers
    
  •  6\. Draw decorations (scrollbars for instance)
    

*/

上面是draw()里面写的绘画顺序。

  1. 绘制背景。
  1. 如果必要的话,保存当前canvas
  1. 绘制View的内容
  1. 绘制子View
  1. 如果必要的话,绘画边缘重新保存图层
  1. 画装饰(例如滚动条)
1. 看一下View#draw()源码的实现

public void draw(Canvas canvas) {

// Step 1, draw the background, if needed

int saveCount;

if (!dirtyOpaque) {

drawBackground(canvas);

}

// skip step 2 & 5 if possible (common case)

final int viewFlags = mViewFlags;

boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;

boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;

if (!verticalEdges && !horizontalEdges) {

// Step 3, draw the content

if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children

dispatchDraw(canvas);

drawAutofilledHighlight(canvas);

// Overlay is part of the content and draws beneath Foreground

if (mOverlay != null && !mOverlay.isEmpty()) {

mOverlay.getOverlayView().dispatchDraw(canvas);

}

// Step 6, draw decorations (foreground, scrollbars)

onDrawForeground(canvas);

// Step 7, draw the default focus highlight

drawDefaultFocusHighlight(canvas);

if (debugDraw()) {

debugDrawFocus(canvas);

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

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

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

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

最后

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

上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。

【Android思维脑图(技能树)】

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

【Android高级架构视频学习资源】

**Android部分精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

8a3f0fd6ac81d625c.png)

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

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

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-SQn96QD9-1712038466656)]

最后

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

上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。

【Android思维脑图(技能树)】

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

[外链图片转存中…(img-jSkLNJ5v-1712038466656)]

【Android高级架构视频学习资源】

**Android部分精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

  • 29
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值