diff --git "a/FAQ/ImageKnife/ImageKnife\346\200\247\350\203\275\346\200\273\347\273\223-OnAreaChange\344\274\230\345\214\226.md" "b/FAQ/ImageKnife/ImageKnife\346\200\247\350\203\275\346\200\273\347\273\223-OnAreaChange\344\274\230\345\214\226.md" new file mode 100644 index 0000000000000000000000000000000000000000..996aec1fe32f324354acd6a50816ef9dd6630bd5 --- /dev/null +++ "b/FAQ/ImageKnife/ImageKnife\346\200\247\350\203\275\346\200\273\347\273\223-OnAreaChange\344\274\230\345\214\226.md" @@ -0,0 +1,62 @@ +# ImageKnife性能总结-OnAreaChange优化 + +## 1、背景 +WeLink、微博在定位性能问题时,发现ImageKnife加载图片时大量监听了OnAreaChange接口,该对系统资源消耗较大,导致应用偶发性丢帧。 + +## 2、原因分析 +1、ImageKnife为了获取组件宽高,进行图片解码,监听了OnAreaChange方法。 + +2、由于系统机制每次触发Vsync渲染都会回调OnAreaChange,引起不必要的资源消耗。 + +代码参考: +```shell +.onAreaChange((oldValue: Area, newValue: Area) => { + if(newValue != undefined && newValue.width != undefined && newValue.height != undefined) { + this.currentWidth = newValue.width as number + this.currentHeight = newValue.height as number + if (this.currentWidth <= 0 || this.currentHeight <= 0) { + // 存在宽或者高为0,此次重回无意义,无需进行request请求 + } else { + // 前提:宽高值均有效,值>0. 条件1:当前宽高与上一次宽高不同 条件2:当前是第一次绘制 + if ((this.currentHeight != this.lastHeight || this.currentWidth != this.lastWidth) || this.firstDrawFlag) { + this.firstDrawFlag = false; + this.lastWidth = this.currentWidth + this.lastHeight = this.currentHeight + this.imageKnifeExecute() + } + } + } else { + LogUtil.log('ImageKnifeComponent onAreaChange Error newValue is undefined') + } +}) +``` + +## 3、优化方案 +在inspector回调时主动调用componentUtils.getRectangleById方法获取宽高。 + +代码参考: +```shell + onLayoutComplete:()=>void = ():void=>{ + this.value = componentUtils.getRectangleById(this.keyCanvas.keyId); + this.currentWidth = px2vp(this.value.size.width) + this.currentHeight = px2vp(this.value.size.height) + if (this.currentWidth <= 0 || this.currentHeight <= 0) { + // 存在宽或者高为0,此次重回无意义,无需进行request请求 + } else { + // 前提:宽高值均有效,值>0. 条件1:当前宽高与上一次宽高不同 条件2:当前是第一次绘制 + if ((this.currentHeight != this.lastHeight || this.currentWidth != this.lastWidth) || this.firstDrawFlag) { + this.firstDrawFlag = false; + this.lastWidth = this.currentWidth + this.lastHeight = this.currentHeight + this.imageKnifeExecute() + } + } + } +``` + +## 4、优化成果 + +| | 图片加载速度平均耗时 | 相同时间CPU指令个数 | +| ------- |-----------------|------------- +| 优化前 | 2ms 915μs 476ns | 3925183432 | +| 优化后 | 2ms 56μs 708ns | 2746425471 | \ No newline at end of file diff --git "a/FAQ/ImageKnife/ImageKnife\346\200\247\350\203\275\346\200\273\347\273\223-\347\275\221\347\273\234\344\274\230\345\214\226.md" "b/FAQ/ImageKnife/ImageKnife\346\200\247\350\203\275\346\200\273\347\273\223-\347\275\221\347\273\234\344\274\230\345\214\226.md" new file mode 100644 index 0000000000000000000000000000000000000000..2e521662cd1fd22a13f6c80777c430634b07e4bf --- /dev/null +++ "b/FAQ/ImageKnife/ImageKnife\346\200\247\350\203\275\346\200\273\347\273\223-\347\275\221\347\273\234\344\274\230\345\214\226.md" @@ -0,0 +1,116 @@ +# ImageKnife性能总结-网络优化 + +## 1、背景 +加载过程中发现网络下载耗时长。 + +## 2、原因分析 +1、使用downloadFile接口:使用该接口流程为:下载网络文件 → 网络数据写入本地磁盘 → 读取本地磁盘文件 → 写缓存 → 解码 → xxx → 显示图片。 + +2、如上流程,可分析出,使用downloadFile接口存在读写磁盘操作存在不必要的时间损耗。 + +## 3、修改方案 +1、使用requestInStream接口: 该接口流程为:下载网络文件 → 写缓存 → 解码 → xxx → 显示图片。 + +代码参考: + +```shell +import { IDataFetch } from '../networkmanage/IDataFetch' +import { RequestOption } from '../RequestOption' +import http from '@ohos.net.http' + +// 数据加载器 +class RequestData { + receiveSize: number = 2000 + totalSize: number = 2000 +} + +export class HttpDownloadClient implements IDataFetch { + async loadData(request: RequestOption, onComplete: (img: ArrayBuffer) => void, onError: (err: string) => void) { + try { + let httpRequest = http.createHttp() + let arrayBuffers = new Array(); + httpRequest.on('headersReceive', (header: Object) => { + // 跟服务器连接成功准备下载 + if (request.progressFunc) { + // 进度条为0 + request.progressFunc.asyncSuccess(0) + } + }) + httpRequest.on('dataReceive', (data: ArrayBuffer) => { + // 下载数据流多次返回 + arrayBuffers.push(data); + }) + httpRequest.on('dataReceiveProgress', (data: RequestData) => { + // 下载进度 + if (data != undefined && (typeof data.receiveSize == 'number') && (typeof data.totalSize == 'number')) { + let percent = Math.round(((data.receiveSize * 1.0) / (data.totalSize * 1.0)) * 100) + if (request.progressFunc) { + request.progressFunc.asyncSuccess(percent) + } + } + }) + httpRequest.on('dataEnd', () => { + // 下载完毕 + let combineArray = this.combineArrayBuffers(arrayBuffers); + onComplete(combineArray) + }) + + const headerObj: Record = {} + request.headers.forEach((value, key) => { + headerObj[key] = value + }) + let promise = httpRequest.requestInStream(request.loadSrc as string, { + header: headerObj, + method: http.RequestMethod.GET, + expectDataType: http.HttpDataType.ARRAY_BUFFER, + connectTimeout: 60000, // 可选 默认60000ms + readTimeout: 0, // 可选, 默认为60000ms + usingProtocol: http.HttpProtocol.HTTP1_1, // 可选,协议类型默认值由系统自动指定 + usingCache: false + }); + await promise.then((data) => { + if (data == 200) { + + } else { + onError(`HttpDownloadClient has error, http code = ` + JSON.stringify(data)) + } + }).catch((err: Error) => { + onError(`HttpDownloadClient has error, http code = ` + JSON.stringify(err)) + }) + } catch (err) { + onError('HttpDownloadClient catch err request uuid =' + request.uuid) + } + } + + combineArrayBuffers(arrayBuffers: ArrayBuffer[]): ArrayBuffer { + // 计算多个ArrayBuffer的总字节大小 + let totalByteLength = 0; + for (const arrayBuffer of arrayBuffers) { + totalByteLength += arrayBuffer.byteLength; + } + + // 创建一个新的ArrayBuffer + const combinedArrayBuffer = new ArrayBuffer(totalByteLength); + + // 创建一个Uint8Array来操作新的ArrayBuffer + const combinedUint8Array = new Uint8Array(combinedArrayBuffer); + + // 依次复制每个ArrayBuffer的内容到新的ArrayBuffer中 + let offset = 0; + for (const arrayBuffer of arrayBuffers) { + const sourceUint8Array = new Uint8Array(arrayBuffer); + combinedUint8Array.set(sourceUint8Array, offset); + offset += sourceUint8Array.length; + } + + return combinedArrayBuffer; + } +} +``` + +## 4、优化成果 + +| | 平均耗时 | max耗时 | min耗时 | +| ------- |---------------------|-------------|-------| +| 优化前 | 154ms 115μs 357.4ns | 832ms 868μs | 728μs | +| 优化后 | 104ms 538μs 164.2ns | 587ms 5μs | 853μs | diff --git a/FAQ/README.md b/FAQ/README.md new file mode 100644 index 0000000000000000000000000000000000000000..a8d955eb66cca483ae3a9f7215869d6543f5cd35 --- /dev/null +++ b/FAQ/README.md @@ -0,0 +1,18 @@ +# Js/Ts/ArkTs三方库常见FAQ + +## 一、ImageKnife + +- [ImageKnife性能总结-网络优化.md](ImageKnife%2FImageKnife%E6%80%A7%E8%83%BD%E6%80%BB%E7%BB%93-%E7%BD%91%E7%BB%9C%E4%BC%98%E5%8C%96.md) + +- [ImageKnife性能总结-OnAreaChange优化.md](ImageKnife%2FImageKnife%E6%80%A7%E8%83%BD%E6%80%BB%E7%BB%93-OnAreaChange%E4%BC%98%E5%8C%96.md) + + +## 二、ohos-MPChart + +- [鸿蒙MPChart图表自定义(一)绘制一条虚实相接的曲线.md](ohos-MPChart%2F%E9%B8%BF%E8%92%99MPChart%E5%9B%BE%E8%A1%A8%E8%87%AA%E5%AE%9A%E4%B9%89%EF%BC%88%E4%B8%80%EF%BC%89%E7%BB%98%E5%88%B6%E4%B8%80%E6%9D%A1%E8%99%9A%E5%AE%9E%E7%9B%B8%E6%8E%A5%E7%9A%84%E6%9B%B2%E7%BA%BF.md) + +- [鸿蒙MPChart图表自定义(二)根据y轴刻度绘制渐变色曲线.md](ohos-MPChart%2F%E9%B8%BF%E8%92%99MPChart%E5%9B%BE%E8%A1%A8%E8%87%AA%E5%AE%9A%E4%B9%89%EF%BC%88%E4%BA%8C%EF%BC%89%E6%A0%B9%E6%8D%AEy%E8%BD%B4%E5%88%BB%E5%BA%A6%E7%BB%98%E5%88%B6%E6%B8%90%E5%8F%98%E8%89%B2%E6%9B%B2%E7%BA%BF.md) + +- [鸿蒙图表MPChart自定义样式(五)左y轴显示数值,右y轴显示百分比.md](ohos-MPChart%2F%E9%B8%BF%E8%92%99%E5%9B%BE%E8%A1%A8MPChart%E8%87%AA%E5%AE%9A%E4%B9%89%E6%A0%B7%E5%BC%8F%EF%BC%88%E4%BA%94%EF%BC%89%E5%B7%A6y%E8%BD%B4%E6%98%BE%E7%A4%BA%E6%95%B0%E5%80%BC%EF%BC%8C%E5%8F%B3y%E8%BD%B4%E6%98%BE%E7%A4%BA%E7%99%BE%E5%88%86%E6%AF%94.md) + + diff --git a/FAQ/ohos-MPChart/media/mpchart_custom_curve1.png b/FAQ/ohos-MPChart/media/mpchart_custom_curve1.png new file mode 100644 index 0000000000000000000000000000000000000000..ad3651813eb6f1fb8c146ffb6e7e4e20a5b45a16 Binary files /dev/null and b/FAQ/ohos-MPChart/media/mpchart_custom_curve1.png differ diff --git a/FAQ/ohos-MPChart/media/mpchart_custom_curve2-1.png b/FAQ/ohos-MPChart/media/mpchart_custom_curve2-1.png new file mode 100644 index 0000000000000000000000000000000000000000..374c0cc3faecbc69debe862f3a1045ed08582fd7 Binary files /dev/null and b/FAQ/ohos-MPChart/media/mpchart_custom_curve2-1.png differ diff --git a/FAQ/ohos-MPChart/media/mpchart_custom_curve2-3.png b/FAQ/ohos-MPChart/media/mpchart_custom_curve2-3.png new file mode 100644 index 0000000000000000000000000000000000000000..f49e388ef29fd2428994504590d343a60ee9d25e Binary files /dev/null and b/FAQ/ohos-MPChart/media/mpchart_custom_curve2-3.png differ diff --git a/FAQ/ohos-MPChart/media/mpchart_custom_curve2-4.png b/FAQ/ohos-MPChart/media/mpchart_custom_curve2-4.png new file mode 100644 index 0000000000000000000000000000000000000000..374c0cc3faecbc69debe862f3a1045ed08582fd7 Binary files /dev/null and b/FAQ/ohos-MPChart/media/mpchart_custom_curve2-4.png differ diff --git a/FAQ/ohos-MPChart/media/mpchart_custom_style.png b/FAQ/ohos-MPChart/media/mpchart_custom_style.png new file mode 100644 index 0000000000000000000000000000000000000000..be9d6db34dfdf9cc366bfa6d6710441e88afdbb9 Binary files /dev/null and b/FAQ/ohos-MPChart/media/mpchart_custom_style.png differ diff --git a/FAQ/ohos-MPChart/media/mpchart_customs_curve2-2.png b/FAQ/ohos-MPChart/media/mpchart_customs_curve2-2.png new file mode 100644 index 0000000000000000000000000000000000000000..ffe8a9a5050dfb4a930789430f0fcf74ada68dc3 Binary files /dev/null and b/FAQ/ohos-MPChart/media/mpchart_customs_curve2-2.png differ diff --git "a/FAQ/ohos-MPChart/\351\270\277\350\222\231MPChart\345\233\276\350\241\250\350\207\252\345\256\232\344\271\211\357\274\210\344\270\200\357\274\211\347\273\230\345\210\266\344\270\200\346\235\241\350\231\232\345\256\236\347\233\270\346\216\245\347\232\204\346\233\262\347\272\277.md" "b/FAQ/ohos-MPChart/\351\270\277\350\222\231MPChart\345\233\276\350\241\250\350\207\252\345\256\232\344\271\211\357\274\210\344\270\200\357\274\211\347\273\230\345\210\266\344\270\200\346\235\241\350\231\232\345\256\236\347\233\270\346\216\245\347\232\204\346\233\262\347\272\277.md" new file mode 100644 index 0000000000000000000000000000000000000000..4080769e8a9f67fa97612cec7549f7a3a4f25801 --- /dev/null +++ "b/FAQ/ohos-MPChart/\351\270\277\350\222\231MPChart\345\233\276\350\241\250\350\207\252\345\256\232\344\271\211\357\274\210\344\270\200\357\274\211\347\273\230\345\210\266\344\270\200\346\235\241\350\231\232\345\256\236\347\233\270\346\216\245\347\232\204\346\233\262\347\272\277.md" @@ -0,0 +1,240 @@ +# 鸿蒙MPChart图表自定义(一)绘制一条虚实相接的曲线 + +本文是基于鸿蒙三方库mpchart(OpenHarmony-SIG/ohos-MPChart)的使用,自定义绘制方法,绘制一条虚实相接的曲线。 mpchart本身的绘制功能是不支持虚实相接的曲线的,要么完全是实线,要么完全是虚线。那么当需求是一半是虚线,一半是实线的曲线时,就需要自己定义方法进行绘制了。 首先,需要写一个MyLineDataSet类,继承自LineDataSet,也就是线型图的数据类。为什么需要这个类呢?因为需要在初始化数据的时候定义这个虚实相接的线是从哪里开始由实线变为虚线的,这里MyLineDataSet类的构造方法比它的父类多了一个interval参数,也就是虚实分隔点。 + +```shell +import { EntryOhos, JArrayList, LineDataSet } from '@ohos/mpchart'; + +export class MyLineDataSet extends LineDataSet { + interval: number = 0; + constructor(yVals: JArrayList | null, label: string, interval: number) { + super(yVals, label); + this.interval = interval; + } +} +``` + +定义好自己的数据类之后,就要定义MyRender类了,实线具体的绘制功能,MyRender类继承自LineChartRenderer,因为是要绘制曲线,所以重写的是drawCubicBezier方法,MyRender类的代码如下: + +```shell +import { EntryOhos, ILineDataSet, Style, Transformer, Utils, LineChartRenderer } from '@ohos/mpchart'; +import { MyLineDataSet } from './MyLineDataSet'; + +export default class MyRender extends LineChartRenderer{ + protected drawCubicBezier(c: CanvasRenderingContext2D, dataSet: MyLineDataSet) { + if(dataSet.interval == undefined){ + super.drawCubicBezier(c, dataSet); + return; + } + if (!this.mChart || !this.mXBounds) { + return; + } + const phaseY: number = this.mAnimator ? this.mAnimator.getPhaseY() : 1; + const trans: Transformer | null = this.mChart.getTransformer(dataSet.getAxisDependency()); + + this.mXBounds.set(this.mChart, dataSet); + + const intensity: number = dataSet.getCubicIntensity(); + + let cubicPath = new Path2D(); + //实线 + let solidLinePath = new Path2D(); + //虚线 + let dashedLinePath = new Path2D(); + if (this.mXBounds.range >= 1) { + let prevDx: number = 0; + let prevDy: number = 0; + let curDx: number = 0; + let curDy: number = 0; + + // Take an extra point from the left, and an extra from the right. + // That's because we need 4 points for a cubic bezier (cubic=4), otherwise we get lines moving and doing weird stuff on the edges of the chart. + // So in the starting `prev` and `cur`, go -2, -1 + // And in the `lastIndex`, add +1 + + const firstIndex: number = this.mXBounds.min + 1; + const lastIndex: number = this.mXBounds.min + this.mXBounds.range; + + let prevPrev: EntryOhos | null; + let prev: EntryOhos | null = dataSet.getEntryForIndex(Math.max(firstIndex - 2, 0)); + let cur: EntryOhos | null = dataSet.getEntryForIndex(Math.max(firstIndex - 1, 0)); + let next: EntryOhos | null = cur; + let nextIndex: number = -1; + + if (cur === null) return; + + Utils.resetContext2DWithoutFont(c, this.mRenderPaint); + // let the spline start + cubicPath.moveTo(cur.getX(), cur.getY() * phaseY); + solidLinePath.moveTo(cur.getX(), cur.getY() * phaseY); + + for (let j: number = this.mXBounds.min + 1; j <= this.mXBounds.range + this.mXBounds.min; j++) { + prevPrev = prev; + prev = cur; + cur = nextIndex === j ? next : dataSet.getEntryForIndex(j); + + nextIndex = j + 1 < dataSet.getEntryCount() ? j + 1 : j; + next = dataSet.getEntryForIndex(nextIndex); + + prevDx = (cur.getX() - prevPrev.getX()) * intensity; + prevDy = (cur.getY() - prevPrev.getY()) * intensity; + curDx = (next.getX() - prev.getX()) * intensity; + curDy = (next.getY() - prev.getY()) * intensity; + + cubicPath.bezierCurveTo( + prev.getX() + prevDx, + (prev.getY() + prevDy) * phaseY, + cur.getX() - curDx, + (cur.getY() - curDy) * phaseY, + cur.getX(), + cur.getY() * phaseY + ); + if(j <= dataSet.interval){ + solidLinePath.bezierCurveTo( + prev.getX() + prevDx, + (prev.getY() + prevDy) * phaseY, + cur.getX() - curDx, + (cur.getY() - curDy) * phaseY, + cur.getX(), + cur.getY() * phaseY + ); + if(j == dataSet.interval) { + dashedLinePath.moveTo(cur.getX(), + cur.getY() * phaseY); + } + }else{ + dashedLinePath.bezierCurveTo( + prev.getX() + prevDx, + (prev.getY() + prevDy) * phaseY, + cur.getX() - curDx, + (cur.getY() - curDy) * phaseY, + cur.getX(), + cur.getY() * phaseY + ); + } + } + } + + // if filled is enabled, close the path + if (dataSet.isDrawFilledEnabled()) { + let cubicFillPath: Path2D = new Path2D(); + // cubicFillPath.reset(); + cubicFillPath.addPath(cubicPath); + + if (c && trans) { + this.drawCubicFill(c, dataSet, cubicFillPath, trans, this.mXBounds); + } + } + + this.mRenderPaint.setColor(dataSet.getColor()); + this.mRenderPaint.setStyle(Style.STROKE); + + if (trans && trans.pathValueToPixel(cubicPath)) { + cubicPath = trans.pathValueToPixel(cubicPath); + solidLinePath = trans.pathValueToPixel(solidLinePath); + dashedLinePath = trans.pathValueToPixel(dashedLinePath); + } + + Utils.resetContext2DWithoutFont(c, this.mRenderPaint); + c.beginPath(); + c.stroke(solidLinePath); + c.closePath(); + + Utils.resetContext2DWithoutFont(c, this.mRenderPaint); + c.beginPath(); + c.setLineDash([5,5,0]); + c.stroke(dashedLinePath); + c.closePath(); + this.mRenderPaint.setDashPathEffect(null); + } +} +``` + +这个方法主要内容就是定义了两条path2D,也就是线段来绘制实线和虚线。 + +```shell +//实线 +let solidLinePath = new Path2D(); +//虚线 +let dashedLinePath = new Path2D(); +``` + +绘制方法如下, + +```shell +solidLinePath.bezierCurveTo( + prev.getX() + prevDx, + (prev.getY() + prevDy) * phaseY, + cur.getX() - curDx, + (cur.getY() - curDy) * phaseY, + cur.getX(), + cur.getY() * phaseY +); +``` + + +就是调用path2D的方法bezierCurveTo方法,这个方法有6个参数,分别是控制点1的(x值,y值 )和 控制点2的(x值,y值)以及目标点的(x值,y值)。直接把父类的方法抄过来即可。 需要有一个if判断,if(j <= dataSet.interval)就是当j小于dataSet.interval时,写绘制实线的方法,当j等于dataSet.interval时,虚线要moveTo当前位置;当j大于dataSet.interval时,就调用dashedLinePath.bezierCurveTo方法绘制虚线了。 绘制方法是调用c.stroke方法绘制的。c.setLineDash([5,5,0]);是设置虚线效果。 + +```shell + Utils.resetContext2DWithoutFont(c, this.mRenderPaint); + c.beginPath(); + c.stroke(solidLinePath); + c.closePath(); + + Utils.resetContext2DWithoutFont(c, this.mRenderPaint); + c.beginPath(); + c.setLineDash([5,5,0]); + c.stroke(dashedLinePath); + c.closePath(); +``` + +使用代码如下: + +```shell +import { + JArrayList,EntryOhos,ILineDataSet,LineData,LineChart,LineChartModel, + Mode, +} from '@ohos/mpchart'; +import { MyLineDataSet } from './MyLineDataSet'; +import MyRender from './MyRender'; +import data from '@ohos.telephony.data'; + +@Entry +@Component +struct Index { + private model: LineChartModel = new LineChartModel(); + + aboutToAppear() { + // 创建一个 JArrayList 对象,用于存储 EntryOhos 类型的数据 + let values: JArrayList = new JArrayList(); + // 循环生成 1 到 20 的随机数据,并添加到 values 中 + for (let i = 1; i <= 20; i++) { + values.add(new EntryOhos(i, Math.random() * 100)); + } + // 创建 LineDataSet 对象,使用 values 数据,并设置数据集的名称为 'DataSet' + let dataSet = new MyLineDataSet(values, 'DataSet', 6); + dataSet.setMode(Mode.CUBIC_BEZIER); + dataSet.setDrawCircles(false); + dataSet.setColorByColor(Color.Blue) + let dataSetList: JArrayList = new JArrayList(); + dataSetList.add(dataSet); + // 创建 LineData 对象,使用 dataSetList数据,并将其传递给model + let lineData: LineData = new LineData(dataSetList); + this.model?.setData(lineData); + this.model.setRenderer(new MyRender(this.model, this.model.getAnimator()!, this.model.getViewPortHandler())) + + } + + build() { + Column() { + LineChart({ model: this.model }) + .width('100%') + .height('100%') + .backgroundColor(Color.White) + } + } +} +``` +其中重要的就是let dataSet = new MyLineDataSet(values, 'DataSet', 6);设置了分隔点为6,以及这行代码设置了renderer类为自定义的render类:this.model.setRenderer(new MyRender(this.model, this.model.getAnimator()!, this.model.getViewPortHandler())) 。 + +![鸿蒙图表MPChart自定义样式(五)左y轴显示数值,右y轴显示百分比](./media/mpchart_custom_curve1.png) \ No newline at end of file diff --git "a/FAQ/ohos-MPChart/\351\270\277\350\222\231MPChart\345\233\276\350\241\250\350\207\252\345\256\232\344\271\211\357\274\210\344\272\214\357\274\211\346\240\271\346\215\256y\350\275\264\345\210\273\345\272\246\347\273\230\345\210\266\346\270\220\345\217\230\350\211\262\346\233\262\347\272\277.md" "b/FAQ/ohos-MPChart/\351\270\277\350\222\231MPChart\345\233\276\350\241\250\350\207\252\345\256\232\344\271\211\357\274\210\344\272\214\357\274\211\346\240\271\346\215\256y\350\275\264\345\210\273\345\272\246\347\273\230\345\210\266\346\270\220\345\217\230\350\211\262\346\233\262\347\272\277.md" new file mode 100644 index 0000000000000000000000000000000000000000..8e5361774e79eb0e708460ab93eca7e007d0d7ca --- /dev/null +++ "b/FAQ/ohos-MPChart/\351\270\277\350\222\231MPChart\345\233\276\350\241\250\350\207\252\345\256\232\344\271\211\357\274\210\344\272\214\357\274\211\346\240\271\346\215\256y\350\275\264\345\210\273\345\272\246\347\273\230\345\210\266\346\270\220\345\217\230\350\211\262\346\233\262\347\272\277.md" @@ -0,0 +1,235 @@ +# 鸿蒙MPChart图表自定义(二)根据y轴刻度绘制渐变色曲线 + +![鸿蒙图表MPChart自定义样式(五)左y轴显示数值,右y轴显示百分比](./media/mpchart_custom_curve2-1.png) + +本文是基于鸿蒙三方库mpchart(OpenHarmony-SIG/ohos-MPChart)的使用,自定义绘制方法,绘制一条颜色渐变的曲线。 + +mpchart本身的绘制功能是不支持颜色渐变的曲线的,只支持渐变色填充大块颜色。那么当需求曲线根据y轴的刻度发生变化,就需要自定义绘制方法了。 + +从图中可以看到,左边的y轴是一个从底部到顶部颜色渐变的直线,从绿色渐变到红色,而且数据曲线根据y轴刻度做同样的渐变色。所以需要修改的就是两个部件的绘制效果,一个是左y轴的绘制效果,一个是数据线的绘制效果。它们涉及到两个绘制类: + +```shell +YAxisRenderer 和 LineChartRenderer +``` + +首先来看数据线的绘制方法,因为这里以曲线为例,所以只需要修改绘制曲线的方法,找到mpchart源码中LineChartRenderer类的drawCubicBezier方法,自己自定义一个MyDataRender类继承自LineChartRenderer类,然后将LineChartRenderer类的drawCubicBezier方法复制到自定义的类中,在其基础上做修改: + +![鸿蒙图表MPChart自定义样式(五)左y轴显示数值,右y轴显示百分比](./media/mpchart_customs_curve2-2.png) + +主要的修改就是将canvas的strokeStyle设置为通过createLinearGradient方法创建的渐变色效果。这段代码创建了一个从底部到顶部的垂直渐变,颜色从绿色到红色变化,并将这个渐变应用到了Canvas的描边样式中。 + +完整代码如下: + +```shell +import { EntryOhos, ILineDataSet, Style, Transformer, Utils, LineChartRenderer } from '@ohos/mpchart'; + +export default class MyDataRender extends LineChartRenderer{ + + protected drawCubicBezier(c: CanvasRenderingContext2D, dataSet: ILineDataSet) { + if (!this.mChart || !this.mXBounds) { + return; + } + const phaseY: number = this.mAnimator ? this.mAnimator.getPhaseY() : 1; + const trans: Transformer | null = this.mChart.getTransformer(dataSet.getAxisDependency()); + + this.mXBounds.set(this.mChart, dataSet); + + const intensity: number = dataSet.getCubicIntensity(); + + let cubicPath = new Path2D(); + // cubicPath.reset(); + if (this.mXBounds.range >= 1) { + let prevDx: number = 0; + let prevDy: number = 0; + let curDx: number = 0; + let curDy: number = 0; + + // Take an extra point from the left, and an extra from the right. + // That's because we need 4 points for a cubic bezier (cubic=4), otherwise we get lines moving and doing weird stuff on the edges of the chart. + // So in the starting `prev` and `cur`, go -2, -1 + // And in the `lastIndex`, add +1 + + const firstIndex: number = this.mXBounds.min + 1; + const lastIndex: number = this.mXBounds.min + this.mXBounds.range; + + let prevPrev: EntryOhos | null; + let prev: EntryOhos | null = dataSet.getEntryForIndex(Math.max(firstIndex - 2, 0)); + let cur: EntryOhos | null = dataSet.getEntryForIndex(Math.max(firstIndex - 1, 0)); + let next: EntryOhos | null = cur; + let nextIndex: number = -1; + + if (cur === null) return; + + Utils.resetContext2DWithoutFont(c, this.mRenderPaint); + // let the spline start + cubicPath.moveTo(cur.getX(), cur.getY() * phaseY); + + for (let j: number = this.mXBounds.min + 1; j <= this.mXBounds.range + this.mXBounds.min; j++) { + prevPrev = prev; + prev = cur; + cur = nextIndex === j ? next : dataSet.getEntryForIndex(j); + + nextIndex = j + 1 < dataSet.getEntryCount() ? j + 1 : j; + next = dataSet.getEntryForIndex(nextIndex); + + prevDx = (cur.getX() - prevPrev.getX()) * intensity; + prevDy = (cur.getY() - prevPrev.getY()) * intensity; + curDx = (next.getX() - prev.getX()) * intensity; + curDy = (next.getY() - prev.getY()) * intensity; + + cubicPath.bezierCurveTo( + prev.getX() + prevDx, + (prev.getY() + prevDy) * phaseY, + cur.getX() - curDx, + (cur.getY() - curDy) * phaseY, + cur.getX(), + cur.getY() * phaseY + ); + } + } + + // if filled is enabled, close the path + if (dataSet.isDrawFilledEnabled()) { + let cubicFillPath: Path2D = new Path2D(); + // cubicFillPath.reset(); + cubicFillPath.addPath(cubicPath); + + if (c && trans) { + this.drawCubicFill(c, dataSet, cubicFillPath, trans, this.mXBounds); + } + } + + // this.mRenderPaint.setColor(dataSet.getColor()); + // this.mRenderPaint.setStyle(Style.STROKE); + + let grad = c.createLinearGradient(0, this.mChart.getHeight(), 0, 0); + grad.addColorStop(0.0, '#00ff00') + grad.addColorStop(1.0, '#ff0000') + c.strokeStyle = grad; + if (trans) { + cubicPath = trans.pathValueToPixel(cubicPath); + } + + Utils.resetContext2DWithoutFont(c, this.mRenderPaint); + c.beginPath(); + c.stroke(cubicPath); + c.closePath(); + this.mRenderPaint.setDashPathEffect(null); + } +} +``` + +数据线的绘制方法修改完了,之后就是修改y轴的绘制方法了。与数据线的绘制类似,定义一个自定义类MyAxisRender继承自YAxisRenderer类,然后修改绘制左y轴的方法,即renderAxisLine方法: + +![鸿蒙图表MPChart自定义样式(五)左y轴显示数值,右y轴显示百分比](./media/mpchart_custom_curve2-3.png) + +同样是创建了一个从底部到顶部的垂直渐变,颜色从绿色到红色变化,并将这个渐变应用到了Canvas的描边样式中。 + +完整代码如下: + +```shell +import { EntryOhos, ILineDataSet, Style, Transformer, Utils, LineChartRenderer, XAxisRenderer, + YAxisRenderer, + AxisDependency} from '@ohos/mpchart'; + +export default class MyAxisRender extends YAxisRenderer{ + public renderAxisLine(c: CanvasRenderingContext2D, extraLength: number): void { + + if (!this.mYAxis || !this.mViewPortHandler || !this.mYAxis.isEnabled() || !this.mYAxis.isDrawAxisLineEnabled()) { + return; + } + + if (this.mAxisLinePaint) { + this.mAxisLinePaint.setColor(this.mYAxis.getAxisLineColor()); + this.mAxisLinePaint.setStrokeWidth(this.mYAxis.getAxisLineWidth()); + + Utils.resetContext2DWithoutFont(c, this.mAxisLinePaint); + if (this.mYAxis.getAxisDependency() == AxisDependency.LEFT) { + c.beginPath() + let grad = c.createLinearGradient(0, this.mViewPortHandler.contentBottom() + extraLength, 0, this.mViewPortHandler.contentTop()); + grad.addColorStop(0.0, '#00ff00') + grad.addColorStop(1.0, '#ff0000') + c.strokeStyle = grad; + c.moveTo(this.mViewPortHandler.contentLeft(), this.mViewPortHandler.contentTop()); + c.lineTo(this.mViewPortHandler.contentLeft(), this.mViewPortHandler.contentBottom() + extraLength) + c.stroke(); + c.closePath(); + } else { + c.beginPath() + c.moveTo(this.mViewPortHandler.contentRight(), this.mViewPortHandler.contentTop()); + c.lineTo(this.mViewPortHandler.contentRight(), this.mViewPortHandler.contentBottom() + extraLength) + c.stroke(); + c.closePath(); + } + } + } + +} +``` + +使用代码如下: + +```shell +import { + JArrayList,EntryOhos,ILineDataSet,LineData,LineChart,LineChartModel, + Mode, + LineDataSet, + AxisDependency, + XAxisPosition, +} from '@ohos/mpchart'; +import MyAxisRender from './MyAxisRender'; +import data from '@ohos.telephony.data'; +import MyAxisLeftRender from './MyAxisRender'; +import MyDataRender from './MyDataRender'; + +@Entry +@Component +struct Index { + private model: LineChartModel = new LineChartModel(); + + aboutToAppear() { + // 创建一个 JArrayList 对象,用于存储 EntryOhos 类型的数据 + let values: JArrayList = new JArrayList(); + // 循环生成 1 到 20 的随机数据,并添加到 values 中 + for (let i = 1; i <= 20; i++) { + values.add(new EntryOhos(i, Math.random() * 100)); + } + // 创建 LineDataSet 对象,使用 values 数据,并设置数据集的名称为 'DataSet' + let dataSet = new LineDataSet(values, 'DataSet'); + dataSet.setMode(Mode.CUBIC_BEZIER); + dataSet.setDrawCircles(false); + let dataSetList: JArrayList = new JArrayList(); + dataSetList.add(dataSet); + // 创建 LineData 对象,使用 dataSetList数据,并将其传递给model + let lineData: LineData = new LineData(dataSetList); + this.model?.setData(lineData); + this.model.getAxisLeft()?.setAxisLineWidth(2); + this.model.getXAxis()?.setPosition(XAxisPosition.BOTTOM); + this.model.getAxisRight()?.setEnabled(false); + this.model.getDescription()?.setEnabled(false); + this.model.setRenderer(new MyDataRender(this.model, this.model.getAnimator()!, this.model.getViewPortHandler())) + this.model.setRendererLeftYAxis(new MyAxisLeftRender(this.model.getViewPortHandler(), this.model.getAxisLeft()!, this.model.getTransformer(AxisDependency.LEFT)!)) + + } + + build() { + Column() { + LineChart({ model: this.model }) + .width('100%') + .height('50%') + .backgroundColor(Color.White) + } + } +} +``` + +其中主要修改的代码就是这里,设置了绘制y轴线的类和绘制数据的类为自己自定义的两个类: + +```shell +this.model.setRenderer(new MyDataRender(this.model, this.model.getAnimator()!, this.model.getViewPortHandler())) +this.model.setRendererLeftYAxis(new MyAxisLeftRender(this.model.getViewPortHandler(), this.model.getAxisLeft()!, this.model.getTransformer(AxisDependency.LEFT)!)) +``` + +好了,再看一遍绘制效果: + +![鸿蒙图表MPChart自定义样式(五)左y轴显示数值,右y轴显示百分比](./media/mpchart_custom_curve2-4.png) \ No newline at end of file diff --git "a/FAQ/ohos-MPChart/\351\270\277\350\222\231\345\233\276\350\241\250MPChart\350\207\252\345\256\232\344\271\211\346\240\267\345\274\217\357\274\210\344\272\224\357\274\211\345\267\246y\350\275\264\346\230\276\347\244\272\346\225\260\345\200\274\357\274\214\345\217\263y\350\275\264\346\230\276\347\244\272\347\231\276\345\210\206\346\257\224.md" "b/FAQ/ohos-MPChart/\351\270\277\350\222\231\345\233\276\350\241\250MPChart\350\207\252\345\256\232\344\271\211\346\240\267\345\274\217\357\274\210\344\272\224\357\274\211\345\267\246y\350\275\264\346\230\276\347\244\272\346\225\260\345\200\274\357\274\214\345\217\263y\350\275\264\346\230\276\347\244\272\347\231\276\345\210\206\346\257\224.md" new file mode 100644 index 0000000000000000000000000000000000000000..ecf05479c2faab57f2cd7fd93a2a28ac52332e2c --- /dev/null +++ "b/FAQ/ohos-MPChart/\351\270\277\350\222\231\345\233\276\350\241\250MPChart\350\207\252\345\256\232\344\271\211\346\240\267\345\274\217\357\274\210\344\272\224\357\274\211\345\267\246y\350\275\264\346\230\276\347\244\272\346\225\260\345\200\274\357\274\214\345\217\263y\350\275\264\346\230\276\347\244\272\347\231\276\345\210\206\346\257\224.md" @@ -0,0 +1,145 @@ +## 鸿蒙图表MPChart自定义样式(五)左y轴显示数值,右y轴显示百分比 + +![鸿蒙图表MPChart自定义样式(五)左y轴显示数值,右y轴显示百分比](./media/mpchart_custom_style.png) + +左y轴数值不变,右y轴改成百分比,需要通过自定义RightAxisFormatter实现IAxisValueFormatter接口,将右y轴的数值改成百分比文本,RightAxisFormatter类如下: + +```shell +class RightAxisFormatter implements IAxisValueFormatter { + maxNumber: number = 0; + constructor(maxNumber: number) { + this.maxNumber = maxNumber; + } + getFormattedValue(value: number, axis: AxisBase): string { + switch (value) { + case this.maxNumber: + return "100%"; + case this.maxNumber * 4 / 5: + return "80%"; + case this.maxNumber * 3 / 5: + return "60%"; + case this.maxNumber * 2 / 5: + return "40%"; + case this.maxNumber * 1 / 5: + return "20%"; + case 0: + return "0%"; + } + return ""; + } +} +``` +使用方法如下: + +```shell +//设置标签文本转换器 +rightAxis?.setValueFormatter(new RightAxisFormatter(this.maxNumber)); +``` + +完整代码如下: + +```shell +import { + JArrayList, + EntryOhos, + ILineDataSet, + LineData, + LineChart, + LineChartModel, + Mode, + LineDataSet, + XAxisPosition, + IAxisValueFormatter, + AxisBase, +} from '@ohos/mpchart'; + +class RightAxisFormatter implements IAxisValueFormatter { + maxNumber: number = 0; + constructor(maxNumber: number) { + this.maxNumber = maxNumber; + } + getFormattedValue(value: number, axis: AxisBase): string { + switch (value) { + case this.maxNumber: + return "100%"; + case this.maxNumber * 4 / 5: + return "80%"; + case this.maxNumber * 3 / 5: + return "60%"; + case this.maxNumber * 2 / 5: + return "40%"; + case this.maxNumber * 1 / 5: + return "20%"; + case 0: + return "0%"; + } + return ""; + } +} + +@Entry +@ComponentV2 +struct LeftRightAxisPage { + private model: LineChartModel = new LineChartModel(); + minNumber: number = 0; + maxNumber: number = 500; + @Local dataReady: boolean = false; + + setData() { + // 创建一个 JArrayList 对象,用于存储 EntryOhos 类型的数据 + let values: JArrayList = new JArrayList(); + // 循环生成 1 到 20 的随机数据,并添加到 values 中 + for (let i = 1; i <= 20; i++) { + values.add(new EntryOhos(i, Math.random() * 500)); + } + // 创建 LineDataSet 对象,使用 values 数据,并设置数据集的名称为 'DataSet' + let dataSet = new LineDataSet(values, 'DataSet'); + dataSet.setMode(Mode.CUBIC_BEZIER); + dataSet.setDrawCircles(false); + let dataSetList: JArrayList = new JArrayList(); + dataSetList.add(dataSet); + // 创建 LineData 对象,使用 dataSetList数据,并将其传递给model + let lineData: LineData = new LineData(dataSetList); + let leftAxis = this.model.getAxisLeft(); + let rightAxis = this.model.getAxisRight(); + + leftAxis?.setAxisMinimum(this.minNumber); + leftAxis?.setAxisMaximum(this.maxNumber); + //设置有6个标签 + leftAxis?.setLabelCount(6, true); + + rightAxis?.setAxisMinimum(this.minNumber); + rightAxis?.setAxisMaximum(this.maxNumber); + //设置有6个标签 + rightAxis?.setLabelCount(6, true); + + //设置标签文本转换器 + rightAxis?.setValueFormatter(new RightAxisFormatter(this.maxNumber)); + this.model.getXAxis()?.setPosition(XAxisPosition.BOTTOM); + this.model.getDescription()?.setEnabled(false); + this.model?.setData(lineData); + this.dataReady = true; + } + + aboutToAppear() { + // 模拟网络请求,延时1秒获取数据 + setTimeout(() => { + this.setData() + }, 1000); + } + + build() { + Column() { + if (this.dataReady) { + LineChart({ model: this.model }) + .width('100%') + .height('50%') + .backgroundColor(Color.White) + } else { + LoadingProgress() + .color(Color.Blue) + } + } + } +} +``` diff --git a/README.md b/README.md index 29d04756f8481edb0c7989e4124a3100e301978a..37683cae2ab3fb49b34b0bccad230d2ede26c62f 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,25 @@ # docs -## Introduction +## 介绍 -docs serves as a documentation repository within the private warehouse organization of the OpenHarmony third-party component library. It stores sub-project documents of OpenHarmony third-party components. +docs是OpenHarmony三方组件库私有仓组织的一个文档仓,存放OpenHarmony三方组件子项目文档。 -## OpenHarmony-TPC Developer Documentation +## OpenHarmony-TPC 开发者文档 -### Developer Documents +### 开发者文档 -- [Creating a Third-Party Library](./contribute/create-third-party-library.md) -- [Porting a Third-Party JavaScript Library](./contribute/adapter-guide/port-third-party-js-library.md) -- [Porting a Third-Party C/C++ Library](./contribute/adapter-guide/port-third-party-c-cpp-library.md) -- [OpenHarmony HAR](./OpenHarmony_har_usage_EN.md) +- [如何创建新的三方库](./contribute/%E5%88%9B%E5%BB%BA%E4%B8%89%E6%96%B9%E5%BA%93%E5%B7%A5%E7%A8%8B%E6%8C%87%E5%AF%BC.md) +- [如何移植js三方库](./contribute/adapter-guide/js%E7%A7%BB%E6%A4%8D%E9%80%82%E9%85%8D%E6%8C%87%E5%AF%BC.md) +- [如何移植c/c++三方库](./contribute/adapter-guide/c_c%2B%2B%E7%A7%BB%E6%A4%8D%E9%80%82%E9%85%8D%E6%8C%87%E5%AF%BC.md) +- [har包使用指导](./OpenHarmony_har_usage.md) -### Contributor References +### 贡献者参考文档 -- [Contributing a Third-Party Library to OpenHarmony Third-Party Library Repository](./contribute/contribute-third-party-library-to-openharmony-third-party-library-repository.md) -- [Contributing a Third-Party Library to Openharmony-TPC](./contribute/contribute-third-party-library-to-openharmony-tpc.md) -- [OpenHarmony Third-Party Library Release Standards](./contribute/third-party-library-release-standards.md) -- [OpenHarmony-TPC Open-Source Compliance Policy](./contribute/rules/openharmony-tpc-open-source-compliance-policy.md) +- [贡献三方库到OpenHarmony三方库中心仓](./contribute/ohpm%E5%8F%91%E5%B8%83%E6%8C%87%E5%AF%BC.md) +- [贡献三方库到Openharmony-TPC](./contribute/TPC%E5%85%B1%E5%BB%BA%E6%8C%87%E5%AF%BC.md) +- [OpenHarmony三方库发布标准](./contribute/TPC%E5%8F%91%E5%B8%83%E6%A0%87%E5%87%86.md) +- [OpenHarmony-TPC开源合规规范](./contribute/rules/OpenHarmony-TPC%E5%BC%80%E6%BA%90%E5%90%88%E8%A7%84%E8%A7%84%E8%8C%83.md) -## License +## 开源协议 -This project is licensed under [Apache License 2.0](https://gitee.com/openharmony-tpc/docs/blob/master/LICENSE). +本项目基于 [Apache License 2.0](https://gitee.com/openharmony-tpc/docs/blob/master/LICENSE) ,请自由地享受和参与开源。