自定义view绘制K线第一版

好吧,家庭作业,要求绘制K线。

资料

K线由4个值控制,分别是开盘价,最高价,最低价,和收盘价。K线图记录了买方和买房实战的过程,如果收盘价高于开盘价,则以实体红线表示,如果收盘价低于开盘价,则以实体绿线表示,最高价和最低价则以影线表示。

Android 中自定义view中包含K线的第三方框架还是蛮多的,当然我们这里是自己去捋一次自定义view,同时结合优秀框架的思路,提升自己,我们先来看一下echarts k线基础使用

第三方maven:

优秀的库太多了,就罗列了这么几个。

正文

当我们把上面贴的几个网站浏览完了之后,接口K线的定义,及其展示效果。我们可以大致将k线分为几个板块:

  • X 轴,主要负责基于缩放或者平移显示时间段及其时间段下面的文字,所以也约束了K线的中某一项在x的范围区间。
  • Y轴,和X轴类似,但是主要负Y轴上价格的显示,也用于约束Y轴值所在区间。同时还有一根水平刻度线。
  • K线的某一个item,这个由X和Y共同约束绘制区域。因为涉及到点击事件,所以我们需要定义一个区域,一个绘制所用的path,毕竟K线这玩意不可能用线和矩形一点点的画吧,那么缩放平移的时候不得裂开,所以我们将线和矩形基于设计合并成一个path,区域主要是便于通过x.y 判断应该归属于那个item。

OK,大的板块就划分完成了,现在就是我们如何去实现了。先来确定设计稿:

  • 默认状态下K线矩形宽度为10dp,线宽1dp
  • x轴一天的宽度默认50dp,字号12sp
  • y 轴基于高度和K线的最大值和最小值进行比例划分,可以加一个比例常量,比如Y 轴最大值为256,然后就设置Y轴最大值为300等等。
  • 缩放最小0.5 最大2.0。

储备知识

这里就考验自定义view的知识储备的时候到了。

如何获取到view的宽高与padding ?

通常而言,我们自定义view而不是viewgroup的时候,并不处理onMeasure() 因为这个玩意会调用多次用于计算view的位置,而onDraw 只有一次,所以我们获取view的宽高就写onDraw即可。那么如何获取呢?

padding

ini 复制代码
    int leftPadding = getPaddingLeft();  
    int topPadding = getPaddingTop();  
    int rightPadding = getPaddingRight();  
    int bottomPadding = getPaddingBottom();  
    // 或者使用 getPaddingStart() 和 getPaddingEnd()  
    int startPadding = getPaddingStart();  
    int endPadding = getPaddingEnd();  

宽高

因为我们暂时写到了onDraw里面的。

bash 复制代码
LogUtils.e("${canvas.width}   ${canvas.height}")
LogUtils.e("${width==canvas.width}   ${height==canvas.height}")

通过log 可以看到通过 canvas 获取到的宽高和view的宽高是一致的,所以我们有两种方式获取到view的宽高,因为onDraw的调度时机里面view的宽高已经确定了。

绘制一个矩形区域,但是这个区域超出了屏幕的X轴,那么通过平移缩放可以完全显示吗?

答案是肯定的,这里涉及到matrix的二维矩阵的叠加和重置。参考:

如何将两个path 拼接到一起?

path官方地址 我们业务述求很简单,就是将一条直线和一个矩形拼到一起。所以看看就行了。

如何判断是否在点击区域内?

结合的这个标题就知道我的思路大概是基于Region。这里直接copy一下自定义中国地图里面的区域判断的代码。

java 复制代码
public boolean isTouch(float x,float y){
    //创建一个矩形
    RectF rectF = new RectF();
    //获取到当前省份的矩形边界
    path.computeBounds(rectF, true);
    //创建一个区域对象
    Region region = new Region();
    //将path对象放入到Region区域对象中
    region.setPath(path, new Region((int)rectF.left, (int)rectF.top,(int)rectF.right, (int)rectF.bottom));
    //返回是否这个区域包含传进来的坐标
    return region.contains((int)x,(int)y);
}

但是,结合业务诉求,我这个只需要判断是否再X区间就行,所以完全不需要这个玩意。

如何处理滚动、缩放、触摸等手势

基于业务诉求,我们大致需要两个手势帮助类。

  1. GestureDetector:GestureDetector 类是 Android 提供的一个手势检测类,可以检测一些常见的手势,如滑动、缩放、旋转等。GestureDetector 可以通过 onDown()、onShowPress()、onSingleTapUp()、onScroll()、onLongPress() 和 onDoubleTap() 等回调方法来处理不同的手势事件。
  2. ScaleGestureDetector:ScaleGestureDetector 类是 Android 提供的一个手势检测类,专门用于检测缩放手势。ScaleGestureDetector 可以通过 onScaleBegin()、onScale()、onScaleEnd() 等回调方法来处理缩放手势事件。

那么两个手势帮助类如何组合使用呢?

ini 复制代码
@Override
public boolean onTouchEvent(MotionEvent event) {
    boolean res = scaleGestureDetector.onTouchEvent(event);
    if (!scaleGestureDetector.isInProgress()) {
        res = gestureDetector.onTouchEvent(event);
    }
    return res;
}

