目录
1)画布是什么?画布如何使用?
2)画笔是什么,画笔如何生成呢?
3)如何画圆、画文字、画矩形
4)路径(Path)遮罩
5)Sufaceview(使用子线程绘画)
一、画布是什么?画布如何使用?
为什么需要画布?前面我们使用MyEditText这些也不需要呀。因为EditText是已经进行了绘制,我们是继承过来进行二次改造开发。
这次学习画布的知识点,我们继承一个View,一个空白内容的控件去开发。
Canvas类在Android中扮演着画布的角色,它提供了各种绘制方法,如绘制线条、矩形、圆形、文本等。通过Canvas,开发者可以在屏幕上绘制出各种图形界面元素。
1.1、在使用之前,我们先了解一下view的方法回调,方便我们知道在绘制期间,应该把逻辑写在哪个方法里面。
//这些方法的作用:总结起来,这些方法分别用于处理视图的初始化、布局、测量、绘制以及生命周期的管理。通过重写这些方法,可以实现对自定义视图的完全控制。 class MyView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { private var mWidth = 0f private var mHeight = 0f //当view的最终尺寸确定之后进行调用。一般用于记录实际的宽高 override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) mWidth = w.toFloat() mHeight = h.toFloat() } //当从xml创建view的时候,创建完毕后调用 override fun onFinishInflate() { super.onFinishInflate() } //当视图被附加到窗口时调用。在该方法中可以执行视图生命周期相关的操作,例如注册监听器或启动动画。 override fun onAttachedToWindow() { super.onAttachedToWindow() } //测量视图的大小。在该方法中需要根据传入的 widthMeasureSpec 和 heightMeasureSpec 参数来计算并设置视图的宽度和高度。 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) } //布局视图的位置。在该方法中需要根据传入的参数来确定视图的左上角和右下角坐标,并将子视图放置在正确的位置。 override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) } //【重点关注】绘制视图的内容。在该方法中可以使用传入的 canvas 对象进行绘制操作,例如绘制文本、图形或图片等。 override fun onDraw(canvas: Canvas) { super.onDraw(canvas) //在这里画图。注意不要在这个方法里面创建对象,因为绘制里面如果存在耗时操作会导致掉帧并创建大量对象的情况出现 canvas.apply { drawAxises(this) } } //当视图从窗口中分离时调用。在该方法中可以执行与视图生命周期相关的清理操作,例如取消监听器或停止动画。 override fun onDetachedFromWindow() { super.onDetachedFromWindow() } }
绘制的时候,也要注意层次,自定义view控制不好,层次太多,就会导致绘制时间长,也就是会掉帧。
当我们继承了View以后,画布也就自然有了,我们可以看到override fun onDraw(canvas: Canvas)方法里面提供了canvas,接下来我们就可以画图了,可以花圆,画文字等等
但现在只有画布,我们需要一个画笔,才能进行绘制。接下来我们了解一下画笔。
二、画笔是什么,画笔如何生成呢?
通过画笔,我们可以在画布(Canvas)上绘制出各种图形,如线条、圆形、矩形、文本等,并可以设置这些图形的颜色、粗细、样式等属性。
(1)创建画笔
//实线线条的画笔 private val solidLinePaint = Paint().apply { style = Paint.Style.STROKE // 设置画笔的绘制样式为描边(不填充) strokeWidth = 5f // 设置线条的宽度为5个浮点单位 color = Color.WHITE // 设置线条的颜色为白色 } //文本的画笔 (textPaint) private val textPaint = Paint().apply { textSize = 50f // 设置文本的字体大小为50个浮点单位 typeface = Typeface.DEFAULT_BOLD // 设置文本的字体为默认加粗字体 color = Color.WHITE // 设置文本的颜色为白色 } //虚线线条的画笔 (dashedLinePaint) private val dashedLinePaint = Paint().apply { style = Paint.Style.STROKE // 设置画笔的绘制样式为描边(不填充) pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f) // 设置路径效果为虚线 strokeWidth = 5f // 设置线条的宽度为5个浮点单位 color = Color.YELLOW // 设置线条的颜色为黄色 }
(2)使用画笔
//绘制视图的内容。在该方法中可以使用传入的 canvas 对象进行绘制操作,例如绘制文本、图形或图片等。 override fun onDraw(canvas: Canvas) { super.onDraw(canvas) //在这里画图。 canvas.apply { drawAxises(this) } } private fun drawAxises(canvas: Canvas){ //绘图的方式:在绝对坐标系,也就是从0,0开始 //画线 // canvas.drawLine(100f,100f,100f,400f,solidLinePaint) //移动画布到居中位置 canvas.withTranslation(mWidth/2,mHeight/2) { //如果我们要绘制一条直线,那么就是从-mWidth/2,到mWidth/2 drawLine(-mWidth/2,0f,mWidth/2,0f,solidLinePaint) } }
三、如何画圆、画文字、画矩形
private fun drawLabel(canvas: Canvas){ canvas.apply { drawRect(100f,100f,600f,250f,solidLinePaint)//画矩形 drawText("大家好",120f,195f,textPaint)//画文字 } } private fun drawDashedCircle(canvas: Canvas){ //画圆,需要有半径,可以自己填写。 canvas.withTranslation(mWidth/2,mHeight/2) { drawCircle(0f,0f,0f,dashedLinePaint) } }
四、如何使用路径实现遮罩
Path类是Android图形编程中非常核心的一个类,它用于表示一系列的图形路径,这些路径可以包含直线、曲线、圆形等多种形状,并可以用于绘制、剪裁或者定义复杂的图形轮廓。
使用Path,你可以构建出复杂的图形轮廓,并通过Canvas的drawPath(Path path, Paint paint)方法将这些图形绘制到屏幕上。比如,你可以绘制出平滑的曲线、不规则的多边形,甚至是基于数学函数的图形等。
Path提供了多种路径变换的方法,如平移(translate)、缩放(scale)、旋转(rotate)等,使得在绘制复杂图形时,可以对图形的各个部分进行精细的控制和调整。
除了绘制图形,Path还可以用于定义剪裁区域。通过Canvas的clipPath(Path path)方法,可以将绘制的区域限制在Path定义的路径内部。这对于创建特定形状的窗口、或者在复杂背景下只显示特定区域的内容非常有用。
下面我们做一个案例:通过路径绘制一个矩形以及圆形,通过裁剪路径达到一个遮罩的效果
package com.example.mymediaplayer.myview import android.content.Context import android.graphics.Canvas import android.graphics.Paint import android.graphics.Path import android.util.AttributeSet import android.util.Log import android.view.MotionEvent import android.view.View import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap import com.example.mymediaplayer.R import kotlin.random.Random class MyView2 @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { private val faceBitmap = ContextCompat.getDrawable(context, R.drawable.baseline_clear_24)?.toBitmap(300, 300) private var faceX = 0f private var faceY = 0f private val path = Path() private val paint = Paint() private fun randomPosition() { faceX = Random.nextInt(width - 300).toFloat() faceY = Random.nextInt(height - 300).toFloat() } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) canvas.apply { faceBitmap.let { drawBitmap(it!!, faceX, faceY, null) } drawPath(path, paint) } } override fun onTouchEvent(event: MotionEvent?): Boolean { event?.apply { Log.d("action", "onTouchEvent: " + action) when (action) { MotionEvent.ACTION_DOWN -> { randomPosition() //重置路径对象,清除之前的路径。 path.reset() //添加一个矩形路径,表示整个视图的范围。 path.addRect(0f, 0f, width.toFloat(), height.toFloat(), Path.Direction.CW) //添加一个圆形路径,以当前触摸点为中心,半径为 400 像素。 path.addCircle(x, y, 400f, Path.Direction.CCW) } MotionEvent.ACTION_MOVE -> { path.reset() path.addRect(0f, 0f, width.toFloat(), height.toFloat(), Path.Direction.CW) path.addCircle(x, y, 400f, Path.Direction.CCW) } MotionEvent.ACTION_UP -> { path.reset() } else -> {} } invalidate()//通知系统重新绘制视图,即调用 onDraw() 方法。 } return true//返回 true 表示已经处理了触摸事件,不再传递给其他监听器或父视图。 } }
当手指按下屏幕时,在视图上随机生成一个位置,并绘制一个以该位置为中心、半径为 400 像素的圆形路径。
当手指在屏幕上移动时,更新圆形路径的位置,使其跟随手指移动。
当手指抬起时,清除路径,不再显示圆形。
Path.Direction.CW和Path.Direction.CCW的相互使用,就达到了裁剪的效果。
五、Sufaceview
为什么这里要讲Sufaceview,因为Sufaceview可以在子线程上绘制,View只能在主线程。
1)View:View的绘制通常是在UI线程(主线程)上进行的。当需要更新视图内容时,UI线程会负责执行绘图操作,这可能会导致在复杂或频繁的绘图操作中UI线程被阻塞,进而影响应用的响应性和流畅性。
3)SurfaceView:SurfaceView则不同,它拥有自己独立的绘制表面(Surface),可以在一个子线程中进行绘制操作。这种机制使得SurfaceView在需要频繁更新画面或进行复杂计算时,能够避免阻塞UI主线程,从而提高应用的性能和响应性。
7.1 使用View模拟卡顿的情况
比如我们在界面上放置一个动态加载圈,会不停的转圈圈加载,同时每次点击屏幕绘制3000个圆,我们就会注意到,加载圈会在创建圆的时候,卡顿一下。
package com.example.mymediaplayer.myview import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.util.AttributeSet import android.view.MotionEvent import android.view.View class MyView3 @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { private var centerX = 0f private var centerY = 0f private val colors = arrayOf(Color.RED,Color.GREEN,Color.YELLOW,Color.MAGENTA,Color.BLUE,Color.GRAY) private val paint = Paint().apply { style = Paint.Style.STROKE strokeWidth = 5f } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) repeat(2000){ paint.color = colors.random() canvas?.drawCircle(centerX,centerY,it.toFloat()/5,paint) } } override fun onTouchEvent(event: MotionEvent?): Boolean { centerX = event?.x?:0f centerY = event?.y?:0f invalidate() return super.onTouchEvent(event) } }
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".hilt.MainActivity"> <ProgressBar android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="220dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.559" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <com.example.mymediaplayer.myview.MyView3 android:id="@+id/myView3" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
改用SufaceView
package com.example.mymediaplayer.myview import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.util.AttributeSet import android.view.MotionEvent import android.view.SurfaceView class MySufaceView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : SurfaceView(context, attrs) { private var centerX = 0f private var centerY = 0f private val colors = arrayOf( Color.RED, Color.GREEN, Color.YELLOW, Color.MAGENTA, Color.BLUE, Color.GRAY) private val paint = Paint().apply { style = Paint.Style.STROKE strokeWidth = 5f } override fun onTouchEvent(event: MotionEvent?): Boolean { centerX = event?.x?:0f centerY = event?.y?:0f val canvas = holder.lockCanvas()//获取一个画布对象 canvas,并设置画布背景颜色为白色(canvas.drawColor(Color.WHITE))。 canvas.drawColor(Color.WHITE) repeat(2000){//循环2000次 paint.color = colors.random() canvas?.drawCircle(centerX,centerY,it.toFloat()/5,paint) } holder.unlockCanvasAndPost(canvas)//将画布内容显示到屏幕上。 return super.onTouchEvent(event) } }