查看原文
其他

安卓自定义view中绘画几何图形和文字及圆角ImageView图片等api使用及举例

汪栋邢 搜狐技术产品 2021-01-15


  

本文字数:9953

预计阅读时间:25分钟


前言

本文旨在介绍自定义View的实现及流程着重处介绍安卓中的Canvas的绘制方法,让你从容面对各种view 并附带自定义圆角ImageView,和图片压缩处理等功能。



View 绘制的三个流程


     在自定义View的时候一般需要重写父类的onMeasure()、onLayout()、onDraw()三个方法,来完成视图的展示过程。当然,这三个暴露给开发者重写的方法只不过是整个绘制流程的冰山一角,更多复杂的幕后工作,都让系统给代劳了。
     一个完整的绘制流程包括measure、layout、draw三个步骤,其中:

     measure:测量。系统会先根据xml布局文件和代码中对控件属性的设置,来获取或者计算出每个View和ViewGrop的尺寸,并将这些尺寸保存下来。

     layout:布局。根据测量出的结果以及对应的参数,来确定每一个控件应该显示的位置。

     draw:绘制。确定好位置后,就将这些控件绘制到屏幕上。
     
     重中之重:draw

Canvas 简单理解:画布
Canvas绘图有三个基本要素:Canvas、绘图坐标系以及Paint。
Canvas是画布,我们通过Canvas的各种drawXXX方法将图形绘制到Canvas上面,在drawXXX方法中我们需要传入要绘制的图形的坐标形状,还要传入一个画笔Paint。
基础知识
Canvas的常用操作速查表
操作类型
相关API
备注



绘制颜色
drawColor, drawRGB, drawARGB
使用单一颜色填充整个画布
绘制基本形状
drawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc
依次为 点、线、矩形、圆角矩形、椭圆、圆、圆弧
绘制图片
drawBitmap, drawPicture
绘制位图和图片
绘制文本
drawText, drawPosText, drawTextOnPath
依次为 绘制文字、绘制文字时指定每个文字位置、根据路径绘制文字
绘制路径
drawPath
绘制路径,绘制贝塞尔曲线时也需要用到该函数
顶点操作
drawVertices, drawBitmapMesh
通过对顶点操作可以使图像形变,drawVertices直接对画布作用、 drawBitmapMesh只对绘制的Bitmap作用
画布剪裁
clipPath, clipRect
设置画布的显示区域
画布快照
save, restore, saveLayerXxx, restoreToCount, getSaveCount
依次为 保存当前状态、 回滚到上一次保存的状态、 保存图层状态、 回滚到指定状态、 获取保存次数
画布变换
translate, scale, rotate, skew
依次为 位移、缩放、 旋转、错切
Matrix(矩阵)
getMatrix, setMatrix, concat
实际画布的位移,缩放等操作的都是图像矩阵Matrix,只不过Matrix比较难以理解和使用,故封装了一些常用的方法。



对应的API进行详解和举例使用


绘制单点、多点

基本思路继承VIew-重写onDraw方法-进行绘制 

//获取画笔,设置画笔属性 

//调起画布canvas的drawPoint方法输入相应的参数即可绘画出对应的点。


Paint对于Text的相关设置 

1.普通设置

paint.setStrokeWidth(5):设置画笔宽度; 

paint.setAntiAlias(true):设置是否使用抗锯齿功能,如果使用,会导致绘图速度变慢; 

paint.setStyle(Paint.Style.FILL):设置绘图样式,对于设置文字和几何图形都有效,可取值有三种 :

    1、Paint.Style.FILL:填充内部 
    2、Paint.Style.FILL_AND_STROKE:填充内部和描边 
    3、Paint.Style.STROKE:仅描边
    paint.setTextAlign(Align.CENTER):设置文字对齐方式
    paint.setTextSize(12):设置文字大小

2.样式设置
    paint.setFakeBoldText(true):设置是否为粗体文字
    paint.setUnderlineText(true):设置下划线
    paint.setTextSkewX((float) -0.25):设置字体水平倾斜度,普通斜体字是 -0.25
    paint.setStrikeThruText(true):设置带有删除线效果

3.其他设置
    paint.setTextScaleX(2):设置水平拉伸,高度不会变

下面是绘制的相应内容图例展示:


详细代码实现


