好久不見!鴻蒙畫分時圖第一彈。

學吧啊,學無止境。若干年前,我啟動的Flutter版本的豆瓣,因為不可抗力原因停更了。很遺憾!今天,我決定用鴻蒙來開發(fā)一個證券版本的App,全面對標同花順。
為什么選擇證券類?第一,證券里面有很多自定義分時圖/K線等,學會這些,基本完全了解了自定義View。第二,證券類的api網(wǎng)上有很多,這是很關(guān)鍵的。第三,常用的列表、數(shù)據(jù)庫等,都會涉及到。
當然,因為很多數(shù)據(jù)源問題,不可能百分百一致,盡力而為。本文,作為先啟篇,先亮出我目前完成的核心之一---分時圖。

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 //分時的區(qū)域
  private chengJiaoLiangRect: DrawRect //分時下面量比的區(qū)域
  private lineRect: DrawRect //分時+量比的所有繪制內(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("價").width
          this.singleTxtHeight = this.context.measureText("價").height
          // this.singleTxtHeight * 2 留給分時跟量比中間的區(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
      //上次計算到的最靠近的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)
        }
      })
      //當前觸摸位置的分時數(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()) {
        //繪制分時區(qū)域價格

        //繪制水平十字軸線
        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 = 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()
      //計算底部時間文本香瓜數(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'
      //繪制底部時間文本框
      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()
  }

  //清空畫布的所有內(nèi)容
  _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'
    //繪制左側(cè)最高價格
    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)
    }
    //繪制左側(cè)最低價格
    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)
    //按照分時數(shù)據(jù)量平分兩個分時數(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)
  }

  //繪制分時線
  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('開始請求')
  //創(chuàng)建http請求
  let httpRequest = http.createHttp()
  //訂閱請求頭
  httpRequest.on('headersReceive', (header) => {
    // callback('獲取到請求頭信息')
    // callback("header:" + JSON.stringify(header))
  })
  //發(fā)起請求
  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ù)并返回
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容