android自定义view【绘图篇】

kotlin语法总结

硬件加速

概述

GPU的英文全名为 graphics processing unit 中文翻译为 图形处理器 。 与CPU不同 ,GPU是专门为处理图形任务而产生的芯片

软件绘制与硬件加速的区别

CPU绘制流程
1.让View层次结构失效
2.绘制View层次结构

硬件加速模式下的GPU绘制流程
1.让View层次结构失效
2.记录、更新显示列表
3.绘制View层次结构

在GPU加速时,流程中多了一步,表示在第一步View层次结构失效后,并不是直接开始逐层绘制的,而是首先把这些View的绘制函数作为绘制指令记录在一个显示列表中,然后在读取显示列表中的绘制指令,调用OpenGL的相关函数完成实际绘制。 也就是说,在GPU加速时,实际上是使用OpenGL的相关函数来完成绘制的
GPU加速优点:
提高了android系统显示和刷新的速度
缺点:
1.兼容性问题。由于是将绘制函数转换成OpenGL指令来绘制的,所以必然会存在PpenGL并不能完全支持原始绘制函数的问题,从而造成在打开GPU加速时效果会失效的问题
2.内存消耗问题。 需要把OpenGL相关的包加载到内存中来
3.电量消耗问题 会更耗电

禁用GPU硬件加速的方法

可以根据不同的类型 实现禁用方法
1.Application 在清单文件中为application 添加属性 android:hardwareAccelerated="true/false" 开启或关闭
2.Activity 在清单文件中危activity添加属性 android:hardwareAccelerated="true/false" 开启或关闭
3.Window 只支持开启

    window.setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)

4.View 只支持关闭
在代码中关闭

        setLayerType(LAYER_TYPE_SOFTWARE,null)

在布局文件中关闭

android:layerType="software"

文字

drawtext 相关四格线与基线 、 textAlign 与 FontMetrics

https://blog.csdn.net/qq_27981847/article/details/50748041

Paint 相关函数
void reset()   //重置画笔
void setColor(@ColorInt int color)  // 设置画笔颜色
void setAlpha(int a) // 设置画笔透明度
void setStyle(Style style) // 设置画笔样式
void setStrokeWidth(float width) // 设置画笔宽度
void setAntiAlias(boolean aa) // 设置画笔是否抗锯齿
void setStrokeMiter(float miter) //设置画笔倾斜度
 PathEffect setPathEffect(PathEffect effect)  //设置路径样式
 void setStrokeCap(Cap cap)  // 设置线帽子=样式
void setDither(boolean dither)  // 设置抗抖动效果
    float measureText(String text)  //计算text所需要的width
    void getTextBounds(@NonNull CharSequence text, int start, int end, @NonNull Rect bounds)  //计算text所需要的最小矩形 并赋值到第三个参数

Paint的setStrokeCap、setStrokeJoin、setPathEffect

贝济埃曲线

https://www.jianshu.com/p/1af5c3655fa3

path 有四个函数与贝济埃曲线相关
二阶贝济埃曲线
void quadTo(float x1, float y1, float x2, float y2)
void rQuadTo(float dx1, float dy1, float dx2, float dy2)

三阶贝济埃曲线
void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3)

这里以quadTo函数为例 x1,y1 是控制点坐标 x2,y2是终点坐标
整条线的起点是通过moveTo函数来指定的 如果连续调用quadTo函数那么上一个函数的终点就是下一个函数的起点
如果没有调用moveTo 默认起点为左上角(0,0)

 var path = Path()
        path.moveTo(100f,300f)
        path.quadTo(200f,200f,300f,300f)
        path.quadTo(400f,400f,500f,300f)
        canvas?.drawPath(path,paint)
