android自定義view【繪圖篇】

kotlin語法總結(jié)

硬件加速

概述

GPU的英文全名為 graphics processing unit 中文翻譯為 圖形處理器 悠瞬。 與CPU不同 ,GPU是專門為處理圖形任務(wù)而產(chǎn)生的芯片

軟件繪制與硬件加速的區(qū)別

CPU繪制流程
1.讓View層次結(jié)構(gòu)失效
2.繪制View層次結(jié)構(gòu)

硬件加速模式下的GPU繪制流程
1.讓View層次結(jié)構(gòu)失效
2.記錄、更新顯示列表
3.繪制View層次結(jié)構(gòu)

在GPU加速時(shí),流程中多了一步,表示在第一步View層次結(jié)構(gòu)失效后,并不是直接開始逐層繪制的摸吠,而是首先把這些View的繪制函數(shù)作為繪制指令記錄在一個(gè)顯示列表中,然后在讀取顯示列表中的繪制指令,調(diào)用OpenGL的相關(guān)函數(shù)完成實(shí)際繪制胸墙。 也就是說,在GPU加速時(shí)按咒,實(shí)際上是使用OpenGL的相關(guān)函數(shù)來完成繪制的
GPU加速優(yōu)點(diǎn):
提高了android系統(tǒng)顯示和刷新的速度
缺點(diǎn):
1.兼容性問題迟隅。由于是將繪制函數(shù)轉(zhuǎn)換成OpenGL指令來繪制的,所以必然會(huì)存在PpenGL并不能完全支持原始繪制函數(shù)的問題励七,從而造成在打開GPU加速時(shí)效果會(huì)失效的問題
2.內(nèi)存消耗問題智袭。 需要把OpenGL相關(guān)的包加載到內(nèi)存中來
3.電量消耗問題 會(huì)更耗電

禁用GPU硬件加速的方法

可以根據(jù)不同的類型 實(shí)現(xiàn)禁用方法
1.Application 在清單文件中為application 添加屬性 android:hardwareAccelerated="true/false" 開啟或關(guān)閉
2.Activity 在清單文件中危activity添加屬性 android:hardwareAccelerated="true/false" 開啟或關(guān)閉
3.Window 只支持開啟

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

4.View 只支持關(guān)閉
在代碼中關(guān)閉

        setLayerType(LAYER_TYPE_SOFTWARE,null)

在布局文件中關(guān)閉

android:layerType="software"

文字

drawtext 相關(guān)四格線與基線 、 textAlign 與 FontMetrics

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

Paint 相關(guān)函數(shù)
void reset()   //重置畫筆
void setColor(@ColorInt int color)  // 設(shè)置畫筆顏色
void setAlpha(int a) // 設(shè)置畫筆透明度
void setStyle(Style style) // 設(shè)置畫筆樣式
void setStrokeWidth(float width) // 設(shè)置畫筆寬度
void setAntiAlias(boolean aa) // 設(shè)置畫筆是否抗鋸齒
void setStrokeMiter(float miter) //設(shè)置畫筆傾斜度
 PathEffect setPathEffect(PathEffect effect)  //設(shè)置路徑樣式
 void setStrokeCap(Cap cap)  // 設(shè)置線帽子=樣式
void setDither(boolean dither)  // 設(shè)置抗抖動(dòng)效果
    float measureText(String text)  //計(jì)算text所需要的width
    void getTextBounds(@NonNull CharSequence text, int start, int end, @NonNull Rect bounds)  //計(jì)算text所需要的最小矩形 并賦值到第三個(gè)參數(shù)

Paint的setStrokeCap掠抬、setStrokeJoin吼野、setPathEffect

貝濟(jì)埃曲線

http://www.reibang.com/p/1af5c3655fa3

path 有四個(gè)函數(shù)與貝濟(jì)埃曲線相關(guān)
二階貝濟(jì)埃曲線
void quadTo(float x1, float y1, float x2, float y2)
void rQuadTo(float dx1, float dy1, float dx2, float dy2)

