自定义 View、ViewGroup

自定义View

自定义一个view步骤:

a.创建一个类继承于 View

b.重写构造函数 (次构造函数)
  class CustomView(context:Context,attrs:AttributeSet?):View(context,attrs){}

c.自定义属性:什么是需要外部配置的
要先判断需不需要自定义属性?
需不需要在 xml 中配置这个 View
  ①在 values 里创建 attr 的 xml 文件
  ②Xml中使用自定义的属性
  ③在自定义的View中,解析xml中自定义的属性(init{}中)
  并把这些属性的值设置给对应的控件 //使用

d.onMeasure()测量视图的尺寸
  ①获取外部设置的尺寸的 mode sizeMeasureSpec getMode getSize
  ②判断mode
     EXACTLY使用外部的值
     AT MOST UNSPECIFIED自己确定自己的尺寸
  ③将尺寸传递给外部viewGroup
     setMeasureDimession(width,height)

e.onDraw()绘制
  画笔 Paint
     color 画笔颜色
     strokewidth 画笔的粗细
     style Paint.style.FILL 填满 STROKE描边 FILL AND_STROKE
     isAntialias:抗锯齿
  canvas

绘制:
①画 v .纸 笔 颜料
画笔:Paint (画文本用TextPaint)
画板:Canvas 扮演的是人的角色 画东西
Bitmap 才是真正的画板 画布

②画 n. 画的东西(一条线段、圆形、矩形、图片、文字)
路径:Path

(一)onMeasure()确定尺寸

onMeasure方法会在View需要进行测量时被调用,开发者可以在这个方法中根据测量模式和大小来计算View的实际大小,并调用 setMeasuredDimension() 方法设置View的测量大小。

View的测量 —> 决定控件的大小
onMeasure()会被调用多次,进行多次测量

MeasureSpec(测量规格):是32位的整数(2+30位)测量参数
2:外部指定测量方式 MeasureSpec.getMode //得到测量模式

Mode(三种):用来做限定的,外部给当前这个View设置的一些限制
①Unspecified [未知名的] :无限制(几乎不用)
②Exactly [精确的] :确定了的尺寸,就不需要计算,直接使用外部尺寸
③AtMost [最多不超过] :最多多少尺寸 子控件不能超过这个尺寸 ——> 具体多大由自己定

例子:
Layout_width:
200dp —> Exactly
wrap_content —> AtMost
match_parent —> Exactly
match_constraint —> Exactly 匹配约束
->所以如果是 wrap_content 或者模式是 AtMost ,需要自己测量自己的尺寸 宽 高
->其他如果是Exactly,不需要测量自身尺寸

30.具体的值 MeasureSpec.getSize //具体给多大的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
var mWidth = 0
var mHeight = 0

//确定高度
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
mWidth = when(widthMode){
MeasureSpec.EXACTLY -> widthSize
else -> 6*mRadius + 4*mSpace
}
//确定宽度
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
mHeight = when(heightMode){
MeasureSpec.EXACTLY -> heightSize
else -> 6*mRadius + 4*mSpace
}
//告诉外部(父容器)自己的尺寸
setMeasuredDimension(mWidth,mHeight)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//可以将getMode和getSize给View写个扩展方法方便使用
//获取测量的模式和尺寸
fun View.getMode(measureSpec:Int) = MeasureSpec.getMode(measureSpec)
fun View.getSize(measureSpec:Int) = MeasureSpec.getSize(measureSpec)


//提供默认宽高
private val mDefaultWidth = context.dp2px(200)
private val mDefaultHeight = context.dp2px(60)

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
var mWidth = 0
var mHeight = 0


//计算宽度
mWidth = When(getMode(widthMeasureSpec)){
MeasureSpec.EXACTLY -> getSize(widthMeasureSpec)
else -> mDefaultWidth.toInt()
}

//计算高度
mHeight = when(getSize(heightMeasureSpec)){
MeasureSpec.EXACTLY -> getSize(widthMeasureSpec)
else -> mDefaultHeight.toInt()
}

//告诉外部(父容器)自己的尺寸
setMeasuredDimension(mWidth,mHeight)
}

(二)onSizeChanged()

在 onSizeChanged() 方法被调用时, View 的尺寸已经确定了
此方法会在 View 的尺寸发生变化时被调用,可以在这个方法中获取到 View 的新尺寸并进行相应的处理。
因此,可以在 onSizeChanged() 方法中进行一些与 View 尺寸相关的操作,例如重新计算绘制的内容或者重新布局子 View 等

1
2
3
4
5
6
7
8
9
10
11
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int){
super.onSizeChanged(w, h, oldw, oldh)
//如果外部规定了宽高,就需要从新计算音浪的尺寸
if(height != mWaveHeight){
mWaveHeight = height
}
if(width != (2*mSpace + 3*mWaveWidth)){
mSpace = (width * 0.1).toInt()
mWaveWidth = (width - 2*mSpace)/3
}
}

