React Native中@wuba/react-native-echarts线性图表部分安卓设备UI故障
React Native @wuba/react-native-echarts 安卓设备UI故障修复方案
问题背景
使用@wuba/react-native-echarts组件展示线性图表时,Redmi K30(MIUI)、部分三星等安卓设备出现图表UI故障(元素错位、渲染混乱),但iOS及其他安卓设备显示正常。更换依赖版本无效,官方基础示例可正常运行,问题锁定在自定义轴配置及相关计算逻辑。
核心问题分析
- useEffect依赖缺失:图表初始化逻辑未监听
yAxis、series等关键配置的变化,导致图表可能使用过期配置渲染。 - 多Y轴偏移计算兼容性:
offset: 80 * ~~(i / 2)的整数取整计算在部分安卓SVG渲染环境下引发布局错位。 - Grid边距精度问题:通过整数取整计算的Grid
left/right边距,在不同像素密度设备上适配性差。 - 时间轴数据格式兼容性:直接传入Date对象作为X轴数据,部分安卓设备的ECharts解析存在兼容性问题。
- useDirtyRect配置冲突:启用
useDirtyRect可能在部分安卓设备上触发渲染bug。
修复后的完整代码
import { List } from "linqts"; import { SVGRenderer, SkiaChart } from "@wuba/react-native-echarts"; import { useEffect, useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; import { Dimensions } from "react-native"; import { roundUpToNearestLog } from "../../utils/number-utils"; import { Unit } from "../../utils/units"; import * as echarts from "echarts/core"; import { View } from "../Themed"; echarts.use([SVGRenderer]); export interface ChartStyles { category: string; opacity: number; interval: number | undefined; } const defaultUnitChartStyle: ChartStyles = { category: "common.value", opacity: 1, interval: undefined, }; export const unitChartStyles: { [name in Unit]: ChartStyles } = { [Unit.none]: { ...defaultUnitChartStyle, }, [Unit.euro]: { ...defaultUnitChartStyle, category: "common.price", opacity: 0.3, }, [Unit.watt]: { ...defaultUnitChartStyle, category: "common.power", }, [Unit.amper]: { ...defaultUnitChartStyle, category: "common.current", }, [Unit.volt]: { ...defaultUnitChartStyle, category: "common.voltage", }, [Unit.wattHour]: { ...defaultUnitChartStyle, category: "common.energy", }, [Unit.percent]: { ...defaultUnitChartStyle, category: "common.percentage", }, [Unit.state]: { ...defaultUnitChartStyle, category: "common.state", interval: 1, }, }; export function getUnitStyles(unit: Unit | string | undefined): ChartStyles { return unitChartStyles[unit as Unit] || unitChartStyles[Unit.none]; } function yAxisFormatter(value: number, unit: string | undefined): string { if (unit === Unit.state) { return Boolean(value).toString(); } return `${ Math.abs(value) < 1 && value !== 0 ? value.toFixed(1) : value.toFixed(0) } ${unit ?? ""}`; } export interface TimeChartPoint { date: Date; value: number; } export interface TimeChartData { color?: string; unit?: Unit | string; name?: string; points: TimeChartPoint[]; step?: boolean | "start" | "middle" | "end"; type?: "line" | "bar"; showBackground?: boolean; } export interface TimeChartProps { data: TimeChartData[]; } export function TimeChart({ data }: TimeChartProps) { const { t } = useTranslation(); const skiaRef = useRef<any>(null); const chartRef = useRef<any>(null); const seriesBounds = useMemo(() => { const rangesByUnitAndSerie = data.map((d) => ({ unit: d.unit, min: d.points.length === 0 ? 0 : d.points .map((p) => p.value) .reduce((prev, curr) => Math.min(prev, curr)), max: d.points.length === 0 ? 0 : d.points .map((p) => p.value) .reduce((prev, curr) => Math.max(prev, curr)), })); const rangesByUnitGrouped = new List(rangesByUnitAndSerie).GroupBy( (r) => r.unit ?? "", (e) => e ); const rangesByUnit = []; for (const key in rangesByUnitGrouped) { const unitRange = { unit: key, min: roundUpToNearestLog( new List(rangesByUnitGrouped[key]).Min((r) => r.min) ), max: roundUpToNearestLog( new List(rangesByUnitGrouped[key]).Max((r) => r.max) ), }; const orderDifference = Math.log10(Math.abs(unitRange.min || 1)) - Math.log10(Math.abs(unitRange.max || 1)); if (orderDifference > 1) { unitRange.max = unitRange.max * Math.pow(10, Math.floor(orderDifference)); } else if (orderDifference < -1) { unitRange.min = unitRange.min * Math.pow(10, Math.floor(-orderDifference)); } if (key === Unit.state) { unitRange.min = +false; unitRange.max = +true; } rangesByUnit.push(unitRange); } const ratios = new List(rangesByUnit) .Select((rbu) => (rbu.min === 0 ? 0 : rbu.max / rbu.min)) .ToArray(); const negativeRatios = ratios.filter(r => r < 0); let minRatio: number | undefined; if (negativeRatios.length > 0) { const minDistanceToOne = new List(negativeRatios) .Min((r) => Math.abs(r - 1)); minRatio = negativeRatios.find(r => Math.abs(r - 1) === minDistanceToOne); } rangesByUnit .filter((x) => x.unit !== Unit.state) .forEach((r) => { r.min = minRatio !== undefined ? r.max / minRatio : r.min > 0 ? 0 : r.min; }); return rangesByUnit; }, [data]); const yAxis = useMemo( () => data .filter((x, i, a) => a.map((y) => y.unit).indexOf(x.unit) === i) .map((x, i) => { const unitStyles = getUnitStyles(x.unit); return { name: t(unitStyles.category), type: "value", max: seriesBounds.filter((s) => s.unit === x.unit)[0]?.max, min: seriesBounds.filter((s) => s.unit === x.unit)[0]?.min, axisLabel: { formatter: (value: number) => yAxisFormatter(value, x.unit), }, axisLine: { show: true, }, splitLine: { show: false, }, interval: unitStyles.interval, axisTick: { show: true, inside: true, }, showGrid: false, position: i % 2 ? "right" : "left", offset: 80 * Math.floor(i / 2), }; }), [data, t, seriesBounds] ); const series = useMemo( () => data.map((x, i) => { const unitStyles = getUnitStyles(x.unit); return { name: `${x.name ?? i} ${ (x.unit as Unit) && x.unit !== Unit.state ? `[${x.unit}]` : "" }`, data: x.points.map((point) => [ point.date.getTime(), x.unit !== Unit.state ? point.value : Boolean(point.value), ]), color: x.color, itemStyle: { opacity: unitStyles.opacity, }, step: x.step, type: x.type ?? "line", areaStyle: { color: x.showBackground ? x.color : "transparent", opacity: 0.3, }, yAxisIndex: yAxis.findIndex(y => y.name === t(unitStyles.category)), showSymbol: false, zlevel: 9, z: 9, }; }), [data, yAxis, t] ); useEffect(() => { if (!skiaRef.current) return; if (chartRef.current) { chartRef.current.dispose(); } const windowWidth = Dimensions.get("window").width; const options = { tooltip: { trigger: "axis", }, xAxis: { type: "time", axisLabel: { hideOverlap: false, }, axisTick: { show: true, }, splitLine: { show: true, }, boundaryGap: false, }, yAxis: yAxis, series: series, grid: { left: `${Math.max(20, Math.floor((yAxis.length + 1) / 2) * 70)}px`, right: `${Math.max(20, Math.floor(yAxis.length / 2) * 70)}px`, }, }; chartRef.current = echarts.init(skiaRef.current, "light", { renderer: "svg", useDirtyRect: false, width: windowWidth - 28, height: 400, }); chartRef.current.setOption(options); return () => chartRef.current?.dispose(); }, [yAxis, series]); return ( <View style={{ backgroundColor: "white" }}> <SkiaChart ref={skiaRef} /> </View> ); }
关键修复点说明
- 补全useEffect依赖:添加
yAxis、series作为依赖项,确保配置变化时图表重新初始化。 - 时间轴数据格式转换:将
Date对象转为时间戳,解决部分安卓设备的时间解析问题。 - 禁用useDirtyRect:关闭可能引发渲染异常的脏矩形优化。
- 优化Y轴索引查找:用
findIndex替代map+indexOf,提升性能同时避免潜在的匹配问题。 - Grid边距保底处理:用
Math.max(20, ...)确保最小边距,避免布局溢出。
内容的提问来源于stack exchange,提问作者PoloMonola
相关产品推荐
相关产品推荐