示例 传统捕捉手势轨迹
要实现手势轨迹我们可以拦截onTouchEvent 然后使用Path.lineTo 函数即可
    override fun onTouchEvent(event: MotionEvent?): Boolean {

        when(event?.action){
            MotionEvent.ACTION_DOWN -> {
                path.moveTo(event.x , event.y)
                return true
            }
            MotionEvent.ACTION_MOVE -> {
                path.lineTo(event.x , event.y)
                postInvalidate()
                return true
            }
        }
        return super.onTouchEvent(event)
    }

 override fun draw(canvas: Canvas?) {
        super.draw(canvas)
        canvas?.drawPath(path,paint)
    }

path.lineTo 最大的问题就是线段转折处不够平滑 把图像放大两点连接处会有很明显的转折

使用quadTo 实现 修改draw方法即可
  var preX  = 0f
    var preY  = 0f
    override fun onTouchEvent(event: MotionEvent?): Boolean {

        when(event?.action){
            MotionEvent.ACTION_DOWN -> {
                path.moveTo(event.x , event.y)
                preX = event.x
                preY = event.y
                return true
            }
            MotionEvent.ACTION_MOVE -> {
//                path.lineTo(event.x , event.y)
                var endX = (preX + event.x)/2
                var endY = (preY + event.y)/2
                path.quadTo(endX,endY , event.x , event.y)
                preX = event.x
                preY = event.y
                postInvalidate()
                return true
            }
        }
        return super.onTouchEvent(event)
    }

void rQuadTo(float dx1, float dy1, float dx2, float dy2)

rQuadTo 的四个参数都是针对上一个终点的坐标 正值表示相加 负值表示相减

阴影效果

需要关闭硬件加速

 override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        var paint = Paint()
        paint.color = Color.BLACK
        paint.textSize = 25f
        // 需要关闭硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE,null)
        // 阴影的半径  阴影x轴的偏移量  阴影y轴的偏移量  阴影的颜色(对图片无效)
        paint.setShadowLayer(10f , 20f,20f, Color.GRAY)
//        清除阴影的两种方法
//        paint.clearShadowLayer()
        // 将阴影半径设为0
//        paint.setShadowLayer(0f , 10f,10f, Color.GRAY)

        var bitmap = BitmapFactory.decodeResource(resources , R.mipmap.animal1)



        canvas?.drawText("hello" , 100f,100f,paint)
        canvas?.drawCircle(300f,100f,50f , paint)
        canvas?.drawBitmap(bitmap , null , Rect(100 , 400 ,100+bitmap.width,400+bitmap.height) ,paint)
    }
shadow.png

发光效果与图片阴影 MaskFilter setMaskFilter(MaskFilter maskfilter)

需要关闭硬件加速
MaskFilter 有两个子类
EmbossMaskFilter 浮雕效果 很少使用
BlurMaskFilter 发光效果
BlurMaskFilter 的构造函数 需要传入style BlurMaskFilter(float radius, Blur style)

Blur 枚举 效果
NORMAL 内外发光
SOLID 外发光
OUTER 仅显示发光效果
INNER 内发光
  override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        setLayerType(LAYER_TYPE_SOFTWARE,null)
        var paint = Paint()
        paint.color = Color.BLACK
        paint.textSize = 25f
        paint.style = Paint.Style.FILL
            paint.maskFilter = BlurMaskFilter(50f, BlurMaskFilter.Blur.INNER)

        canvas?.drawCircle(200f,200f,100f,paint)

        }
maskfilter.png

给图片设置纯色阴影

class View6imageShadow : View {

    constructor(context: Context?, attrs: AttributeSet?):super(context, attrs){
        setLayerType(LAYER_TYPE_SOFTWARE,null)
    }


    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas?.translate(100f,100f)
        var paint = Paint()
        paint.color = Color.BLACK
        paint.textSize = 25f

        // 绘制原图
        var bitmap = BitmapFactory.decodeResource(resources , R.mipmap.animal_dog)
        canvas?.drawBitmap(bitmap , Matrix(),paint)
        canvas?.drawText("原图",0f, (bitmap.height+20).toFloat(),paint)


        //绘制setShadowLayer 的阴影图
        canvas?.translate(200f , 0f)
        canvas?.drawText("添加阴影效果",0f , (bitmap.height+40).toFloat(), paint)
        paint.setShadowLayer(10f,20f,20f,Color.GREEN)
        canvas?.drawBitmap(bitmap , Matrix(),paint)

        //使用extractAlpha函数获取alpha图
        paint.clearShadowLayer()
        var alphaBitmp = bitmap.extractAlpha()
        canvas?.translate(200f , 0f)
        canvas?.drawBitmap(alphaBitmp , Matrix(),paint)
        canvas?.drawText("alpha图",0f , (alphaBitmp.height+20).toFloat(), paint)

        // 使用alpha图 和原图组合成阴影效果
        canvas?.translate(200f , 0f)

        canvas?.drawBitmap(alphaBitmp , Matrix(),paint)
        canvas?.translate(-10f,-10f)
        canvas?.drawBitmap(bitmap , Matrix(),paint)
        canvas?.drawText("纯色阴影图",0f , (alphaBitmp.height+20).toFloat(), paint)


    }
}
图片纯色阴影.png

Shader

// Shader setShader(Shader shader)
// Shader 的效果与ps中的印章类似 用来实现图片 颜色 渐变的 填充效果
// BitmapShader 基本用法

// BitmapShader(@NonNull Bitmap bitmap, @NonNull TileMode tileX, @NonNull TileMode tileY)
// TitleMode 取值如下
// CLAMP 用边缘色彩来填充剩余空间
// REPEAT 重复图像来填充空间
// MIRROR 重复使用镜像图像来填充空间

class View6Shader : View {

    constructor(context: Context?, attrs: AttributeSet?):super(context, attrs){
        setLayerType(LAYER_TYPE_SOFTWARE,null)
    }


    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        var paint = Paint()
        paint.color = Color.BLACK
        paint.textSize = 50f

        var bitmap = BitmapFactory.decodeResource(resources , R.mipmap.mgirl)

        // 绘制原图
        canvas?.drawBitmap(bitmap , Matrix() ,paint)

        var fm = paint.getFontMetrics()
        //  (bitmap.width+20, -fm.top)  text的基线坐标
        canvas?.drawText("原图",(bitmap.width+20).toFloat() , -fm.top , paint)

        // 绘制矩形 使用图片填充
        canvas?.translate(0f , (bitmap.height+20).toFloat())
        paint.setShader(BitmapShader(bitmap ,Shader.TileMode.MIRROR,Shader.TileMode.MIRROR))
        canvas?.drawRect(0f,0f , width.toFloat(), height.toFloat(), paint)

    }
}
图片填充.png

放大镜效果

在手指触摸的地方绘制一个圆 然后显示该圆范围内的图像


class View6ShaderSample1 : View {

    constructor(context: Context?, attrs: AttributeSet?):super(context, attrs){
        setLayerType(LAYER_TYPE_SOFTWARE,null)
    }

    var dx = -1f
    var dy = -1f

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when(event?.action){
            MotionEvent.ACTION_DOWN -> {
                Log.e("view5","touch--down")
                // down事件返回true  或者 xml中 android:clickable="true"  否则只会触发down事件 不会触发move事件
                return true
            }
            MotionEvent.ACTION_MOVE -> {
                Log.e("view5","touch--move")
                dx = event?.x
                dy = event?.y
            }
        }
        postInvalidate()
        return super.onTouchEvent(event)
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        var paint = Paint()
        paint.color = Color.BLACK
        paint.textSize = 50f

        var bitmap = BitmapFactory.decodeResource(resources , R.mipmap.mgirl)

        // 该步骤是为了画出一张能够填满整个控件大小的背景图
        var bitmapBg = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888)
        var canvasBg = Canvas(bitmapBg)
        canvasBg?.drawBitmap(bitmap , null ,Rect(0 ,0 ,width ,height) , paint)

        if(dx != -1f && dy != -1f){
            // 也可直接使用原图 bitmap
//            paint.setShader(BitmapShader(bitmap , Shader.TileMode.REPEAT , Shader.TileMode.REPEAT))
            paint.setShader(BitmapShader(bitmapBg , Shader.TileMode.REPEAT , Shader.TileMode.REPEAT))
            canvas?.drawCircle(dx, dy, 150f ,paint)
        }

    }
}

