阿里面试官: 自定义View跟绘制流程相关知识点?(标准参考解答,值得收藏)

总结

学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!

最后如何才能让我们在面试中对答如流呢?

答案当然是平时在工作或者学习中多提升自身实力的啦,那如何才能正确的学习,有方向的学习呢?有没有免费资料可以借鉴?为此我整理了一份Android学习资料路线:

这里是一部分我工作以来以及参与过的大大小小的面试收集总结出来的一套BAT大厂面试资料专题包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家。

好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划。来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。

最后,祝愿即将跳槽和已经开始求职的大家都能找到一份好的工作!

这些只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢再关注一下~

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

这里会涉及到画笔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);

}

// we’re done…

return;

}

}

由上面可以看到 先调用drawBackground(canvas) ->onDraw(canvas)->dispatchDraw(canvas)->onDrawForeground(canvas)越是后面绘画的越是覆盖在最上层。

drawBackground(canvas):画背景,不可重写

onDraw(canvas):画主体

  • 代码写在super.onDraw()前:会被父类的onDraw覆盖

  • 代码写在super.onDraw()后:不会被父类的onDraw覆盖

dispatchDraw() :绘制子 View 的方法

  • 代码写在super.dispatchDraw(canvas)前:把绘制代码写在 super.dispatchDraw() 的上面,这段绘制就会在 onDraw() 之后、 super.dispatchDraw() 之前发生,也就是绘制内容会出现在主体内容和子 View 之间。而这个…… 其实和重写 onDraw() 并把绘制代码写在 super.onDraw() 之后的做法,效果是一样的。

  • 代码写在super.dispatchDraw(canvas)后:只要重写 dispatchDraw(),并在 super.dispatchDraw() 的下面写上你的绘制代码,这段绘制代码就会发生在子 View 的绘制之后,从而让绘制内容盖住子 View 了。

onDrawForeground(canvas):包含了滑动边缘渐变和滑动条跟前景

一般来说,一个 View(或 ViewGroup)的绘制不会这几项全都包含,但必然逃不出这几项,并且一定会严格遵守这个顺序。例如通常一个 LinearLayout 只有背景和子 View,那么它会先绘制背景再绘制子 View;一个 ImageView 有主体,有可能会再加上一层半透明的前景作为遮罩,那么它的前景也会在主体之后进行绘制。需要注意,前景的支持是在 Android 6.0(也就是 API 23)才加入的;之前其实也有,不过只支持 FrameLayout,而直到 6.0 才把这个支持放进了 View 类里。

最后

由于题目很多整理答案的工作量太大,所以仅限于提供知识点,详细的很多问题和参考答案我都整理成了 PDF文件

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

或 ViewGroup)的绘制不会这几项全都包含,但必然逃不出这几项,并且一定会严格遵守这个顺序。例如通常一个 LinearLayout 只有背景和子 View,那么它会先绘制背景再绘制子 View;一个 ImageView 有主体,有可能会再加上一层半透明的前景作为遮罩,那么它的前景也会在主体之后进行绘制。需要注意,前景的支持是在 Android 6.0(也就是 API 23)才加入的;之前其实也有,不过只支持 FrameLayout,而直到 6.0 才把这个支持放进了 View 类里。

最后

由于题目很多整理答案的工作量太大,所以仅限于提供知识点,详细的很多问题和参考答案我都整理成了 PDF文件

[外链图片转存中…(img-TurDmGIc-1715477637235)]

[外链图片转存中…(img-Pz8K4ThC-1715477637236)]

[外链图片转存中…(img-7rngP12Y-1715477637236)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值