自定义View-基础

自定义绘制

  • 自定义绘制的方式是重写绘制方法,其中最常用的是 onDraw()
  • 绘制的关键是 Canvas 的使用
    Canvas 的绘制类方法: drawXXX() (关键参数:Paint)
    Canvas 的辅助类方法:范围裁切和几何变换
  • 可以使用不同的绘制方法来控制遮盖关系
Canvas.drawXXX() 和 Paint 基础
  • Canvas 类下的所有 draw- 打头的方法,例如 drawCircle() drawBitmap()。
  • Paint 类的几个最常用的方法。
    paint提供基本信息之外的所有风格信息,例如颜色、线条粗细、阴影等。
  1. Paint.setColor(int color) 设置颜色
  2. Paint.setStyle(Paint.Style style)设置绘制模式
    FILL 是填充模式,STROKE 是画线模式(即勾边模式),FILL_AND_STROKE 是两种模式一并使用:既画线又填充。它的默认值是 FILL,填充模式。
  3. Paint.setStrokeWidth(float width) 设置线条宽度
    在 STROKE 和 FILL_AND_STROKE 下来设置线条的宽度
  4. Paint.setAntiAlias(boolean aa) 设置抗锯齿开关
    要在 new Paint() 的时候加上一个 ANTI_ALIAS_FLAG 参数就行
  5. Paint.setTextSize(float textSize) 设置文字大小

Canvas.drawColor(@ColorInt int color) 颜色填充
drawCircle(float centerX, float centerY, float radius, Paint paint) 画圆
drawRect(float left, float top, float right, float bottom, Paint paint) 画矩形
drawPoint(float x, float y, Paint paint) 画点
drawPoints(float[] pts, int offset, int count, Paint paint) / drawPoints(float[] pts, Paint paint) 画点(批量)
drawOval(float left, float top, float right, float bottom, Paint paint) 画椭圆
drawLine(float startX, float startY, float stopX, float stopY, Paint paint) 画线
drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint) 画圆角矩形
drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint) 绘制弧形或扇形
drawPath(Path path, Paint paint) 画自定义图形
drawBitmap(Bitmap bitmap, float left, float top, Paint paint) 画 Bitmap
drawText(String text, float x, float y, Paint paint) 绘制文字

Path 方法
  • Path 方法第一类:直接描述路径。
    第一组: addXxx() ——添加子图形
    如:addCircle(float x, float y, float radius, Direction dir) 添加圆
    第二组:xxxTo() ——画线(直线或曲线)
  1. lineTo(float x, float y) / rLineTo(float x, float y) 画直线
  2. quadTo(float x1, float y1, float x2, float y2) / rQuadTo(float dx1, float dy1, float dx2, float dy2) 画二次贝塞尔曲线
  3. cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) / rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) 画三次贝塞尔曲线
  4. moveTo(float x, float y) / rMoveTo(float x, float y) 移动到目标位置
  5. arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) / arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo) / arcTo(RectF oval, float startAngle, float sweepAngle) 画弧形
  6. addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle) / addArc(RectF oval, float startAngle, float sweepAngle)
    addArc() 只是一个直接使用了 forceMoveTo = true 的简化版 arcTo()
  7. close() 封闭当前子图形
  • Path 方法第二类:辅助的设置或计算
    Path.setFillType(Path.FillType ft) 设置填充方式

Paint

1.颜色
  • 基本颜色
    Canvas 的颜色填充类方法 drawColor/RGB/ARGB() 的颜色,是直接写在方法的参数里,通过参数来设置的; drawBitmap() 的颜色,是直接由 Bitmap 对象来提供的;
    除此之外,是图形和文字的绘制,它们的颜色就需要使用 paint 参数来额外设置了。