放大镜效果

class View6ShaderSample1 : View {

    constructor(context: Context?, attrs: AttributeSet?):super(context, attrs){
        setLayerType(LAYER_TYPE_SOFTWARE,null)
    }

    var dx = -1f
    var dy = -1f

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when(event?.action){
            MotionEvent.ACTION_DOWN -> {
                Log.e("view5","touch--down")
                // down事件返回true  或者 xml中 android:clickable="true"  否则只会触发down事件 不会触发move事件
                return true
            }
            MotionEvent.ACTION_MOVE -> {
                Log.e("view5","touch--move")
                dx = event?.x
                dy = event?.y
            }
        }
        postInvalidate()
        return super.onTouchEvent(event)
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        var paint = Paint()
        paint.color = Color.BLACK
        paint.textSize = 50f

        var bitmap = BitmapFactory.decodeResource(resources , R.mipmap.mgirl)

        // 该步骤是为了画出一张能够填满整个控件大小的背景图
        var bitmapBg = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888)
        var canvasBg = Canvas(bitmapBg)
        canvasBg?.drawBitmap(bitmap , null ,Rect(0 ,0 ,width ,height) , paint)

        if(dx != -1f && dy != -1f){
            // 也可直接使用原图 bitmap
//            paint.setShader(BitmapShader(bitmap , Shader.TileMode.REPEAT , Shader.TileMode.REPEAT))
            paint.setShader(BitmapShader(bitmapBg , Shader.TileMode.REPEAT , Shader.TileMode.REPEAT))
            canvas?.drawCircle(dx, dy, 150f ,paint)
        }

    }
}
放大镜.gif

圆形头像

class View6ShaderSample2 : View {

    constructor(context: Context?, attrs: AttributeSet?):super(context, attrs){
        setLayerType(LAYER_TYPE_SOFTWARE,null)
    }


    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        var paint = Paint()
        paint.color = Color.BLACK

        var bitmap = BitmapFactory.decodeResource(resources , R.mipmap.girl)
        var matrix = Matrix()
        var scale = width.toFloat()/bitmap.width
        matrix.setScale(scale,scale)
        var bitmapShader = BitmapShader(bitmap , Shader.TileMode.CLAMP , Shader.TileMode.CLAMP)
        bitmapShader.setLocalMatrix(matrix)
        paint.setShader(bitmapShader)
        var half = width.toFloat()/2
        canvas?.drawCircle(half,half,half,paint)

    }
}
圆形头像.jpg

线性渐变 LinearGradient

// 构造函数 坐标1到坐标2 实现 color1到color2的渐变
// LinearGradient(float x0, float y0, float x1, float y1, @ColorInt int color0, @ColorInt int color1,@NonNull TileMode tile)
// 构造函数 坐标1到坐标2 实现 color1到colorn 的渐变 positions取值0-1 表示每种颜色在整条渐变线中的百分比位置
// LinearGradient(float x0, float y0, float x1, float y1, @NonNull @ColorInt int[] colors, @Nullable float[] positions, @NonNull TileMode tile)

  // 双色渐变
        var linearGradient = LinearGradient(0f, 0f ,width.toFloat() , height/2.toFloat()-50 ,Color.BLUE , Color.GREEN , Shader.TileMode.CLAMP)
        paint.setShader(linearGradient)
        canvas?.drawRect(0f,0f,width.toFloat() , height/2.toFloat()-50  , paint)

//        多色渐变
        var colors =  intArrayOf(Color.BLUE,Color.CYAN,Color.RED ,Color.YELLOW)
        var position = floatArrayOf(0f,.3f , .7f , 1f)
        var linearGradient2 = LinearGradient(0f, height/2.toFloat()+50 ,width.toFloat() , height.toFloat() , colors , position , Shader.TileMode.CLAMP)
        paint.setShader(linearGradient2)
        canvas?.drawRect(0f,height/2.toFloat()+50,width.toFloat() , height.toFloat() , paint)
