Android 自定义View(二):画布、画笔、路径(遮罩)以及Sufaceview

目录

1)画布是什么?画布如何使用?

2)画笔是什么,画笔如何生成呢?

3)如何画圆、画文字、画矩形

4)路径(Path)遮罩

5)Sufaceview(使用子线程绘画)

一、画布是什么?画布如何使用?

为什么需要画布?前面我们使用MyEditText这些也不需要呀。因为EditText是已经进行了绘制,我们是继承过来进行二次改造开发。

这次学习画布的知识点,我们继承一个View,一个空白内容的控件去开发。

Canvas类在Android中扮演着画布的角色,它提供了各种绘制方法,如绘制线条、矩形、圆形、文本等。通过Canvas,开发者可以在屏幕上绘制出各种图形界面元素。

1.1、在使用之前,我们先了解一下view的方法回调,方便我们知道在绘制期间,应该把逻辑写在哪个方法里面。

kt 复制代码
//这些方法的作用:总结起来,这些方法分别用于处理视图的初始化、布局、测量、绘制以及生命周期的管理。通过重写这些方法,可以实现对自定义视图的完全控制。

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)创建画笔

kt 复制代码
//实线线条的画笔 
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)使用画笔

kt 复制代码
  //绘制视图的内容。在该方法中可以使用传入的 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)
        }
    }

三、如何画圆、画文字、画矩形

kt 复制代码
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定义的路径内部。这对于创建特定形状的窗口、或者在复杂背景下只显示特定区域的内容非常有用。

下面我们做一个案例:通过路径绘制一个矩形以及圆形,通过裁剪路径达到一个遮罩的效果

kt 复制代码
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个圆,我们就会注意到,加载圈会在创建圆的时候,卡顿一下。

kt 复制代码
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 复制代码
<?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

kt 复制代码
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)
    }
}
相关推荐
阿洵Rain4 小时前
【Linux】环境变量
android·linux·javascript
hong1616886 小时前
PhpStorm中配置调试功能
android·ide·phpstorm
DongGei8 小时前
安卓-音频焦点
android·微信·音视频
冬田里的一把火38 小时前
[Android] [SnapdragonCamera] 单摄(横屏)阶段总结
android·数码相机
夏目艾拉9 小时前
flutter开发多端平台应用的探索 下 (跨模块、跨语言通信之平台通道)
android·java·flutter·设计模式
似霰9 小时前
安卓源码libvulkan“ depends on undefined module “libgpud_sys“报错解决
android
Arms20611 小时前
Android Fragment 学习备忘
android·学习
落魄的Android开发11 小时前
Android 跳转至各大应用商店应用详情页
android
技术无疆11 小时前
DDComponentForAndroid:探索Android组件化方案
android·java·开源·android-studio·组件化
繁依Fanyi12 小时前
【Python 千题 —— 算法篇】数字反转
android·java·开发语言·python·算法·eclipse·tomcat