文章目录
自定义
自定义老生常谈的技能了,年底没事干,希望这篇文章能够开启你的自定义大门。API并不难,难在
开始
,难在想法
和设计
。各种案例逐步深入,直到画出你能想到的。
一、基础操作
- 了解基本的坐标系,画笔,画布等操作。
1.新建类
LHC_Line_View继承View,重写onDraw方法,最简单代码架子。
class LHC_Line_View @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0):View(context, attrs, defStyle) {
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
}
}
2.坐标系
- View默认坐标系是自身的左上角。如下所示:我们在坐标(x,y)为(0,0)地方绘制圆圈。
首先我们的布局设置在屏幕中间xml如下:
<com.zj.utils.utils.view.LHC_Line_View
android:background="@color/black"
android:layout_centerInParent="true"
android:layout_width="@dimen/dp_300"
android:layout_height="@dimen/dp_200"/>
此时我们看到屏幕中间有一块黑色的View。我们在onDraw方法里面进行绘制一个白色圆圈
:
class LHC_Line_View @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : View(context, attrs, defStyle) {
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val back_paint=Paint()
back_paint.style= Paint.Style.FILL
back_paint.color=Color.WHITE
back_paint.strokeWidth=10f
canvas.drawCircle(0f,0f,25f,back_paint)
}
}
因为圆心在左上角,而自身的宽度限制导致绘制出上图扇形。这里只要认识坐标系原点在左上角即可。
接下来转换坐标系为熟悉的坐标系,如下图三
x轴右为正方向,y轴上为正方向。圆点为左下方,这里我们涉及到坐标系(canvas)的变换。
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val back_paint=Paint()
back_paint.style= Paint.Style.FILL
back_paint.color=Color.WHITE
back_paint.strokeWidth=10f
canvas.save()
//竟变化坐标。y轴向上为正
canvas.scale(1f,-1f)
//平移坐标系到左下角
canvas.translate(0f, -(measuredHeight.toFloat()))
canvas.drawCircle(0f,0f,125f,back_paint)
}
坐标系的变换如下:
绘制过程和坐标系变化对比:
到这里已经成为我们熟悉的坐标系方向:
接下来我们绘制网格便于我们绘制过程更加直观
- 设置我们每格子宽高都为40像素。y轴格子个数 =measuredHeight/DensityUtils.px2dp(context,40f)
- 已知高度,我们每格子40像素。x轴格子个数 =measuredWidth/DensityUtils.px2dp(context,40f)
不妨我们先画两条线段
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val back_paint=Paint()
val grid_paint=Paint()
back_paint.style= Paint.Style.FILL
back_paint.color=Color.WHITE
back_paint.strokeWidth=10f
grid_paint.style= Paint.Style.STROKE
grid_paint.color=Color.WHITE
grid_paint.strokeWidth=2f
canvas.save()
//竟变化坐标。y轴向上为正
canvas.scale(1f,-1f)
//平移坐标系到左下角
canvas.translate(0f, -(measuredHeight.toFloat()))
//平行y轴的线段
val pathY=Path()
pathY.moveTo(DensityUtils.px2dp(context,40f),0f)
pathY.lineTo(DensityUtils.px2dp(context,40f), measuredWidth.toFloat())
canvas.drawPath(pathY,grid_paint)
//平行x轴的线段
val pathX=Path()
pathX.moveTo(0f,DensityUtils.px2dp(context,40f))
pathX.lineTo(measuredWidth.toFloat(),DensityUtils.px2dp(context,40f))
canvas.drawPath(pathX,grid_paint)
}
上面我们已经绘制出了两条线段。只需要计算出每条线段位置并绘制或者平移画布进行绘制。
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val back_paint=Paint()
val grid_paint=Paint()
back_paint.style= Paint.Style.FILL
back_paint.color=Color.WHITE
back_paint.strokeWidth=10f
grid_paint.style= Paint.Style.STROKE
grid_paint.color=Color.WHITE
grid_paint.strokeWidth=2f
canvas.save()
//竟变化坐标。y轴向上为正
canvas.scale(1f,-1f)
//平移坐标系到左下角
canvas.translate(0f, -(measuredHeight.toFloat()))
//平行x轴的线段
val pathX=Path()
pathX.moveTo(0f,DensityUtils.px2dp(context,40f))
pathX.lineTo(measuredWidth.toFloat(),DensityUtils.px2dp(context,40f))
canvas.drawPath(pathX,grid_paint)
//x轴个数
val countX=measuredWidth/DensityUtils.px2dp(context,40f)
//y轴个数
val countY=measuredHeight/DensityUtils.px2dp(context,40f)
//平行y轴的线段
for (index in 0 until countY.toInt()){
val pathX=Path()
pathX.moveTo(0f,DensityUtils.px2dp(context,40f)*(index+1))
pathX.lineTo(measuredWidth.toFloat(),DensityUtils.px2dp(context,40f)*(index+1))
canvas.drawPath(pathX,grid_paint)
}
}
当然画布的变换可以更加方便的操作和实现效果:
for (index in 0 until countY.toInt()){
//每画一条线就将画布平移40像素的单位进行下一个绘制。
canvas.translate(0f,DensityUtils.px2dp(context,40f))
canvas.drawPath(pathX,grid_paint)
}
最后我们画出所有的x,y方向的线段即可:
class LHC_Line_View @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : View(context, attrs, defStyle) {
val grid_wh=DensityUtils.px2dp(context, 60f)
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val back_paint=Paint()
val grid_paint=Paint()
back_paint.style= Paint.Style.FILL
back_paint.color=Color.WHITE
back_paint.strokeWidth=10f
grid_paint.style= Paint.Style.STROKE
grid_paint.color=Color.argb(66,255,255,255)
grid_paint.strokeWidth=2f
canvas.save()
//变换坐标系为我们常见的
changeCanvaXY(canvas)
//画网格
drawGridView(canvas, grid_paint)
}
//变换为熟悉的坐标系
private fun changeCanvaXY(canvas: Canvas) {
//竟变化坐标。y轴向上为正
canvas.scale(1f, -1f)
//平移坐标系到左下角
canvas.translate(0f, -(measuredHeight.toFloat()))
}
//绘制网格
private fun drawGridView(canvas: Canvas, grid_paint: Paint) {
//平行y轴的线段
val pathY = Path()
pathY.moveTo(grid_wh, 0f)
pathY.lineTo(grid_wh, measuredHeight.toFloat())
canvas.drawPath(pathY, grid_paint)
//平行x轴的线段
val pathX = Path()
pathX.moveTo(0f, grid_wh)
pathX.lineTo(measuredWidth.toFloat(), grid_wh)
canvas.drawPath(pathX, grid_paint)
//x轴个数
val countX = measuredWidth /grid_wh
//y轴个数
val countY = measuredHeight /grid_wh
canvas.save()
for (index in 0 until countY.toInt()) {
canvas.translate(0f,grid_wh)
canvas.drawPath(pathX, grid_paint)
}
canvas.restore()
for (index in 0 until countX.toInt()) {
canvas.translate(grid_wh, 0f)
canvas.drawPath(pathY, grid_paint)
}
}
}
3.简单的折线图
- 多练习Path和canvas的一些Api。之前写过绘制相关文章可以去看看,绘制简单的折线图开始。
1.学会Path相关api绘制折线图
将下一个轮廓的起点设置为点(x,y)
public void moveTo(float x, float y)
设置相对于上一个轮廓的最后一个点为相对位置下一个轮廓的起点。如果没有先前的轮廓就于moveTo ()相同。
public void rMoveTo(float dx, float dy)
与lineTo相同,但是将坐标视为相对于此轮廓上的最后一点。如果没有先前的点,就同moveTo(0,0)
public void rLineTo(float dx, float dy)
移动当前的路径
public void offset(float dx, float dy)
设置最后一个点
public void setLastPoint(float dx, float dy)
通过矩阵变换此路径中的点
public void transform(@NonNull Matrix matrix)
/**给当前的路径添加形状路径**/
public void addCircle(float x, float y, float radius, @NonNull Direction dir)
给当前路径添加扇形路径等
public void addArc(@NonNull RectF oval, float startAngle, float sweepAngle)
给当前路径添加圆角矩形
public void addRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Direction dir)
给当前路径添加一个椭圆路径
public void addOval(@NonNull RectF oval, @NonNull Direction dir)
/**添加曲线相关**/
从最后一个点开始添加二次贝塞尔曲线,逼近控制点(x1,y1),并在(x2,y2)处结束。如果没有为此轮廓调用moveTo(),则第一个点将自动设置为(0,0)
public void quadTo(float x1, float y1, float x2, float y2)
从最后一点添加一个三次方贝塞尔曲线,逼近控制点*(x1,y1)和(x2,y2),并在(x3,y3)处结束。如果尚未对该轮廓进行moveTo()调用,则第一个点将自动设置为(0,0)。
public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
将指定的弧形作为新轮廓附加到路径。如果路径的起点与路径的当前最后一个点不同,则将添加自动lineTo()以将当前轮廓连接到弧的起点。但是,如果路径为空,则使用圆弧的第一点调用moveTo()
public void arcTo(@NonNull RectF oval, float startAngle, float sweepAngle,boolean forceMoveTo)
自动关闭路径轮廓,如果最后一个点和第一个点不重合就会自动连接第一个点进行关闭
public void close()
2.灵活的进行设置各种特效【渐变,动画,色彩等】
paint相关的API...看之前链接中写过的文章
我们来进行绘制折线且每个折线顶点都有一个圆圈。
- 首先我们需要一个集合存储每个坐标点。
- 然后进行遍历连接各个点同时绘制定点圆圈。
新建类存储每个点坐标
data class ViewPoint @JvmOverloads constructor(var x:Float,var y:Float)
初始化集合画线
//绘制折线图
private fun drawLine(pointList: java.util.ArrayList<ViewPoint>, canvas: Canvas) {
val linePaint=Paint()
val path=Path()
linePaint.style= Paint.Style.STROKE
linePaint.color=Color.argb(255,34,192,255)
linePaint.strokeWidth=10f
//连线
for (index in 0 until pointList.size){
path.lineTo(pointList[index].x,pointList[index].y)
}
canvas.drawPath(path,linePaint)
}
我们进行绘制每个顶点的圆
//绘制折线图
private fun drawLine(pointList: java.util.ArrayList<ViewPoint>, canvas: Canvas) {
val linePaint = Paint()
val path = Path()
linePaint.style = Paint.Style.STROKE
linePaint.color = Color.argb(255, 34, 192, 255)
linePaint.strokeWidth = 10f
val circle_paint = Paint()
circle_paint.strokeWidth = 10f
circle_paint.style = Paint.Style.FILL
circle_paint.color = Color.argb(255, 34, 192, 255)
//连线
for (index in 0 until pointList.size) {
path.lineTo(pointList[index].x, pointList[index].y)
}
canvas.drawPath(path, linePaint)
//画定点圆圈
for (index in 0 until pointList.size) {
canvas.drawCircle(pointList[index].x, pointList[index].y,16f,circle_paint)
}
}
渐变的色彩填充
都很实用,可能显得高大上接下来我们进行折线图一下部分-进行渐变填充。
- 将折线形成一个闭合的区域,通过画笔设置
Style.Fill
然后设置shader
即可变成你想要的渐变填充。
//绘制折线图
private fun drawLine(pointList: java.util.ArrayList<ViewPoint>, canvas: Canvas) {
val linePaint = Paint()
val path = Path()
linePaint.style = Paint.Style.STROKE
linePaint.color = Color.argb(255, 34, 192, 255)
linePaint.strokeWidth = 10f
val circle_paint = Paint()
circle_paint.strokeWidth = 10f
circle_paint.style = Paint.Style.FILL
circle_paint.color = Color.argb(255, 34, 192, 255)
//连线
for (index in 0 until pointList.size) {
path.lineTo(pointList[index].x, pointList[index].y)
}
canvas.drawPath(path, linePaint)
//渐变色菜的填充
//连线
for (index in 0 until pointList.size) {
path.lineTo(pointList[index].x, pointList[index].y)
}
val endIndex=pointList.size-1
path.lineTo(pointList[endIndex].x, 0f)
path.close()
linePaint.style= Paint.Style.FILL
linePaint.shader=getShader()
canvas.drawPath(path, linePaint)
//画定点圆圈
for (index in 0 until pointList.size) {
canvas.drawCircle(pointList[index].x, pointList[index].y, 16f, circle_paint)
}
}
private fun getShader(): Shader {
val shadeColors = intArrayOf(Color.argb(255, 250, 49, 33), Color.argb(165, 234, 115, 9), Color.argb(200, 32, 208, 88))
return LinearGradient((measuredWidth/2).toFloat(), measuredHeight.toFloat(), (measuredWidth/2).toFloat(), 0f, shadeColors, null, Shader.TileMode.CLAMP)
}
网格和黑色去掉之后。
class LHC_Line_View @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : View