好久不见!鸿蒙画分时图第一弹。

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

Gif图片可能略卡,可以忽略。大家可以猜一猜这是哪支股票。。

jphwp-ewpwg.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)); // 将结果保留两位小数并返回
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容