自定义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 和绘制的类型是又关联的,否则绘制出来不显示。
  • 多个手势如何操作如何操作。

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

相关推荐
m0_748235953 小时前
CentOS 7使用RPM安装MySQL
android·mysql·centos
ac-er88887 小时前
Yii框架中的队列:如何实现异步操作
android·开发语言·php
流氓也是种气质 _Cookie9 小时前
uniapp 在线更新应用
android·uniapp
zhangphil11 小时前
Android ValueAnimator ImageView animate() rotation,Kotlin
android·kotlin
徊忆羽菲11 小时前
CentOS7使用源码安装PHP8教程整理
android
编程、小哥哥12 小时前
python操作mysql
android·python
Couvrir洪荒猛兽13 小时前
Android实训十 数据存储和访问
android
五味香15 小时前
Java学习,List 元素替换
android·java·开发语言·python·学习·golang·kotlin
十二测试录16 小时前
【自动化测试】—— Appium使用保姆教程
android·经验分享·测试工具·程序人生·adb·appium·自动化
Couvrir洪荒猛兽17 小时前
Android实训九 数据存储和访问
android