万字长文!面试官问你:自定义View跟绘制流程懂吗?帮你搞定面试官
点击上方 "程序员小乐"关注, 星标或置顶一起成长
关注订阅号「程序员小乐」,收看更多精彩内容
每日英文
If you're brave enough to say GOODBYE, life will reward you with a new HELLO.
只要你勇敢地说出再见,生活一定会赐予你一个新的开始。
每日掏心话
人与人之间的很多矛盾都是从傲慢中来的,学会把自己的心态放低,放平,多看自己的缺点,多看别人的优点,让傲慢的心变得谦虚,恭敬,所处的环境自然就融洽了。
来自:未扬帆的小船 | 责编:乐乐
链接:juejin.im/user/57d822c90bd1d000585cad57
程序员小乐(ID:study_tech)第 1009 次推文
往日回顾: 《最受欢迎的女友职业排行榜Top10》
正文
/ 前言 /
本文用于记录自定义View的基础步骤以及一些基础的信息,后期可能针对具体的点写一些补充性的文章。
/ View中关于四个构造函数参数 /
自定义View中View的构造函数有四个。
// 主要是在java代码中生命一个View时所调用,没有任何参数,一个空的View对象
public ChildrenView(Context context) {super(context);
}
// 在布局文件中使用该自定义view的时候会调用到,一般会调用到该方法
public ChildrenView(Context context, AttributeSet attrs) {this(context, attrs,0);
}
//如果你不需要View随着主题变化而变化,则上面两个构造函数就可以了
//下面两个是与主题相关的构造函数
public ChildrenView(Context context, AttributeSet attrs, int defStyleAttr) {this(context, attrs, defStyleAttr, 0);
}
//
public ChildrenView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);
}
四个参数解释:
context:上下文
AttributeSet attrs:从xml中定义的参数
intdefStyleAttr:主题中优先级最高的属性
intdefStyleRes:优先级次之的内置于View的style(这里就是自定义View设置样式的地方)
多个地方定义属性,优先级排序 Xml直接定义 > xml中style引用 > defStyleAttr>defStyleRes > theme直接定义。
/ 自定义属性说明 /
除了基本类型的不说 讲一下其它几个吧:
color:引用颜色
dimension:引用字体大小
//定义
<attr name = "text_size" format = "dimension" />
//使用:app:text_size = "28sp"
或者 app:text_size = "@android:dimen/app_icon_size"
enum:枚举值
//定义<attr name="orientation"><enum name="horizontal" value="0" /><enum name="vertical" value="1" /></attr>
//使用:app:orientation = "vertical"
flags:标志 (位或运行) 主要作用=可以多个值
//定义<attr name="gravity"><flag name="top" value="0x01" /><flag name="bottom" value="0x02" /><flag name="left" value="0x04" /><flag name="right" value="0x08" /><flag name="center_vertical" value="0x16" /></attr>
// 使用
app:gravity = Top|left
fraction:百分数
//定义:
<attr name = "transparency" format = "fraction" />
//使用:app:transparency = "80%"
reference:参考/引用某一资源ID
//定义:<attr name="leftIcon" format="reference" />
//使用:
app:leftIcon = "@drawable/图片ID"
混合类型:属性定义时指定多种类型值
//属性定义<attr name = "background" format = "reference|color" />
//使用
android:background = "@drawable/图片ID"
//或者
android:background = "#FFFFFF"
/ 自定义控件类型 /
自定义组合控件步骤
自定义属性
在res/values目录下的attrs.xml文件中。
<resources>
<declare-styleable name="CustomView"><attr name="leftIcon" format="reference" /><attr name="state" format="boolean"/><attr name="name" format="string"/></declare-styleable>
</resources>
布局中使用自定义属性
在布局中使用。
<com.myapplication.view.CustomViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"app:leftIcon="@mipmap/ic_temp"app:name="温度"app:state="false" />
view的构造函数获取自定义属性
class DigitalCustomView : LinearLayout {constructor(context: Context) : super(context)constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {LayoutInflater.from(context).inflate(R.layout.view_custom, this)var ta = context.obtainStyledAttributes(attrs, R.styleable.CustomView)mIcon = ta.getResourceId(R.styleable.CustomView_leftIcon, -1) //左图像mState = ta.getBoolean(R.styleable.DigitalCustomView_state, false)mName = ta.getString(R.styleable.CustomView_name)ta.recycle()initView()}}
上面给出大致的代码,记得获取context.obtainStyledAttributes(attrs, R.styleable.CustomView),最后要释放掉ta.recycle()。
继承系统控件
就是继承系统已经提供好给我们的控件例如TextView、LinearLayout等,分为View类型或者ViewGroup类型的两种。主要根据业务需求进行实现,实现重写的空间也很大,主要看需求。比如需求 :在文字后面加个颜色背景。
搜索公众号程序员小乐回复关键字“Java”,获取Java面试题和答案。
根据需要一般这种情况下我们是希望可以复用系统的onMeaseur和onLayout流程.直接复写onDraw方法。
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 = layoutbounds.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);
}//画一个圆@Overrideprotected 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 = childCountif (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 = childCountvar maxWidth = 0for (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 = childCountvar height = 0for (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 = childCountvar currentHeight = tfor (i in 0 until count) {val child = getChildAt(i)val h = child.measuredHeightval w = child.measuredWidth//摆放子viewchild.layout(l, currentHeight, l + w, currentHeight + h)currentHeight += h}}
}
主要两点 先 measureChildren()轮训遍历子View获取宽高,并根据测量模式逻辑计算最后所有的控件的所需宽高,最后setMeasuredDimension()保存一下。
/ 绘制流程相关知识点 /
View的绘制流程相关 最基本的三个相关函数 measure() ->layout()->draw()。
/ onMeasure()相关知识点 /
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);
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的时候 要对两种模式进行处理。
ViewGroup中并没有measure()也没有onMeasure()
因为ViewGroup除了测量自身的宽高,还需要测量各个子View的宽高,不同的布局测量方式不同 (例如 LinearLayout跟RelativeLayout等布局),所以直接交由继承者根据自己的需要去复写。但是里面因为子View的测量是相对固定的,所以里面已经提供了基本的measureChildren()以及measureChild()来帮助我们对子View进行测量。
/ onLayout相关 /
View.java的onLayout方法是空实现:因为子View的位置,是由其父控件的onLayout方法来确定的。onLayout(int l, int t, int r, int b)中的参数l、t、r、b都是相对于其父 控件的位置。自身的mLeft, mTop, mRight, mBottom都是相对于父控件的位置。
Android坐标系
内部View坐标系跟点击坐标
看一下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()都是空实现,交给继承者根据自身需求去定位。
搜索公众号程序员小乐回复关键字“offer”,获取offer面试题和答案。
部分零散知识点:
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
如果必要的话,绘画边缘重新保存图层
画装饰(例如滚动条)
看一下View#draw()源码的实现
public void draw(Canvas canvas) {// Step 1, draw the background, if neededint 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 contentif (!dirtyOpaque) onDraw(canvas);// Step 4, draw the childrendispatchDraw(canvas);drawAutofilledHighlight(canvas);// Overlay is part of the content and draws beneath Foregroundif (mOverlay != null && !mOverlay.isEmpty()) {mOverlay.getOverlayView().dispatchDraw(canvas);}// Step 6, draw decorations (foreground, scrollbars)onDrawForeground(canvas);// Step 7, draw the default focus highlightdrawDefaultFocusHighlight(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 类里。
注意事项
在 ViewGroup 的子类中重写除 dispatchDraw() 以外的绘制方法时,可能需要调用 setWillNotDraw(false);
出于效率的考虑,ViewGroup 默认会绕过 draw() 方法,换而直接执行 dispatchDraw(),以此来简化绘制流程。所以如果你自定义了某个 ViewGroup 的子类(比如 LinearLayout)并且需要在它的除 dispatchDraw() 以外的任何一个绘制方法内绘制内容,你可能会需要调用 View.setWillNotDraw(false) 这行代码来切换到完整的绘制流程(是「可能」而不是「必须」的原因是,有些 ViewGroup 是已经调用过 setWillNotDraw(false) 了的,例如 ScrollView)。
在重写的方法有多个选择时,优先选择 onDraw()
一段绘制代码写在不同的绘制方法中效果是一样的,这时你可以选一个自己喜欢或者习惯的绘制方法来重写。但有一个例外:如果绘制代码既可以写在 onDraw() 里,也可以写在其他绘制方法里,那么优先写在 onDraw() ,因为 Android 有相关的优化,可以在不需要重绘的时候自动跳过 onDraw() 的重复执行,以提升开发效率。享受这种优化的只有 onDraw() 一个方法。
/ 在Activity中获取宽高 /
Activity获取view的宽高, 在onCreate , onResume等方法中获取到的都是0, 因为View的测量过程并不是和Activity的声明周期同步执行的。
view.postpost可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候, View也已经初始化好了。
view.post(new Runnable() {@Overridepublic void run() {int width = view.getMeasuredWidth();int height = view.getMeasuredHeight(); }});
ViewTreeObserver使用addOnGlobalLayoutListener接口, 当view树的状态发生改变或者View树内部的view的可见性发生改变时,onGlobalLayout都会被调用, 需要注意的是,onGlobalLayout方法可能被调用多次, 代码如下:
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {@Overridepublic void onGlobalLayout() {view.getViewTreeObserver().removeOnGlobalLayoutListener(this);int width = view.getMeasuredWidth();int height = view.getMeasuredHeight();}});
onWindowFocusChanged这个方法的含义是View已经初始化完毕了, 宽高已经准备好了, 需要注意的就是这个方法可能会调用多次, 在Activity onResume和onPause的时候都会调用, 也会有多次调用的情况。
@Overridepublic void onWindowFocusChanged(boolean hasWindowFocus) {super.onWindowFocusChanged(hasWindowFocus);if(hasWindowFocus){int width = view.getMeasuredWidth();int height = view.getMeasuredHeight();}}
欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,欢迎转发分享给更多人。欢迎加入程序员小乐技术交流群,在后台回复“加群”或者“学习”即可。
猜你还想看
阿里、腾讯、百度、华为、京东最新面试题汇集
记一次由Redis分布式锁造成的重大事故,避免以后踩坑!
审阅“史上“最烂的代码
使用IntelliJ IDEA查看类图,内容极度舒适
嘿,你在看吗?
万字长文!面试官问你:自定义View跟绘制流程懂吗?帮你搞定面试官相关推荐
- concurrent 底层_万字长文!从底层开始带你了解并发编程,彻底帮你搞懂Java锁!
线程是否要锁住同步资源 锁住 悲观锁 不锁住 乐观锁 锁住同步资源失败 线程是否要阻塞 阻塞 不阻塞自旋锁,适应性自旋锁 多个线程竞争同步资源的流程细节有没有区别 不锁住资源,多个线程只有一个能修改资 ...
- java底层编程_万字长文!从底层开始带你了解并发编程,彻底帮你搞懂Java锁!
线程是否要锁住同步资源锁住 悲观锁 不锁住 乐观锁 锁住同步资源失败 线程是否要阻塞阻塞 不阻塞自旋锁,适应性自旋锁 多个线程竞争同步资源的流程细节有没有区别不锁住资源,多个线程只有一个能修改资源成功 ...
- 面试绕不开的 CAP 理论,这篇文章帮你搞定!
点击关注公众号,实用技术文章及时了解 文章转载于:JAVA日知录 案例背景 CAP 理论是分布式系统中最核心的基础理论,虽然在面试中,面试官不会直白地问你 CAP 理论的原理,但是在面试中遇到的分 ...
- 面试第一问:简单做个自我介绍吧,怎么回答才让面试官频频点头?
面试第一问:简单做个自我介绍吧,怎么回答才让面试官频频点头? 前言 个人的基本信息,扬长避短 突出自己的技能 个人兴趣爱好与结尾 总体案例: 前言 面试问题第一问,99.99999%都是:请先做个自我 ...
- 如何在一分钟内搞定面试官
转载自 如何在一分钟内搞定面试官 很多人的求职面试的过程中都会遇到这个问题: "请做个自我介绍." 有的人,可以口若悬河.妙语连珠讲3分钟,有的人,可能磕磕巴巴,讲了30秒, ...
- android自定义view流程,Android 自定义View--从源码理解View的绘制流程
前言 在Android的世界里,View扮演着很重要的角色,它是Android世界在视觉上的具体呈现.Android系统本身也提供了很多种原生控件供我们使用,然而在日常的开发中我们很多时候需要去实现一 ...
- HenCoder Android 开发进阶:自定义 View 1-5 绘制顺序
这期是 HenCoder 自定义绘制的第 1-5 期:绘制顺序 之前的内容在这里: HenCoder Android 开发进阶 自定义 View 1-1 绘制基础 HenCoder Android ...
- Android自定义View系列之详解View的绘制流程
目录 一.开场白 二.View的绘制流程 2.1测量的过程 2.2布局的过程 2.3绘制的过程 一.开场白 开讲之前我们先预设一种自定义ViewGroup的场景:我们知道LinearLayout.Fr ...
- 【Android面试】View的绘制流程
目录 View的绘制流程简介 Activity和window和view 的关系 Activity和Window是什么时候建立联系的呢? ViewRootImpl View的绘制流程总结 View的绘制 ...
- Android之View的绘制流程解析
转载请标明出处:[顾林海的博客] 个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持! ##前言 自定义View在Android中占据着非常重要的地位,因此了解View的 ...
最新文章
- android ble 设备扫描程序,Android应用开发Android 7.0 BLE scan 问题:程序无错但扫描不到BLE设备...
- python选择语句_3.1Python的判断选择语句
- WordPress百度快速提交插件-加速百度爬虫和收录
- 4岁的拼多多超越20岁的百度,成为中国第五大互联网公司!
- 《代码大全》阅读心得二
- 精通开关电源设计第三版pdf_看漫画,学电源(一)丨线性电源与开关电源的构造...
- 【包邮免费送】Python 全栈知识图谱
- spark structured stream的Update模式
- getTime()的00:00:00问题。
- vim保存文件”:wq与“:x的区别以及小写:x与大写“:X”的区别
- LeetCode 中文刷题手册:LeetCode Cookbook下载
- [论文阅读笔记17]MAT: Motion-Aware Multi-Object Tracking
- 京东深圳手Q微信事业部测试工程师面试总结
- 揭秘!“真假美猴王事件”其实是如来的一次运维事故
- hbuilder基座_3图标基座的禅宗
- 世界上最伟大的推销员-羊皮卷之八
- freerdp 解压安装_linux下使用FreeRDP 连接 Windows 远程桌面
- Login with Vimeo in iOS App using oAuth tutorial
- dede的文档关键词维护,就是自动加内链锚文本
- ubuntu 9.10下的网络电视sopcast的安装及消除播放杂音