线性渐变.png
示例 文字渐变效果
class View6LinearGradientText : View {

    var paint : Paint
    var length  = 0f
    var text = "文字渐变效果"
    var fontMetrics : Paint.FontMetrics
    var dx  = 0f

    // 延迟初始化
    lateinit var linearGradient : LinearGradient

    constructor(context: Context?, attrs: AttributeSet?):super(context, attrs){
        setLayerType(LAYER_TYPE_SOFTWARE,null)

        paint = Paint()
        paint.color = Color.BLACK
        paint.textSize = 50f

        fontMetrics = paint.getFontMetrics()
        var length =  paint.measureText(text)
        createAnim(length)
        createLinearGradient(length)
    }

    private fun  createAnim( length : Float){
        var anim =ValueAnimator.ofFloat(0f , 2 * length)
        anim.addUpdateListener({
            dx = it.animatedValue as Float
            postInvalidate()
        })
        anim.repeatMode = ValueAnimator.RESTART
        anim.repeatCount = ValueAnimator.INFINITE
        anim.duration = 2000
        anim.start()
    }

    fun createLinearGradient(length : Float){
        linearGradient = LinearGradient(-length , 0f , 0f , 0f , intArrayOf(Color.BLACK , Color.GREEN, Color.YELLOW , Color.BLACK) ,
            floatArrayOf(0f , 0.3f  , .7f , 1f) , Shader.TileMode.CLAMP)
    }


    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        var matrix = Matrix()
        matrix.setTranslate(dx , 0f )
        linearGradient.setLocalMatrix(matrix)
        paint.setShader(linearGradient)

        canvas?.drawText(text , 0f , -fontMetrics.top , paint)



    }
文字渐变.gif

放射渐变 RadialGradient


class View6RadialGradient : View {

    var paint : Paint


    constructor(context: Context?, attrs: AttributeSet?):super(context, attrs){
        setLayerType(LAYER_TYPE_SOFTWARE,null)

        paint = Paint()
        paint.color = Color.BLACK
        paint.textSize = 50f


    }


    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

//        RadialGradient  从一个点向周围扩散的渐变
//        双色渐变
        var radialGradient = RadialGradient(100f , 100f , 100f ,Color.BLUE , Color.YELLOW ,Shader.TileMode.REPEAT)
        paint.setShader(radialGradient)
        canvas?.drawCircle(100f , 100f , 100f , paint)
        // 多色渐变
        radialGradient = RadialGradient(width/2.toFloat() , height/2.toFloat() , 200f ,
            intArrayOf(Color.BLUE , Color.RED,Color.GREEN , Color.YELLOW) , floatArrayOf(0f, .2f ,.6f , 1f),Shader.TileMode.REPEAT)
        paint.setShader(radialGradient)
        canvas?.drawCircle(width/2.toFloat() , height/2.toFloat() , 200f  , paint)

    }
放射渐变.png

混合模式 Xfermode

// 混合模式相关知识是Paint绘图中最难的部分 它可以将两张图片无缝结合,实现类似ps的效果
// 混合模式通过 Xfermode类 来实现 paint.setXfermode

// 在使用Xfermode时需要做两件事
// 完全不支持硬件加速
// 离屏绘制 canvas.saveLayer() 绘制代码 canvas.restoreToCount()

//替换颜色 API 16之后无法使用
// paint.xfermode = AvoidXfermode()

// PorterDuffXfermode(PorterDuff.Mode mode) 16种模式可选
// paint.xfermode = PorterDuffXfermode()
// 可以实现 橡皮檫 刮刮卡等效果

图像操纵大师Xfermode讲解与实战——Android高级UI

Canvas

// 如何获取canvas对象
// 1重写onDraw 用于绘制自身 dispatchDraw用于绘制子视图
// 2 使用bitmap 创建 new Canvas(bitmap) 或者 Canvas c = new Canvas() c.setBitmap(bitmap)
// 3 SurfaceHolder.lockCanvas() 函数

