9afce6f6创建于 2025年5月7日历史提交
/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import DateUtils from '../utils/DateUtils';
import Log from '../utils/Log';

const TAG = '[Sample_HeartRateGraph]';

const SIZE: number = 12;
const COORDINATE_SIZE: number = 5;
const LINE_WIDTH: number = px2vp(6);
const COLOR_LINE: string = '#FF0A59F7';
const MAX_HEART_RATE: number = 200;
const WIDTH_CHANGE_POINT: number = px2vp(900);

const X_COORDINATE_TEXT_HEIGHT: number = px2vp(30);
const START_X: number = px2vp(40);
const PADDING_VERTICAL: number = px2vp(40);
const PADDING_HORIZONTAL: number = px2vp(40);

@Component
export default struct HeartRateGraph {
  @StorageLink('heartRate') @Watch('onHeartRate') heartRate: number = 0;
  @Prop @Watch('onViewSizeChange') viewWidth: number;
  @Prop @Watch('onViewSizeChange') viewHeight: number;
  @State heartRateArr: Array<number> = [];
  @State timeArr: Array<string> = [];
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  private mCoordinateLineEndX: number = 0;
  private mOffset: number = 0;
  private mHeightRatio: number = 0;
  private mHeartRateCoordinateArr: Array<number> = [];

  aboutToAppear() {
    Log.showInfo(TAG, `HeartRateGraph aboutToAppear`);
    this.calculateLayoutConfig();

    for (let i = 0; i < SIZE; i++) {
      this.heartRateArr[i] = 0;
    }
    for (let i = 0; i < SIZE; i++) {
      this.timeArr[i] = `--:--:--`;
    }
  }

  /**
   * 心率变动事件
   */
  onHeartRate() {
    Log.showInfo(TAG, `onHeartRate: heartRate = ${this.heartRate}`);

    // update heart rate arr
    this.heartRateArr.push(this.heartRate);
    if (this.heartRateArr.length > SIZE) {
      this.heartRateArr.shift();
    }
    Log.showInfo(TAG, `onHeartRate: heartRateArr = ${JSON.stringify(this.heartRateArr)}`);

    // update time arr
    this.timeArr.push(DateUtils.format(new Date(), 'HH:mm:ss'));
    if (this.timeArr.length > SIZE) {
      this.timeArr.shift();
    }
    Log.showInfo(TAG, `onHeartRate: timeArr = ${JSON.stringify(this.timeArr)}`);

    this.draw();
  }

  onViewSizeChange() {
    Log.showInfo(TAG, `onViewSizeChange: viewWidth = ${this.viewWidth}, viewHeight = ${this.viewHeight}`);
    this.calculateLayoutConfig();
  }

  /**
   * 调整布局
   */
  calculateLayoutConfig() {
    this.mCoordinateLineEndX = this.viewWidth - PADDING_HORIZONTAL;
    this.mOffset = (this.viewWidth - START_X - PADDING_HORIZONTAL * 2) / (SIZE - 1);
    this.mHeightRatio = (this.viewHeight - PADDING_VERTICAL * 2 - X_COORDINATE_TEXT_HEIGHT * 2) / MAX_HEART_RATE;
    let heartRateCoordinate: number = MAX_HEART_RATE / (COORDINATE_SIZE - 1);
    this.mHeartRateCoordinateArr =
      [0, heartRateCoordinate, heartRateCoordinate * 2, heartRateCoordinate * 3, heartRateCoordinate * 4];
    Log.showInfo(TAG, `calculateLayoutConfig: mOffset = ${this.mOffset}, mHeightRatio = ${this.mHeightRatio}`);
  }

  /**
   * 画心率变动图
   */
  draw() {
    this.context.clearRect(0, 0, this.viewWidth, this.viewHeight);
    this.drawCoordinate();
    this.drawHeartRateLine();
  }