首先准备画笔工作——关于画笔Paint需强调说明一下onDraw方法中不要创建Paint等对象,因为这个方法是多次调用的;
所以我这边做了一个处理在项目初起就已经把Paint进行全局初始化,然后在其他地方调用的时候,直接设其属性即可。
代码如下:
`lateinit var projectResources: ProjectResources

class ProjectResources(private val resources: Resources) {

    val paintLight by lazy {
        getBasePaint().apply {
            color = Color.LTGRAY
            alpha = 128
            strokeWidth = resources.dpToPx(2)
            textSize = resources.dpToPx(30)
        }
    }

    val paintDark by lazy {
        getBasePaint().apply {
            color = Color.BLACK
            alpha = 128
            strokeWidth = resources.dpToPx(4)
            textSize = resources.dpToPx(30)
        }
    }

    private fun getBasePaint(): Paint {
        return Paint().apply {
            style = Paint.Style.STROKE
            strokeCap = Paint.Cap.ROUND
            strokeJoin = Paint.Join.ROUND
            isAntiAlias = true
            textAlign = Paint.Align.CENTER
            textSize = resources.dpToPx(30)
        }
    }
}`



🔺绘制单点 drawPoint🔺
/**
 * 绘制单点  drawPoint
 */
class DrawPointView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawPoint(
            0f + paddingStart,//x坐标
            0f + paddingTop,//Y坐标
            getPaint()//画笔paint
        )
    }
//获取画笔 设置画笔属性
    private fun getPaint(): Paint {
        return Paint().apply {
            style = Paint.Style.STROKE
            strokeCap = Paint.Cap.ROUND
            strokeJoin = Paint.Join.ROUND
            isAntiAlias = true
            textAlign = Paint.Align.CENTER
            textSize = resources.dpToPx(30)
            color = Color.GREEN//画笔颜色
            strokeWidth = resources.dpToPx(16)//画笔宽度
        }
    }
}
说明:文中贴出来的代绘制过程中在ondraw调用getPaint方法,是为了更直观的展示出来,看到必须设这个属性其实demo中都已经做了全局处理,不会说每次都会创建paint这个对象。
🔺绘制多点 drawPoints🔺
/**
 * 绘制多点  drawPoints
 */
class DrawPointsView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    override fun onDraw(canvas: Canvas) {

        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawPoints(
            listOf(
                0f + paddingStart, 0f + paddingTop,//第1个点的 x,y
                ((width - paddingEnd)).toFloat(), 0f + paddingTop, //第2个点的 x,y
                ((width - paddingEnd)/2).toFloat(),((height - paddingBottom)/2).toFloat(), //第3个点的 x,y
                0f + paddingStart, ((height - paddingBottom)).toFloat(), //第4个点的 x,y
                (width - paddingEnd).toFloat(), (height - paddingBottom).toFloat() //第5个点的 x,y
            ).toFloatArray(),
            projectResources.paintPoint
        )
    }
}


线


🔺绘制直线 drawLine🔺

//绘制线条  drawLine
class DrawLineView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawLine(
            0f + paddingStart,//起始位置 x坐标
            0f + paddingTop,//起始位置 y坐标
            (width - paddingEnd).toFloat(),//终点位置  X坐标
            (height - paddingBottom).toFloat(),//终点位置 Y坐标
            projectResources.paint// 获取画笔
        )
    }
}

🔺同时绘制多条线🔺
 drawLines
/**
 * 同时绘制多条线(1) drawLines
 */

class DrawLinesView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawLines(
            listOf(
                //第一条线的起始x坐标和Y坐标  末尾位置的X坐标和Y坐标
                0f + paddingStart, 0f + paddingTop, (width / 2f), (height - paddingBottom).toFloat(),
                //第二条线的起始x坐标和Y坐标  末尾位置的X坐标和Y坐标
                width / 2f, (height - paddingBottom).toFloat(), (width - paddingEnd).toFloat(), (0f + paddingStart),
                //第三条线的起始x坐标和Y坐标  末尾位置的X坐标和Y坐标
                    (width - paddingEnd).toFloat(), (0f + paddingStart),    width / 2f, (height/2f).toFloat()
                ).toFloatArray(),
            projectResources.paint//画笔
        )
    }
}