三階貝濟(jì)埃曲線
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函數(shù)為例 x1,y1 是控制點(diǎn)坐標(biāo) x2,y2是終點(diǎn)坐標(biāo)
整條線的起點(diǎn)是通過moveTo函數(shù)來指定的 如果連續(xù)調(diào)用quadTo函數(shù)那么上一個(gè)函數(shù)的終點(diǎn)就是下一個(gè)函數(shù)的起點(diǎn)
如果沒有調(diào)用moveTo 默認(rèn)起點(diǎn)為左上角(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)
示例 傳統(tǒng)捕捉手勢(shì)軌跡
要實(shí)現(xiàn)手勢(shì)軌跡我們可以攔截onTouchEvent 然后使用Path.lineTo 函數(shù)即可
    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 最大的問題就是線段轉(zhuǎn)折處不夠平滑 把圖像放大兩點(diǎn)連接處會(huì)有很明顯的轉(zhuǎn)折

使用quadTo 實(shí)現(xiàn) 修改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 的四個(gè)參數(shù)都是針對(duì)上一個(gè)終點(diǎn)的坐標(biāo) 正值表示相加 負(fù)值表示相減

陰影效果

需要關(guān)閉硬件加速

 override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        var paint = Paint()
        paint.color = Color.BLACK
        paint.textSize = 25f
        // 需要關(guān)閉硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE,null)
        // 陰影的半徑  陰影x軸的偏移量  陰影y軸的偏移量  陰影的顏色(對(duì)圖片無效)
        paint.setShadowLayer(10f , 20f,20f, Color.GRAY)
//        清除陰影的兩種方法
//        paint.clearShadowLayer()
        // 將陰影半徑設(shè)為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

發(fā)光效果與圖片陰影 MaskFilter setMaskFilter(MaskFilter maskfilter)

需要關(guān)閉硬件加速
MaskFilter 有兩個(gè)子類
EmbossMaskFilter 浮雕效果 很少使用
BlurMaskFilter 發(fā)光效果
BlurMaskFilter 的構(gòu)造函數(shù) 需要傳入style BlurMaskFilter(float radius, Blur style)

Blur 枚舉 效果
NORMAL 內(nèi)外發(fā)光
SOLID 外發(fā)光
OUTER 僅顯示發(fā)光效果
INNER 內(nèi)發(fā)光
  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

給圖片設(shè)置純色陰影

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函數(shù)獲取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中的印章類似 用來實(shí)現(xiàn)圖片 顏色 漸變的 填充效果
// BitmapShader 基本用法

// BitmapShader(@NonNull Bitmap bitmap, @NonNull TileMode tileX, @NonNull TileMode tileY)
// TitleMode 取值如下
// CLAMP 用邊緣色彩來填充剩余空間
// REPEAT 重復(fù)圖像來填充空間
// MIRROR 重復(fù)使用鏡像圖像來填充空間

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的基線坐標(biāo)
        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

放大鏡效果

在手指觸摸的地方繪制一個(gè)圓 然后顯示該圓范圍內(nèi)的圖像


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"  否則只會(huì)觸發(fā)down事件 不會(huì)觸發(fā)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)

        // 該步驟是為了畫出一張能夠填滿整個(gè)控件大小的背景圖
        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"  否則只會(huì)觸發(fā)down事件 不會(huì)觸發(fā)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)

        // 該步驟是為了畫出一張能夠填滿整個(gè)控件大小的背景圖
        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

// 構(gòu)造函數(shù) 坐標(biāo)1到坐標(biāo)2 實(shí)現(xiàn) color1到color2的漸變
// LinearGradient(float x0, float y0, float x1, float y1, @ColorInt int color0, @ColorInt int color1,@NonNull TileMode tile)
// 構(gòu)造函數(shù) 坐標(biāo)1到坐標(biāo)2 實(shí)現(xiàn) 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  從一個(gè)點(diǎn)向周圍擴(kuò)散的漸變
//        雙色漸變
        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

// 混合模式相關(guān)知識(shí)是Paint繪圖中最難的部分 它可以將兩張圖片無縫結(jié)合,實(shí)現(xiàn)類似ps的效果
// 混合模式通過 Xfermode類 來實(shí)現(xiàn) paint.setXfermode

