공식 문서에 그라데이션을 적용하는 방법이 소개되어 있긴 하지만, 디자인 구현을 위해 커스텀 렌더러를 문서에 나온 것 이상으로 더 깊게 건드리는 과정에서 여러 문제를 겪음
- 값이 음수인 경우 gradient 방향을 반대로 하는 옵션 제공하지 않음
- 마찬가지로 border radius로 방향을 반대로 설정하는 옵션 없음
- 상기 두 요구사항을 달성하기 위한 커스텀 과정에서 애니메이션 적용 어려움
- 여러 차트를 한 틱에서 보여줄 때 순서가 바뀌는 문제를 해결 필요
- 목표는
ColumnSeries
로 요구사항 구현
문제 1. 음수일 때 그라데이션 효과 반대 방향으로 적용하기
onPaint를 오버라이딩
class _ColumnCustomPainter extends ColumnSegment<ChartData, DateTime> {
@override
void onPaint(Canvas canvas) {
// 원래 구현 호출 (트래커 바운드 및 기본 렌더링)
super.onPaint(canvas);
// segmentRect가 없으면 아무것도 그리지 않음
if (segmentRect == null) return;
// 이미 그려진 컬럼 위에 그라디언트로 다시 그리기
final Paint gradientPaint = Paint()..style = PaintingStyle.fill;
final double y = series.dataSource![currentSegmentIndex].y;
final Rect rect = segmentRect!.outerRect;
gradientPaint.shader = ui.Gradient.linear(
rect.topCenter,
rect.bottomCenter,
y >= 0
? [MyColors.blue1, MyColors.gray3]
: [MyColors.blue2, MyColos.gray3],
[0.0, 1.0],
);
// 동일한 segmentRect를 그라디언트로 다시 그림
canvas.drawRRect(segmentRect!, gradientPaint);
}
문제 2. 음수일 때 border radius 반대 방향으로 적용하기
class _ColumnCustomPainter extends ColumnSegment<ChartData, DateTime> {
@override
void onPaint(Canvas canvas) {
// 원래 구현 호출 (트래커 바운드 및 기본 렌더링)
super.onPaint(canvas);
// segmentRect가 없으면 아무것도 그리지 않음
if (segmentRect == null) return;
// 이미 그려진 컬럼 위에 그라디언트로 다시 그리기
final Paint gradientPaint = Paint()..style = PaintingStyle.fill;
final double y = series.dataSource![currentSegmentIndex].y;
final Rect rect = segmentRect!.outerRect;
// 값에 따라 다른 border radius 적용
final RRect customRRect = y >= 0
? RRect.fromRectAndCorners(
rect,
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
)
: RRect.fromRectAndCorners(
rect,
bottomLeft: Radius.circular(4),
bottomRight: Radius.circular(4),
);
gradientPaint.shader = ui.Gradient.linear(
rect.topCenter,
rect.bottomCenter,
y >= 0
? [MyColors.blue1, MyColors.gray3]
: [MyColors.blue2, MyColos.gray3],
[0.0, 1.0],
);
// 동일한 segmentRect를 그라디언트로 다시 그림
canvas.drawRRect(segmentRect!, gradientPaint);
}
문제 3. 애니메이션 효과 복원하기
- 여기까지 왔다면 애니메이션 효과가 사라진 것을 확인할 수 있음
onPaint
메서드가 내부적으로 애니메이션까지 처리하는데 덮어쓰기했기 때문에 사라진 것임- 애니메이션을 처리할 때 사용하는
_oldSegmentRect
라는 프라이빗 멤버에 접근할 수 없기 때문에_customSegementRect
와_customOldSegementRect
를 만들어서 활용
최종 코드
class BipolarGradientColumnRenderer
extends ColumnSeriesRenderer<ChartData, DateTime> {
final List<Color> colors;
final List<double> steps;
final Radius radius;
BipolarGradientColumnRenderer({
this.colors = const [MyColors.blue1, MyColors.gray3],
this.steps = const [0.0, 1.0],
this.radius = const Radius.circular(4),
});
@override
ColumnSegment<DateTimeChartData, DateTime> createSegment() {
return _ColumnCustomPainter(
colors: colors,
steps: steps,
radius: radius,
);
}
}
class _ColumnCustomPainter extends ColumnSegment<ChartData, DateTime> {
_ColumnCustomPainter({
required this.colors,
required this.steps,
required this.radius,
});
final List<Color> colors;
final List<double> steps;
final Radius radius;
RRect? _customOldSegmentRect;
RRect? _customSegmentRect;
/*
onPaint를 override하면서 애니메이션도 적용하기 위한 코드
라이브러리가 _segmentRect를 _oldSegmentRect에 저장하는 것처럼
애니메이션을 적용하기 커스텀하게 만든 _cusomSegmentRect를 _customOldSegmentRect에 저장
*/
@override
void copyOldSegmentValues(
double seriesAnimationFactor,
double segmentAnimationFactor,
) {
super.copyOldSegmentValues(seriesAnimationFactor, segmentAnimationFactor);
_customOldSegmentRect = _customSegmentRect;
}
/*
onPaint를 override하여 커스텀 페인팅을 적용하기 위한 코드
기존 로직을 최대한 모방, 프라이빗 멤버나 헬퍼에 접근할 수 없다면 일부 코드는 직접 작성해야 함
*/
@override
void onPaint(Canvas canvas) {
if (series.isTrackVisible) {
super.onPaint(canvas);
}
if (segmentRect == null) return;
final double y = series.dataSource![currentSegmentIndex].y.toDouble();
final Rect rect = segmentRect!.outerRect;
final bool isPositive = y >= 0;
// border radius 방향
_customSegmentRect = isPositive
? RRect.fromRectAndCorners(
rect,
topLeft: radius,
topRight: radius,
)
: RRect.fromRectAndCorners(
rect,
bottomLeft: radius,
bottomRight: radius,
);
// animation 적용
RRect? paintRRect;
if (series.parent!.isLegendToggled &&
_customOldSegmentRect != null &&
series.animationType != AnimationType.loading) {
paintRRect = RRect.lerp(
_customOldSegmentRect, _customSegmentRect, animationFactor);
} else {
_customOldSegmentRect ??= isPositive
? RRect.fromRectAndCorners(
Rect.fromLTWH(rect.left, rect.bottom, rect.width, 0),
topLeft: radius,
topRight: radius,
)
: RRect.fromRectAndCorners(
Rect.fromLTWH(rect.left, rect.top, rect.width, 0),
bottomLeft: radius,
bottomRight: radius,
);
paintRRect = RRect.lerp(
_customOldSegmentRect, _customSegmentRect, animationFactor);
}
if (paintRRect == null || paintRRect.isEmpty) {
return;
}
// gradient 적용
final Paint gradientPaint = Paint()..style = PaintingStyle.fill;
gradientPaint.shader = Gradient.linear(
isPositive
? paintRRect.outerRect.topCenter
: paintRRect.outerRect.bottomCenter,
isPositive
? paintRRect.outerRect.bottomCenter
: paintRRect.outerRect.topCenter,
colors,
steps,
);
canvas.drawRRect(paintRRect, gradientPaint);
}
}
그리고 이것저것 주입해서 쓰도록 함
문제 4. 다중 커스텀 렌더렁 사용할 때 발생하는 문제 해결
- 필터에 따라 하나의 tick에서 하나 이상의 차트를 보여줄 때 커스텀 렌더러가 이상하게 동작하는 이슈가 있었음
- 특정 패턴을 발견했는데
A - B - C - D
순으로 선언된 Column에서 모두 커스텀 렌더러가 사용될 때, 자신보다 나중에 선언된 차트보다 화면에 늦게 그려지는 경우(뒤늦게 필터에서 선택된 경우)에는 먼저 그려진 차트의 색상으로 적용되는 현상 - 처음에는 설마 싱글턴인가 싶었는데 아무리봐도 그건 아니었음
- 꽤 오랜 시간 헤매다 우연치 않게
ColumnSeries
에 key 속성을 전달하면서 해결됨 - 여전히 명확한 이유는 모르겠음. 아마도 Flutter의 렌더링에 관한 특성 때문이 아닌가 싶지만 더 조사 필요
ColumnSeries<ChartData, DateTime>(
key: ValueKey(id),
name: name,
dataSource: dataSource,
xValueMapper: (ChartData data, _) => data.x,
yValueMapper: (ChartData data, _) => data.y,
onCreateRenderer: (series) {
return BipolarGradientColumnRenderer(
colors: colors,
);
},
spacing: 0.1,
initialIsVisible: initialIsVisible,
onRendererCreated: onRendererCreated,
);
문제 5. 차트 순서 문제 해결하기
- 한 tick에서 커스텀 필터로 여러 Column을 보여줄 때 필터의 적용 여부에 따라 컬럼의 순서가 바뀜
- 순서가 보장되는 것이 요구사항
build()
안에서 조건문을 사용하는 기존 방식으로는 순서 보장이 안되는 이슈가 있었음
이 문제는 공식 문서의 onRendererCreated
에 대한 내용을 보고 힌트를 얻었는데
- 이 메서드는 시리즈 렌더러가 생성될 때 호출됨
- 콜백에서 전달하는 controller를 인스턴스를 얻어서 각 시리즈의
isVisible
속성을 변경할 수 있음 - 이 예제는 initialIsVisible에 관한 문서에서 확인 가능