这一篇博客也算是对第一篇博客的一个深入补充吧。虽然第一篇介绍的东西多一点,但是有个别知识还是没有真正解释透,所以在这里又整理了一下。
上一篇博客
自定义View小总结
自定义View涉及三个方法,分别是onDraw
,onMeasure
,onLayout
。onDraw
里面经常涉及属性动画ObjectAnimation
的应用,以及在set
方法中进行重绘方法的调用。onMeasure
里面的知识点有,父ViewGroup
的onMeasure
调用子View
的measure
方法,measure
方法再调用自己的onMeasure
方法。如果父ViewGroup
里面还嵌套了子ViewGroup
,那么子ViewGroup
再调用子View
的meaure
方法,这是一个递归的过程。通过setMeasureDimision
来告诉父ViewGroup
自己的测量结果。父ViewGroup
在调用子View
的measure
方法的时候,会传入限制,有三种,分别是不限制,有上限和固定值。
扯点其他的
Java和kotlin是语言基础,自定义view是安卓基础。
所以,绘制UI的能力其实是入门的功夫。
一.什么是自定义View
一个效果只要它能够在手机上面实现,你就应该具备实现它的能力
1.自定义View包含了一些什么东西?
布局:onMeasure onLayout
显示 :onDraw:(画布(canvas),画笔(paint),matrix clip,rect,animation,path(贝塞尔曲线等等),line等等)
事件分发 (交互):onTouchEvent:用的最多的是组合的ViewGroup
这些类似于是二十六个字母,音标,这是最基本的。但是学会这些还不能说英语,认全字母之后还需要再去背单词。也就是大量的练习。
2.自定义View的分类?
①自定义View:在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View
②自定义ViewGroup:自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout
3.自定义View的绘制流程
4.什么是流式布局?
FlowLayout,流式布局,这个概念在移动端或者前端开发中很常见,特别是在多标签的展示中,往往起到了关键的作用。然而Android官方,并没有为开发者提供这样一个布局。一般也是学习自定义ViewGroup的入门级项目。
效果如下
5.两个必须
在自定义ViewGroup中必须实现onMeasure和onLayout方法
在自定义View中实现onMeasure和onDraw方法
二.onMeasure
1.onMeasure思想
总体来说,测量的过程是一个递归算法。绝大部分是先度量孩子再度量自己(ViewPager除外)
父ViewGroup在触发onMeasure方法的时候会调用子View的onMeasure方法,并且传给他两个参数,也就是我们经常用到的widthMeasureSpec和heightMeasureSpec参数。子View测量完了之后会进行一个保存。
那按理说onMeasure函数应该有返回值呀,应该return个数给父控件呀,但是为什么返回值是void呢?
这是因为父控件可以通过childView.getMeasuredWidth与childView.getMeasuredHeight获得子控件保存的值。
那父亲给的两个参数究竟是什么?
那首先就要知道MeasureSpec是个什么东西:
2.MeasureSpec
它是View里面的一个静态类,基本都是二进制运算。由于int是32位的,所以MeasureSpec用高两位表示mode,低30位表示size。MODE_SHIFT=30的作用是移位。
其中三种mode分别为:
UNSPECIFIED:不对View大小做限制,系统使用
EXACTLY:确切的大小,如100dp
AT_MOST:大小不可超过某个值,如MATCH_PARENT
3.如何把LayoutParams转化为MeasureSpec呢?
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
先度量孩子再度量自己:绝大部分
4.1度量孩子的步骤:
①先获取每个孩子
②通过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)
}
}
4.2度量父亲的步骤:
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是其他的,那就要管子控件了。
三.onLayout
1.安卓的两种坐标系
在介绍布局之前,要先掌握Android的两种坐标系
一种是Android屏幕坐标系,一种是视图坐标系。他们的使用规则如下:
视图坐标系以父view的左上角为原点,我们自定义ViewGroup进行布局的时候,使用的就是视图坐标系。
2.getMeasuredWidth和getWidth有什么区别
①getMeasuredWidth在measure()过程结束后就可以获取到对应的值,这个值是通过setMeasuredDimension()方法来进行设置的。
②而getWidth是在layout()过程结束后才能获取到的,这个值是通过视图右边的坐标减去左边的坐标计算出来的。
简而言之,就是如果生命周期在onMeasure和onLayout之间就用前者,如果生命周期在onLayout和onDraw之间就用后者
3.补充的知识点:
margin和padding
我找到了一篇很棒的关于margin和padding的博客
4.流式布局效果演示
5.流式布局代码
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]
}
}
}
隔了四个月之后又写了一个,感觉这个更标准一些,上面的可以不看
有的更改的地方,其实影响不大,重要的是总体思路要正确
package com.example.a202133;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
public class FlowLayout extends ViewGroup {
int mHorizontalSpacing = 80;//每个View之间的间隔
int mVerticalSpacing = 10;//行与行之间的间隔
private List<List<View>> allLines = new ArrayList<>();//记录所有的行,一行一行的存储,用于layout
List<Integer> lineHeights = new ArrayList<>();//记录每一行的行高,用于layout
private void clearMeasureParams(){
allLines.clear();
lineHeights.clear();
}
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//度量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//一定要在onMeasure中进行清0,因为onMeasure可能调用多次,应该在每次调用Measure的时候清空
//initMeasureParams();
//但是可能会出现内存抖动问题!因为频繁的初始化会产生大量的内存碎片
//所以应该是情空,而不是重新初始化
clearMeasureParams();
//先度量孩子
int childCount = getChildCount();
//获取父亲的“养老地方”
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int selfWidth = MeasureSpec.getSize(widthMeasureSpec);//从具有mode和size的measurespec中解析出ViewGroup的宽度
int selfHeight = MeasureSpec.getSize(heightMeasureSpec);
int parentNeededWidth = 0;//measure过程中给,子View要求的父ViewGroup的宽
int parentNeededHeight = 0;
List<View> lineViews = new ArrayList<>();//保存一行中所有的View
int lineWidthUsed = 0;//记录这行已经使用了多宽的size
int lineHeight = 0;//一行的行高
for(int i = 0;i < childCount;i++){
View childView = getChildAt(i);
LayoutParams childLP = childView.getLayoutParams();
if(childView.getVisibility() != View.GONE){
//将LayoutParams转变成为measureSpec
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight,childLP.width);
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom,childLP.height);
childView.measure(childWidthMeasureSpec,childHeightMeasureSpec);
//获取子View的度量宽高,这个值是上面measure之后得到的
int childMeasuredWidth = childView.getMeasuredWidth();
int childMeasuredHeight = childView.getMeasuredHeight();
//判断是否需要换行
if(childMeasuredWidth + lineWidthUsed + mHorizontalSpacing +paddingLeft + paddingRight> selfWidth){
//一旦换行,我们就可以判断当前行需要的宽和高了,所以此时需要记录下来
allLines.add(lineViews);
lineHeights.add(lineHeight);
//这里留有疑问,值得推敲一下
parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed + paddingLeft + paddingRight);
//换行了,需要清0
lineViews = new ArrayList<>();
lineWidthUsed = 0;
lineHeight = 0;
}
//View是分行Layout的,所以要记录每一行有哪些View,这样可以方便layout布局
lineViews.add(childView);
//设置行宽和行高
lineWidthUsed = lineWidthUsed + childMeasuredWidth + mHorizontalSpacing;//这个10是一行中每个View之间的间距
lineHeight = Math.max(lineHeight,childMeasuredHeight);
//处理最后一行的数据
if(i == childCount - 1){
allLines.add(lineViews);
lineHeights.add(lineHeight);
parentNeededHeight = parentNeededHeight + lineHeight + paddingBottom + paddingTop;//别忘了把上间距和下间距算上
parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed + paddingLeft + paddingRight);
}
}
}
//根据子View的度量结果,来重新度量自己ViewGroup
//作为一个ViewGroup,它自己也是一个View,它的大小也需要根据它的父亲给他提供的宽高来度量
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int realWidth = (widthMode == MeasureSpec.EXACTLY) ? selfWidth : parentNeededWidth;
int realHeight = (heightMode == MeasureSpec.EXACTLY) ? selfHeight : parentNeededHeight;
/**
* 别忘了这里selfWidth和selfHeight是前面MeasureSpec.getSize得到的。是经过MeasureSpec算法之后得到的大小
*/
//再度量自己,保存
setMeasuredDimension(realWidth,realHeight);
/**
* 这个parent其实是ViewGroup自己,之所以是parent其实是相对于子View来说的。
*/
}
//布局
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int lineCount = allLines.size();
int curL = getPaddingLeft();
int curT = getPaddingTop();
for(int i = 0;i < lineCount;i++){
List<View> lineViews = allLines.get(i);
int lineHeight = lineHeights.get(i);
for(int j = 0;j < lineViews.size();j++){
View view = lineViews.get(j);
int left = curL;
int top = curT;
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
view.layout(left,top,right,bottom);
curL = right + mHorizontalSpacing;
}
curT = curT + lineHeight + mVerticalSpacing;
curL = getPaddingLeft();
}
}
}
效果图如下
为什么下面间距这么大呢?因为这里
我设置的是80