// 在使用Xfermode時(shí)需要做兩件事
// 完全不支持硬件加速
// 離屏繪制 canvas.saveLayer() 繪制代碼 canvas.restoreToCount()

//替換顏色 API 16之后無法使用
// paint.xfermode = AvoidXfermode()

// PorterDuffXfermode(PorterDuff.Mode mode) 16種模式可選
// paint.xfermode = PorterDuffXfermode()
// 可以實(shí)現(xiàn) 橡皮檫 刮刮卡等效果

圖像操縱大師Xfermode講解與實(shí)戰(zhàn)——Android高級(jí)UI

Canvas

// 如何獲取canvas對(duì)象
// 1重寫onDraw 用于繪制自身 dispatchDraw用于繪制子視圖
// 2 使用bitmap 創(chuàng)建 new Canvas(bitmap) 或者 Canvas c = new Canvas() c.setBitmap(bitmap)
// 3 SurfaceHolder.lockCanvas() 函數(shù)

這里我們關(guān)注兩個(gè)方法
saveLayer 與 restoreToCount
saveLayer 會(huì)生成一個(gè)新的圖層
之后所有的操作都在新的圖層上進(jìn)行
然后調(diào)用restoreToCount 將圖層合并到原圖層上

與saveLayer相關(guān)的還有 一個(gè)函數(shù) saveLayerAlpha 用于生成一個(gè)帶透明度的圖層

int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint,
@Saveflags int saveFlags) 最后一個(gè)參數(shù) saveFlags 有許多個(gè)選擇

flag 描述
ALL_SAVE_FLAG 保存所有的標(biāo)識(shí)
MATRIX_SAVE_FLAG 保存canvas的matrix信息
CLIP_SAVE_FLAG 保存canvas 的大小信息
HAS_ALPHA_LAYER_SAVE_FLAG 標(biāo)識(shí)新建的bmp具有透明度 在與上層畫布結(jié)合時(shí) 透明位置顯示上層圖像
FULL_COLOR_LAYER_SAVE_FLAG 標(biāo)識(shí)新建的bmp顏色完全獨(dú)立 在與上層畫布結(jié)合時(shí) 先清空上層畫布在進(jìn)行覆蓋
CLIP_TO_LAYER_SAVE_FLAG 在保存圖層前先把當(dāng)前畫布根據(jù)bounds裁剪

SurfaceView

概述 android 屏幕刷新的時(shí)間間隔是16ms 如果View能夠在16ms內(nèi)完成所需執(zhí)行的繪圖操作那么在視圖上就是流暢的,否則就會(huì)出現(xiàn)卡頓两波。 有時(shí)候箫锤,在自定義View的日志中,經(jīng)常會(huì)看到如下警告:
Skipped 60 frames! The application may be doing too much work on its main thread
之所以會(huì)出現(xiàn)警告 大部分是因?yàn)槲覀儾粌H在繪制過程中執(zhí)行了繪圖操作 也夾雜了很多邏輯處理 導(dǎo)致在指定的16ms內(nèi)沒有完成繪制
然而很多情況下這些邏輯操作處理又是必須的 為了解決這個(gè)問題 Android引入了SurfaceView雨女。
SurfaceView在兩個(gè)方面改進(jìn)了View的繪圖操作
1.使用雙緩沖技術(shù)
2.自帶畫布谚攒,支持在子線程中更新畫布內(nèi)容

所謂的雙緩沖技術(shù) ,簡(jiǎn)單的說就是多加一塊緩沖畫布氛堕, 當(dāng)需要執(zhí)行繪圖操作時(shí)馏臭,先在緩沖畫布上繪制,繪制好之后在將緩沖畫布上的內(nèi)容更新到主畫布上

SurfaceView 繼承自View 所以SurfaceView可以使用View中的所有方法讼稚,即用View實(shí)現(xiàn)的自定義控件都可以使用SurfaceView來實(shí)現(xiàn) 括儒。

使用SurfaceView來實(shí)現(xiàn)捕捉用戶手勢(shì)軌跡的自定義控件
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)
    }
}

