frindlymap/lib/modules/ui/src/widgets/ring_view.dart

125 lines
4.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:friendly_map/modules/circles/circles.dart';
import '../theme.dart';
import 'common.dart';
/// 원 위의 한 점: 이름표와 색, 눌렀을 때 할 일.
class RingDot {
const RingDot({required this.label, required this.color, this.sub, this.selected = false, this.faded = false, this.onTap});
final String label;
final String? sub;
final Color color;
final bool selected;
final bool faded;
final VoidCallback? onTap;
}
/// 가운데에 [center] 를 두고, [rings] 의 고리마다 안쪽에서 바깥쪽 원 위에 점을 고르게 놓는다.
/// 고리 하나가 6명을 넘으면 바깥 고리가 하나 더 생긴다 (rings() 로 미리 나눠서 넘긴다).
class RingView extends StatelessWidget {
const RingView({super.key, required this.center, required this.rings, this.centerLabel = '나', this.dotSize = 44});
final Widget center;
final List<List<RingDot>> rings;
final String centerLabel;
final double dotSize;
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, box) {
final w = box.maxWidth;
// 첫 고리는 가운데 "나"와 이름표가 겹치지 않을 만큼 떨어뜨리고, 고리 사이는 점 + 이름표 두 줄 높이만큼 둔다.
final first = dotSize + 52;
final gap = dotSize + 52;
final radius = [for (var i = 0; i < rings.length; i++) first + gap * i];
final reach = (radius.isEmpty ? first : radius.last) + 46;
final canvasW = reach * 2 > w ? reach * 2 : w;
final h = reach * 2 + 16;
// 사람이 많아 원이 화면보다 커지면 통째로 줄여서 화면 안에 맞춘다.
final scale = canvasW > w ? w / canvasW : 1.0;
final cx = canvasW / 2, cy = h / 2;
return SizedBox(
width: w,
height: h * scale,
child: FittedBox(
fit: BoxFit.scaleDown,
child: SizedBox(
width: canvasW,
height: h,
child: Stack(children: [
for (var i = 0; i < rings.length; i++)
Positioned(
left: cx - radius[i],
top: cy - radius[i],
child: Container(width: radius[i] * 2, height: radius[i] * 2, decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: Pal.line))),
),
Positioned(left: cx - 30, top: cy - 30, child: SizedBox(width: 60, height: 60, child: center)),
for (var i = 0; i < rings.length; i++)
for (final e in _spots(rings[i], radius[i]).indexed)
Positioned(
left: cx + e.$2.$1 - 42,
top: cy + e.$2.$2 - dotSize / 2,
child: _Dot(rings[i][e.$1], dotSize),
),
]),
),
),
);
});
}
List<(double, double)> _spots(List<RingDot> ring, double r) => ringPositions(ring.length, r);
}
class _Dot extends StatelessWidget {
const _Dot(this.d, this.size);
final RingDot d;
final double size;
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: d.onTap,
child: SizedBox(
width: 84,
child: Column(mainAxisSize: MainAxisSize.min, children: [
Opacity(
opacity: d.faded ? 0.4 : 1,
child: Stack(clipBehavior: Clip.none, children: [
Avatar(d.label, size: size, color: d.color),
if (d.selected)
Positioned(
right: -4,
top: -4,
child: Container(
decoration: const BoxDecoration(color: Pal.accent, shape: BoxShape.circle),
padding: const EdgeInsets.all(2),
child: const Icon(Icons.check, size: 14, color: Colors.black),
),
),
]),
),
const SizedBox(height: 3),
Text(d.label, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600)),
if (d.sub != null) Text(d.sub!, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 10, color: Pal.muted)),
]),
),
);
}
}
/// 가운데 "나" 동그라미.
class MeDot extends StatelessWidget {
const MeDot(this.name, {super.key});
final String name;
@override
Widget build(BuildContext context) => Container(
decoration: BoxDecoration(shape: BoxShape.circle, color: Pal.accent.withValues(alpha: 0.2), border: Border.all(color: Pal.accent, width: 2)),
alignment: Alignment.center,
child: Text(name.isEmpty ? '나' : name.characters.first, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w800)),
);
}