🔺同时绘制多条线🔺
//使用指定的画笔绘制指定的路径线路)。路径(线路)将根据绘画的风格填充或装裱。
两条线交
同时绘制多条线(2)
//使用指定的画笔绘制指定的路径(线路)。路径(线路)将根据绘画的风格填充或装裱。
两条线交叉
class DrawPathView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    private val path by lazy {
        val path = Path()
        path.moveTo(0f + paddingStart, 0f + paddingTop)
        path.lineTo((width - paddingEnd).toFloat(), (height - paddingBottom).toFloat())
        path.moveTo(0f + paddingStart, (height - paddingBottom).toFloat())
        path.lineTo((width - paddingEnd).toFloat(), 0f + paddingTop)
        path
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        //  * Draw the specified path using the specified paint. The path will be filled or framed based on
        //     * the Style in the paint.
        canvas.drawPath(path, projectResources.paint)
    }
}

🔺绘画圆形图 drawCircle🔺
/**
 * 绘画圆形图  drawCircle
 */
class DrawCircleView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawCircle(
            width/2f,//圆点中心 X坐标
            height/2f,//圆点中心 Y坐标
            height/2f - paddingTop,//圆半径
            projectResources.paint//画笔
        )
    }
}

🔺绘制矩形框 drawRect🔺
确定确定一个矩形最少需要四个数据,就是对角线的两个点的坐标值,这里一般采用左上角和右下角的两个点的坐标。
/**
 * 绘制矩形框  drawRect
确定确定一个矩形最少需要四个数据,就是对角线的两个点的坐标值,这里一般采用左上角和右下角的两个点的坐标。
 */
class DrawRectView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {


    private val rect by lazy {
        Rect(
            0 + paddingStart,//左
            0 + paddingTop,//顶
            width - paddingEnd,//右
            height - paddingBottom//底
        )
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawRect(rect, projectResources.paint)
    }

}


🔺绘制圆角矩形框 drawRoundRect🔺
class DrawRoundRectView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {


    private val rect by lazy {
        RectF(
            0f + paddingStart,
            0f + paddingTop,
            (width - paddingEnd).toFloat(),
            (height - paddingBottom).toFloat()
        )
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawRoundRect(rect, resources.dpToPx(10), resources.dpToPx(10), projectResources.paint)
    }
}

🔺绘制椭圆框 drawOval🔺
//绘制椭圆框  drawOval
class DrawOvalView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    private val rect by lazy {
        RectF(
           0f + paddingStart,//包围椭圆的矩形左上角端点x
            0f + paddingTop,//包围椭圆的矩形左上角端点Y
            width.toFloat() - paddingEnd,//椭圆的高度
            height.toFloat() - paddingBottom//椭圆的宽度  当高度和宽度相等时 就是圆
        )
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawOval(rect, projectResources.paint)
    }
}

🔺扇形绘制 (椭圆和圆的)🔺
Canvas中提供了drawArc方法用于绘制弧,这里的弧指两种:弧面和弧线,弧面即用弧围成的填充面,弧线即为弧面的轮廓线。 
api参数解析:
oval是RecF类型的对象,其定义了椭圆的形状。 
startAngle指的是绘制的起始角度,如果传入的startAngle小于0或者大于等于360,那么用startAngle对360进行取模后作为起始绘制角度。 
sweepAngle指的是从startAngle开始沿着钟表的顺时针方向旋转扫过的角度。如果sweepAngle大于等于360,那么会绘制完整的椭圆弧。如果sweepAngle小于0,那么会用sweepAngle对360进行取模后作为扫过的角度。 
useCenter是个boolean值,如果为true,表示在绘制完弧之后,用椭圆的中心点连接弧上的起点和终点以闭合弧;如果值为false,表示在绘制完弧之后,弧的起点和终点直接连接,不经过椭圆的中心点。
class DrawArcView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    private val rect by lazy {
        RectF(
            0f + paddingStart,
            0f + paddingTop,
            height.toFloat() - paddingBottom,
            height.toFloat() - paddingBottom
        )//// 设置方形区域
    }

    private val rect2 by lazy {
        RectF(
            0f + paddingStart,
            0f + paddingTop,
            width.toFloat() - paddingEnd,
            height.toFloat() - paddingBottom
        )//// 设置矩形区域
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        /* 设置渐变色 这个正方形的颜色是改变的 */
        val mShader = LinearGradient(
            0f,
            0f,
            100f,
            100f,
            intArrayOf(Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.LTGRAY),
            null,
            Shader.TileMode.REPEAT
        ) // 一个材质,打造出一个线性梯度沿著一条线。
        projectResources.paint.shader = mShader
        if (width == 0 || height == 0) return
        canvas.drawArc(rect,
            0f,//从哪个弧度开始
            330f,//扇形弧度
            true, //是否经过圆心
            projectResources.paint)
        canvas.drawArc(rect2,
            0f,//从哪个弧度开始
            30f,//扇形弧度
            true, //是否经过圆心
            projectResources.paint)
    }
}