(三)onDraw()绘制

onDraw方法会在View需要进行绘制时被调用,开发者可以在这个方法中使用Canvas和Paint来绘制View的内容

一个视图通过onMeasure计算自己的尺寸,计算完之后,通过onDraw绘制将其显示出来

Canvas 提供了一系列绘制的方法 画线 画圆 画弧线 画文字
Bitmap 所有绘制的东西都是绘制到这个bitmap位图上的

invalidate() 重新绘制,重新调用onDraw()方法

(1)画笔 Paint

颜色、粗细、样式、抗锯齿

  1. color = Color.MAGENTA //画笔的颜色
  • color = Color.rgb(172,51,241) //要转化成对应的十进制值
  • color = Color.argb( ) //有透明度
  • color = Color.parseColor(“#00BAAD”)
  • color = resource.getColor(R.color.dot_bg_color,null) //引用资源颜色
  1. strokeWidth = dp2px (20) //画笔的粗细宽度 要的是像素
  2. isAntiAlias = true //抗锯齿
  3. style = Paint.Style.STROKE //画笔样式
  • stroke 描边
  • fill 有颜色 有填充
  • strokeandfill 都有
  1. strokeCap =Paint.Cap.ROUND //变圆润 //两端的切面效果 //画弧线的笔
    (线条的端点样式)
  • BUTT 截面
  • ROUND
  • SQUARE 方形(根BUTT一样)
  1. strokeJoin = Paint.Join.ROUND //拐角连接处连接处
    (线条的连接样式)
  • ROUND 圆角
  • BUTT 横切
  1. setDither 防抖动
1
2
3
4
5
private val mPaint:TextPaint by lazy{
TextPaint().apply{
color = Color.GRAY
}
}

(2)画板 Canvas(绘制工具) ——> Bitmap

  1. canvas?.drawCircle(cx[中心点x],cy[中心点y],radius[半径],Paint):画圆
  2. canvas?.drawOval(left,top,right,bottom,paint) 画椭圆
  3. canvas?.drawArc(left,top,right,bottom,startAngle[从哪开始画],sweepAngle[弧线扫过的角度],useCenter[是否连接中心点],paint) 画弧
  • -90开始 顺时针转动
  1. canvas?.drawText(text[要画的文本],x,y,paint) 画文本
  • mPaint.measureText(String text) —> 返回文本框的宽度
  • mPaint.fontMetrics —> Returns the font metrics value for the given text
  1. canvas?.drawRoundRect(left,top,right,bottom,rx[圆角x半径],ry[圆角y半径])//圆角矩形(圆角半径不一定都相同所以要分别填写rx,ry) 画圆角矩形
  2. canvas?.drawPath()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    private var mPath = Path()
    mPath.reset()//重置(之前有的路径全重置了 重新画)

    mPath.moveTo(x , y) //设置起点
    mPath.LineTo(x , y) //从上一个起点到当前点画一条线

    mPath.cubicTo(x1,y1,x2,y2,x3,y3) //画贝塞尔曲线
    mPath.close() //两端连起来形成封闭曲线
    canvas?.drawPath(mPath,mPaint)

    mPath.addArc() //画弧线
    mPaint.strokeCap =Paint.Cap.ROUND//变圆润

    path.offset()
    用于在路径的基础上进行偏移操作。
    偏移操作可以沿着路径的方向将路径的位置移动一定距离。这个方法通常需要传入一个偏移量作为参数,用来指定路径偏移的距离和方向。
    偏移后的路径将会保持原路径的形状和曲线,只是位置发生了改变。
    这个方法在绘制图形时可以用来实现路径的平移、旋转等效果。

  • moveTo(x: Float, y: Float):将路径移动到指定的坐标点(x, y)
  • lineTo(x: Float, y: Float):从当前位置画一条直线到指定的坐标点(x, y)
  • quadTo(x1: Float, y1: Float, x2: Float, y2: Float):从当前位置画一条二次贝塞尔曲线到指定的坐标点(x2, y2),控制点为(x1, y1)
  • cubicTo(x1: Float, y1: Float, x2: Float, y2: Float, x3: Float, y3: Float):从当前位置画一条三次贝塞尔曲线到指定的坐标点(x3, y3),控制点为(x1, y1)和(x2, y2)
  • addRect(left: Float, top: Float, right: Float, bottom: Float, dir: Path.Direction):添加一个矩形路径
  • addCircle(x: Float, y: Float, radius: Float, dir: Path.Direction):添加一个圆形路径
  • addRoundRect(left: Float, top: Float, right: Float, bottom: Float, radii: FloatArray, dir: Path.Direction):添加一个圆角矩形路径
  • close():闭合路径,将路径起点和终点连接起来
  1. canvas?.drawLine(sx,xy,ex,ey,paint) 画线
  2. canvas?.drawBitmap() 绘制位图(画图片)
    参数:
  • bitmap: Bitmap - 要绘制的位图对象
  • src: Rect? - 可选参数,指定要绘制的位图的源矩形区域,如果为null,则绘制整个位图
  • dst: Rect - 目标矩形区域,指定了位图在Canvas上的绘制位置和大小
  • paint: Paint? - 可选参数,用于指定绘制位图时的画笔样式,如颜色、透明度等属性
    1
    canvas?.drawBitmap(bitmap, srcRect, dstRect, paint)

drawBitmap 方法有多种重载形式,可以接受不同的参数。
以下是 Canvas 的 drawBitmap 方法的常见参数形式:

  1. drawBitmap(Bitmap bitmap, float left, float top, Paint paint): 在指定位置绘制位图
    其中 left 和 top 表示位图左上角的坐标,
    paint 参数是绘制位图时的画笔属性。

  2. drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint): 绘制位图的指定区域到目标区域
    src 表示位图中的要绘制的区域
    dst 表示目标区域的位置和大小
    paint 参数是绘制位图时的画笔属性

  3. drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint): 绘制位图的指定区域到目标区域
    src 表示位图中的要绘制的区域 null
    dst 表示目标区域的位置和大小 (使用 RectF ) 画图的区域
    paint 参数是绘制位图时的画笔属性

  4. drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint): 通过矩阵变换来绘制位图
    matrix 参数表示要应用的变换矩阵
    paint 参数是绘制位图时的画笔属性