我們會(huì)發(fā)現(xiàn)上述代碼沒有反應(yīng) 通過添加日志我們可以發(fā)現(xiàn) onDraw方法并沒有被執(zhí)行
然后我們把其中一行注釋打開 就可以了 setWillNotDraw(false)

setWillNotDraw

這個(gè)函數(shù)存在于View類中, 它主要用在View派生子類的初始化中锐想, 如果 設(shè)置為true 表示當(dāng)前控件沒有繪制內(nèi)容帮寻,當(dāng)屏幕重繪時(shí),這個(gè)控件不需要繪制 所以在重繪的時(shí)候也不會(huì)調(diào)用這個(gè)類的onDraw函數(shù) 相反如果設(shè)置為false 則表示當(dāng)前控件在每次重繪時(shí)都需要繪制該控件
可見setWillNotDraw 是一種優(yōu)化策略赠摇,它可以顯示的告訴系統(tǒng)固逗,在重繪屏幕時(shí)浅蚪,哪個(gè)控件需要重繪,哪個(gè)控件不需要重繪烫罩,這樣就可以大大提高重繪效率
一般而言惜傲,像LinearLayout、relativelayout 等布局控件贝攒,它們的主要功能是布局其中的控件盗誊,它們本身是不需要繪制的, 所以它們?cè)跇?gòu)造的時(shí)候都會(huì)顯式地設(shè)置
setWillNotDraw(true) 而上面SurfaceView重繪時(shí),之所以沒有調(diào)用onDraw函數(shù) 是因?yàn)镾urfaceView在初始化的時(shí)候顯式調(diào)用了setWillNotDraw(true)

雖然我們找到了原因 但是從這里也看的出來SurfaceView的開發(fā)人員并不想讓我們通過重寫onDraw方法來繪制界面 而要使用SurfaceView獨(dú)有的特性來操作畫布

使用SurfaceView來實(shí)現(xiàn)捕捉用戶手勢(shì)軌跡的自定義控件 方法二
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
//監(jiān)聽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時(shí)postInvalidate + onDraw
方法二 ACTION_MOVE時(shí) 調(diào)用 drawCanvas

SurfaceView移動(dòng)背景


class View10SurfaceViewBg : SurfaceView {

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

    //  圖片移動(dòng)的距離
    var bitPosX = 0f
    lateinit var mCanvas : Canvas
    lateinit var thread : Thread

    //  圖片移動(dòng)的方向
    enum class State{
        LEFT ,RIGHT
    }
     var state = State.LEFT

    // 圖片每一步移動(dòng)的單位
    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) {
                // 設(shè)置監(jiān)聽 創(chuàng)建時(shí)啟動(dòng)
                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 雙緩沖技術(shù)

/**

  • 依次繪制 0-9 按照我們的邏輯一共有兩塊畫布 那么兩塊畫布依次繪制的是 0 2 4 6 8 和 1 3 5 7 9
  • 因?yàn)樽詈罄L制的是9 那么最后顯示在屏幕上的應(yīng)該是1 3 5 7 9 但是實(shí)際最后顯示的是 0 3 6 9
  • 為什么是0 3 6 9 那是因?yàn)檫@里有三塊緩沖畫布
  • 假如我們?cè)诿看卫L制完數(shù)字的時(shí)候 讓線程休息一段時(shí)間 那么就可以看見每次繪制的數(shù)字了 這時(shí)最后顯示的是 1 3 5 7 9 說明此時(shí)只有兩塊畫布
  • 根據(jù)官方的解釋 緩沖畫布的數(shù)量是根據(jù)需求動(dòng)態(tài)分配的 如果用戶獲取畫布的頻率較慢哈踱,那么分配兩塊緩沖畫布 。 否則將分配3的倍數(shù)快緩沖畫布 具體數(shù)量由實(shí)際情況而定
  • 總的來說 Surface肯定會(huì)配分配>=2個(gè)緩沖區(qū)域 具體分配多少由實(shí)際情況而定
  • 雙緩沖局部更新 holder.lockCanvas(Rect())
  • lockCanvas 用于獲取整屏畫布 當(dāng)前屏幕顯示的內(nèi)容不會(huì)被更新到新畫布上
  • lockCanvas(Rect()) 用于獲取指定區(qū)域的畫布 梨熙, 畫布以外的區(qū)域會(huì)與屏幕內(nèi)容一致 畫布以內(nèi)的區(qū)域依然保持對(duì)應(yīng)的緩沖畫布的內(nèi)容

*/

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()) 則需要先對(duì)緩沖屏幕進(jìn)行清屏 不然我們拿到的畫圖區(qū)域不一定是我們指定的區(qū)域

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
    }
}

