文章目录
文章整理中… …
1. View工作原理了解
onAttachedToWindow
onMeasure
onSizeChanged
onLayout
onDraw
从Android 的 FrameLayout 看 测量,布局,绘制 的自定义view的世界吧.
整个工作流程的核心 ViewRootImpl(ViewRoot) 的 performTraversals 开始的,
分别是 performMeasure->measure,performLayout->layout,performDraw->draw.
// ViewRootImpl.java
private void performTraversals() {
... ...
if (!mStopped || mReportNextDraw) {
... ...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 测量measure的关键函数
... ...
}
... ...
if (didLayout) {
performLayout(lp, desiredWindowWidth, desiredWindowHeight); // 布局layout的关键函数
...
}
... ...
if (!cancelDraw && !newSurface) {
if (!skipDraw || mReportNextDraw) {
... ...
performDraw(); // 绘制 draw 的关键函数
}
}
}
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
final View host = mView;
host.layout(... ...)
}
private void performDraw() {
... ...
draw(fullRedrawNeeded)->drawSoftware->mView.draw
... ...
}
mView 是根视图 DecorView(继承关系)->FrameLayout->ViewGroup->View,看看下图的布局关系(转自网上的图片)
// 获取上层的视图view 的相关代码,了解下.
getWindow().getDecorView()
findViewById(android.R.id.content)
来看看整体的performTraversals调用过程 的 流程图
1.1 View测量measure
measure 流程了解下:
ViewRootImpl.performTraversals,performTraversals函数里面执行了 performMeasure
// ViewRootImpl.java
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
// mView 是 DecorView(View树的根节点是DecorView), 调用 measure
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
FrameLayout 的 onMeasure 中 调用measure相关函数的,我们先看看
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// measureMatchParentChildren,false 表示 width 与 height 同时设置了 match_parent 或者指定了大小, true 反之.
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
... ...
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 当子控件为 wrap_content
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
... ...
}
}
... ...
// 设置 FrameLayout 的宽高
setMeasuredDimension(
resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
... ...
// 自身是不确定大小的模式,子view又是MATCH_PARENT属性的,就需要为这些子view重新测量。
for (int i = 0; i < count; i++) {
if (lp.width == LayoutParams.MATCH_PARENT) {
... ...
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
... ...
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(
heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
// 子控件调用 measure,层层调用下去.
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
widthMeasureSpec 与 heightMeasureSpec 是一个int值(java中int型变量由4个字节(32bit)组成),
其中 高2位 用来保存 MeasureMode,低30位 用来保存 size.
MeasureSpec有3种模式:
- UNSPECIFIED # 父容器不对子view的大小做限制,一般用于系统内部,或者ListView ScrollView等滑动控件。
- EXACTLY # 父控件已为子控件确定了一个确切的大小,孩子将被给予这些界限,不管子控件自己希望的是多大,对应于 match_parent 和 具体的值(比如 android:layout_width=“200px”),父容器测量出 View所需要的大小,也就是SpecSize的值。
- AT_MOST # 在此模式下,父容器未能检测出子view的大小,但指定了一个最大大小spec size,子view的大小不能超过此值。父控件会给子控件尽可能大的尺寸,对应于 wrap_comtent, 子view 的最终大小是父View指定的SpecSize值, 并且子view的大小小于这个值.
MeasureSpec 常用的三个函数:
- makeMeasureSpec # 根据提供的 size 和 mode 创建一个 测量值
- getMode # 从所提供的测量值 中 提取 模式mode
- getSize # 从所提供的测量值 中提取 尺寸size
View:
setMeasuredDimension // 设置宽高的,基本上都会使用,很多自定义控件或者Android原生控件
measure
resolveSizeAndState //
getDefaultSize //
combineMeasuredStates
View.MeasureSpec:
MeasureSpec.makeMeasureSpec // 根据所提供的size和mode创建一个测量规范
ViewGroup:
measureChildWithMargins // 子view,多宽,多高, 内部加上了viewGroup的padding值、子view的margin值和传入的宽高wUsed、hUsed (padding + margin + used) ,把 margin 及 padding 也作为子视图大小的一部分
measureChild // 某一个子view,多宽,多高, 内部加上了viewGroup的padding值
measureChildren // 循环的调用所有子child的 measureChild
getChildMeasureSpec // 给子view 计算出正确的 MeasureSpec
RecyclerView: 为了适应自己的那套东西,重写了很多函数,比如 getChildMeasureSpec等,也新增了很多函数,比如 measureChildWithMargins等
// 在自定义控件的时候,就看自己的需求和定义吧,调用相应的函数或者自己重写,新增都可以.
getChildMeasureSpec 的规则(具体可以查看源码,了解下就好了,调用就行了)
<FrameLayout
android:background="#000000"
// android:padding="50px"
android:layout_width="500px"
android:layout_height="500px">
<Button
android:text="textTest"
// android:layout_margin="50px"
// android:layout_width="wrap_content"
// android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
// FrameLayout.onMeasure->measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
... ...
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// 加上父控件的 mPaddingLeft + mPaddingRight + 还有子控件的 lp.leftMargin + lp.rightMargin
// 传入 getChildMeasureSpec padding参数.
// specSize - padding,得到子控件 初步的 size,还需要 用 specMode 进行相应的操作,
// 最后才会选择 size 还是 childDimension.
// 假设前提: 并且父控件有确切的大小,属于 MeasureSpec.EXACTLY
// 1. 没有 padding, margin,子控件为 match_parent,那么子控件 resultSize = 500px; resultMode = MeasureSpec.EXACTLY;
// 2. 父控件的 padding, button 的 margin 为 50px,那么子控件 resultSize = 300px; resultMode = MeasureSpec.EXACTLY;
// 3. 没有 padding, marigin,子控件为 wrap_content,那么子控件 resultSize = 500px; resultMode = MeasureSpec.AT_MOST;
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
... ...
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
// 默认 View 的 onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
没有 padding, marigin,子控件为 wrap_content,
那么子控件 resultSize = 500px; resultMode = MeasureSpec.AT_MOST 的 demo.
为何 button不占满全屏,因为传递给 button 的 widthMeasureSpec 或 heightMeasureSpec,基本属于 Size=500px, mode=MeasureSpec.AT_MOST.
因为button继承的 TextView,内部的 onMeasure 进行了处理的(代码可以自行查看).
// 我将 button 的 widthMeasureSpec 与 heightMeasureSpec 进行了一个处理
public class TestButton extends Button {
... ...
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
}
// 看了代码我们知道,父控件给 修改后的 TestButton 传递的 mode 是 MeasureSpec.AT_MOST,size 为 500px,
// 因为 getDefaultSize 进行了处理,result = specSize; 所以最后占满了父控件.
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;
}
最后 button占满了父控件
getWidth()和getMeasuredWidth()的区别
getMeasuredWidth(): 只要一执行完 setMeasuredDimension()方法, 就有值了, 并且不再改变.
getWidth(): 必须执行完 onMeasure() 才有值, 可能发生改变.
如果 onLayout 没有对子 View 实际显示的宽高进行修改, 那么 getWidth() 的值 == getMeasuredWidth() 的值.
1.2. View布局layout
measure完成之后,就会调用 layout.
从 ViewRootImpl 的 performTraversals->performLayout 函数开始,然后 DecorView.layout
// ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
final View host = mView;
... ...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
... ...
}
layout相对 measure 操作就简单一些了,将子控件放在合适的位置上.
// View.java
public void layout(int l, int t, int r, int b) {
... ...
// 1. 调用 setFrame 位置保存起来,设定 view的四个位置(left, right, top, bottom).
boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
... ...
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 2. 子控件进行位置分配,一般是 继承ViewGroup后的控件 实现onLayout,比如FrameLayout等.
onLayout(changed, l, t, r, b);
// 3. 清除 PFLAG_LAYOUT_REQUIRED 标识,layout完成了操作.
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
... ...
}
}
// ViewGroup.java 将 onLayout 设置成了 abstract 类型,所以,继承 ViewGroup 都需要实现 onLayout,比如 FrameLayout等.
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
// FrameLayout.java
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
... ...
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
... ...
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
... ...
}
看下整体的流程,不断的一层层往下调用.
这里就不过的讲解 layout, onLayout 具体可以参考 Android原生继承了 ViewGroup 实现 onLayout的控件.
1.3. View绘制流程
从 ViewRootImpl 的 performTraversals->performDraw 函数开始,然后 DecorView.draw
// DecorView,调用了 View 的 draw 函数.
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
... ...
}
看看 View.draw 函数做了一些什么事情,具体关注4个关键的函数.
1. 绘制背景(颜色值或图片,Drawable对象,比如 Shader, DrawableState等),
调用 setBackgroundColor, setBackgroundDrawable, setBackgroundResouce设置不同的背景.
2. 绘制自身内容,比如 ImageView(绘制图片), TextView(绘制文字), LinearLayout(Divider 分割线)
3. 绘制子控件,遍历所有子控件的 draw方法,一层层调用下去.
4. 绘制装饰 (foreground, scrollbars)
public void draw(Canvas canvas) {
if (!dirtyOpaque) {
drawBackground(canvas); // 1. 绘制背景
}
... ...
if (!verticalEdges && !horizontalEdges) {
if (!dirtyOpaque) onDraw(canvas); // 2. 绘制自身内容
dispatchDraw(canvas); // 3. 绘制子控件
... ...
onDrawForeground(canvas); // 4. 绘制装饰
... ...
}
... ...
}
虚线代表了 Override,比如 ImageView
TV开发经常要对 继承了ViewGroup上面onDraw绘制东西,是无法显示出来的,
是因为在Viewgroup 调用 initViewGroup,函数里面 setFlags(WILLL_NOT_DRAW,DRAW_MASK),相当于调用了setWillNotDraw(true),
如果想绘制内容->onDraw,使用 setWillNotDraw(false) 才能显示内容出来.
这就是为何在一些继承了ViewGroup的自定义控件上,想要绘制阴影,内容等 无法显示出来的原因以及解决方案.
// ViewGroup.java
private void initViewGroup() {
setFlags(WILL_NOT_DRAW, DRAW_MASK);
... ...
}
TV开发中还会遇到 放大被挡住的问题,如何进行处理:
第一种方式 修改绘制顺序:
//ViewGroup.java
setChildrenDrawingOrderEnabled(true);
private int position = 0;
// 也可以重写此函数,bringToFront 调用的绘制顺序就被更改.
public void bringChildToFront(ViewGroup vg, View child) {
position = vg.indexOfChild(child);
if (position != -1) {
vg.postInvalidate();
}
}
/**
* 此函数 dispatchDraw 中调用. <br>
* 原理就是和最后一个要绘制的view,交换了位置. <br>
* 因为dispatchDraw最后一个绘制的view是在最上层的. <br>
* 这样就避免了使用 bringToFront 导致焦点错乱问题. <br>
*/
public int getChildDrawingOrder(int childCount, int i) {
if (position != -1) {
if (i == childCount - 1) {
return position;
}
if (i == position) {
return childCount - 1;
}
}
return i;
}
RecyclerView的绘制顺序,可以参考Leanback的.
int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) {
View view = findViewByPosition(mFocusPosition);
if (view == null) {
return i;
}
int focusIndex = recyclerView.indexOfChild(view);
// supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
// drawing order is 0 1 2 3 9 8 7 6 5 4
if (i < focusIndex) {
return i;
} else if (i < childCount - 1) {
return focusIndex + childCount - 1 - i;
} else {
return focusIndex;
}
}
第二种方式调用:bringToFront 函数. 这个函数建议尽量不使用,好像有问题.
1.3.1 绘制例子
- draw,一般很少会去在上面做一些绘制的事情,除非有特定的需要,比如你绘制的东西需要在背景以及子控件之前.
- onDraw,不必多说,比如LinearLayout的分割线等等,TV上的一些自绘的内容或者其它.
- 真正的圆角控件,dispatchDraw
protected void dispatchDraw(Canvas canvas) {
canvas.saveLayer(new RectF(0, 0, canvas.getWidth(),canvas.getHeight()), null, Canvas.ALL_SAVE_FLAG);
// 绘制子控件
super.dispatchDraw(canvas); //目的,需要显示的内容
// 绘制带有圆角的 Path
//roundPaint.setColor(Color.WHITE);
roundPaint.setAntiAlias(true);
// 绘制模式为填充
roundPaint.setStyle(Paint.Style.FILL);
// 混合模式为 DST_IN, 即仅显示当前绘制区域和背景区域交集的部分,并仅显示背景内容。
roundPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
Path mPath = new Path();
float r = 300;
PointF center = new PointF(canvas.getWidth() / 2, canvas.getHeight() / 2);
mPath.addCircle(center.x, center.y, r, Path.Direction.CW);
// 圆角的矩形,使用此方法,addRoundRect
mPath.moveTo(0, 0); // 通过空操作让Path区域占满画布
mPath.moveTo(canvas.getWidth(), canvas.getHeight());
canvas.drawPath(mPath, roundPaint); // 源内容,用于遮罩层.
canvas.restore();
}
子view集合,只在圆形里面显示.
与属性动画结合的绘制方法 参考 AndroidTvwidget 的 AnimView 的一个 demo.
https://gitee.com/kumei/android-tv-frame-new/blob/develop/AndroidTvWidget/app/src/main/java/com/open/test/view/AnimView.java
1.3.2 动画的原理简单了解
属性动画 关键的两个类ObjectAnimator和ValueAnimator
ObjectAnimator继承了ValueAnimator,ObjectAnimator#start(),ValueAnimator#start()
ValueAnimator.doAnimationFrame,animationFrame中将调用animateValue
void animateValue(float fraction) {
fraction = mInterpolator.getInterpolation(fraction); // 插值器
mCurrentFraction = fraction;
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].calculateValue(fraction);
}
if (mUpdateListeners != null) {
int numListeners = mUpdateListeners.size();
for (int i = 0; i < numListeners; ++i) {
mUpdateListeners.get(i).onAnimationUpdate(this); // 最后调用
}
}
}
最后调用 get与set方法(比如 setXXX getXXX)将会被调用属性(通过反射进行调用)的函数
比如 setTranslationX 或者 setScaleY 的调用,是一个什么过程,了解下
最后还是调用 invalidate
// ViewRootImpl.java,最后调用 scheduleTraversals
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//暂停了handler的后续消息处理,防止界面刷新的时候出现同步问题
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//将runnable发送给handler执行
//performTraversals 在一个runnable中被调用的,通过将这个runnable加入队列来执行
//performTraversals就是开头讲的 performMeasure->measure,performLayout->layout,performDraw->draw 的开始
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
2. 自定义控件了解
2.1 自定义属性
public NewFrameLayout(Context context) {
this(context, null);
}
public NewFrameLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
// 这里可以配合主题定义自己的属性
// 比如 AppTheme.Light,里面的 <item name="cardViewStyle">@style/CardViewStyle.Light</item>
// this(context, attrs, attr.NewFrameLayoutStyle);
}
public NewFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.NewFrameLayout, defStyleAttr, defStyleRes);
... ...
a.recycle(); // 这个不要忘记写
}
2.2 组合控件
参考 tvWidget 里面的多状态UI控件,了解下.
public class TVUICommonItemView extends RelativeLayout {
}
xml 布局使用 多个控件组合一起使用
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 左侧图标 -->
<ImageView />
</merge>
2.3 属性修改
简单的看一下.
又比如 LinearLayout 这些 Android原生的控件,还有网上的一些自定义控件,比如 FlowLayout.
比如 RecyclerView,addChild… 修改了 onMeasure,onLayout,onDraw,draw 来满足自己的一些特定需求.
LayoutManager : onLayoutChildren,measureChildWithMargins 等等.
如果想做一些相关自定义的 LayoutManager,只需要关注 LayoutManager的几个重要函数.:onLayoutChildren
https://blog.csdn.net/u010072711/article/details/78867096 图文详解LinearLayoutManager填充、测量、布局过程
https://blog.csdn.net/qibin0506/article/details/52676670?locationNum=2 RecyclerView自定义LayoutManager,打造不规则布局
https://www.jianshu.com/p/61a4811bb5ca 最清晰的 RecyclerView 使用及源码解析
https://juejin.im/entry/59c0ccfd6fb9a00a3c4b16d9 一个有特点的正六边形RecyclerView—HexagonRecyclerView详解篇
https://blog.csdn.net/u011387817/article/details/81875021 Android自定义LayoutManager第十一式之飞龙在天
https://github.com/JadynAi/InfinateCard 卡牌堆叠滑动效果,增加回滚动画
https://github.com/leochuan/ViewPagerLayoutManager ViewPager-LayoutManager
https://www.jianshu.com/p/715b59c46b74 你可能误会了!原来自定义LayoutManager可以这么简单
https://blog.csdn.net/lylodyf/article/details/52846602 自定义LayoutManager的详解及其使用
2.4 注意事项
onDraw或者相关绘制的函数中 避免重复创建变量,内存会抖动.
在 onDetachedFromWindow 做一些销毁的事情,避免出现不必要的麻烦… …
最好按照谷歌的制定的标准规范来写.
组合控件的布局建议使用 merge,减少层级.
3. 参考资料
谷歌源码 Android-24,Android内核剖析,Android 开发艺术探索 等等.
自定义控件测量模式真的和 match_parent,wrap_content 一一对应吗?