好吧,家庭作业,要求绘制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区间就行,所以完全不需要这个玩意。
如何处理滚动、缩放、触摸等手势
基于业务诉求,我们大致需要两个手势帮助类。
- GestureDetector:GestureDetector 类是 Android 提供的一个手势检测类,可以检测一些常见的手势,如滑动、缩放、旋转等。GestureDetector 可以通过 onDown()、onShowPress()、onSingleTapUp()、onScroll()、onLongPress() 和 onDoubleTap() 等回调方法来处理不同的手势事件。
- 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 和绘制的类型是又关联的,否则绘制出来不显示。
- 多个手势如何操作如何操作。
涉及到的知识,都再储备知识里面了,这里就不重新占字数了。