开整

基于上面的逻辑,这个K线view的视线就两个思路了,一个是每次绘制所有,然后通过matrix 进行缩放平移。另外一种就是超出屏幕的不绘制,也是基基于matrix进行绘制,那么我们就细分一下,X,Y 轴的还是画完,然后只有k线的item是需要判断是否在屏幕内部的,所以,我们先一次性画完,然后再进行代码优化。

K线的数据其实是一个列表,所以说,我们还是先定义数据模型。基于上面的对于K线的信息。

定义K线的数据模型

我们先怎么简单怎么来。

kotlin 复制代码
/**
 * x:x 轴
 * max:最高价
 * min:最低价
 * open:开盘价
 * close:收盘价
 * 
 */
data class KItem (val x:String,val max:Float,val min:Float,val open:Float,val close:Float){
    /**
     *  绘制k线的画笔的颜色
     */
    fun getColor():Int{
        return if ( open>close){ Color.RED} else{Color.GREEN}
    }
}

绘制X与Y轴

无论是X轴还是Y轴,我们都需要绘制一条直线。然后绘制绘很多文本,绘制几个点。绘制线条还好说,绘制文本的时候,基于绘制文本的对其策略,我们需要确定我们要绘制文本的对其策略,参考:Paint详解

绘制X轴

我们X轴的文本是居中对其的。所以我们先设置画笔的属性。

ini 复制代码
val txPaint:Paint by lazy {
    Paint().apply {
        textSize=24f
        color=Color.BLACK
        // 抗锯齿
        isAntiAlias=true
        //居中对齐
        textAlign=Paint.Align.CENTER
    }
}

然后就是计算每个文本的居中的位置了。先在KItem 里面新增几个字段。

ini 复制代码
var tXStart:Float=0f
var tXEnd:Float=0f
var txCenter:Float=0f

循环列表,计算出X轴线的最大值。同时计算出每个文本的位置:

ini 复制代码
val step=150f
var startX=0f+paddingLeft+step
kItems.forEach {
    it.tXStart=startX
    it.tXEnd=startX+step
    it.txCenter=startX+step/2
    startX = it.tXEnd
}
// 最后添加一丢丢长度。防止最后位移的时候,滚动出绘制区域,所以我们先计算出长度,然后便于后期限制平移。
lineXEnd=startX+step/2
invalidate()

我们假定每个文本的区域是150px。最后是绘制X轴:

kotlin 复制代码
private fun drawXLine(canvas: Canvas) {
    canvas.save()
    // 先确定 X 轴的 Y的位置
    val startX=paddingLeft.toFloat()+step
    // 我们先设置这个位置是距离底部100px的位置.减去底部内间距,减去画笔的粗细的一半
    val lineW=lPaint.strokeWidth/2
    val startY=canvas.height-100-paddingBottom-lPaint.strokeWidth/2
    // 因为我们绘制的是水平直线,所以Y 的坐标是一致的。
    canvas.drawLine(startX, startY, lineXEnd, startY, lPaint)
    // 开始绘制 文本,所以需要循环
    // 我先不设置文本相对矩形居中。
    // 先确定 绘制文本基线的Y位置,因为是水平的,所以Y 坐标还是不变的。
    val tStartY= startY+50
​
    kItems.forEach{
        // 我们基于每一item的结束位置,绘制我们需要点。
        canvas.drawLine(it.tXEnd-lineW,startY-10,it.tXEnd-lineW,startY,lPaint)
        // 绘制文本
        canvas.drawText(it.x,it.txCenter,tStartY,txPaint)
    }
    canvas.restore()
}

基于业务场景,我们X轴的是不可在Y轴上缩放平移的,所以X轴的需要一个自己的Matrix。但是这个在这个代码中还没体现。我们先把不可缩放平移的画出来。

绘制Y轴

Y轴就和X 轴一样的,还是怎么简单怎么来,我们假设Y轴上的值就是我们定义到屏幕上的px 值。我们在X的绘制点时候,已经将X轴开始位置平移了一个step 长度,用于渲染Y轴上的文本。所以我们还是来定义Y轴上的文本的画笔:

ini 复制代码
val tyPaint:Paint by lazy {
    Paint().apply {
        textSize=24f
        color=Color.BLACK
        // 抗锯齿
        isAntiAlias=true
        // 右对其
        textAlign=Paint.Align.RIGHT
    }
}

计算文本的相对位置:这里就和X轴不一样了,屏幕的Y轴是左上角到左下角增长:y++,但是我们汇总的Y轴是view左下角到view的左上角为:y++。所以我们绘制点线条是从view 左下角往上绘制。而且Y轴上的点的个数并不是由数据提供的。所以,我们先绘制10个点,步长依旧是150,这样我们就可以得出绘制Y轴线的开始或者结束位置,这个值一定是为负数。

