学吧啊,学无止境。若干年前,我启动的Flutter版本的豆瓣,因为不可抗力原因停更了。很遗憾!今天,我决定用鸿蒙来开发一个证券版本的App,全面对标同花顺。
为什么选择证券类?第一,证券里面有很多自定义分时图/K线等,学会这些,基本完全了解了自定义View。第二,证券类的api网上有很多,这是很关键的。第三,常用的列表、数据库等,都会涉及到。
当然,因为很多数据源问题,不可能百分百一致,尽力而为。本文,作为先启篇,先亮出我目前完成的核心之一---分时图。
Gif图片可能略卡,可以忽略。大家可以猜一猜这是哪支股票。。

本篇,先放出代码,以及很多很多注释,下一篇会详细讲解思路以及对应的API讲解。
import http from '@ohos.net.http'
import { DrawRect } from './DrawRect'
import { StockDataBean, StockItemData } from './StockDataBean'
@Entry
@Component
struct CanvasLinePage {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
private canvasDrawMargin: number = 20 //画布的边距
@State stockData: StockDataBean = new StockDataBean()
private canvasW: number //整个画布的宽度
private canvasH: number //整个画布的高度
private controller: TextInputController = new TextInputController()
private searchStockCode: string = ''
private maxPriceTxtWidth: number = 0 //当日的最高价格文本的宽度
private singleTxtWidth: number = 0 //普通文本的宽度
private singleTxtHeight: number = 0 //普通文本的高度
private minuteRect: DrawRect //分时的区域
private chengJiaoLiangRect: DrawRect //分时下面量比的区域
private lineRect: DrawRect //分时+量比的所有绘制内容的区域
aboutToAppear() {
}
build() {
Column() {
Canvas(this.context)
.onReady(() => {
this.canvasW = this.context.width
this.canvasH = this.context.height
this.lineRect = new DrawRect(this.canvasDrawMargin, this.canvasDrawMargin, this.canvasW - this.canvasDrawMargin * 2, this.canvasH - this.canvasDrawMargin * 2)
this.minuteRect = new DrawRect(this.canvasDrawMargin, this.canvasDrawMargin, this.lineRect.width, this.lineRect.height * 0.7)
this.context.font = '40px'
this.singleTxtWidth = this.context.measureText("价").width
this.singleTxtHeight = this.context.measureText("价").height
// this.singleTxtHeight * 2 留给分时跟量比中间的区域
let liangBiStartY = this.minuteRect.getEndY() + this.singleTxtHeight
this.chengJiaoLiangRect = new DrawRect(this.lineRect.startX, liangBiStartY, this.lineRect.width, this.lineRect.height - this.minuteRect.height - this.singleTxtHeight * 2)
this.reDrawAllCanvas()
})
.backgroundColor('#ffffff')
.width('100%')
.height('50%')
.onTouch((event) => {
this._handlerCanvasTouchEvent(event)
})
Row() {
TextInput({ placeholder: 'Stock Code', controller: this.controller })
.onChange((value: string) => {
console.info(value);
this.searchStockCode = value
})
.type(InputType.Number)
.width('50%')
Button("Search")
.onClick(() => {
requestData(this.searchStockCode, (value) => {
if (value != null) {
console.log("解析成功")
this.stockData = value
this.context.font = '40px'
this.maxPriceTxtWidth = this.context.measureText(roundUpToTwoDecimalPlaces(this.stockData.lineHighest))
.width
this.reDrawAllCanvas()
}
})
})
}
.justifyContent(FlexAlign.SpaceAround)
.width('100%')
}
.width('100%')
.height('100%')
}
_handlerCanvasTouchEvent(event?: TouchEvent) {
if (event.type == TouchType.Move) {
let touchedY = event.touches[0].y
if (touchedY < this.canvasDrawMargin || touchedY > this.context.height - this.canvasDrawMargin) {
return
}
var x = event.touches[0].x
//遍历离自己最新的X轴位置的数据索引
var minDistanceIndex = 0
//上次计算到的最靠近的X位置
var lastXData = 0
//上次计算的差距
var lastDistance = this.stockData.line[minDistanceIndex].lineX
this.stockData.line.forEach((item, index) => {
if (Math.abs(item.lineX - x) < lastDistance) {
lastXData = item.lineX
minDistanceIndex = index
lastDistance = Math.abs(item.lineX - x)
}
})
//当前触摸位置的分时数据
x = lastXData
this._clearCanvas()
this._drawMinuteLine()
this._drawChengJiaoLiang()
let touchStockData = this.stockData.line[minDistanceIndex]
this._drawJiaFuJunLiang(touchStockData)
if (touchedY >= this.minuteRect.startY && touchedY <= this.minuteRect.getEndY()) {
//绘制分时区域价格
//绘制水平十字轴线
this.context.strokeStyle = '#666666'
this.context.lineWidth = 0.8600009
this.context.beginPath()
this.context.moveTo(this.minuteRect.startX, touchedY)
this.context.lineTo(this.minuteRect.getEndX(), touchedY)
this.context.stroke()
//绘制水平十字轴线左侧的分时价格
let txtYValue = roundUpToTwoDecimalPlaces(this.stockData.lineHighest - (touchedY - this.minuteRect.startY) / this.minuteRect.height * (this.stockData.lineHighest - this.stockData.lineLowest))
let textMetrics2 = this.context.measureText(txtYValue)
let txtW2 = textMetrics2.width
let txtH2 = textMetrics2.height
this.context.fillStyle = '#364d92'
this.context.fillRect(this.minuteRect.startX, touchedY - txtH2 / 2, txtW2, txtH2)
this.context.font = '40px'
this.context.fillStyle = '#ffffff'
this.context.textAlign = 'center'
this.context.textBaseline = 'middle'
this.context.fillText(txtYValue, this.minuteRect.startX + txtW2 / 2, touchedY)
} else if (touchedY >= this.chengJiaoLiangRect.startY && touchedY <= this.chengJiaoLiangRect.getEndY()) {
//绘制成交量
//绘制水平十字轴线
this.context.strokeStyle = '#666666'
this.context.lineWidth = 0.8600009
this.context.beginPath()
this.context.moveTo(this.minuteRect.startX, touchedY)
this.context.lineTo(this.minuteRect.getEndX(), touchedY)
this.context.stroke()
//绘制水平十字轴线左侧的成交量
let txtYValue = parseInt((((this.stockData.maxChengJiaoLiang - (touchedY - this.chengJiaoLiangRect.startY) / this.chengJiaoLiangRect.height * (this.stockData.maxChengJiaoLiang - this.stockData.minChengJiaoLiang))) / 100).toString())
.toString()
let textMetrics2 = this.context.measureText(txtYValue)
let txtW2 = textMetrics2.width
let txtH2 = textMetrics2.height
this.context.fillStyle = '#364d92'
this.context.fillRect(this.minuteRect.startX, touchedY - txtH2 / 2, txtW2, txtH2)
this.context.font = '40px'
this.context.fillStyle = '#ffffff'
this.context.textAlign = 'center'
this.context.textBaseline = 'middle'
this.context.fillText(txtYValue, this.minuteRect.startX + txtW2 / 2, touchedY)
}
//绘制垂直十字轴线
this.context.beginPath()
this.context.moveTo(x, this.minuteRect.startY)
this.context.lineTo(x, this.chengJiaoLiangRect.getEndY())
this.context.stroke()
//计算底部时间文本香瓜数据
this.context.font = '40px'
let txt = '' + touchStockData.time
if (txt.length <= 0) {
return
}
if (txt.length == 5) {
txt = '0' + txt
}
if (txt.length < 6) {
return
}
txt = txt.substring(0, 2) + ":" + txt.substring(2, 4)
let textMetrics = this.context.measureText(txt)
let txtW = textMetrics.width
let txtH = textMetrics.height
this.context.fillStyle = '#364d92'
//绘制底部时间文本框
this.context.fillRect(x - txtW / 2, this.minuteRect.getEndY(), txtW, txtH)
// 绘制底部时间文本
this.context.fillStyle = '#ffffff'
this.context.textAlign = 'center'
this.context.textBaseline = 'middle'
this.context.fillText(txt, x, this.minuteRect.getEndY() + txtH / 2)
}
}
//重新绘制
reDrawAllCanvas() {
this._clearCanvas()
let lastMinJiaFujunLiang = this.stockData.line[this.stockData.line.length-1]
if (lastMinJiaFujunLiang != null && lastMinJiaFujunLiang != undefined) {
this._drawJiaFuJunLiang(lastMinJiaFujunLiang)
}
this._drawMinuteLine()
this._drawChengJiaoLiang()
}
//清空画布的所有内容
_clearCanvas() {
this.context.clearRect(0, 0, this.canvasW, this.canvasH)
}
_drawJiaFuJunLiang(stockData: StockItemData) {
//---------绘制触摸时刻分钟线对应的时刻的 价格/涨跌幅/均价-----start------
this.context.font = '40px'
let price = stockData.price
let priceColor = ''
if (price > this.stockData.prev_close) {
priceColor = '#e2233e'
} else if (price == this.stockData.prev_close) {
priceColor = '#fcfcfc'
} else {
priceColor = '#228B22'
}
//这里文本left-align
this.context.textAlign = 'left'
this.context.textBaseline = 'middle'
let txtColor = '#666666'
let valueY = this.canvasDrawMargin - this.singleTxtHeight / 2
let jia_fu_jun_liang_data: Array<Array<string>> = [
['价', '' + price],
['幅', '' + roundUpToTwoDecimalPlaces((price - this.stockData.prev_close) * 100 / this.stockData.prev_close) + "%"],
['均', '' + roundUpToTwoDecimalPlaces(stockData.junjia)],
['量', '' + stockData.chengJiaoLiang / 100 + '']
]
jia_fu_jun_liang_data.forEach((item, index) => {
this.context.fillStyle = txtColor
this.context.fillText(item[0], this.minuteRect.startX + (12 + this.singleTxtWidth + this.maxPriceTxtWidth) * index, valueY)
this.context.fillStyle = priceColor
this.context.fillText(item[1], this.minuteRect.startX + (12 + this.singleTxtWidth + this.maxPriceTxtWidth) * index + this.singleTxtWidth, valueY)
})
//---------绘制触摸时刻分钟线对应的时刻的 价格/涨跌幅/均价-----end------
}
//绘制背景的方格线
drawBackgroundLine() {
let w = this.minuteRect.width
//-------------绘制背景的分时方格线-------start---------
let h = this.minuteRect.height
//绘制4x4方格背景矩形
this._drawStrokeRect(this.minuteRect, '#ececec')
this.context.strokeStyle = '#ececec'
let itemDistanceY = h / 4
//3条水平线
let startY = this.minuteRect.startY + itemDistanceY
for (let i = 0; i < 3; i++) {
this.context.beginPath();
this.context.moveTo(this.minuteRect.startX, startY)
this.context.lineTo(this.minuteRect.getEndX(), startY)
startY = startY + itemDistanceY
this.context.stroke()
}
let itemDistanceX = w / 4
//3条垂直线
this.context.beginPath()
let startX = this.minuteRect.startX + itemDistanceX
for (let i = 0; i < 3; i++) {
this.context.beginPath()
this.context.moveTo(startX, this.minuteRect.startX)
this.context.lineTo(startX, this.minuteRect.getEndY())
startX = startX + itemDistanceX
this.context.stroke()
}
//-------------绘制背景的分时方格线-------end---------
//-------------绘制背景的量比方格线-------start---------
//矩形
this._drawStrokeRect(this.chengJiaoLiangRect, '#ececec')
//水平
startY = this.chengJiaoLiangRect.startY + this.chengJiaoLiangRect.height / 2
this.context.beginPath();
this.context.moveTo(this.chengJiaoLiangRect.startX, startY)
this.context.lineTo(this.chengJiaoLiangRect.getEndX(), startY)
this.context.stroke()
//-------------绘制背景的量比方格线-------end---------
}
//绘制分钟线
_drawMinuteLine() {
this.drawBackgroundLine()
this.drawMinuteLine()
this.drawAveragePriceLine()
this.context.font = '40px'
this.context.textAlign = 'center'
this.context.textBaseline = 'middle'
//绘制左侧最高价格
if (this.stockData.lineHighest != null) {
this.context.fillStyle = '#e2233e'
let txtH = roundUpToTwoDecimalPlaces(this.stockData.lineHighest)
this.context.fillText(txtH, this.minuteRect.startX + this.maxPriceTxtWidth / 2, this.minuteRect.startY + this.singleTxtHeight)
}
//绘制昨收价格
if (this.stockData.prev_close != null) {
this.context.fillStyle = '#6c6c6c'
let txtH = roundUpToTwoDecimalPlaces(this.stockData.prev_close)
this.context.fillText(txtH, this.minuteRect.startX + this.maxPriceTxtWidth / 2, this.minuteRect.height / 2 + this.singleTxtHeight)
}
//绘制左侧最低价格
if (this.stockData.lineLowest != null) {
this.context.fillStyle = '#228B22'
let txtH = roundUpToTwoDecimalPlaces(this.stockData.lineLowest)
this.context.fillText(txtH, this.minuteRect.startX + this.maxPriceTxtWidth / 2, this.minuteRect.getEndY() - this.singleTxtHeight / 2)
}
}
//绘制均价
drawAveragePriceLine() {
this.context.strokeStyle = '#e99a4c'
this.context.lineWidth = 0.8600009
this.context.beginPath()
let itemCount = Math.max(this.stockData.line.length, 240)
//按照分时数据量平分两个分时数据之间的间距
let itemDistance = this.minuteRect.width / itemCount
let path = new Path2D()
this.stockData.line.forEach((value, index) => {
if (index >= itemCount) {
return
}
let x = this.minuteRect.startX + index * itemDistance
let y = this.minuteRect.height / 2 - (value.junjia - this.stockData.prev_close) / this.stockData.maxDistancePrice * (this.minuteRect.height / 2) + this.minuteRect.startY
if (index == 0) {
path.moveTo(x, y)
} else {
path.lineTo(x, y)
}
})
this.context.stroke(path)
}
//绘制分时线
drawMinuteLine() {
this.context.strokeStyle = '#364d92'
this.context.lineWidth = 0.8600009
this.context.beginPath()
let itemCount = Math.max(this.stockData.line.length, 240)
let itemDistance = this.minuteRect.width / itemCount
let path = new Path2D()
this.stockData.line.forEach((value, index) => {
if (index >= itemCount) {
return
}
let x = this.minuteRect.startX + index * itemDistance
value.lineX = x
let y = this.minuteRect.height / 2 - (value.price - this.stockData.prev_close) / this.stockData.maxDistancePrice * (this.minuteRect.height / 2) + this.minuteRect.startY
if (index == 0) {
path.moveTo(x, y)
} else {
path.lineTo(x, y)
}
})
this.context.stroke(path)
}
//绘制成交量
_drawChengJiaoLiang() {
this.context.strokeStyle = '#e99a4c'
this.context.lineWidth = 0.8600009
this.stockData.line.forEach((value, index) => {
if (value.price)
this.context.beginPath()
this.context.moveTo(value.lineX, ((this.stockData.maxChengJiaoLiang - value.chengJiaoLiang) / this.stockData.maxChengJiaoLiang) * this.chengJiaoLiangRect.height + this.chengJiaoLiangRect.startY)
this.context.lineTo(value.lineX, this.chengJiaoLiangRect.getEndY())
this.context.stroke()
})
}
_drawStrokeRect(rect: DrawRect, color: string) {
this.context.strokeStyle = color
this.context.strokeRect(rect.startX, rect.startY, rect.width, rect.height)
}
}
export interface Callback<T> {
(data: T): void;
}
function requestData(stockCode: string, callback: Callback<StockDataBean>) {
// callback('开始请求')
//创建http请求
let httpRequest = http.createHttp()
//订阅请求头
httpRequest.on('headersReceive', (header) => {
// callback('获取到请求头信息')
// callback("header:" + JSON.stringify(header))
})
//发起请求
var market = stockCode.startsWith('0') ? 'sz' : 'sh'
httpRequest.request("http://xxxx" {
method: http.RequestMethod.GET,
extraData: {},
connectTimeout: 5000,
readTimeout: 5000,
header: {
'Content-Type': 'application/json'
}
}).then((data) => {
if (data.responseCode == http.ResponseCode.OK) {
let response = data.result
// console.log("接口返回:" + response)
let obj = JSON.parse(response as string)
let bean = new StockDataBean()
bean.code = obj.code
bean.prev_close = obj.prev_close
bean.highest = obj.highest
bean.lowest = obj.lowest
bean.time = obj.time
bean.total = obj.total
bean.begin = obj.begin
bean.date = obj.date
bean.end = obj.end
let lineList: Array<Array<number>> = obj.line
var lineHighest = bean.highest
var lineLowest = bean.lowest
var currentTimePrice = bean.prev_close
lineList.forEach((value: Array<number>, index) => {
let item = new StockItemData()
item.time = value[0]
item.price = value[1]
item.chengJiaoLiang = value[2]
item.junjia = value[3]
item.chengjiaoe = value[4]
currentTimePrice = item.price
if (item.price > lineHighest) {
lineHighest = item.price
}
if (item.price < lineLowest) {
lineLowest = item.price
}
bean.line.push(item)
if (item.chengJiaoLiang > bean.maxChengJiaoLiang) {
bean.maxChengJiaoLiang = item.chengJiaoLiang
}
if (item.chengJiaoLiang < bean.minChengJiaoLiang) {
bean.minChengJiaoLiang = item.chengJiaoLiang
}
})
if (Math.abs(lineHighest - bean.prev_close) > Math.abs(lineLowest - bean.prev_close)) {
lineLowest = bean.prev_close - Math.abs(lineHighest - bean.prev_close)
//获取
bean.maxDistancePrice = Math.abs(lineHighest - bean.prev_close)
} else {
bean.maxDistancePrice = Math.abs(lineLowest - bean.prev_close)
lineHighest = bean.prev_close + Math.abs(lineLowest - bean.prev_close)
}
bean.lastNewPrice = currentTimePrice
bean.lineLowest = lineLowest
bean.lineHighest = lineHighest
bean.maxDistancePrice = roundUpToTwoDecimal(bean.maxDistancePrice)
callback(bean)
} else {
callback(null)
}
}).catch((error) => {
callback(null)
console.log('error:' + JSON.stringify(error));
})
}
function roundUpToTwoDecimalPlaces(num: number): string {
const roundedNumber = Math.ceil(num * 100) / 100; // 先将数字乘以 100,然后向上取整,再除以 100
return roundedNumber.toFixed(2); // 将结果保留两位小数并返回
}
function roundUpToTwoDecimal(num: number): number {
const roundedNumber = Math.ceil(num * 100) / 100; // 先将数字乘以 100,然后向上取整,再除以 100
return parseFloat(roundedNumber.toFixed(2)); // 将结果保留两位小数并返回
}
网友评论