Android自定义-滑动缩放渐变填充曲线折线图表

本文详细介绍了如何在Android中自定义绘制滑动缩放和渐变填充的曲线图表,包括基础的坐标系理解、折线图绘制、文字修饰、手势操作以及曲线图的实现。通过实例展示了如何进行坐标变换、区域点击事件处理以及手势缩放功能,帮助开发者掌握自定义View的高级技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述
在这里插入图片描述

自定义

自定义老生常谈的技能了,年底没事干,希望这篇文章能够开启你的自定义大门。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
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值