kotlin 复制代码
private fun drawYLine(canvas: Canvas) {
    canvas.save()
    // 先确定 X 轴的 Y的位置
    val startX = paddingLeft.toFloat() + step
    var startY = canvas.height - 100 - paddingBottom - lPaint.strokeWidth / 2
    canvas.drawLine(startX, height - paddingBottom.toFloat(), startX, lineYEnd, lPaint)
    //开启循环绘制Y轴上的点。
    for (i in 1..10) {
        startY -= step
        canvas.drawText((i * 10).toString(), startX - 20, startY, tyPaint)
        // 我们基于每一item的结束位置,绘制我们需要点。
        canvas.drawLine(startX, startY, startX + 10, startY, lPaint)
    }
    canvas.restore()
}

可以看到,我们Y 轴的线是从底部往上绘制的。然后分别绘制了1o个点,及其文本。

绘制K线

我们假定数据是正确的。结合上面绘制X轴的定义,然后我们就直接组合成一个path:

scss 复制代码
val path: Path by lazy {
    Path().apply {
        moveTo(txCenter, yStart - min * 15)
        lineTo(txCenter, yStart - max * 15)
        close()
        addRect(
            txCenter - 10,
            yStart - (open * 15),
            txCenter + 10,
            yStart - close * 15,
            Path.Direction.CW
        )
        fillType = Path.FillType.WINDING
    }
}

我们绘制K线的画笔和xy轴的画笔分开:

ini 复制代码
val kPaint: Paint by lazy {
    Paint().apply {
        color = Color.BLACK
        // 抗锯齿
        isAntiAlias = true
        style = Paint.Style.FILL_AND_STROKE
        strokeWidth = 2f
​
    }
}

然后定义是否是当前区域及其函数:

kotlin 复制代码
var regionContains = false
fun regionContains(dx: Float) {
    regionContains = dx > tXStart && dx < tXEnd
}

因为这个触摸高亮线是通过line 绘制的,所以画笔也得单独设置:

ini 复制代码
val downPaint: Paint by lazy {
    Paint().apply {
        color = Color.BLUE
        // 抗锯齿
        isAntiAlias = true
        style = Paint.Style.STROKE
        strokeWidth = 2f
​
    }
}

开始绘制:

scss 复制代码
private fun drawKChart(canvas: Canvas) {
    canvas.save()
    canvas.concat(kChartMatrix)
    kItems.forEach {
        kPaint.color = it.getColor()
        canvas.drawPath(it.path, kPaint)
        if (it.regionContains) {
            canvas.drawLine(
                it.txCenter,
                height - paddingBottom.toFloat(),
                it.txCenter,
                lineYEnd,
                downPaint
            )
        }
    }
    canvas.restore()
}

处理平移

从业务逻辑上讲X轴,无论怎么平移,他的Y轴位置是永远不动的,Y轴的X的位置的不动的,而K线是可以都可以移动的,所以我们需要定义3个Matrix(),当绘制不同的区域的时候,设置进去。

kotlin 复制代码
private val kChartMatrix: Matrix by lazy {
    Matrix()
}
private val xMatrix: Matrix by lazy {
    Matrix()
}
private val yMatrix: Matrix by lazy {
    Matrix()
}

然后是处理平移:先只是做单方向的平移

kotlin 复制代码
var xTranslate = 0f
fun postTranslate(dx: Float, dy: Float) {
    yMatrix.postTranslate(0f, dy)
    if (dx.absoluteValue > dy.absoluteValue) {
        xTranslate -= dx
        xMatrix.postTranslate(dx, 0f)
        kChartMatrix.postTranslate(dx, 0f)
    } else {
        yMatrix.postTranslate(0f, dy)
        kChartMatrix.postTranslate(0f, dy)
    }
    invalidate()
}

可以看到,我们并没处理平移的边界值问题。所以不停的平移,会导致X轴和Y轴分开。

处理缩放

和平移类似。我们依旧是对于matrix 进行处理。依旧只是处理一个方向,也同样为处理极限值。

kotlin 复制代码
fun postScale( sx:Float,  sy:Float,  px:Float, py:Float){
    xMatrix.postScale(sx, 0f, px, 0f)
    yMatrix.postScale(0f, sy, 0f, py)
    kChartMatrix.postScale(sx, sy, px, py)
}

总结

这个是打算分为多期完成的,今天只是完成了数据渲染,平移,缩放函数定义了。还是遇到了些问题。

  • 画笔的style 和绘制的类型是又关联的,否则绘制出来不显示。
  • 多个手势如何操作如何操作。

涉及到的知识,都再储备知识里面了,这里就不重新占字数了。

相关推荐
小比卡丘2 小时前
C语言进阶版第17课—自定义类型:联合和枚举
android·java·c语言
前行的小黑炭3 小时前
一篇搞定Android 实现扫码支付:如何对接海外的第三方支付;项目中的真实经验分享;如何高效对接,高效开发
android
落落落sss4 小时前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
代码敲上天.5 小时前
数据库语句优化
android·数据库·adb
GEEKVIP7 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
model20059 小时前
android + tflite 分类APP开发-2
android·分类·tflite
彭于晏6899 小时前
Android广播
android·java·开发语言
与衫10 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql
500了16 小时前
Kotlin基本知识
android·开发语言·kotlin
人工智能的苟富贵17 小时前
Android Debug Bridge(ADB)完全指南
android·adb