Paint 设置颜色的方法有两种:

  1. 直接设置颜色
    paint.setColor(Color.parseColor("#009688"));
    setARGB(int a, int r, int g, int b)
    setShader(Shader shader) 设置 Shader
    1.1 LinearGradient 线性渐变
    LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, Shader.TileMode tile)
    TileMode 一共有 3 个值可选:CLAMP 会在端点之外延续端点处的颜色;MIRROR 是镜像模式;REPEAT 是重复模式。
    1.2 RadialGradient 辐射渐变
    RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, TileMode tileMode)
    1.3 SweepGradient 扫描渐变
    SweepGradient(float cx, float cy, int color0, int color1)
    1.4 BitmapShader
    BitmapShader(Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)
    1.5 ComposeShader 混合着色器
    所谓混合,就是把两个 Shader 一起使用。
    ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)
    mode: 两个 Shader 的叠加模式
    PorterDuff.Mode :
    PorterDuff.Mode.SRC_OVER 把源图像直接铺在目标图像上
    PorterDuff.Mode.DST_OUT 挖空效果
    PorterDuff.Mode.DST_IN 蒙版抠图

  2. setColorFilter(ColorFilter colorFilter) 基于原始颜色的过滤
    ColorFilter 并不直接使用,而是使用它的子类。它共有三个子类:
    2.1 LightingColorFilter(int mul, int add)
    模拟简单的光照效果的
    2.2 PorterDuffColorFilter(int color, PorterDuff.Mode mode)
    使用一个指定的颜色和一种指定的 PorterDuff.Mode 来与绘制对象进行合成
    2.3 ColorMatrixColorFilter

  3. setXfermode(Xfermode xfermode)
    设置绘制内容和 View 中已有内容的混合计算方式
    直接用 Xfermode 的一个子类PorterDuffXfermode
    要想使用 setXfermode() 正常绘制,必须使用Canvas.saveLayer()离屏缓存 (Off-screen Buffer) 把内容绘制在额外的层上,再把绘制好的内容贴回 View 中


    image.png
2.效果

抗锯齿、填充/轮廓、线条宽度等等

  1. setAntiAlias (boolean aa) 设置抗锯齿
    或者Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
  2. setStyle(Paint.Style style)
    用来设置图形是线条风格还是填充风格的(也可以二者并用)
    Paint.Style.FILL填充
    Paint.Style.STROKE画线
    Paint.Style.FILL_AND_STROKE填充 + 画线
  3. 线条形状
    3.1 setStrokeWidth(float width)
    设置线条宽度。单位为像素,默认值是 0。
    3.2 setStrokeCap(Paint.Cap cap)
    设置线头的形状。线头形状有三种:BUTT 平头、ROUND 圆头、SQUARE 方头。默认为 BUTT
    3.3 setStrokeJoin(Paint.Join join)
    设置拐角的形状。有三个值可以选择:MITER 尖角、 BEVEL 平角和 ROUND 圆角。默认为 MITER。
    3.4 setStrokeMiter(float miter)
    线条在 Join 类型为 MITER 时对于 MITER 的长度限制
    miter 参数是对于转角长度的限制
  4. 色彩优化
    4.1 setDither(boolean dither)
    设置抖动来优化色彩深度降低时的绘制效果
    抖动更多的作用是在图像降低色彩深度绘制时,避免出现大片的色带与色块
    4.2 setFilterBitmap(boolean filter)
    设置是否使用双线性过滤来绘制 Bitmap
    设置双线性过滤来优化 Bitmap 放大绘制的效果
  5. setPathEffect(PathEffect effect)
    使用 PathEffect 来给图形的轮廓设置效果。对 Canvas 所有的图形绘制有效
    5.1.CornerPathEffect(float radius)
    把所有拐角变成圆角。.
    5.2.DiscretePathEffect(float segmentLength, float deviation
    把线条进行随机的偏离,让轮廓变得乱七八糟
    5.3.DashPathEffect(float[] intervals, float phase)
    使用虚线来绘制线条。
    第一个参数 intervals 是一个数组,它指定了虚线的格式:数组中元素必须为偶数(最少是 2 个),按照「画线长度、空白长度、画线
    长度、空白长度」……的顺序排列;第二个参数 phase 是虚线的偏移量。
    5.4.PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style)
    它是使用一个 Path 来绘制「虚线」
    shape 参数是用来绘制的 Path ; advance 是两个相邻的 shape 段之间的间隔,不过注意,这个间隔是两个 shape 段的起点的间隔,而不是前一个的终点和后一个的起点的距离; phase 和 DashPathEffect 中一样,是虚线的偏移;最后一个参数 style,是用来指定拐弯改变的时候 shape 的转换方式。
    style 的类型为 PathDashPathEffect.Style ,是一个 enum ,具体有三个值:TRANSLATE:位移,ROTATE:旋转,MORPH:变体
    5.5.SumPathEffect
    分别按照两种 PathEffect 分别对目标进行绘制
    5.6.ComposePathEffect(PathEffect outerpe, PathEffect innerpe)
    它是先对目标 Path 使用一个 PathEffect,然后再对这个改变后的 Path 使用另一个 PathEffect
  6. setShadowLayer(float radius, float dx, float dy, int shadowColor)
    在之后的绘制内容下面加一层阴影
    radius 是阴影的模糊范围; dx dy 是阴影的偏移量; shadowColor 是阴影的颜色
  7. setMaskFilter(MaskFilter maskfilter)
    设置的是在绘制层上方的附加效果,是基于整个画面来进行过滤
    7.1 BlurMaskFilter(float radius, BlurMaskFilter.Blur style)
    模糊效果的 MaskFilter
    radius 参数是模糊的范围, style 是模糊的类型:
    NORMAL: 内外都模糊绘制;SOLID: 内部正常绘制,外部模糊;INNER: 内部模糊,外部不绘制
    7.2 EmbossMaskFilter(float[] direction, float ambient, float specular, float blurRadius)
    浮雕效果的 MaskFilter
    direction 是一个 3 个元素的数组,指定了光源的方向; ambient 是环境光的强度,数值范围是 0 到 1; specular 是炫光的系数; blurRadius 是应用光线的范围
  8. 获取绘制的 Path
    8.1 getFillPath(Path src, Path dst)
    获取这个实际 Path
    src 是原 Path ,而 dst 就是实际 Path 的保存位置
    8.2 getTextPath(String text, int start, int end, float x, float y, Path path) / getTextPath(char[] text, int index, int count, float x, float y, Path path)
3.drawText() 相关
4.初始化类

是用来初始化 Paint 对象,或者是批量设置 Paint 的多个属性的方法
1.reset()
重置 Paint 的所有属性为默认值
2.set(Paint src)
把 src 的所有属性全部复制过来。相当于调用 src 所有的 get 方法,然后调用这个 Paint 对应的 set 方法来设置它们
3.setFlags(int flags)
批量设置 flags
如:paint.setFlags(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);

文字绘制

1 Canvas 绘制文字的方式

1.1 drawText(String text, float x, float y, Paint paint)
text 是文字内容,x 和 y 是文字的坐标。但需要注意:这个坐标并不是文字的左上角,而是一个与左下角比较接近的位置
drawText() 参数中的 y ,指的是文字的基线( baseline )的位置
1.2 drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint)
沿着一条 Path 来绘制文字
hOffset 和 vOffset。它们是文字相对于 Path 的水平偏移量和竖直偏移量,利用它们可以调整文字的位置。
1.3 StaticLayout
StaticLayout 多行文字的绘制,支持换行,它既可以为文字设置宽度上限来让文字自动换行,也会在 \n 处主动换行
StaticLayout(CharSequence source, TextPaint paint, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad)
width 是文字区域的宽度,文字到达这个宽度后就会自动换行;align 是文字的对齐方向;spacingmult 是行间距的倍数,通常情况下填 1 就好;spacingadd 是行间距的额外增加值,通常情况下填 0 就好;includeadd 是指是否在文字上下添加额外的空间,来避免某些过高的字符的绘制出现越界。