  /**
   * 画坐标
   */
  drawCoordinate() {
    this.context.lineWidth = LINE_WIDTH / 3;
    this.context.font = '16px sans-serif';

    // draw y-coordinate(heart rate text)
    let path: Path2D = new Path2D();
    this.context.fillStyle = '#999999';
    for (let i = 0; i < COORDINATE_SIZE; i++) {
      let text = `${this.mHeartRateCoordinateArr[i]}bpm`;
      let offsetY = (this.viewHeight - PADDING_VERTICAL * 2 - X_COORDINATE_TEXT_HEIGHT * 2) / (COORDINATE_SIZE - 1);
      let x = 0;
      let y =
        this.viewHeight - PADDING_VERTICAL - X_COORDINATE_TEXT_HEIGHT * 2 - offsetY * i + this.context.measureText(text)
          .height / 4;
      path.moveTo(x, y);
      this.context.fillText(text, x, y);
      this.context.stroke(path);
    }
    // draw coordinate line
    this.context.strokeStyle = '#1A000000';
    for (let i = 0; i < COORDINATE_SIZE; i++) {
      let offsetY = (this.viewHeight - PADDING_VERTICAL * 2 - X_COORDINATE_TEXT_HEIGHT * 2) / (COORDINATE_SIZE - 1);
      let x = START_X + PADDING_HORIZONTAL;
      let y = this.viewHeight - PADDING_VERTICAL - X_COORDINATE_TEXT_HEIGHT * 2 - offsetY * i;
      let path1: Path2D = new Path2D();
      path1.moveTo(x, y);
      path1.lineTo(this.mCoordinateLineEndX, y);
      this.context.stroke(path1);
    }
    // draw x-coordinate(time text)
    this.context.fillStyle = '#999999';
    let path2: Path2D = new Path2D();
    for (let i = 0; i < SIZE; i++) {
      if (this.viewWidth <= WIDTH_CHANGE_POINT && i % 2 === 0) {
        // viewWidth not wide enough, Ignore half of the content and don't draw.
        continue;
      }
      let text = this.timeArr[i];
      let x = START_X + this.mOffset * i + PADDING_HORIZONTAL - this.context.measureText(text).width / 2;
      let y = this.viewHeight - PADDING_VERTICAL - X_COORDINATE_TEXT_HEIGHT;
      path2.moveTo(x, y);
      this.context.fillText(text, x, y);
      this.context.stroke(path2);
    }

    this.context.fillStyle = '#333333';
    let path3: Path2D = new Path2D();
    for (let i = 0; i < SIZE; i++) {
      if (this.viewWidth <= WIDTH_CHANGE_POINT && i % 2 === 0) {
        // viewWidth not wide enough, Ignore half of the content and don't draw.
        continue;
      }
      let text = `${this.heartRateArr[i]}bpm`;
      let x = START_X + this.mOffset * i + PADDING_HORIZONTAL - this.context.measureText(text).width / 2;
      let y = this.viewHeight - PADDING_VERTICAL;
      path2.moveTo(x, y);
      this.context.fillText(text, x, y);
      this.context.stroke(path3);
    }
  }

  /**
   * 画心率变动曲线
   */
  drawHeartRateLine() {
    this.context.lineWidth = LINE_WIDTH;
    this.context.strokeStyle = COLOR_LINE;

    // draw broken line
    let path: Path2D = new Path2D();
    for (let i = 0; i < SIZE; i++) {
      let x = START_X + this.mOffset * i + PADDING_HORIZONTAL;
      let y =
        this.viewHeight - PADDING_VERTICAL - X_COORDINATE_TEXT_HEIGHT * 2 - this.heartRateArr[i] * this.mHeightRatio;
      if (i === 0) {
        path.moveTo(x, this.viewHeight - PADDING_VERTICAL - X_COORDINATE_TEXT_HEIGHT * 2);
        path.lineTo(x, y);
      } else {
        path.lineTo(x, y);
      }

      if (i === SIZE - 1) {
        path.lineTo(x, this.viewHeight - PADDING_VERTICAL - X_COORDINATE_TEXT_HEIGHT * 2);
      }
    }
    let gradient = this.context.createLinearGradient(0, PADDING_VERTICAL, 0,
      this.viewHeight - PADDING_VERTICAL - X_COORDINATE_TEXT_HEIGHT * 2);
    gradient.addColorStop(0.0, '#660A59F7');
    gradient.addColorStop(1.0, '#000A59F7');
    this.context.fillStyle = gradient;
    this.context.fill(path);
    this.context.stroke(path);

    // remove excess draw
    this.context.clearRect(START_X + PADDING_HORIZONTAL - LINE_WIDTH / 2, 0, LINE_WIDTH,
      this.viewHeight - PADDING_VERTICAL - X_COORDINATE_TEXT_HEIGHT * 2 + LINE_WIDTH / 2);
    this.context.clearRect(this.viewWidth - PADDING_HORIZONTAL - LINE_WIDTH / 2, 0, LINE_WIDTH,
      this.viewHeight - PADDING_VERTICAL - X_COORDINATE_TEXT_HEIGHT * 2 + LINE_WIDTH / 2);
  }

  build() {
    Canvas(this.context)
      .width(this.viewWidth)
      .height(this.viewHeight)
      .onReady(() => {
        this.draw();
      })
  }
}