(3)示例

①canvas?.drawCircle 画圆

canvas?.drawCircle(cx[中心点x],cy[中心点y],radius[半径],Paint):画圆

canvas?.drawCircle(cx[中心点x],cy[中心点y],radius[半径],Paint):画圆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//画笔
private val mCirclePaint: Paint by lazy{
Paint().apply{
Paintcolor = Color.MAGENTA //画笔的颜色
strokeWidth = dp2px( dp: 20)//画笔粗细
isAntiAlias = true //抗锯齿
}
}

overrde fun onDraw(canvas:Canvas?){
drawCircle(canvas)
}

fun drawCircle(canvas:Canvas?){
val cx = (width/2).toFloat()
val cy = (height/2).toFloat()
val radius = Math.min(width,height)/2 - dp2px(20)
canvas?.drawCircle(cx,cy,radius,mCirclePaint)
}

②canvas?.drawArc 画弧

canvas?.drawArc(left,top,right,bottom,startAngle[从哪开始画],sweepAngle[弧线扫过的角度],useCenter[是否连接中心点],paint)
//左上右下是先确定一个矩形,也就是确定了中心点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//画笔
private val mArcPaint: Paint by lazy{
Paint().apply{
color = Color.GREEN //画笔的颜色
strokeWidth = dp2px( dp: 20)//画笔粗细
isAntiAlias = true //抗锯齿
style = Paint.Style.STROKE
}
}

overrde fun onDraw(canvas:Canvas?){
drawCircle(canvas)
drawArc(canvas)
}

fun drawArc(canvas:Canvas?){
val cx = (width/2).toFloat()
val cy = (height/2).toFloat()
val radius = Math.min(width,height)/2 - dp2px(20)

//左 上 右 下 起始角度 扫过的角度 是否连接中心点 画笔
canvas?.drawArc(cx - radius,cy - radius,cx + radius,cy - radius,-90f,90f,false,mArcPaint)
}

③canvas?.drawText 画文本

绘制中最难的
自定义绘制文本可以在一个视图中控制文字显示不同的颜色
图示.png

canvas?.drawText(text[要画的文本],x,y,paint)

mPaint.fontMetrics: top bottom acent decent 相对于 baseline
acent top 负
bottom decent 正
文字高度: bottom -top
让文字显示在:

  • 左上角: x: 0 y:-top
  • 左下角: x: 0 y: view.height - bottom
  • 右上角: x: view.width - text.width y = -top
  • 右上角: x: view.width-text.width y = view.height - bottom
    正中心:
    x:(view.width-text.width)/2
    halfHeight: (bottom-top) 2
    baselineToCenter: halfHeight- bottom
    y = view.height/2 + baselineToCenter
        height/2 +(bottom-top)2 - bottom
        height/2 - top/2 - bottom/2
        (height - top - bottom)2
1
2
3
4
5
6
7
8
//居中公式
val mText = "$someString"
val sx = (width-mPaint.measureText(mText))/2 //减去文字宽度的一半
val metrics = mPaint.fontMetrics
val sy = (height- metrics.bottom - metrics.top)/2

mPaint.color = Color.GRAY
canvas?.drawText(mText,sx,sy.toFloat(),mPaint)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private var rateText = "0%"


