前言
上一篇做了一个类似ViewPager的控件,算是温习了一下自定义view的常识,也完成了比较凶猛的作用。这一篇简略点,是我发现前面几篇对于onDraw函数讲的仍是不多,并且又发现Paint这个类仍是适当杂乱的,就运用一个六边形评分控件学习一下,罗列了一下Paint的功用,简略试了试。
需求
需求很简略,看下面核心思维:
- 1、六个极点连成六边形作为鸿沟,极点上需求有字提示数据类型
- 2、六个数据作为得分,在中心到极点连线上,六个评分再围成六边形
- 3、鸿沟六边形为空心,内部六边形为实心
- 4、中心和极点用虚线衔接,再内部有虚线构成参阅六边形
作用图
编写代码
这儿首要便是制作了,本来还想增加缩放和旋转作用的,可是最后把代码删了,这儿常识已经够多了。
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制作了流畅的线条,有爱好能够瞧瞧。