这里我们关注两个方法
saveLayer 与 restoreToCount
saveLayer 会生成一个新的图层
之后所有的操作都在新的图层上进行
然后调用restoreToCount 将图层合并到原图层上

与saveLayer相关的还有 一个函数 saveLayerAlpha 用于生成一个带透明度的图层

int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint,
@Saveflags int saveFlags) 最后一个参数 saveFlags 有许多个选择

flag 描述
ALL_SAVE_FLAG 保存所有的标识
MATRIX_SAVE_FLAG 保存canvas的matrix信息
CLIP_SAVE_FLAG 保存canvas 的大小信息
HAS_ALPHA_LAYER_SAVE_FLAG 标识新建的bmp具有透明度 在与上层画布结合时 透明位置显示上层图像
FULL_COLOR_LAYER_SAVE_FLAG 标识新建的bmp颜色完全独立 在与上层画布结合时 先清空上层画布在进行覆盖
CLIP_TO_LAYER_SAVE_FLAG 在保存图层前先把当前画布根据bounds裁剪

SurfaceView

概述 android 屏幕刷新的时间间隔是16ms 如果View能够在16ms内完成所需执行的绘图操作那么在视图上就是流畅的,否则就会出现卡顿。 有时候,在自定义View的日志中,经常会看到如下警告:
Skipped 60 frames! The application may be doing too much work on its main thread
之所以会出现警告 大部分是因为我们不仅在绘制过程中执行了绘图操作 也夹杂了很多逻辑处理 导致在指定的16ms内没有完成绘制
然而很多情况下这些逻辑操作处理又是必须的 为了解决这个问题 Android引入了SurfaceView。
SurfaceView在两个方面改进了View的绘图操作
1.使用双缓冲技术
2.自带画布,支持在子线程中更新画布内容

所谓的双缓冲技术 ,简单的说就是多加一块缓冲画布, 当需要执行绘图操作时,先在缓冲画布上绘制,绘制好之后在将缓冲画布上的内容更新到主画布上

SurfaceView 继承自View 所以SurfaceView可以使用View中的所有方法,即用View实现的自定义控件都可以使用SurfaceView来实现 。

使用SurfaceView来实现捕捉用户手势轨迹的自定义控件
class View10SurfaceView : SurfaceView {

    var paint: Paint
    var path: Path

    // 方法一
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
//        setLayerType(LAYER_TYPE_SOFTWARE , null)
//        setWillNotDraw(false)
        paint = Paint()
        paint.isAntiAlias = true
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = 10f
        paint.color = Color.BLUE

        path = Path()

    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        var x = event?.x
        var y = event?.y
        if (x == null || y == null) {
            return super.onTouchEvent(event)
        }
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                path.moveTo(x, y)
                return true
            }
            MotionEvent.ACTION_MOVE -> {
                path.lineTo(x, y)
                postInvalidate()
            }
        }
        return super.onTouchEvent(event)
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas?.drawPath(path,paint)
    }
}

我们会发现上述代码没有反应 通过添加日志我们可以发现 onDraw方法并没有被执行
然后我们把其中一行注释打开 就可以了 setWillNotDraw(false)

setWillNotDraw

这个函数存在于View类中, 它主要用在View派生子类的初始化中, 如果 设置为true 表示当前控件没有绘制内容,当屏幕重绘时,这个控件不需要绘制 所以在重绘的时候也不会调用这个类的onDraw函数 相反如果设置为false 则表示当前控件在每次重绘时都需要绘制该控件
可见setWillNotDraw 是一种优化策略,它可以显示的告诉系统,在重绘屏幕时,哪个控件需要重绘,哪个控件不需要重绘,这样就可以大大提高重绘效率
一般而言,像LinearLayout、relativelayout 等布局控件,它们的主要功能是布局其中的控件,它们本身是不需要绘制的, 所以它们在构造的时候都会显式地设置
setWillNotDraw(true) 而上面SurfaceView重绘时,之所以没有调用onDraw函数 是因为SurfaceView在初始化的时候显式调用了setWillNotDraw(true)