文字绘制


在Android开发中,Canvas.drawText不会换行,即使一个很长的字符串也只会显示一行,超出部分会隐藏在屏幕之外。

class DrawTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    companion object {
        private const val TEXT = "三国演义-西游记-"
    }
    private val textBound by lazy {
        val textBound = Rect()
        projectResources.paint.getTextBounds(TEXT, 0, TEXT.length, textBound)
        textBound
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawText(TEXT,//:要绘制的文字
            width/2f,//绘制原点x坐标
            height/2f - textBound.exactCenterY(),//绘制原点y坐标   基线的位置。
            projectResources.paint)
    }
}

🔺可换行的文字🔺  
StaticLayout是android中处理文字的一个工具类,StaticLayout 处理了文字换行的问题。

class DrawStaticLayoutTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    companion object {
        private const val TEXT = "Too long a line to display in one line, and let it auto wrap go to next line.\nCan handle \"\\n\" as well."
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return

        val staticLayout =
            StaticLayout.Builder.obtain(
                TEXT, 0, TEXT.length, projectResources.textPaint, width
            ).build()

        canvas.save()
        canvas.translate(width / 2f, (height - staticLayout.height)/2f)
        staticLayout.draw(canvas)
        canvas.restore()
    }
}

🔺自定义每个文字的位置🔺
class DrawPosTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    companion object {
        private const val TEXT = "Testing 123"
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawPosText(TEXT.toCharArray(),//:要绘制的文字数组
            1, //第一个要绘制的文字的索引
            3,//要绘制的文字的个数,用来算最后一个文字的位置,从第一个绘制的文字开始算起
            listOf(width/1.5f, height/1.5f,
                width/2f, height/2f,
                width/3f, height/3f).toFloatArray(),//每个字体的位置,同样两两一组,
            projectResources.paint)
    }
}

🔺绘制文字——沿着路线走🔺
class DrawTextOnPathView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    companion object {
        private const val TEXT = "Test 123"
    }

    private val path by lazy {
        val path = Path()
        path.moveTo(0f + paddingStart, 0f + paddingTop)
        path.lineTo((width/2f), (height - paddingBottom).toFloat())
        path.lineTo((width - paddingEnd).toFloat(), 0f + paddingTop)
        path
    }

    private val textBound by lazy {
        val textBound = Rect()
        projectResources.paint.getTextBounds(TEXT, 0, TEXT.length, textBound)
        textBound
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawPath(path, projectResources.paintLight)
        canvas.drawTextOnPath(TEXT, //文字
            path,//路径 轨迹
            0f,//与路径起始点的水平偏移距离
            -textBound.exactCenterY(),//与路径中心的垂直偏移量
            projectResources.paint)
    }
}

🔺将图片直接绘制到canvas🔺
class DrawBitmapView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    private val bitmap by lazy {
        BitmapFactory.decodeResource(resources, R.drawable.image)
    }

    private val rect by lazy {
        Rect(0 + paddingLeft, 0 + paddingTop, width - paddingRight, height - paddingBottom)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return

        canvas.drawBitmap(bitmap, null, rect, null)
    }
}

🔺用颜色充填canvas🔺
class DrawColorView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawColor(context.getColor(R.color.colorPrimary))
    }
}

🔺用RGB颜色充填🔺
class DrawRGBView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawRGB(255, 0 , 0)
    }
}

🔺图片和颜色重合🔺
class DrawARGBView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    private val bitmap by lazy {
        BitmapFactory.decodeResource(resources, R.drawable.image)
    }

    private val rect by lazy {
        Rect(0 + paddingLeft, 0 + paddingTop, width - paddingRight, height - paddingBottom)
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return

        canvas.drawBitmap(bitmap, null, rect, null)
        canvas.drawARGB(128, 255, 0 , 0)
    }
}

🔺用pain笔的颜色去填充整个画布🔺
class DrawPaintView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    companion object {
        private const val TEXT = "Testing 123"
    }
    private val gradientPaint by lazy {
        Paint().apply {
            shader = RadialGradient(
                width/2f,
                height/2f,
                height/2f,
                Color.GREEN,
                Color.RED,
                Shader.TileMode.MIRROR
            )
        }
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawPaint(gradientPaint)
    }
}