2 Paint 对文字绘制的辅助
  • 设置显示效果类
    2.1 setTextSize(float textSize)
    设置文字大小
    2.2 setTypeface(Typeface typeface)
    设置字体
    2.3 setFakeBoldText(boolean fakeBoldText)
    是否使用伪粗体
    2.4 setStrikeThruText(boolean strikeThruText)
    是否加删除线
    2.5 setUnderlineText(boolean underlineText)
    是否加下划线
    2.6 setTextSkewX(float skewX)
    设置文字横向错切角度。其实就是文字倾斜度
    2.7 setTextScaleX(float scaleX)
    设置文字横向放缩
    2.8 setLetterSpacing(float letterSpacing)
    设置字符间距。默认值是 0、
    2.9 setFontFeatureSettings(String settings)
    用 CSS 的 font-feature-settings 的方式来设置文字
    2.10 setTextAlign(Paint.Align align)
    设置文字的对齐方式。一共有三个值:LEFT CENTER 和 RIGHT,默认值为 LEFT
    2.11 setTextLocale(Locale locale)
    设置绘制所使用的 Locale
  • 测量文字尺寸类
    1 getFontSpacing()
    获取推荐的行距,即推荐的两行文字的 baseline 的距离
    2 FontMetircs getFontMetrics()
    获取 Paint 的 FontMetrics
    FontMetrics 是个相对专业的工具类,它提供了几个文字排印方面的数值:ascent, descent, top, bottom, leading。
    FontMetrics.ascent:float 类型 baseline - ascent 负值
    FontMetrics.descent:float 类型, descent - baseline 正值
    FontMetrics.top:float 类型, baseline - top 负值
    FontMetrics.bottom:float 类型,bottom - baseline 正值
    FontMetrics.leading:float 类型, 上行bottom - 下行top
    3 getTextBounds(String text, int start, int end, Rect bounds)
    获取文字的显示范围。
    text 是要测量的文字,start 和 end 分别是文字的起始和结束位置,bounds 是存储文字显示范围的对象,方法在测算完成之后会把结果写进 bounds。
paint.setStyle(Paint.Style.FILL);
canvas.drawText(text, offsetX, offsetY, paint);

paint.getTextBounds(text, 0, text.length(), bounds);
bounds.left += offsetX;
bounds.top += offsetY;
bounds.right += offsetX;
bounds.bottom += offsetY;
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(bounds, paint);

4 float measureText(String text)
测量的是文字绘制时所占用的宽度
5 getTextWidths(String text, float[] widths)
获取字符串中每个字符的宽度,并把结果填入参数 widths
6 int breakText(String text, boolean measureForwards, float maxWidth, float[] measuredWidth)
测量文字宽度的,breakText() 是在给出宽度上限的前提下测量文字的宽度。如果文字的宽度超出了上限,那么在临近超限的位置截断文字。
breakText() 的返回值是截取的文字个数,参数中, text 是要测量的文字;measureForwards 表示文字的测量方向,true 表示由左往右测量;maxWidth 是给出的宽度上限;measuredWidth 是用于接受数据
这个方法可以用于多行文字的折行计算
7 光标相关
7.1 getRunAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset)
计算出某个字符处光标的 x 坐标
start end 是文字的起始和结束坐标;contextStart contextEnd 是上下文的起始和结束坐标;isRtl 是文字的方向;offset 是字数的偏移,即计算第几个字符处的光标。
7.2 getOffsetForAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, float advance)
给出一个位置的像素值,计算出文字中最接近这个位置的字符偏移量(即第几个字符最接近这个坐标)。
text 是要测量的文字;start end 是文字的起始和结束坐标;contextStart contextEnd 是上下文的起始和结束坐标;isRtl 是文字方向;advance 是给出的位置的像素值。填入参数,对应的字符偏移量将作为返回值返回。
8 hasGlyph(String string)
检查指定的字符串中是否是一个单独的字形 (glyph)

Canvas 对绘制的辅助

1 范围裁切

1.1 clipRect(left, top, right, bottom)
记得要加上 Canvas.save() 和 Canvas.restore() 来及时恢复绘制范围

canvas.save();
canvas.clipRect(left, top, right, bottom);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();

1.2 clipPath()

2 几何变换
2.1 使用 Canvas 来做常见的二维变换:

2.1.1 Canvas.translate(float dx, float dy) 平移
dx 和 dy 表示横向和纵向的位移。
2.1.2 Canvas.rotate(float degrees, float px, float py) 旋转
degrees 是旋转角度,方向是顺时针为正向; px 和 py 是轴心的位置
2.1.3 Canvas.scale(float sx, float sy, float px, float py) 缩放
sx sy 是横向和纵向的放缩倍数; px py 是放缩的轴心
2.1.4 skew(float sx, float sy) 错切
sx 和 sy 是 x 方向和 y 方向的错切系数

2.2 使用 Matrix 来做变换

