前言

上一篇做了一个类似ViewPager的控件,算是温习了一下自定义view的常识,也完成了比较凶猛的作用。这一篇简略点,是我发现前面几篇对于onDraw函数讲的仍是不多,并且又发现Paint这个类仍是适当杂乱的,就运用一个六边形评分控件学习一下,罗列了一下Paint的功用,简略试了试。

需求

需求很简略,看下面核心思维:

  • 1、六个极点连成六边形作为鸿沟,极点上需求有字提示数据类型
  • 2、六个数据作为得分,在中心到极点连线上,六个评分再围成六边形
  • 3、鸿沟六边形为空心,内部六边形为实心
  • 4、中心和极点用虚线衔接,再内部有虚线构成参阅六边形

作用图

自定义view实战(8):六边形评分控件

编写代码

这儿首要便是制作了,本来还想增加缩放和旋转作用的,可是最后把代码删了,这儿常识已经够多了。

import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import com.silencefly96.module_common.R
import kotlin.math.cos
import kotlin.math.sin
/**
 * 六边形评分view
 *
 * @author silence
 * @date 2022-10-26
 */
class HexagonRankView @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyleAttr: Int = 0
): View(context, attributeSet, defStyleAttr){
    /**
     * 六个数据
     */
    @Suppress("MemberVisibilityCanBePrivate")
    val data = ArrayList<PointInfo>(6)
    // 鸿沟六极点颜色
    private val mOutPointColor: Int
    // 鸿沟六边的颜色
    private val mOutLineColor: Int
    // 内部六极点颜色
    private val mInPointColor: Int
    // 内部六极点颜色
    private val mInLineColor: Int
    // 内部填充颜色
    private val mInFillColor: Int
    // 虚线颜色
    private val mDottedLineColor: Int
    // 字体巨细
    private val mTextSize: Float
    // 画笔粗细
    private val mStrokeWidth: Float
    // 填充透明度
    private val mFillAlpha: Int
    // 半径占边框中最小值的份额
    private val mRadiusPercent: Float
    // 各个点的半径
    private val mPointRadius: Float
    // 开始相位
    private val mStartPhase: Int
    // 文字间隔极点的值
    private val mTextMargin: Float
    // 画笔
    private val mPaint: Paint
    // 中点坐标
    private var mCenterX: Int = 0
    private var mCenterY: Int = 0
    // 六边形半径
    private var mRadius: Int = 0
    init {
        // 读取XML参数
        val typedArray =
            context.obtainStyledAttributes(attributeSet, R.styleable.HexagonRankView)
        mOutPointColor = typedArray.getColor(R.styleable.HexagonRankView_outPointColor,
            Color.BLACK)
        mOutLineColor = typedArray.getColor(R.styleable.HexagonRankView_outLineColor,
            Color.DKGRAY)
        mInPointColor = typedArray.getColor(R.styleable.HexagonRankView_inPointColor,
            Color.BLUE)
        mInLineColor =typedArray.getColor(R.styleable.HexagonRankView_inLineColor,
            Color.GREEN)
        mInFillColor = typedArray.getColor(R.styleable.HexagonRankView_inFillColor,
            Color.YELLOW)
        mDottedLineColor = typedArray.getColor(R.styleable.HexagonRankView_dottedLineColor,
            Color.LTGRAY)
        mTextSize = typedArray.getDimension(R.styleable.HexagonRankView_textSize, 40f)
        mStrokeWidth = typedArray.getDimension(R.styleable.HexagonRankView_strokeWidth, 5f)
        mFillAlpha = typedArray.getInt(R.styleable.HexagonRankView_fillAlpha, 50)
        mRadiusPercent = typedArray.getFraction(R.styleable.HexagonRankView_radiusPercent,
            1, 1, 0.8f)
        mPointRadius = typedArray.getDimension(R.styleable.HexagonRankView_pointRadius, 10f)
        mStartPhase = typedArray.getInt(R.styleable.HexagonRankView_startPhase, -90)
        mTextMargin = typedArray.getDimension(R.styleable.HexagonRankView_textMargin, 50f)
        typedArray.recycle()
        // 初始化画笔
        mPaint = Paint().apply {
            // 内容参阅:https://blog.csdn.net/qq_27061049/article/details/102574020
            /******* 常用办法 *******/
            // 颜色
            color = Color.BLACK
            // 粗细,设置为0时无论怎样扩大 都是1像素
            strokeWidth = mStrokeWidth
            // 透明度[0, 255]
            alpha = 255
            // 带透明度画笔
            setARGB(255, 255, 255,255)
            // 抗锯齿
            flags = Paint.ANTI_ALIAS_FLAG
            // 设置填充形式,FILL、STROKE、FILL_AND_STROKE(更大一些)
            style = Paint.Style.STROKE
            /******* 线条款式 *******/
            // 线条衔接处款式,BEVEL(斜角)、MITER(平斜接)、ROUND(圆角)
            strokeJoin = Paint.Join.ROUND
            // 斜接形式延长线长度约束(MITER款式下),miter = len / width = 1 / sin (  / 2 )
            // 默许值为4,越大视点越小,比这个视点的视点,接壤地方的超长三角形会被截断移除
            strokeMiter = 4f
            // 落笔和完毕时那点(point)的款式,BUTT(不增加)、ROUND(增加半圆)、SQUARE(增加矩形)
            strokeCap = Paint.Cap.ROUND
            // 设置途径作用:
            // 直线,segmentLength: 分段长度,deviation: 偏移间隔
            // pathEffect = DiscretePathEffect(float segmentLength, float deviation)
            // 圆角,参数为衔接处的半径
            // pathEffect = CornerPathEffect(20f)
            // 虚线,intervals:必须为偶数,用于控制显现和隐藏的长度; phase:相位
            // pathEffect = DashPathEffect(float intervals[], float phase)
            // 运用 path 制作虚线,shapePath(构成shape的path),advance(两个shape之间间隔),phase(相位)
            // 指定拐弯改变的时分 shape 的转化办法,TRANSLATE:位移、ROTATE:旋转、MORPH:变体(压缩变小)
            // pathEffect = PathDashPathEffect(shapePath, advance, phase, PathDashPathEffectStyle.TRANSLATE);
            // 设置线条随机偏移(变得杂乱无章),segmentLength: 分段长度,deviation: 偏移间隔
            // pathEffect = DiscretePathEffect(float segmentLength, float deviation)
            // 两种线条形式都执行(一条线变两条线)
            // pathEffect = SumPathEffect(dashEffect, discreteEffect)
            // 线条组合形式(一条线两种形式)
            // pathEffect = ComposePathEffect(dashEffect, discreteEffect)
            /******* 上色突变及烘托 *******/
            // 突变
            // LinearGradient 线性突变
            // (x0,y0)(x1,y1) 两点确定线性方向,color0、color1突变两颜色(在两点上), 突变形式: Shader.TileMode.MIRROR
            // shader = LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, TileMode tile)
            // 三种颜色以上形式,positions:颜色的方位(份额)
            // shader = LinearGradient(float x0, float y0, float x1, float y1, int colors[], float positions[], TileMode tile)
            // RadialGradient 径向突变(从圆心沿极径改变)
            // x、y:中心点坐标, radius:突变半径, color0:开始颜色, color1:完毕颜色, TileMode:突变形式
            // shader = RadialGradient(float x, float y, float radius, int color0, int color1, TileMode tile)
            // 三种颜色以上形式,positions:颜色的方位(份额)
            // shader = RadialGradient(float x, float y, float radius, int colors[], float positions[], TileMode tile)
            // SweepGradient 扫描突变(随视点改变颜色)
            // cx、cy:圆点坐标, color0:开始颜色, color1:完毕颜色
            // shader = SweepGradient(float cx, float cy, int color0, int color1)
            // 多种颜色的扫描突变, positions:颜色的方位(份额)
            // shader = SweepGradient(float cx, float cy, int colors[], float positions[])
            // BitmapShader 位图突变(运用图片填充)
            // bitmap 位图,TileMode 横纵坐标上的形式
            // shader = BitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY)
            // Shader.TileMode 突变形式
            // TileMode.CLAMP 形式不平铺
            // TileMode.REPEAT 形式表示平铺
            // TileMode.MIRROR 形式也表示平铺,可是交错的位图是彼此的镜像,方向相反
            // ComposeShader 混合突变(对上面四种突变进行混合)
            // 混合形式 PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
            // ComposeShader(Shader shaderA, Shader shaderB, Xfermode mode)
            // 位图运算(16种): PorterDuff.Mode.XO
            // ComposeShader(Shader shaderA, Shader shaderB, Mode mode)
            /******* 颜色作用处理 *******/
            // LightingColorFilter 设定基本色素(过滤颜色), mul 用来和方针像素相乘,add 用来和方针像素相加
            // colorFilter = LightingColorFilter(0x00ffff, 0x000000); //去掉红色
            // PorterDuffColorFilter 设置颜色 形式运算
            // PorterDuffColorFilter(Color.GREEN, PorterDuff.Mode.XOR); //去掉 和 绿色结合的部分
            // ColorMatrixColorFilter 颜色锐度等
            // 运用一个 ColorMatrix 来对颜色进行处理, 内部是一个 4x5 的矩阵 A, B = [R, G, B, A]-T, A x B
            //colorFilter =  ColorMatrix(new float[]{
            //    -1f, 0f, 0f, 0f, 255f,
            //    0f, -1f, 0f, 0f, 255f,
            //    0f, 0f, -1f, 0f, 255f,
            //    0f, 0f, 0f, 1f, 0f }); //去掉 和 绿色结合的部分
            // setXfermode 图片转化形式
            // “Xfermode” 其实便是“Transfer mode”, Xfermode 指的是 你要制作的内容 和 canvas 的方针方位的内容应该怎样结合核算出终究的颜色。
            // 浅显的讲便是要你以制作的图形作为源图像,以View中已有的内容做为方针图像,选取一个PorterDuff.Mode 作为制作内容的颜色处理计划
            // val bitmapOne = BitmapFactory.decodeResource(resources,R.mipmap.ic_launcher_2)
            // val bitmapTwo = BitmapFactory.decodeResource(resources,R.mipmap.rect_2)
            // val xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN); //取交集,交集款式取决于基层,颜色取决于上层
            // val saved = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
            // canvas.drawBitmap(bitmapTwo, 0, 0, paint);
            // paint.setXfermode(xfermode); // 设置 Xfermode
            // canvas.drawBitmap(bitmapOne, 0, 0, paint);
            // paint.setXfermode(null); // 用完及时铲除 Xfermode
            // canvas.restoreToCount(saved)
            /******* 颜色优化 *******/
            // setDither(boolean dither) 设置图像颤动
            // 在实践的应用场景中,颤动更多的作用是在图像降低颜色深度制作时,避免出现大片的色带与色块
            // 挑选 16 位色的 ARGB_4444 或许 RGB_565 的时分,敞开它才会有比较明显的作用
            // setFilterBitmap(boolean filter) 线性过滤
            // 图像在扩大制作的时分,默许运用的是最近邻插值过滤,这种算法简略,但会出现马赛克现象;
            // 而如果敞开了双线性过滤,就能够让结果图像显得愈加平滑
            /******* 设置暗影或许上层作用 *******/
            // setShadowLayer() 设置暗影、clearShadowLayer() 清楚暗影
            // radius 是暗影的含糊规模; dx dy 是暗影的偏移量; shadowColor 是暗影的颜色
            // setShadowLayer(float radius, float dx, float dy, int shadowColor)
            // setMaskFilter(MaskFilter filter) 制作层上附件作用,暗影是基层
            // 含糊作用的 MaskFilter,
            // NORMAL: 表里都含糊制作、/SOLID: 内部正常制作,外部含糊、INNER: 内部含糊,外部不制作、/OUTER: 内部不制作,外部含糊
            // maskFilter = BlurMaskFilter(float radius, BlurMaskFilter.Blur style)
            // EmbossMaskFilter 浮雕作用
            // direction 是一个 3 个元素的数组,指定了光源的方向; ambient 是环境光的强度,数值规模是 0 到 1; specular 是炫光的系数; blurRadius 是应用光线的规模
            // EmbossMaskFilter(float[] direction, float ambient, float specular, float blurRadius)
            /******* 获取实践途径 *******/
            // 获取线条实践途径,当线条比较粗时,途径实践是一个关闭的矩形
            // getFillPath(Path src, Path dst)
            // 获取文本的实践途径,获取到path后通过canvas去制作path
            // getTextPath(String text, int start, int end, float x, float y, Path path)
        }
    }
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        // 自定义view要设置好默许巨细
        val width = getDefaultSize(100, widthMeasureSpec)
        val height = getDefaultSize(100, heightMeasureSpec)
        // 由控件宽高取得中心点坐标
        mCenterX = width / 2
        mCenterY = height / 2
        // 半径,设置为最小宽度的80%
        mRadius = ((if (mCenterX < mCenterY) mCenterX else mCenterY) * mRadiusPercent).toInt()
        // 核算数据坐标
        calculateLocation()
        setMeasuredDimension(width, height)
    }
    private fun calculateLocation() {
        // 以中点为圆心,每隔60度制作一个极点
        var angle: Int
        var radians: Double
        // 循环制作
        for (i in 0..5) {
            angle = 60 * i + mStartPhase
            radians = Math.toRadians(angle.toDouble())
            // 核算横纵坐标
            data[i].x = (mRadius * cos(radians)).toFloat() + mCenterX
            data[i].y = (mRadius * sin(radians)).toFloat() + mCenterY
            // 核算分数对应的坐标
            val scan = data[i].rank / 100f
            data[i].curX = (mRadius * scan * cos(radians)).toFloat() + mCenterX
            data[i].curY = (mRadius * scan * sin(radians)).toFloat() + mCenterY
        }
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // 辅佐的虚线, 放在底层
        drawDottedLine(canvas)
        // 外面的边和极点以及标题
        drawOuter(canvas)
        // 里边多边形
        drawInner(canvas)
    }
    // 辅佐的虚线,这儿将半径三等分,画三个虚线六边形
    private fun drawDottedLine(canvas: Canvas) {
        var x: Float
        var y: Float
        // 途径
        val path = Path()
        mPaint.color = mDottedLineColor
        val array = FloatArray(2)
        array[0] = 10f
        array[1] = 10f
        mPaint.pathEffect = DashPathEffect(array, 0f)
        // 两层层虚线六边形
        for (i in 1..2) {
            path.reset()
            // 循环一遍取得途径
            for (point in data) {
                // 运用两点坐标核算等间隔的点
                x = (point.x - mCenterX) / 3 * i + mCenterX
                y = (point.y - mCenterY) / 3 * i + mCenterY
                if (data.indexOf(point) == 0) path.moveTo(x, y)
                path.lineTo(x, y)
            }
            // 关闭
            path.close()
            // 制作虚线六边形
            canvas.drawPath(path, mPaint)
        }
        // 制作衔接中点和极点的虚线
        path.reset()
        for (point in data) {
            canvas.drawLine(mCenterX.toFloat(), mCenterY.toFloat(), point.x, point.y, mPaint)
        }
        //  去除虚线作用
        mPaint.pathEffect = null
    }
    // 外面的极点
    private fun drawOuter(canvas: Canvas) {
        // 外边途径
        val path = Path()
        path.moveTo(data[0].x, data[0].y)
        // 制作标题,在切线方向制作
        // 极点在最上面时,(60 * i + mStartPhase) -> (-90) => -90 - (60 * i + mStartPhase)
        val startAngle = -90 - mStartPhase
        canvas.save()
        canvas.rotate(startAngle.toFloat(), mCenterX.toFloat(), mCenterY.toFloat())
        mPaint.textAlign = Paint.Align.CENTER
        mPaint.textSize = mTextSize
        // 制作字体要先设置为0
        mPaint.strokeWidth = 0f
        // 要画出切线作用,移动的是画布,每次在最上面横着画就行
        val x = mCenterX.toFloat()
        val y = mCenterY - mRadius - mTextMargin
        // 循环制作
        for (i in 0..5) {
            // 制作标题
            canvas.drawText(data[i].name, x, getBaseline(mPaint, y), mPaint)
            // 旋转60度画下一个
            canvas.rotate(-60f, mCenterX.toFloat(), mCenterY.toFloat())
        }
        // 制作标题完毕,康复画布
        canvas.restore()
        // 循环制作
        mPaint.color = mOutPointColor
        mPaint.strokeWidth = mStrokeWidth
        mPaint.style = Paint.Style.FILL
        for (point in data) {
            // 制作点
            canvas.drawCircle(point.x, point.y, mPointRadius, mPaint)
            // 也能够直接用点
            // paint.setStrokeCap(Paint.Cap.SQUARE);
            // canvas.drawPoint(x, y, paint);
            // 制作外边
            path.lineTo(point.x, point.y)
        }
        // 关闭
        path.close()
        mPaint.color = mOutLineColor
        mPaint.style = Paint.Style.STROKE
        canvas.drawPath(path, mPaint)
    }
    private fun getBaseline(paint: Paint, tempY: Float): Float {
        //制作字体的参数,受字体巨细款式影响
        val fmi = paint.fontMetricsInt
        //top为基线到字体上边框的间隔(负数),bottom为基线到字体下边框的间隔(正数)
        //基线中心点的y轴核算公式,即中心点加上字体高度的一半,基线中心点x便是中心点x
        return tempY - (fmi.top + fmi.bottom) / 2f
    }
    // 里边多边形
    private fun drawInner(canvas: Canvas) {
        // 里边多边形途径
        val path = Path()
        path.moveTo(data[0].curX, data[0].curY)
        // 循环制作
        mPaint.color = mInPointColor
        mPaint.style = Paint.Style.FILL
        for (point in data) {
            // 制作点
            canvas.drawCircle(point.curX, point.curY, mPointRadius, mPaint)
            // 增加外边途径到path
            path.lineTo(point.curX, point.curY)
        }
        // 关闭
        path.close()
        // 制作途径
        mPaint.color = mInLineColor
        mPaint.style = Paint.Style.STROKE
        canvas.drawPath(path, mPaint)
        // 制作内部填充
        mPaint.color = mInFillColor
        mPaint.style = Paint.Style.FILL
        mPaint.alpha = mFillAlpha
        canvas.drawPath(path, mPaint)
        // 康复style
        mPaint.style = Paint.Style.STROKE
    }
    data class Pair(var x: Float, var y: Float)
    // 数据类,标题、分数、外边点坐标、分数点坐标
    data class PointInfo(var name: String, var rank: Int,
                         var x: Float = 0f, var y: Float = 0f,
                         var curX: Float = 0f, var curY: Float = 0f)
}

首要问题

这儿Paint相关的常识我就不具体说了,首要提下下面几点:

canvas.save()和canvas.restore()

canvas.save()和canvas.restore()这两个函数中心能够对canvas进行改变,制作的东西会保留,比如我这旋转了很屡次制作标题,最后用canvas.restore()康复本来的方位,就不用去核算康复的视点,自己去旋转了。Canvas的制作办法,有爱好能够自己找资料看看,也就那几个。

制作字体要先设置strokeWidth为0

这儿制作字体时会受strokeWidth影响,导致加粗许多,所以制作前要设置下。

path的moveTo

path初始默许从(0, 0)开始,需求首先运用moveTo移到开始方位。至于Path的一些办法,有爱好也能够自己找资料看看。我的这篇文章:安卓带过程的手写签名(附源码),用path制作了流畅的线条,有爱好能够瞧瞧。