🔺图像形变🔺
通过对顶点操作可以使drawBitmapMesh只对绘制Bitmap作用
//通过对顶点操作可以使图像形变, drawBitmapMesh只对绘制的Bitmap作用
class DrawBitmapMeshView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    private val bitmap by lazy {
        BitmapFactory.decodeResource(resources, R.drawable.santou)
    }

    private val firstX by lazy { 0f + paddingLeft }
    private val firstY by lazy { 0f + paddingTop }
    private val secondX by lazy { width/5f }
    private val secondY by lazy { height/3f }
    private val thirdX by lazy { width.toFloat() - paddingRight }
    private val thirdY by lazy { height.toFloat() - paddingBottom }


    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return

        canvas.drawBitmapMesh(bitmap, 2, 2,
            floatArrayOf(
                firstX, firstY, secondX, firstY, thirdX, firstY,
                firstX, secondY, secondX, secondY, thirdX, secondY,
                firstX, thirdY, secondX, thirdY, thirdX, thirdY),
            0, null, 0, null)

        canvas.drawLine(firstX, firstY, firstX, thirdY, projectResources.paintLight)
        canvas.drawLine(secondX, firstY, secondX, thirdY, projectResources.paintLight)
        canvas.drawLine(thirdX, firstY, thirdX, thirdY, projectResources.paintLight)

        canvas.drawLine(firstX, firstY, thirdX, firstY, projectResources.paintLight)
        canvas.drawLine(firstX, secondY, thirdX, secondY, projectResources.paintLight)
        canvas.drawLine(firstX, thirdY, thirdX, thirdY, projectResources.paintLight)
    }
}

对图片进行放大切割,及对应点可以给各个顶点颜色纹理渲染
// 通过对顶点操作可以使图像形变,drawVertices直接对画布作用、
class DrawVerticesView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    companion object {
        const val DUMMYCOLOR =  -0x100000
    }

    val paint = Paint().apply {
        val bitmap = BitmapFactory.decodeResource(
            resources, R.drawable.santou)
        val shader = BitmapShader(
            bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
        this.shader = shader
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return

        setLayerType(View.LAYER_TYPE_SOFTWARE, null)
        val indices = shortArrayOf(0, 1, 2, 0, 2, 3)

        val verticesColors = intArrayOf(
            Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW,
            DUMMYCOLOR, DUMMYCOLOR, DUMMYCOLOR, DUMMYCOLOR, DUMMYCOLOR, DUMMYCOLOR)

        val verts = floatArrayOf(width/2f, 0f, 0f, height/2f, width/2f, height.toFloat(), width.toFloat(), height/2f)

        canvas.drawVertices(
            Canvas.VertexMode.TRIANGLES,//顶点类型    比如他是三角形(连续3个顶点)或者 四边形  (连续4个顶点)等等
            verts.size,//顶点数   总共有多少个顶点绘制。
            verts, //顶点数组  [0,0,0,1,1,0,...]  前面有xy 3组 如果是类型是三角形 他就构成一个三角形的绘制基元,往后类推。
            0,//顶点数据 起始位置  可能全部绘制,也可能只绘制部分顶点。与 vertexCount 配置使用 一般为0
            verts,//   纹理数组  就是对图片等进行采样,然后去渲染顶点。
            0,//同上offset  就是偏移量
            verticesColors, //   颜色数组  直接用颜色渲染顶点
            0,//同上offset  就是偏移量
            indices,// 顶点索引 可能只绘制部分顶点 这个就是存放那些顶点的index ,  即verts[index]
            0,// 同上offset  就是偏移量
            indices.size,//绘制多少个索引点。
            paint
        )
    }
}