2.2.1 使用 Matrix 来做常见变换
Matrix 做常见变换的方式:

  1. 创建 Matrix 对象;
  2. 调用 Matrix 的 pre/postTranslate/Rotate/Scale/Skew() 方法来设置几何变换;
  3. 使用 Canvas.setMatrix(matrix) 或 Canvas.concat(matrix) 来把几何变换应用到 Canvas。
Matrix matrix = new Matrix();
...
matrix.reset();
matrix.postTranslate();
matrix.postRotate();

canvas.save();
canvas.concat(matrix);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();

注:把 Matrix 应用到 Canvas, 尽量用 concat(matrix)
2.2.2 使用 Matrix 来做自定义变换
Matrix.setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount) 用点对点映射的方式设置变换
src 和 dst 是源点集和目标点集;srcIndex 和 dstIndex 是第一个点的偏移;pointCount 是采集的点的个数(个数不能大于 4,因为大于 4 个点就无法计算变换了)

2.3 使用 Camera 来做三维变换

2.3.1 Camera.rotate() 三维旋转
Camera.rotate
() 一共有四个方法: rotateX(deg) rotateY(deg) rotateZ(deg) rotate(x, y, z)。
Camera 和 Canvas 一样也需要保存和恢复状态才能正常绘制

canvas.save();

camera.save(); // 保存 Camera 的状态
camera.rotateX(30); // 旋转 Camera 的三维空间
camera.applyToCanvas(canvas); // 把旋转投影到 Canvas
camera.restore(); // 恢复 Camera 的状态

canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();

如果你需要图形左右对称,需要配合上 Canvas.translate(),在三维旋转之前把绘制内容的中心点移动到原点,即旋转的轴心,然后在三维旋转后再把投影移动回来:

canvas.save();

camera.save(); // 保存 Camera 的状态
camera.rotateX(30); // 旋转 Camera 的三维空间
canvas.translate(centerX, centerY); // 旋转之后把投影移动回来
camera.applyToCanvas(canvas); // 把旋转投影到 Canvas
canvas.translate(-centerX, -centerY); // 旋转之前把绘制内容移动到轴心(原点)
camera.restore(); // 恢复 Camera 的状态

canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();

注:Canvas 的几何变换顺序是反的,所以要把移动到中心的代码写在下面,把从中心移动回来的代码写在上面。
2.3.2 Camera.translate(float x, float y, float z) 移动
2.3.3 Camera.setLocation(x, y, z) 设置虚拟相机的位置
Camera 的位置单位是英寸,英寸和像素的换算单位在是 72 像素
在 Camera 中,相机的默认位置是 (0, 0, -8)(英寸)。8 x 72 = 576,所以它的默认位置是 (0, 0, -576)(像素)。
Camera.setLocation(x, y, z) 的 x 和 y 参数一般不会改变,直接填 0 就好

绘制顺序

1 super.onDraw() 前 or 后?

自定义绘制最基本的形态:继承 View 类,在 onDraw() 中完全自定义它的绘制
在 View.java 的源码中,onDraw() 是空实现,把绘制代码全都写在了 super.onDraw() 的下面,上面或者删除都一样
基于已有控件的自定义绘制,即继承一个具有某种功能的控件,就不能不考虑 super.onDraw() 了
1.1 写在 super.onDraw() 的下面
绘制内容就会盖住控件原来的内容
1.2 写在 super.onDraw() 的上面
绘制的内容会被控件的原内容盖住

2 dispatchDraw():绘制子 View 的方法

由于 View 是没有子 View 的,所以一般来说 dispatchDraw() 这个方法只对 ViewGroup(以及它的子类)有意义。


image.png

2.1 写在 super.dispatchDraw() 的下面
只要重写 dispatchDraw(),并在 super.dispatchDraw() 的下面写上你的绘制代码,这段绘制代码就会发生在子 View 的绘制之后,从而让绘制内容盖住子 View 了。
2.2 写在 super.dispatchDraw() 的上面
绘制内容会出现在主体内容和子 View 之间

3 绘制过程简述