虽然我们找到了原因 但是从这里也看的出来SurfaceView的开发人员并不想让我们通过重写onDraw方法来绘制界面 而要使用SurfaceView独有的特性来操作画布

使用SurfaceView来实现捕捉用户手势轨迹的自定义控件 方法二
class View10SurfaceView : SurfaceView {

    var paint: Paint
    var path: Path

   constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
//        setLayerType(LAYER_TYPE_SOFTWARE , null)
//        setWillNotDraw(false)
        paint = Paint()
        paint.isAntiAlias = true
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = 10f
        paint.color = Color.BLUE
        path = Path()

    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        var x = event?.x
        var y = event?.y
        if (x == null || y == null) {
            return super.onTouchEvent(event)
        }
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                path.moveTo(x, y)
                return true
            }
            MotionEvent.ACTION_MOVE -> {
                path.lineTo(x, y)
//                postInvalidate()
            }
        }
        drawCanvas()
        return super.onTouchEvent(event)
    }

    fun drawCanvas() {
        Thread(Runnable {
            var holder = holder
            var canvas = holder.lockCanvas()
            canvas.drawPath(path, paint)
            holder.unlockCanvasAndPost(canvas)
        }).start()


        var holder = holder
//监听Surface生命周期
        holder.addCallback(object : SurfaceHolder.Callback {
            override fun surfaceChanged(
                holder: SurfaceHolder,
                format: Int,
                width: Int,
                height: Int
            ) {
            }

            override fun surfaceDestroyed(holder: SurfaceHolder) {
            }

            override fun surfaceCreated(holder: SurfaceHolder) {
            }
        })

    }

}

方法一 setWillNotDraw(false) + ACTION_MOVE时postInvalidate + onDraw
方法二 ACTION_MOVE时 调用 drawCanvas

SurfaceView移动背景


class View10SurfaceViewBg : SurfaceView {

    var surfaceHolder : SurfaceHolder
    var flag = false
    lateinit var bitmapBg : Bitmap
    var surfaceWidth = 0
    var surfaceHeight = 0

    //  图片移动的距离
    var bitPosX = 0f
    lateinit var mCanvas : Canvas
    lateinit var thread : Thread

    //  图片移动的方向
    enum class State{
        LEFT ,RIGHT
    }
     var state = State.LEFT

    // 图片每一步移动的单位
    val BITMAP_STEP = 10

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
//        setLayerType(LAYER_TYPE_SOFTWARE , null)
//        setWillNotDraw(false)
        surfaceHolder = holder
        surfaceHolder.addCallback(object  : SurfaceHolder.Callback{
            override fun surfaceChanged(
                holder: SurfaceHolder,
                format: Int,
                width: Int,
                height: Int
            ) {
            }

            override fun surfaceDestroyed(holder: SurfaceHolder) {
                flag = false
            }

            override fun surfaceCreated(holder: SurfaceHolder) {
                // 设置监听 创建时启动
                flag = true
                startAnimation()
            }
        } )


    }

    fun startAnimation(){
        surfaceWidth = width
        surfaceHeight = height
        var minWidth = surfaceWidth*3/2

        // 获取原图
        var bitmap = BitmapFactory.decodeResource(resources , R.mipmap.bg)
        // 绘制缩放图  高==屏幕高度  宽==1.5倍屏幕宽度
        bitmapBg = Bitmap.createScaledBitmap(bitmap ,minWidth , height ,true)
        thread = Thread(Runnable {
            while (flag){
                mCanvas = surfaceHolder.lockCanvas()
                drawView()
                surfaceHolder.unlockCanvasAndPost(mCanvas)
                try {
                    Thread.sleep(50)
                }catch (e : Exception){
                    e.printStackTrace()
                }
            }
        })
        thread.start()


    }

    fun drawView(){
        // 清空屏幕
        mCanvas.drawColor(Color.TRANSPARENT , PorterDuff.Mode.CLEAR)

        // 绘制显示图片 从图片的 (bitPosX,0)位置开始
        mCanvas.drawBitmap(bitmapBg , bitPosX , 0f ,null)
        when(state){
            State.LEFT -> {
                bitPosX -= BITMAP_STEP
            }
            State.RIGHT -> {
                bitPosX += BITMAP_STEP
            }
        }

        if(bitPosX <= -surfaceWidth/2){
            state = State.RIGHT
        }

        if(bitPosX >= 0){
            state = State.LEFT
        }

    }

}
movebackground.gif