对图片进行缩小和放大,位移等等操作
//如果想对绘制位置和比例进行控制用Canvas提供的drawPicture方法绘制
//如果想对绘制位置和比例进行控制用Canvas提供的drawPicture方法绘制
class DrawPictureView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    private val bitmap by lazy {
        BitmapFactory.decodeResource(resources, R.drawable.santou)
    }

    private val rect by lazy {//将高减为一般半,绘制出的Picture在y轴方向压缩了一半,也就是rect并不是对图形进行裁剪,而是缩小或放大,
        Rect(0 + paddingLeft, 0 + paddingTop, width - paddingRight, (height - paddingBottom)/2)
    }
    private val rect1 by lazy {//看到绘制的形状未变,只不过在y轴上上移了200px,
        Rect(0 + paddingLeft, 200, width - paddingRight, 200+height - paddingBottom)
    }

    private val picture by lazy {
        val picture = Picture()
        val pCanvas = picture.beginRecording(width, height)//开始录制,在返回的Canvas上进行绘制
        pCanvas.drawBitmap(bitmap, null, rect, null)
        picture.endRecording()//结束录制
        picture
    }
    private val picture1 by lazy {
        val picture = Picture()
        val pCanvas = picture.beginRecording(width, height)//开始录制,在返回的Canvas上进行绘制
        pCanvas.drawBitmap(bitmap, null, rect1, null)
        picture.endRecording()//结束录制
        picture
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawPicture(picture)
        canvas.drawPicture(picture1)
    }
}
以上即是调用onDraw方法绘制View的具体内容。