一个完整的绘制过程会依次绘制以下几个内容:
1.背景
背景的绘制在drawBackground() 方法中,这个方法是private的,不能重写,只能通过xml 布局文件的 android:background 属性以及 Java 代码的 View.setBackgroundXxx() 方法
2.主体(onDraw())
3.子 View(dispatchDraw())
4.滑动边缘渐变和滑动条
5.前景
而第 4、5 两步——滑动边缘渐变和滑动条以及前景,这两部分被合在一起放在了 onDrawForeground() 方法里,这个方法是可以重写的
滑动边缘渐变和滑动条可以通过 xml 的 android:scrollbarXXX 系列属性或 Java 代码的 View.setXXXScrollbarXXX() 系列方法来设置;前景可以通过 xml 的 android:foreground 属性或 Java 代码的 View.setForeground() 方法来设置


image.png
4 onDrawForeground()

这个方法是 API 23 才引入
在 onDrawForeground() 中,会依次绘制滑动边缘渐变、滑动条和前景
4.1 写在 super.onDrawForeground() 的下面
绘制内容将会盖住滑动边缘渐变、滑动条和前景。
4.2 写在 super.onDrawForeground() 的上面
绘制内容会盖住子 View,但会被滑动边缘渐变、滑动条以及前景盖住:
4.3 想在滑动边缘渐变、滑动条和前景之间插入绘制代码?
不行。

5 draw() 总调度方法

一个 View 的整个绘制过程都发生在 draw() 方法里

// View.java 的 draw() 方法的简化版大致结构(是大致结构,不是源码哦):
public void draw(Canvas canvas) {
    ...
    drawBackground(Canvas); // 绘制背景(不能重写)
    onDraw(Canvas); // 绘制主体
    dispatchDraw(Canvas); // 绘制子 View
    onDrawForeground(Canvas); // 绘制滑动相关和前景
    ...
}
image.png

5.1 写在 super.draw() 的下面
绘制内容会盖住其他的所有绘制内容
5.2 写在 super.draw() 的上面
这部分绘制内容会被其他所有的内容盖住,包括背景。


image.png

注:在 ViewGroup 的子类中重写除 dispatchDraw() 以外的绘制方法时,可能需要调用 setWillNotDraw(false);在重写的方法有多个选择时,优先选择 onDraw()

硬件加速

指的是把某些计算工作交给专门的硬件来做,而不是和普通的计算工作一样交给CPU处理。
在 Android 里,硬件加速专指把 View 中绘制的计算工作交给 GPU 来处理;
在硬件加速关闭时,绘制内容被CPU转换成实际的像素,然后直接渲染到屏幕;
硬件加速的原因:
1.用了GPU,绘制变快了
2.绘制机制的改变,导致界面内容改变时的刷新效率极大提高

布局

布局过程:
就是程序在运行时利用布局文件的代码来计算出实际尺寸的过程
布局过程的工作内容:测量阶段和布局阶段

  1. 测量阶段:从上到下递归地调用每个 View 或者 ViewGroup 的 measure() 方法,测量他们的尺寸并计算它们的位置;
  2. 布局阶段:从上到下递归地调用每个 View 或者 ViewGroup 的 layout() 方法,把测得的它们的尺寸和位置赋值给它们。
View 或 ViewGroup 的布局过程

测量阶段:

  1. View: View在onMeasure()中会计算出自己的尺寸然后保存;
  2. ViewGroup: ViewGroup在onMeasure()中会调用所有子View的measure()让它们进行自我测量,并根据子View计算出的期望尺寸来计算它们的实际尺寸和位置然后保存,同时,它会根据子View的尺寸和位置来计算出自己的尺寸然后保存;

布局阶段:

  1. View: 由于没有子View,所以View的onLayout()什么也不做
  2. ViewGroup:ViewGroup在onLayout()中会调用自己的子View的layout()方法,把它们的尺寸和位置传给它们,让它们完成自我的内部布局;

布局过程自定义的方式

一. 重写onMeasure()来修改已有View的尺寸;
  1. 重写onMeasure()方法,并在里面调用super.onMeasure(), 触发原有的自我测量;
  2. 在super.onMeasure()的下面用getMeasureWidth()和getMeasuredHeight()取出之前计算出的结果,然后把它们修改成新的尺寸,再保存下来;
  3. 调用setMeasuredDimension()来保存新的结果;
二. 重写onMeasure()来全新自定义View的尺寸;
  1. 重写onMeasure(),并计算出View的尺寸
  2. 使用resolveSize()来让子View的计算结果符合父View的限制;

