學(xué)吧啊趟庄,學(xué)無(wú)止境。若干年前伪很,我啟動(dòng)的Flutter版本的豆瓣戚啥,因?yàn)椴豢煽沽υ蛲8恕:苓z憾锉试!今天猫十,我決定用鴻蒙來(lái)開發(fā)一個(gè)證券版本的App,全面對(duì)標(biāo)同花順呆盖。
為什么選擇證券類拖云?第一,證券里面有很多自定義分時(shí)圖/K線等应又,學(xué)會(huì)這些宙项,基本完全了解了自定義View。第二株扛,證券類的api網(wǎng)上有很多尤筐,這是很關(guān)鍵的。第三洞就,常用的列表盆繁、數(shù)據(jù)庫(kù)等,都會(huì)涉及到旬蟋。
當(dāng)然油昂,因?yàn)楹芏鄶?shù)據(jù)源問(wèn)題,不可能百分百一致倾贰,盡力而為冕碟。本文,作為先啟篇匆浙,先亮出我目前完成的核心之一---分時(shí)圖安寺。
Gif圖片可能略卡,可以忽略吞彤。大家可以猜一猜這是哪支股票我衬。。
jphwp-ewpwg.gif
本篇饰恕,先放出代碼挠羔,以及很多很多注釋,下一篇會(huì)詳細(xì)講解思路以及對(duì)應(yīng)的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 //整個(gè)畫布的寬度
private canvasH: number //整個(gè)畫布的高度
private controller: TextInputController = new TextInputController()
private searchStockCode: string = ''
private maxPriceTxtWidth: number = 0 //當(dāng)日的最高價(jià)格文本的寬度
private singleTxtWidth: number = 0 //普通文本的寬度
private singleTxtHeight: number = 0 //普通文本的高度
private minuteRect: DrawRect //分時(shí)的區(qū)域
private chengJiaoLiangRect: DrawRect //分時(shí)下面量比的區(qū)域
private lineRect: DrawRect //分時(shí)+量比的所有繪制內(nèi)容的區(qū)域
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("價(jià)").width
this.singleTxtHeight = this.context.measureText("價(jià)").height
// this.singleTxtHeight * 2 留給分時(shí)跟量比中間的區(qū)域
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軸位置的數(shù)據(jù)索引
var minDistanceIndex = 0
//上次計(jì)算到的最靠近的X位置
var lastXData = 0
//上次計(jì)算的差距
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)
}
})
//當(dāng)前觸摸位置的分時(shí)數(shù)據(jù)
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()) {
//繪制分時(shí)區(qū)域價(jià)格
//繪制水平十字軸線
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()
//繪制水平十字軸線左側(cè)的分時(shí)價(jià)格
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()
//繪制水平十字軸線左側(cè)的成交量
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()
//計(jì)算底部時(shí)間文本香瓜數(shù)據(jù)
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'
//繪制底部時(shí)間文本框
this.context.fillRect(x - txtW / 2, this.minuteRect.getEndY(), txtW, txtH)
// 繪制底部時(shí)間文本
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()
}
//清空畫布的所有內(nèi)容
_clearCanvas() {
this.context.clearRect(0, 0, this.canvasW, this.canvasH)
}
_drawJiaFuJunLiang(stockData: StockItemData) {
//---------繪制觸摸時(shí)刻分鐘線對(duì)應(yīng)的時(shí)刻的 價(jià)格/漲跌幅/均價(jià)-----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>> = [
['價(jià)', '' + 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)
})
//---------繪制觸摸時(shí)刻分鐘線對(duì)應(yīng)的時(shí)刻的 價(jià)格/漲跌幅/均價(jià)-----end------
}
//繪制背景的方格線
drawBackgroundLine() {
let w = this.minuteRect.width
//-------------繪制背景的分時(shí)方格線-------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()
}
//-------------繪制背景的分時(shí)方格線-------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'
//繪制左側(cè)最高價(jià)格
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)
}
//繪制昨收價(jià)格
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)
}
//繪制左側(cè)最低價(jià)格
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)
}
}
//繪制均價(jià)
drawAveragePriceLine() {
this.context.strokeStyle = '#e99a4c'
this.context.lineWidth = 0.8600009
this.context.beginPath()
let itemCount = Math.max(this.stockData.line.length, 240)
//按照分時(shí)數(shù)據(jù)量平分兩個(gè)分時(shí)數(shù)據(jù)之間的間距
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)
}
//繪制分時(shí)線
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('開始請(qǐng)求')
//創(chuàng)建http請(qǐng)求
let httpRequest = http.createHttp()
//訂閱請(qǐng)求頭
httpRequest.on('headersReceive', (header) => {
// callback('獲取到請(qǐng)求頭信息')
// callback("header:" + JSON.stringify(header))
})
//發(fā)起請(qǐng)求
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; // 先將數(shù)字乘以 100破加,然后向上取整,再除以 100
return roundedNumber.toFixed(2); // 將結(jié)果保留兩位小數(shù)并返回
}
function roundUpToTwoDecimal(num: number): number {
const roundedNumber = Math.ceil(num * 100) / 100; // 先將數(shù)字乘以 100雹嗦,然后向上取整范舀,再除以 100
return parseFloat(roundedNumber.toFixed(2)); // 將結(jié)果保留兩位小數(shù)并返回
}