//画笔
private val mRatePaint: TextPaint by lazy{
TextPaint().apply{
color = Color.MAGENTA
textSize = sp2px(18f)
typeface = Typeface.DEFAULT //系统字体
}
}

fun drawProgressText(canvas:Canvas?){
val fontMetrics = mRatePaint.fontMetrics
val sx = (width - mRatePaint.measureText(rateText))/2
val sy = (height - fontMetrics.top - fontMetrics.bottom)/2

canvas?.drawText(rateText,sx,sy,mRatePaint)
}




//onMeasure中调用
//调用画笔的方法 measureText()测量文字的宽度
//val textWidth = mTextPaint.measure(text)
fun Paint.textWidth(text: String): Float {
return this.measureText(text)
}


//测量文字的高度
fun Paint.textHeight(): Float {
val fontMetrics = this.fontMetrics
return fontMetrics.bottom - fontMetrics.top
}

⑤ canvas?.drawBitmap 画图片

canvas?.drawBitmap() //画图片

BitmapFactory类是用于创建Bitmap对象的工具类。
BitmapFactory类提供了一些静态方法,可以从不同的数据源(如资源、文件、流等)中加载图像数据,并将其转换为Bitmap对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

fun drawPicture(canvas:Canvas?){
//1.获取需要绘制的图片 -> Bitmap
val okBitmap = BitmapFactory.decodeResource(context.resources,R.drawable.icon ok foreground) //传入的drawable资源必须是png资源,不能是xml文件
//2.确定绘制的位置
val bx = (width - okBitmap.width).toFloat()/2
val by = (height- okBitmap.height).toFloat()/2
//3.画笔
val picPaint = Paint()
//4.绘制
canvas?.drawBitmap(okBitmap,bx,by, paint: null)
}




onAttachedToWindow()

onWindowVisibilityChanged()

1
2
3
4
5
6
7
8
9
10
11
12
//可以实现进入界面就开始动画
override fun onAttachedToWindow(){
super.onAttachedToWindow()
mAnimatorSet.start()
}
n
override fun onWindowVisibilityChanged(visibility: Int){
super.onWindowVisibilityChanged(visibility)
if (visibility == INVISIBLE){
mAnimatorSet.cancel()
}
}

Canvas 画布操作

在 Canvas 中,可以使用以下方法对图形进行操作:

  1. translate() - 平移画布的原点
  2. rotate() - 旋转画布
  3. scale() - 缩放画布
  4. skew() - 倾斜画布(斜切面移动)
  5. clipRect() - 裁剪画布的区域
  6. clipPath() - 使用路径来裁剪画布
  7. save() - 保存当前画布状态

    将需要操作的图形单独移动到一个画布上
    保存其他不作操作的图形到一个画布上

  8. restore() - 恢复之前保存的画布状态

    将操作之后的图形和其他图形合并到同一个画布上

这些方法可以帮助开发者对画布进行各种变换和操作,从而实现更加复杂和有趣的图形效果。通过组合这些方法,可以实现各种炫酷的动画和交互效果。

自定义 ViewGroup

方法的调用顺序是init -> onMeasure -> onSizeChanged -> onLayout

  1. init{自定义属性、添加子控件}

  2. 得到容器的尺寸 onMeasure()

在 onMeasure() 方法之后可以得到 measureWidth 和 measureHeight

  1. onLayout()布局子控件
    方法必须实现(子控件的摆放方式)

childCount 于控件个数
getChildAt() 获取某个子控件
child.layout(l,t,r,b)

在Android开发中自定义ViewGroup时,子控件能够得到容器的width和height的值的时机通常是在onLayout方法中。在onLayout方法中,会确定每个子View的位置并调用子View的layout方法来放置子View。在这个过程中,子View可以通过getWidth()getHeight()方法来获取容器的宽度和高度。

因此,在onLayout方法中,子View可以得到容器的width和height的值,并根据这些值来进行布局和绘制。开发者可以在onLayout方法中实现子View的布局逻辑,确保子View能够正确获取容器的尺寸信息。

onFinishInflate()

onFinishInflate() 方法是 ViewGroup 类中的一个回调方法,用于在布局文件中的所有子视图都被实例化和添加到父视图后被调用。
该方法通常用于执行一些与子视图相关的初始化操作。

在自定义ViewGroup中重写onFinishInflate()方法可以在所有子视图都被添加到父视图后执行一些操作,
例如设置子视图的属性、添加监听器等。
这样可以确保在子视图完全初始化之后再进行相关操作,避免出现空指针异常或其他问题。

总之,onFinishInflate()方法在Android开发中可以用于在子视图被添加到父视图后执行一些初始化操作,保证程序的稳定性和正确性。

onSizeChanged

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2023-2025 Annie
  • Visitors: | Views:

嘿嘿 请我吃小蛋糕吧~

支付宝
微信