onMeasure的两个参数就是父View传进来的限制,一个是宽度限制,一个是高度限制,在计算完宽度和高度之后,分别调用一次resolveSize()方法,把你计算出来的宽度和高度以及对应的父View的限制一起传进去,返回的结果就是符合父View限制的修正之后的尺寸;然后把这个修正之后的尺寸用setMeasuredDimension保存起来就行了。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    measuredWidth = ...;
    measuredHeight = ...;
    measuredWidth = resolveSize(measuredWidth, widthMeasuredSpec);
    measuredHeight = resolveSize(measuredHeight, heightMeasuredSpec);

    setMeasuredDimension(measuredWidth, measuredHeight);
}

public static int resolveSize(int size, int measureSpec) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    switch(specMode){
        case MeasureSpec.UNSPECIFIE:
              return size;
        case MeasureSpec.AT_MOST:
              if(size<=specSize){
                  return size;
              }else{
                  return specSize;
              }
        case MeasureSpec.EXACTLY:
              return specSize;
        default:
              return size;
    }
}

父View的尺寸限制:

  1. 由来:开发者的要求(布局文件中layout_打头的属性)经过父View处理计算后的更精确的要求;
  2. 限制的类型:
    UNSPECIFIED不限制,AT_MOST限制上限,EXACTLY限制固定值
三. 重写onMeasure()和onLayout()来全新定制自定义ViewGroup的内部布局;
1. 重写onMeasure()来计算内部布局

也就是子View的位置和尺寸,以及自己的尺寸

onMeasure()的重写:

  1. 调用每个子View的measure(), 让子View自我测量;
    计算子View尺寸的关键: 在于measure()方法的两个参数,也就是子View的两个MeasureSpec的计算;
    子View的MeasureSpec的计算方式:
    1.结合开发者的要求(xml中layout_打头的属性)和自己的可用空间(自己的尺寸上线 - 已用尺寸)
    2.尺寸上限根据自己的MeasureSpec中的mode而定,EXACTLY/AT_MOST:尺寸上限为MeasureSpec中的size,UNSPECIFIED:尺寸无上限;
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);

            LayoutParams lp = childView.getLayoutParams();
            int selfWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
            int selfWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
            switch (lp.width) {
                case MATCH_PARENT:
                    if (selfWidthSpecMode == EXACTLY || selfWidthSpecMode == AT_MOST) {
                        childWidthSpec = MeasureSpec.makeMeasureSpec(selfWidthSpecSize - usedWidth, EXACTLY);//可用宽度
                    } else {//UNSPECIFIED
                        childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
                    }
                    break;
                case WRAP_CONTENT:
                    if (selfWidthSpecMode == EXACTLY || selfWidthSpecMode == AT_MOST) {
                        childWidthSpec = MeasureSpec.makeMeasureSpec(selfWidthSpecSize - usedWidth, AT_MOST);//可用宽度
                    } else {//UNSPECIFIED
                        childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
                    }
                    break;
                default://固定尺寸值
                    childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, EXACTLY);
                    break;
            }
        }

    }

这两个条件:开发者的要求和自己的可用空间,开发者的要求在地位上是要绝对高于可用空间的,例如开发者写了layout_width="48dp",那么就不必管你的内部有没有足够的空间给子View用,直接限制子View的尺寸是48dp就好,也就是说mode是EXACTLY,而size是48dp所对应的像素值;
对于每个子View,计算它的MeasureSpec,也就是尺寸限制的时候,依次查看它们的layout_width和layout_height这两个属性,分别用它们结合自己当前的可用宽度和可用高度,来计算出子View的限制;
xml文件里面的layout_width和layout_height,在java代码里面会被转换成View里的两个属性,在父View里调用子View的getLayoutParams()方法可以获得一个LayoutParams对象,它包含了xml文件里的layout_打头的参数的对应值,其中它的widht和height这两个属性值就分别对应了layout_width和layout_height的值,而且是转换过了的值,它们在xml如果是wrap_content或者match_parent就会被分别转换成WRAP_CONTENT和MATCH_PARENT这两个常量,而如果它们在xml里面是具体的数值,是多少多少dp或者sp,那么width和height这两个属性里就是它们被转换后的具体的像素值;通过LayoutParams的width和height这两个属性就可以得到开发者对子View的尺寸要求;利用它们结合自己的可用空间来计算出对子View的宽度和高度的限制;