這是因?yàn)橛幸粔K畫布初始化的被顯示在了屏幕上 嚣鄙,已經(jīng)被默認(rèn)填充為黑色 而另外兩塊畫布都還沒有被畫過
雖然我們指定了獲取畫布的區(qū)域范圍,但是系統(tǒng)認(rèn)為 整塊畫布都是臟區(qū)域串结, 都應(yīng)該被畫上哑子,所以會(huì)返回屏幕大小的畫布
只有我們將每塊畫布都畫過以后, 才會(huì)按照我們指定的區(qū)域來返回畫布大小

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末肌割,一起剝皮案震驚了整個(gè)濱河市卧蜓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌把敞,老刑警劉巖弥奸,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異奋早,居然都是意外死亡盛霎,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門耽装,熙熙樓的掌柜王于貴愁眉苦臉地迎上來愤炸,“玉大人,你說我怎么就攤上這事掉奄」娓觯” “怎么了?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵姓建,是天一觀的道長(zhǎng)诞仓。 經(jīng)常有香客問我,道長(zhǎng)速兔,這世上最難降的妖魔是什么墅拭? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮涣狗,結(jié)果婚禮上谍婉,老公的妹妹穿的比我還像新娘舒憾。我一直安慰自己,他們只是感情好屡萤,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布珍剑。 她就那樣靜靜地躺著掸宛,像睡著了一般死陆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上唧瘾,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天措译,我揣著相機(jī)與錄音,去河邊找鬼饰序。 笑死领虹,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的求豫。 我是一名探鬼主播塌衰,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼蝠嘉!你這毒婦竟也來了最疆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤蚤告,失蹤者是張志新(化名)和其女友劉穎努酸,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體杜恰,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡获诈,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了心褐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片舔涎。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖逗爹,靈堂內(nèi)的尸體忽然破棺而出终抽,到底是詐尸還是另有隱情,我是刑警寧澤桶至,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布昼伴,位于F島的核電站,受9級(jí)特大地震影響镣屹,放射性物質(zhì)發(fā)生泄漏圃郊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一女蜈、第九天 我趴在偏房一處隱蔽的房頂上張望持舆。 院中可真熱鬧色瘩,春花似錦、人聲如沸逸寓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽竹伸。三九已至泥栖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間勋篓,已是汗流浹背吧享。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留譬嚣,地道東北人钢颂。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像拜银,于是被迫代替她去往敵國(guó)和親殊鞭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 【Android 自定義View之繪圖】 基礎(chǔ)圖形的繪制 一尼桶、Paint與Canvas 繪圖需要兩個(gè)工具操灿,筆和紙。...
    maiduoduo閱讀 3,190評(píng)論 0 3
  • 【Android 自定義View之繪圖】 基礎(chǔ)圖形的繪制 一疯汁、Paint與Canvas 繪圖需要兩個(gè)工具牲尺,筆和紙。...
    Rtia閱讀 11,650評(píng)論 5 34
  • 1幌蚊、常用方法 方法描述方法描述setARGB設(shè)置繪制的顏色谤碳,a代表透明setAlpha設(shè)置繪制圖形的透明度setC...
    Active_Loser閱讀 553評(píng)論 0 0
  • 前言 View的繪制相當(dāng)于我們平時(shí)根據(jù)需求去畫出各種圖案,而Canvas 和 Paint 就像我們平時(shí)畫畫需要的畫...
    Active_Loser閱讀 632評(píng)論 0 0
  • 前言 自定義 View 有幾種實(shí)現(xiàn)類型溢豆,分別為: 繼承自 View 完全自定義蜒简; 繼承自現(xiàn)有控件(如 ImageV...
    Little丶Jerry閱讀 617評(píng)論 0 0