下面再对bitmap着重说明一下,大家应该对Bitmap这个词有很深的印象,做不好处理,就会经常在你的BUGLIST中出现OOM字段,内存溢出,app莫名其妙的就死了,奔溃掉了,最后看到日志才发现问题所在。 
下面介绍一下笔者项目中是如何处理这一块内容的:
先说一下情景——笔者这边有一个图片搜索功能,简单来说就是根据图片搜索出对应的商品,用户可以拍照,从相册选取。 
这其中包括图片存储,压缩,上传,删除等一系列操作: 
文中会根据图片路径然后先去判断该图片的大小像素等等然后去压缩图片: 
(1)图片比例压缩
 //图片按比例大小压缩方法(根据路径获取图片并压缩)
    private static Bitmap getImage(String srcPath) {
        BitmapFactory.Options newOpts = new BitmapFactory.Options();
        newOpts.inJustDecodeBounds = true;
        Bitmap bitmap = BitmapFactory.decodeFile(srcPath, newOpts);// 此时返回bm为空

        newOpts.inJustDecodeBounds = false;
        int w = newOpts.outWidth;
        int h = newOpts.outHeight;
        // 现在主流手机比较多是800*480分辨率,所以高和宽我们设置为
        float hh = 800f;// 这里设置高度为800f
        float ww = 480f;// 这里设置宽度为480f
        // 缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
        int be = 1;// be=1表示不缩放
        if (w > h && w > ww) {// 如果宽度大的话根据宽度固定大小缩放
            be = (int) (newOpts.outWidth / ww);
        } else if (w < h && h > hh) {// 如果高度高的话根据宽度固定大小缩放
            be = (int) (newOpts.outHeight / hh);
        }
        if (be <= 0)
            be = 1;
        newOpts.inSampleSize = be;// 设置缩放比例
        // 重新读入图片,注意此时已经把options.inJustDecodeBounds 设回false
        bitmap = BitmapFactory.decodeFile(srcPath, newOpts);
        return compressImage(bitmap);// 压缩好比例大小后再进行质量压缩
    }`

(2)图片质量压缩
 /**
     * 质量压缩方法
     *
     * @param image
     * @return
     */
    private static Bitmap compressImage(Bitmap image) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        int options = 100;
        while (baos.toByteArray().length / 1024 > 100) {
            baos.reset();
            image.compress(Bitmap.CompressFormat.JPEG, options, baos);
            options -= 10;
        }
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());
        Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);
        return bitmap;
    }


(3)将压缩的图片保存至本地
 /**
     * 将压缩的bitmap保存到SDCard卡临时文件夹,用于上传
     *
     * @param filename
     * @param bit
     * @return
     */
    private static String saveMyBitmap(String filename, Bitmap bit) {
        String baseDir = Environment.getExternalStorageDirectory().getAbsolutePath() + "/chdd/";
        String filePath = baseDir + filename;
        File dir = new File(baseDir);
        if (!dir.exists()) {
            dir.mkdir();
        }

        File f = new File(filePath);
        try {
            f.createNewFile();
            FileOutputStream fOut = null;
            fOut = new FileOutputStream(f);
            bit.compress(Bitmap.CompressFormat.PNG, 100, fOut);
            fOut.flush();
            fOut.close();
        } catch (IOException e1) {
            e1.printStackTrace();
        }

        return filePath;
    }


(4)最后在适当的时候清楚缓存文件和删除图片
 /**
     * 清除缓存文件
     */
    public static void deleteCacheFile() {
        File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/chdd/");
        RecursionDeleteFile(file);
    }

    /**
     * 递归删除
     */
    private static void RecursionDeleteFile(File file) {
        if (file.isFile()) {
            file.delete();
            return;
        }
        if (file.isDirectory()) {
            File[] childFile = file.listFiles();
            if (childFile == null || childFile.length == 0) {
                file.delete();
                return;
            }
            for (File f : childFile) {
                RecursionDeleteFile(f);
            }
            file.delete();
        }
    }

下面还做了一个自定义圆角图形的类,继承自ImageView,可以在布局中直接使用,如下
  <com.hexun.logic.widget.RoundImageView
                android:id="@+id/ivAvatar"
                android:layout_width="60dp"
                android:layout_height="60dp"
                android:layout_centerVertical="true"
                android:layout_marginLeft="@dimen/space_20"
                android:src="@drawable/personal_t" />

下面是代码实现:
下面是圆形图像的代码,实现自定义弧度圆角——调整方法setUp()中的 mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f); 这行代码即可,文中写的横竖一样,所以是绘制出圆形图像。
‘public class RoundImageView extends ImageView {

    private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;

    private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
    private static final int COLORDRAWABLE_DIMENSION = 2;

    private static final int DEFAULT_BORDER_WIDTH = 0;
    private static final int DEFAULT_BORDER_COLOR = Color.BLACK;
    private static final int DEFAULT_FILL_COLOR = Color.TRANSPARENT;
    private static final boolean DEFAULT_BORDER_OVERLAY = false;

    private final RectF mDrawableRect = new RectF();
    private final RectF mBorderRect = new RectF();

    private final Matrix mShaderMatrix = new Matrix();
    private final Paint mBitmapPaint = new Paint();
    private final Paint mBorderPaint = new Paint();
    private final Paint mFillPaint = new Paint();

    private int mBorderColor = DEFAULT_BORDER_COLOR;
    private int mBorderWidth = DEFAULT_BORDER_WIDTH;
    private int mFillColor = DEFAULT_FILL_COLOR;

    private Bitmap mBitmap;
    private BitmapShader mBitmapShader;
    private int mBitmapWidth;
    private int mBitmapHeight;

    private float mDrawableRadius;
    private float mBorderRadius;

    private ColorFilter mColorFilter;

    private boolean mReady;
    private boolean mSetupPending;
    private boolean mBorderOverlay;

    public RoundImageView(Context context) {
        super(context);
        init();
    }

    public RoundImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RoundImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0);

        mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_civ_border_width, DEFAULT_BORDER_WIDTH);
        mBorderColor = a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR);
        mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY);
        mFillColor = a.getColor(R.styleable.CircleImageView_civ_fill_color, DEFAULT_FILL_COLOR);

        a.recycle();

        init();
    }

    private void init() {
        super.setScaleType(SCALE_TYPE);
        mReady = true;

        if (mSetupPending) {
            setup();
            mSetupPending = false;
        }
    }

    @Override
    public ScaleType getScaleType() {
        return SCALE_TYPE;
    }

    @Override
    public void setScaleType(ScaleType scaleType) {
        if (scaleType != SCALE_TYPE) {
            throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType));
        }
    }

    @Override
    public void setAdjustViewBounds(boolean adjustViewBounds) {
        if (adjustViewBounds) {
            throw new IllegalArgumentException("adjustViewBounds not supported.");
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mBitmap == null) {
            return;
        }

        if (mFillColor != Color.TRANSPARENT) {
            canvas.drawCircle(getWidth() / 2.0f, getHeight() / 2.0f, mDrawableRadius, mFillPaint);
        }
        canvas.drawCircle(getWidth() / 2.0f, getHeight() / 2.0f, mDrawableRadius, mBitmapPaint);
        if (mBorderWidth != 0) {
            canvas.drawCircle(getWidth() / 2.0f, getHeight() / 2.0f, mBorderRadius, mBorderPaint);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        setup();
    }

    public int getBorderColor() {
        return mBorderColor;
    }

    public void setBorderColor(int borderColor) {
        if (borderColor == mBorderColor) {
            return;
        }

        mBorderColor = borderColor;
        mBorderPaint.setColor(mBorderColor);
        invalidate();
    }

    public void setBorderColorResource(@ColorRes int borderColorRes) {
        setBorderColor(ContextCompat.getColor(getContext(), borderColorRes));
    }

    public int getFillColor() {
        return mFillColor;
    }

    public void setFillColor(int fillColor) {
        if (fillColor == mFillColor) {
            return;
        }

        mFillColor = fillColor;
        mFillPaint.setColor(fillColor);
        invalidate();
    }

    public void setFillColorResource(@ColorRes int fillColorRes) {
        setFillColor(ContextCompat.getColor(getContext(), fillColorRes));
    }

    public int getBorderWidth() {
        return mBorderWidth;
    }

    public void setBorderWidth(int borderWidth) {
        if (borderWidth == mBorderWidth) {
            return;
        }

        mBorderWidth = borderWidth;
        setup();
    }

    public boolean isBorderOverlay() {
        return mBorderOverlay;
    }

    public void setBorderOverlay(boolean borderOverlay) {
        if (borderOverlay == mBorderOverlay) {
            return;
        }

        mBorderOverlay = borderOverlay;
        setup();
    }

    @Override
    public void setImageBitmap(Bitmap bm) {
        super.setImageBitmap(bm);
        mBitmap = bm;
        setup();
    }

    @Override
    public void setImageDrawable(Drawable drawable) {
        super.setImageDrawable(drawable);
        mBitmap = getBitmapFromDrawable(drawable);
        setup();
    }

    @Override
    public void setImageResource(@DrawableRes int resId) {
        super.setImageResource(resId);
        mBitmap = getBitmapFromDrawable(getDrawable());
        setup();
    }

    @Override
    public void setImageURI(Uri uri) {
        super.setImageURI(uri);
        mBitmap = uri != null ? getBitmapFromDrawable(getDrawable()) : null;
        setup();
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
        if (cf == mColorFilter) {
            return;
        }

        mColorFilter = cf;
        mBitmapPaint.setColorFilter(mColorFilter);
        invalidate();
    }

    private Bitmap getBitmapFromDrawable(Drawable drawable) {
        if (drawable == null) {
            return null;
        }

        if (drawable instanceof BitmapDrawable) {
            return ((BitmapDrawable) drawable).getBitmap();
        }

        try {
            Bitmap bitmap;

            if (drawable instanceof ColorDrawable) {
                bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);
            } else {
                bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG);
            }

            Canvas canvas = new Canvas(bitmap);
            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
            drawable.draw(canvas);
            return bitmap;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private void setup() {
        if (!mReady) {
            mSetupPending = true;
            return;
        }

        if (getWidth() == 0 && getHeight() == 0) {
            return;
        }

        if (mBitmap == null) {
            invalidate();
            return;
        }

        mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

        mBitmapPaint.setAntiAlias(true);
        mBitmapPaint.setShader(mBitmapShader);

        mBorderPaint.setStyle(Paint.Style.STROKE);
        mBorderPaint.setAntiAlias(true);
        mBorderPaint.setColor(mBorderColor);
        mBorderPaint.setStrokeWidth(mBorderWidth);

        mFillPaint.setStyle(Paint.Style.FILL);
        mFillPaint.setAntiAlias(true);
        mFillPaint.setColor(mFillColor);

        mBitmapHeight = mBitmap.getHeight();
        mBitmapWidth = mBitmap.getWidth();

        mBorderRect.set(0, 0, getWidth(), getHeight());
        mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f);

        mDrawableRect.set(mBorderRect);
        if (!mBorderOverlay) {
            mDrawableRect.inset(mBorderWidth, mBorderWidth);
        }
        mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f);

        updateShaderMatrix();
        invalidate();
    }

    private void updateShaderMatrix() {
        float scale;
        float dx = 0;
        float dy = 0;

        mShaderMatrix.set(null);

        if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
            scale = mDrawableRect.height() / (float) mBitmapHeight;
            dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;
        } else {
            scale = mDrawableRect.width() / (float) mBitmapWidth;
            dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;
        }

        mShaderMatrix.setScale(scale, scale);
        mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);

        mBitmapShader.setLocalMatrix(mShaderMatrix);
    }
}`
View的子类需要重写onDraw方法,并根据自身的特点来编写绘制View内容的逻辑。





也许你还想看

(▼点击文章标题或封面查看)



ELK日常使用基础篇 2020-06-18
iOS 隐形水印之 LSB 实现 2020-06-11
积木法搭建 iOS 应用—— VIPER 2020-06-04
Android死锁初探 2020-05-21
DialogFragment引起的内存泄露 2020-05-14


加入搜狐技术作者天团

千元稿费等你来!

戳这里!☛





    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存