125 lines
4.7 KiB
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)),
|
|
);
|
|
}
|