对子View的测量是发生在父View(也就是自己)的onMeasure()方法里面的,所以这个时候自己的尺寸是还没有确定的,你只能得到一个可用宽度或者高度,或者叫做可用空间,那么这个可用空间是怎么获取的呢?这个可用空间,是从自己的onMeasure()方法的两个参数,也就是自己的宽度和高度限制里面获得的,onMeasure()里的那两个MeasureSpec,虽然只是一份限制,不能直接决定自己的尺寸,但依据这份限制自己可以得到一个可用空间,我还没算出来自己多大,但是我可以知道我最多能有多大地方去给自己和子View用 ,这个最多是多少,要看MeasureSpec的mode,对于EXACTLY这种mode,你的可用宽度就是MeasureSpec里面的size;对于AT_MOST来说就是MeasureSpec的size,所以AT_MOST和EXACTLY在计算自己的可用空间的时候它们是完全一样的,都是把自己的MeasureSpec里面的size拿来当成自己的可用空间,它们只是在测量完子View再测量自己的时候有区别;UNSPECIFIED这个mode表示自己的父View,这个layout的父View对自己没有尺寸限制,也就是说自己的可用空间是无限的;

wrap_content,它除了让子View自己测量之外,其实还有一个隐藏的限制条件,那就是不能超过父View的边界,或者说要在父View的可用空间之内

  1. 根据子View给出的尺寸,得出子View的位置,并保存它们的位置和尺寸;
    99%的情况每一个View它所测量的尺寸就是它的最终尺寸,在稍后要用的时候去调用getMeasuredWidth()和getMeasuredHeight()就行,为什么要保存它们呢?因为现在是测量阶段,在接下来的布局阶段,在onLayout()方法里面这些位置和尺寸才会被传给子View,所以在这期间,需要把它们的值暂时保存,以备稍后使用;
    说明:1.不是所有的Layout都需要保存子View的位置(因为有的Layout可以在布局阶段实时推导出子View的位置,例如LinearLayout);2.有时候对某些子View需要重复测量两次或多次才能得到正确的尺寸和位置

  2. 根据子View的位置和尺寸计算出自己的尺寸,并用setMeasuredDimension()保存;
    根据子View的排布来就算出边界,这个边界就是你的尺寸了

2. 重写onLayout()来摆放子View

调用每个子View的layout()方法,把之前在onMeasure()里保存下来的它们的位置和尺寸作为参数传进去,让它们把自己的位置和尺寸保存下来,并进行自我布局

image.png

触摸反馈

  1. 重写onTouchEvent(), 在里面写上你的触摸反馈算法,并返回true(关键是ACTION_DOWN事件时返回true);
  2. 如果是会发生触摸冲突的ViewGroup,还需要重写onInterceptTouchEvent(),在事件流开始时返回false,并在确认接管事件流时返回一次true,以实现对事件的拦截;
  3. 当子View临时需要组织父View拦截事件流时,可以调用父View的requestDisallowInterceptTouchEvent(),通知父View在当前事件流中不再尝试通过onInterceptTouchEvent()来拦截;

扔物线
HenCoder Plus

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容

  • 前言: 在接触Android这么长时间,看到很多大牛都在和大家分享自己的知识,深有体会,刚好前段时间写了一个Dem...
    杨艳伟阅读 1,251评论 0 5
  • 系列文章之 Android中自定义View(一)系列文章之 Android中自定义View(二)系列文章之 And...
    YoungerDev阅读 4,379评论 3 11
  • 系列文章之 Android中自定义View(一)系列文章之 Android中自定义View(二)系列文章之 And...
    YoungerDev阅读 2,159评论 0 4
  • 【Android 自定义View之绘图】 基础图形的绘制 一、Paint与Canvas 绘图需要两个工具,笔和纸。...
    Rtia阅读 11,643评论 5 34
  • 告别一座城市 就如同风告别了雨 来势汹汹却又苍白无力 这里不是终点 我要用这雨滴与你告别 在下一个昼夜交替 要么洗...
    蜗汼阅读 293评论 1 2