* SurfaceView 双缓冲技术

/**

  • 依次绘制 0-9 按照我们的逻辑一共有两块画布 那么两块画布依次绘制的是 0 2 4 6 8 和 1 3 5 7 9
  • 因为最后绘制的是9 那么最后显示在屏幕上的应该是1 3 5 7 9 但是实际最后显示的是 0 3 6 9
  • 为什么是0 3 6 9 那是因为这里有三块缓冲画布
  • 假如我们在每次绘制完数字的时候 让线程休息一段时间 那么就可以看见每次绘制的数字了 这时最后显示的是 1 3 5 7 9 说明此时只有两块画布
  • 根据官方的解释 缓冲画布的数量是根据需求动态分配的 如果用户获取画布的频率较慢,那么分配两块缓冲画布 。 否则将分配3的倍数快缓冲画布 具体数量由实际情况而定
  • 总的来说 Surface肯定会配分配>=2个缓冲区域 具体分配多少由实际情况而定
  • 双缓冲局部更新 holder.lockCanvas(Rect())
  • lockCanvas 用于获取整屏画布 当前屏幕显示的内容不会被更新到新画布上
  • lockCanvas(Rect()) 用于获取指定区域的画布 , 画布以外的区域会与屏幕内容一致 画布以内的区域依然保持对应的缓冲画布的内容

*/

class View10SurfaceView3p3 : SurfaceView {

    var paint: Paint

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
        paint = Paint()
        paint.isAntiAlias = true
        paint.color = Color.BLUE
        paint.textSize = 30f

        holder.addCallback(object : SurfaceHolder.Callback {
            override fun surfaceChanged(
                holder: SurfaceHolder,
                format: Int,
                width: Int,
                height: Int
            ) {

            }

            override fun surfaceDestroyed(holder: SurfaceHolder) {

            }

            override fun surfaceCreated(holder: SurfaceHolder) {
                drawCanvas()
            }
        })

    }


    fun drawCanvas() {
        Thread(Runnable {
            var holder = holder

            for (index in 0..9) {
                var canvas = holder.lockCanvas()

                canvas.drawText(index.toString() , (index+1) * 30f , 50f , paint)
                holder.unlockCanvasAndPost(canvas)

                Thread.sleep(500)
            }
        }).start()


    }

}

若使用holder.lockCanvas(Rect()) 则需要先对缓冲屏幕进行清屏 不然我们拿到的画图区域不一定是我们指定的区域

while (true){
    var dirtyRecf = Rect(0,0,1,1)
    var locakcanvas = holder.lockCanvas(dirtyRecf)
    var canvasRect = locakcanvas.clipBounds
    if(width == canvasRect.width() &&  height == canvasRect.height() ){
        locakcanvas.drawColor(Color.BLACK)
        holder.unlockCanvasAndPost(locakcanvas)
    }else{
        holder.unlockCanvasAndPost(locakcanvas)
        break
    }
}

这是因为有一块画布初始化的被显示在了屏幕上 ,已经被默认填充为黑色 而另外两块画布都还没有被画过
虽然我们指定了获取画布的区域范围,但是系统认为 整块画布都是脏区域, 都应该被画上,所以会返回屏幕大小的画布
只有我们将每块画布都画过以后, 才会按照我们指定的区域来返回画布大小

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

推荐阅读更多精彩内容