Android-自定义View前传-View的三大流程-Layout
Android-自定义View前传-View的三大流程-Layout
参考
- 《Android开发艺术探索》
- https://github.com/hongyangAndroid/FlowLayout
写在前头
在之前的文章中 , 我们学习了Android View的 Measure的流程, 本篇文章来学习一下View的 Layout 的过程。 学完了这一篇文章后,我们可以尝试自己去自定义一个自己的Layout。
Overview
我对于Layout过程的理解:Layout的过程就是给Child安家的过程
Layout的过程主要是放在 ViewGroup 中的,ViewGroup不仅需要定位自己,还需要定位Child。
View和ViewGroup
Layout流程的起点也是在 ViewRootImpl 中的 performTraversals 方法中。
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
if (host == null) {
return;
}
try {
//首先调用了host的layout方法 host = mView = DecorView
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
//....
}
我们接着来看View的layout方法
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;
//调用 setOpticalFrame或者 setFrame 来确定自己的位置
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {、
//调用onLayout
onLayout(changed, l, t, r, b);
//.....
}
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;
// Remember our drawn bit
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 our old position
invalidate(sizeChanged);
//确定4个点
//这四个点一旦确定了那么View在ViewGroup中的位置也就确定了
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
//......
View的 layout 方法主要是做了:
- 通过
setFrame确定了自己的位置,一篇Left,Top,Right,Bottom这几个值确定了,那么View的位置也就确定了。 - 紧接着调用了
onLayout方法。
LinearLayout的onLayout
View是不需要实现onLayout方法的,只用ViewGroup才需要实现。由于各种ViewGroup的布局方式的不同,无法统一,所以ViewGroup也并没有实现onLayout. 而是将onLayout的过程放到了子类中。我们还是通过 LinearLayout 来学习。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
onLayout根据 Orientation 属性来调用 layoutVertical 或者 layoutHorizontal
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight;
// Space available for child
//获取Child可用的空间
int childSpace = width - paddingLeft - mPaddingRight;
//Child的Group
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
//根据Gravity确定初始的childTop
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
//LayoutView
//遍历
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {//过滤Child的Visibility是GONE的情况
//获取Child的大小
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
//获取LP
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
//处理Child的Gravity
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
//水平居中
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
//居右
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
//childTop 加上 Divider
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
//加上Margin
childTop += lp.topMargin;
//调用Child的layout方法
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
上面的代码还是比较清晰的,首先是有一个childTop变量,来确定child与ViewGroup顶部的距离,通过不断的遍历Child然后不断增加childTop的值,这样就实现了LinearLayout的垂直布局的效果。当然其中也有处理LinearLayout和Child的Gravity的过程。
FlowLayout
通过学习LinearLayout的Layout的过程,发现其实Layout的过程就是确定View的Left,Top,Right,Bottom 4个值的过程,学习了 Measure 和 Layout 的过程以后,我们就已经可以着手做一个自己的Layout了,这里我选的是模仿 hongyang大神的FlowLayout。效果图如下:

package layout
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
/**
* Created by ShyCoder on 2019/1/16.
*/
class MyFlowLayout(context: Context?, attrs: AttributeSet?, defStyleAttr: Int)
: ViewGroup(context, attrs, defStyleAttr) {
constructor(context: Context?, attributeSet: AttributeSet?) : this(context, attributeSet, 0)
constructor(context: Context?) : this(context, null)
init {
}
/**
* 存贮所有的View根据行来存贮
* */
private val mAllViews = mutableListOf<List<View>>()
/**
* 存贮每一行View的高度
* */
private val mHeightList = mutableListOf<Int>()
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//从MeasureSpec获取Mode和Size
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getMode(heightMeasureSpec)
//计算Wrap_content的情况
var totalHeight = 0//总高度
var totalWidth = 0//总宽度
var lineWidth = 0//当前行的宽度
var lineHeight = 0//当前行的高度
val viewCount = childCount
for (i in 0.until(viewCount)) {
val child = getChildAt(i)
//如果是GONE状态不用测量
if (child.visibility == View.GONE) {
continue
}
//测量Child
measureChild(child, widthMeasureSpec, heightMeasureSpec)
val lp = child.layoutParams as MarginLayoutParams
//计算child的width = 测量后宽度+左右的两个Margin
val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
//计算child的Height = 测量后高度 + 上下两个Margin
val childHeight = child.measuredHeight + lp.bottomMargin + lp.topMargin
//需要换行时候的处理方式
//如果已有的行宽+当前child的宽度> FlowLayout的宽度(减去左右的Padding)
if (childWidth + lineWidth > widthSize - this.paddingLeft + this.paddingRight) {
totalWidth = Math.max(totalWidth, lineWidth)
lineWidth = childWidth
totalHeight += lineHeight
lineHeight = childHeight
} else {//如果不需要换行增加行宽
lineWidth += childWidth
//获取最大高度
lineHeight = Math.max(lineHeight, childHeight)
}
//最后一个View
if (i == viewCount - 1) {
totalWidth = Math.max(totalWidth, lineWidth)
totalHeight += lineHeight
}
}
this.setMeasuredDimension(
//如果MeasureSpec的Mode是EXACTLY 的话,测量后大小等会传进来的测量大小,
//否则则是我们自己计算的大小
if (widthMode == MeasureSpec.EXACTLY) widthSize else totalWidth,
if (heightMode == MeasureSpec.EXACTLY) heightSize else totalHeight
)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
this.mAllViews.clear()
this.mHeightList.clear()
val viewCount = this.childCount
var lineHeight = 0
var lineWidth = 0
var lineViews = mutableListOf<View>()
for (i in 0.until(viewCount)) {
val child = this.getChildAt(i)
if (child.visibility == View.GONE) {
continue
}
val lp = child.layoutParams as MarginLayoutParams
val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin
//行上无法继续放置View
if (childWidth + lineWidth > this.width - this.paddingLeft - this.paddingRight) {
//添加line height
mHeightList.add(lineHeight)
lineWidth = 0
//添加正行的View到集合中
this.mAllViews.add(lineViews)
lineViews = mutableListOf()
}
//区当前行的View的最大高度
lineHeight = StrictMath.max(lineHeight, childHeight)
lineWidth += childWidth
//向行上添加View
lineViews.add(child)
}
//进行Layout
var left = this.paddingLeft
var top = this.paddingTop
for (i in 0.until(this.mAllViews.size)) {
val lineViews = mAllViews[i]
val lineHeight = mHeightList[i]
left = this.paddingLeft
for (j in 0.until(lineViews.size)) {
val child = lineViews[j]
val lp = child.layoutParams as MarginLayoutParams
//view的四个边
val l = left + lp.leftMargin
val t = top + lp.topMargin
val r = l + child.measuredWidth
val b = t + child.measuredHeight
//调用Child的Layout方法
child.layout(l, t, r, b)
left += child.measuredWidth + lp.leftMargin + lp.rightMargin
}
top += lineHeight
}
}
}
在onMeasure对wrap_content的情况进行了处理,计算出所需要的大小。
在onLayout方法中,首先计算出每一行的高度并存储和获取每一行的View的List进行存储,当这些都计算完之后,进行layout操作。
在layout操作的时候,首先是在同一行上的View的top是统一的,每当这一行的View处理完成之后,就执行换行的操作,即增加view的大小。
写在最后
本篇文章就到此结束了,Layout的过程还是相对简单的,在下一篇文章呢,我们将会学习View的最后一个流程 Draw 流程。
Android-自定义View前传-View的三大流程-Layout的更多相关文章
- Android自定义View前传-View的三大流程-Measure
Android自定义View前传-View的三大流程-Measure 参考 <Android开发艺术探索> https://developer.android.google.cn/refe ...
- 【转】ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解
原文地址:http://blog.csdn.net/a396901990/article/details/36475213 简介: 在自定义view的时候,其实很简单,只需要知道3步骤: 1.测量—— ...
- ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解
简介: 在自定义view的时候,其实很简单,只需要知道3步骤: 1.测量--onMeasure():决定View的大小 2.布局--onLayout():决定View在ViewGroup中的位置 3. ...
- android 自定义ViewGroup和对view进行切图动画实现滑动菜单SlidingMenu
示意图就不展示了,和上一节的一样,滑动菜单SlidingMenu效果如何大家都比较熟悉,在这里我简单说明一下用自定义ViewGroup来实现. 实现方法:我们自定义一个ViewGroup实现左右滑动, ...
- android 自定义ViewGroup和对view进行切图动画实现滑动菜单SlidingMenu[转]
http://blog.csdn.net/jj120522/article/details/8095852 示意图就不展示了,和上一节的一样,滑动菜单SlidingMenu效果如何大家都比较熟悉,在这 ...
- android自定义View_3——Optimizing the View
前言:现在你已经有了一个自定义的view了,并且支持手势和过度动画等属性,现在你要确认这个view 有够快的速度,为了使其在UI上不会显得迟钝或是卡停,你要保证view动画,能每秒最好有60帧. 一: ...
- android自定义View_2——Making the View Interactive
前言:绘制出一个view只是自定义view的一个部分,还需要自定义一些view的行为,来反馈用户的使用操作,反馈的 行为要合理性,就像真是的物理世界一样,不要太玄幻哦. 可以将view的行为封装到in ...
- Android 自定义可拖拽View,界面渲染刷新后不会自动回到起始位置
以自定义ImageView为例: /** * 可拖拽ImageView * Created by admin on 2017/2/21. */ public class FloatingImageVi ...
- Android学习笔记---前传
在正式的撰写个人的学习笔记前,先对个人的学习经历做一个简要的介绍.座右铭:诚不欺我 1. 前言 本人非软件工程出身,属于半路出家,误打误撞进入这个行业,初心是软件开发的门槛低,自以为学习过C语言,轻度 ...
随机推荐
- Django框架之Form组件
一.初探Form组件 在介绍Form组件之前,让大家先看看它强大的功能吧!Go... 下面我们来看看代码吧! 1.创建Form类 from django.forms import Form from ...
- Python基础之继承与派生
一.什么是继承: 继承是一种创建新的类的方式,新建的类可以继承一个或过个父类,原始类成为基类或超类,新建的类则称为派生类 或子类. 其中,继承又分为:单继承和多继承. class parent_cla ...
- AI学习吧-公钥私钥、沙箱环境
公钥私钥 公钥.私钥 可以互相解密 应用:数字签名和加密数据 数字签名:使用私钥加密,公钥解密 加密数据:使用公钥加密,私钥解密泄密时:当有人拿走了你的公钥,你可以到CI证书中心,使用你的私钥和公钥办 ...
- OpenCV trackbar 避免使用全局变量
OpenCV trackbar 避免使用全局变量 在OpenCV中使用trackbar是交互式地图像处理的一种方式,例如用于图像阈值分割,用户可以一边调整阈值一边看效果.但是很不幸,绝大多数教程上使用 ...
- .NET成年了,然后呢?
作者|Lex Li 编辑|郭蕾 这可能是唯一一篇系统回顾 .NET 发展的文章..NET 的成年礼到了,你会送它什么? 2014 年 11 月 12 日,美国纽约曼哈顿,多云,气温适宜.微软公司执行副 ...
- WPF编程之找不到资源mainWindow.xaml
原文: WPF编程之找不到资源“window1.xaml”之谜 因为将启动窗口移动到了一个新建的文件夹中,启动调试时报找不到资源mainWindow.xaml,原来是App.xaml里面设置的启动窗口 ...
- Encountered IOException running import job: org.apache.hadoop.mapred.FileAlreadyExistsException: Output directory hdfs://slaver1:9000/user/hadoop/tb_user already exists
1.当时初学Sqoop的时候,mysql导入到hdfs导入命令执行以后,在hdfs上面没有找到对应的数据,今天根据这个bug,顺便解决这个问题吧,之前写的http://www.cnblogs.com/ ...
- sed 详解【转】
原文地址:http://www.cnblogs.com/sparkdev/archive/2017/07/10/7138073.html 基本命令格式 sed [常用选项] 命令文本 输入 常用选项 ...
- 安装CentOS 7(转)
转载地址:https://www.cnblogs.com/wcwen1990/p/7630545.html CentOS7安装详解 本文基于vmware workstations进行CentOS7 ...
- 【loj6142】「2017 山东三轮集训 Day6」A 结论题+Lucas定理
题解: 当奇数 发现答案就是C(n,1)^2+C(n,3)^2+...C(n,n)^2 倒序相加,发现就是C(2n,n) 所以答案就是C(2n,n)/2 当偶数 好像并不会证 打表出来可以得到 2.当 ...