这一篇博客也算是对第一篇博客的一个深入补充吧。虽然第一篇介绍的东西多一点,但是有个别知识还是没有真正解释透,所以在这里又整理了一下。
上一篇博客
所以,绘制UI的能力其实是入门的功夫。
一个效果只要它能够在手机上面实现,你就应该具备实现它的能力
布局:onMeasure onLayout
显示 :onDraw:(画布(canvas),画笔(paint),matrix clip,rect,animation,path(贝塞尔曲线等等),line等等)
事件分发 (交互):onTouchEvent:用的最多的是组合的ViewGroup
这些类似于是二十六个字母,音标,这是最基本的。但是学会这些还不能说英语,认全字母之后还需要再去背单词。也就是大量的练习。
①自定义View:在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View
②自定义ViewGroup:自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout
FlowLayout,流式布局,这个概念在移动端或者前端开发中很常见,特别是在多标签的展示中,往往起到了关键的作用。然而Android官方,并没有为开发者提供这样一个布局。一般也是学习自定义ViewGroup的入门级项目。
效果如下
在自定义ViewGroup中必须实现onMeasure和onLayout方法
在自定义View中实现onMeasure和onDraw方法
总体来说,测量的过程是一个递归算法。
父ViewGroup在触发onMeasure方法的时候会调用子View的onMeasure方法,并且传给他两个参数,也就是我们经常用到的widthMeasureSpec和heightMeasureSpec参数。子View测量完了之后会进行一个保存。
那按理说onMeasure函数应该有返回值呀,应该return个数给父控件呀,但是为什么返回值是void呢?
这是因为父控件可以通过childView.getMeasuredWidth与childView.getMeasuredHeight获得子控件保存的值。
那父亲给的两个参数究竟是什么?
那首先就要知道MeasureSpec是个什么东西:
它是View里面的一个静态类,基本都是二进制运算。由于int是32位的,所以MeasureSpec用高两位表示mode,低30位表示size。MODE_SHIFT=30的作用是移位。
其中三种mode分别为:
UNSPECIFIED:不对View大小做限制,系统使用
EXACTLY:确切的大小,如100dp
AT_MOST:大小不可超过某个值,如MATCH_PARENT
LayoutParams是什么?
LayoutParams把控件的layout_width和layout_height以及他们的值解析出来
两者如何进行转换?
那就要用到一个非常重要的算法:getChildMeasureSpec。
他们之间转换的规则表如下
这个图的形象表示就是:
从源码来分析,比如拿出前三个情况(最后三种情况基本遇不到)
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
解释:
在确定子控件的MeasureSpec时:当父亲是个确切的值的时候,如果子控件是个确切的值,那么子控件的size就是子控件layoutParams得到的size,mode就是EXACTLY。如果子控件是MATCH_PARENT,那么子控件的size就是父控件的size,mode也是EXACTLY,因为这也是明确的全部给你。如果子控件是WRAP_CONTENT,那么子控件的size就是父控件的size,mode是AT_MOST。其他的情况都是类似的道理。
从这里也可以看出,getChildMeasureSpec得到的是一个参考的的值而不是一个具体的值。具体的值等measure完了之后才能得到。也就是说measure传入的是一个参考值,子控件onMeasure方法两个参数就是这两个参考值。
所以,在这里再次理解一下getChildMeasureSpec的三个参数,就会恍然大悟:
getChildMeasureSpec(int spec, int padding, int childDimension)第一个参数是父亲的spec,用于得到父亲的size和mode,第二个参数是间距,只有减去这个间距才是父控件”真正“留给子控件的大小。(形象一点来说就是父控件要给自己留一点养老金),第三个参数是子控件的LayoutParams
所以现在就可以回答一开始的那个问题了
这个onMeasure的两个参数,对于当前我写的这个ViewGroup来说,如果把它当作父亲,那么这两个参数就是爷爷给它传过来的参考值,而具体值需要等最后setMeasuredDimension方法执行完了之后才确定。
此时TextView的Mode就是AT_MOST,FlowLayout的Mode也是AT_MOST
那么是先度量孩子还是先度量自己呢?
这是不一定的。
先度量自己再度量孩子:ViewPager
先度量孩子再度量自己:绝大部分
①先获取每个孩子
②通过LayoutParams把孩子的layout_width和layout_height存放的值变成-1那些值。具体为:
③通过getChildMeasureSpec将孩子的layoutParams值转化MeasureSpec
④进行度量
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
for(i in 0 until childCount){
var childView = getChildAt(i)
//得到子控件的LayoutParams
var childLP = childView.layoutParams
//将layoutParams转化为MeasureSpec
var childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight,childLP.width)
var childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom,childLP.height)
childView.measure(childWidthMeasureSpec,childHeightMeasureSpec)
}
}
var widthMode = MeasureSpec.getMode(widthMeasureSpec)
var heightMode = MeasureSpec.getMode(heightMeasureSpec)
var realWidth = if(widthMode == MeasureSpec.EXACTLY) selfWidth else (parentNeededWidth + paddingLeft + paddingRight)
var realHeight = if(heightMode == MeasureSpec.EXACTLY) selfHeight else (parentNeededHeight + paddingTop + paddingBottom)
setMeasuredDimension(realWidth,realHeight)
这里比较难的地方就是realWidth和realHeight的来源。
selfWidth 和selfHeight代表爷爷给ViewGroup(父控件)传过来的参考size,parentNeededWidth和parentNeededHeight代表测量完子控件之后父控件真正需要的大小(当然还要加上padding才是真真正正的大小)。当父控件的mode是EXACTLY时,比如宽和高都是100dp,那么它就根本不用管子控件多大,直接就可以得出测量自己的结果了。
那么又有问题了:如果子控件宽或者高大小超过100dp怎么办?其实很简单,多出去的部分不显示不就得了。所以如果mode是EXACTLY的话就不用管子控件,直接用爷爷给父控件传过来的参考值就是最后的结果。如果mode是其他的,那就要管子控件了。
在介绍布局之前,要先掌握Android的两种坐标系
一种是Android屏幕坐标系,一种是视图坐标系。他们的使用规则如下:
视图坐标系以父view的左上角为原点,我们自定义ViewGroup进行布局的时候,使用的就是视图坐标系。
①getMeasuredWidth在measure()过程结束后就可以获取到对应的值,这个值是通过setMeasuredDimension()方法来进行设置的。
②而getWidth是在layout()过程结束后才能获取到的,这个值是通过视图右边的坐标减去左边的坐标计算出来的。
简而言之,就是如果生命周期在onMeasure和onLayout之间就用前者,如果生命周期在onLayout和onDraw之间就用后者
margin和padding
我找到了一篇很棒的关于margin和padding的博客
package com.example.test01
import android.content.Context
import android.icu.util.Measure
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
class FlowLayout:ViewGroup {
//定义两控件之间的间距
var mHorizontalSpace = 30
var mVerticalSpace = 20
//保存所有的View
var allLineViews: MutableList<MutableList<View>> = mutableListOf()
//保存所有的高度
var everyLineHeights: MutableList<Int> = mutableListOf()
constructor(context: Context):super(context){
}
constructor(context: Context,attrs: AttributeSet):super(context,attrs){
}
//清空函数
private fun clearFun(){
allLineViews.clear()
everyLineHeights.clear()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//清空,而不是重新申请空间,防止内存抖动
clearFun()
//得到爷爷给此ViewGroup的参考值
var selfWidth = MeasureSpec.getSize(widthMeasureSpec)
var selfHeight = MeasureSpec.getSize(heightMeasureSpec)
//记录此ViewGroup实际需要多宽多高
var parentNeededWidth = 0
var parentNeededHeight = 0
//保存这一行的行宽以及行高
var lineWidthUsed = 0
var lineHeightUsed = 0
//保存这一行所有的View
var oneLineViews: MutableList<View> = mutableListOf()
//测量孩子
for(i in 0 until childCount){
var childView = getChildAt(i)
//得到layoutParams
var childLP = childView.layoutParams
//得到MeasureSpec
var childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight,childLP.width)
var childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom,childLP.height)
//进行测量
childView.measure(childWidthMeasureSpec,childHeightMeasureSpec)
if(lineWidthUsed + childView.measuredWidth + mHorizontalSpace +paddingLeft + paddingRight> selfWidth){
//换行
allLineViews.add(oneLineViews)
everyLineHeights.add(lineHeightUsed)
parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed)
parentNeededHeight += lineHeightUsed
//数据重置或者清0
lineWidthUsed = 0
lineHeightUsed = 0
oneLineViews = mutableListOf()
}
oneLineViews.add(childView)
lineWidthUsed += childView.measuredWidth + mHorizontalSpace
lineHeightUsed = Math.max(lineHeightUsed,childView.measuredHeight + mVerticalSpace)
//保存最后一行的数据(这个很容易忘记)
if(i == childCount - 1){
//注意这个判断条件
//换行
allLineViews.add(oneLineViews)
everyLineHeights.add(lineHeightUsed)
parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed)
parentNeededHeight += lineHeightUsed
}
}
//测量父亲
//首先得到父亲的mode,看看是不是EXACTLY
var parentWidthMode = MeasureSpec.getMode(widthMeasureSpec)
var parentHeightMode = MeasureSpec.getMode(heightMeasureSpec)
var realWidth = if(parentWidthMode == MeasureSpec.EXACTLY) selfWidth else (parentNeededWidth + paddingLeft + paddingRight)
var realHeight = if(parentHeightMode == MeasureSpec.EXACTLY) selfHeight else (parentNeededHeight + paddingTop + paddingBottom)
//这里要加上间距
setMeasuredDimension(realWidth,realHeight)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var padL = paddingLeft
var padT = paddingTop
for(i in 0 until allLineViews.size){
for(j in 0 until allLineViews.get(i).size){
//注意:下面这里不是getChildAt,而是得到二维数组的每一个值
var childView = allLineViews.get(i).get(j)
var left = padL
var top = padT
var right = left + childView.measuredWidth
var bottom = top + childView.measuredHeight
childView.layout(left,top,right,bottom)
padL += childView.width + mHorizontalSpace
}
//一定要注意,左起始点要进行归0
padL = paddingLeft
padT += everyLineHeights[i]
}
}
}