总结
学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!
最后如何才能让我们在面试中对答如流呢?
答案当然是平时在工作或者学习中多提升自身实力的啦,那如何才能正确的学习,有方向的学习呢?有没有免费资料可以借鉴?为此我整理了一份Android学习资料路线:
这里是一部分我工作以来以及参与过的大大小小的面试收集总结出来的一套BAT大厂面试资料专题包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家。
好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划。来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。
最后,祝愿即将跳槽和已经开始求职的大家都能找到一份好的工作!
这些只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢再关注一下~
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
这里会涉及到画笔
Paint()
、画布canvas
、路径Path
、绘画顺序等的一些知识点,后面再详细说明
这种就是类似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);
}
类似实现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
MeasureSpec
是View
的内部类,它封装了一个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. 如果有设置背景,则默认大小为mMinWidth
和mBackground.getMinimumWidth()
当中的较大值。 -
getDefaultSize(int size, int measureSpec):用来获取View默认的宽高,在**getDefaultSize()**中对
MeasureSpec.AT_MOST
,MeasureSpec.EXACTLY
两个的处理是一样的,我们自定义View
的时候 要对两种模式进行处理。
3. ViewGroup中并没有measure()也没有onMeasure()
因为ViewGroup除了测量自身的宽高,还需要测量各个子View
的宽高,不同的布局测量方式不同 (例如 LinearLayout
跟RelativeLayout
等布局),所以直接交由继承者根据自己的需要去复写。但是里面因为子View
的测量是相对固定的,所以里面已经提供了基本的measureChildren()
以及measureChild()
来帮助我们对子View
进行测量 这个可以看一下我另一篇文章:LinearLayout # onMeasure()
LinearLayout onMeasure源码阅读
六、onLayout()相关
- View.java的onLayout方法是空实现:因为子View的位置,是由其父控件的onLayout方法来确定的。
- onLayout(int l, int t, int r, int b)中的参数l、t、r、b都是相对于其父 控件的位置。
- 自身的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()
里面写的绘画顺序。
- 绘制背景。
- 如果必要的话,保存当前
canvas
- 绘制
View
的内容
- 绘制子
View
- 如果必要的话,绘画边缘重新保存图层
- 画装饰(例如滚动条)
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行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!