new project stable version
This commit is contained in:
170
lib/presentation/components/scooter/battery_indicator.dart
Normal file
170
lib/presentation/components/scooter/battery_indicator.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BatteryIndicator extends StatelessWidget {
|
||||
final double percent;
|
||||
|
||||
const BatteryIndicator({super.key, required this.percent});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 400,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CustomPaint(
|
||||
size: const Size(320, 320),
|
||||
painter: _BatteryRingPainter(percent: percent),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Заряд батареи',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${(percent * 100).toInt()}%',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BatteryRingPainter extends CustomPainter {
|
||||
final double percent;
|
||||
|
||||
_BatteryRingPainter({required this.percent});
|
||||
|
||||
// 🔹 Возвращает цвета и stops для текущего диапазона
|
||||
(List<Color>, List<double>?) _getGradientForPercent() {
|
||||
final p = percent * 100;
|
||||
|
||||
if (p >= 51) {
|
||||
return (
|
||||
const [
|
||||
Color(0xFF86EFAC),
|
||||
Color(0xFF67E8F9),
|
||||
Color(0xFF86EFAC),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
} else if (p >= 16) {
|
||||
return (
|
||||
const [
|
||||
Color(0xFFF1FF8B),
|
||||
Color(0xFF8BFFAA),
|
||||
Color(0xFFF1FF8B),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
const [
|
||||
Color(0xFFFF5757),
|
||||
Color(0xFFF1FF8B),
|
||||
Color(0xFFFF5757),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = size.center(Offset.zero);
|
||||
final radius = size.width / 2 - 20;
|
||||
|
||||
// Фоновое кольцо
|
||||
final backgroundPaint = Paint()
|
||||
..color = Colors.white.withOpacity(0.08)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 40;
|
||||
canvas.drawCircle(center, radius, backgroundPaint);
|
||||
|
||||
// Деления
|
||||
final tickPaint = Paint()
|
||||
..color = Colors.white.withOpacity(0.15)
|
||||
..strokeWidth = 2;
|
||||
for (int i = 0; i < 100; i++) {
|
||||
final angle = 2 * pi * i / 100 - pi / 2;
|
||||
final isMajor = i % 10 == 0;
|
||||
|
||||
final innerRadius = radius - 40;
|
||||
final outerRadius = isMajor ? radius - 28 : radius - 32;
|
||||
|
||||
final p1 = Offset(
|
||||
center.dx + cos(angle) * innerRadius,
|
||||
center.dy + sin(angle) * innerRadius,
|
||||
);
|
||||
final p2 = Offset(
|
||||
center.dx + cos(angle) * outerRadius,
|
||||
center.dy + sin(angle) * outerRadius,
|
||||
);
|
||||
|
||||
canvas.drawLine(p1, p2, tickPaint);
|
||||
}
|
||||
|
||||
// Получаем градиент по проценту
|
||||
final (colors, stops) = _getGradientForPercent();
|
||||
|
||||
// Glow дуга
|
||||
final glowPaint = Paint()
|
||||
..shader = SweepGradient(
|
||||
colors: colors,
|
||||
stops: stops,
|
||||
).createShader(Rect.fromCircle(center: center, radius: radius))
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 25
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 85)
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
-pi / 2,
|
||||
2 * pi * percent,
|
||||
false,
|
||||
glowPaint,
|
||||
);
|
||||
|
||||
// Основная дуга
|
||||
final progressPaint = Paint()
|
||||
..shader = SweepGradient(
|
||||
colors: colors,
|
||||
stops: stops,
|
||||
).createShader(Rect.fromCircle(center: center, radius: radius))
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 20
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
-pi / 2,
|
||||
2 * pi * percent,
|
||||
false,
|
||||
progressPaint,
|
||||
);
|
||||
|
||||
// Внутренний круг
|
||||
final innerCircle = Paint()
|
||||
..color = const Color(0xFF16233F)
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawCircle(center, radius - 60, innerCircle);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MiniBatteryIndicator extends StatelessWidget {
|
||||
final int percent;
|
||||
|
||||
const MiniBatteryIndicator({super.key, required this.percent});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CustomPaint(
|
||||
painter: _MiniBatteryRingPainter(percent: percent),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MiniBatteryRingPainter extends CustomPainter {
|
||||
final int percent;
|
||||
|
||||
_MiniBatteryRingPainter({required this.percent});
|
||||
|
||||
(List<Color>, List<double>?) _getGradientForPercent() {
|
||||
final p = percent;
|
||||
|
||||
if (p >= 51) {
|
||||
return (
|
||||
const [
|
||||
Color(0xFF86EFAC),
|
||||
Color(0xFF67E8F9),
|
||||
Color(0xFF86EFAC),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
} else if (p >= 16) {
|
||||
return (
|
||||
const [
|
||||
Color(0xFFF1FF8B),
|
||||
Color(0xFF8BFFAA),
|
||||
Color(0xFFF1FF8B),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
const [
|
||||
Color(0xFFFF5757),
|
||||
Color(0xFFF1FF8B),
|
||||
Color(0xFFFF5757),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = size.center(Offset.zero);
|
||||
final radius = size.width / 2 - 4; // поменьше
|
||||
|
||||
// Фоновое кольцо
|
||||
final backgroundPaint = Paint()
|
||||
..color = Colors.white.withOpacity(0.08)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 4;
|
||||
canvas.drawCircle(center, radius, backgroundPaint);
|
||||
|
||||
// Получаем цвета
|
||||
final (colors, stops) = _getGradientForPercent();
|
||||
|
||||
// Основная дуга
|
||||
final progressPaint = Paint()
|
||||
..shader = SweepGradient(
|
||||
colors: colors,
|
||||
stops: stops,
|
||||
).createShader(Rect.fromCircle(center: center, radius: radius))
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 4
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
-pi / 2,
|
||||
2 * pi * (percent / 100),
|
||||
false,
|
||||
progressPaint,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
33
lib/presentation/components/scooter/scooter_info_item.dart
Normal file
33
lib/presentation/components/scooter/scooter_info_item.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/app_colors.dart';
|
||||
|
||||
class ScooterInfoItem extends StatelessWidget {
|
||||
final String icon;
|
||||
final String text;
|
||||
|
||||
const ScooterInfoItem({super.key, required this.icon, required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
icon,
|
||||
height: 20,
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'scooter_info_item.dart';
|
||||
|
||||
class ScooterInfoSection extends StatelessWidget {
|
||||
const ScooterInfoSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: const [
|
||||
ScooterInfoItem(icon: 'assets/icons/bolt.png', text: '47 км или 4 часа 17 минут'),
|
||||
ScooterInfoItem(icon: 'assets/icons/speed.png', text: 'max = 25 км/ч'),
|
||||
ScooterInfoItem(icon: 'assets/icons/location.png', text: 'пр. Московский, 33'),
|
||||
Row(
|
||||
children: [
|
||||
ScooterInfoItem(icon: 'assets/icons/person.png', text: '120 м'),
|
||||
SizedBox(width: 16),
|
||||
ScooterInfoItem(icon: 'assets/icons/time.png', text: '1 минута'),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
172
lib/presentation/components/scooter/slide_to_reserve_button.dart
Normal file
172
lib/presentation/components/scooter/slide_to_reserve_button.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SlideToReserveButton extends StatefulWidget {
|
||||
final VoidCallback onSlideComplete;
|
||||
|
||||
const SlideToReserveButton({
|
||||
super.key,
|
||||
required this.onSlideComplete,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SlideToReserveButton> createState() => _SlideToReserveButtonState();
|
||||
}
|
||||
|
||||
class _SlideToReserveButtonState extends State<SlideToReserveButton>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _dragAnimation;
|
||||
double _dragOffset = 0;
|
||||
final double _maxDrag = 240; // ширина кнопки - ширина круга
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
_dragAnimation = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onDragUpdate(DragUpdateDetails details) {
|
||||
setState(() {
|
||||
_dragOffset += details.delta.dx;
|
||||
if (_dragOffset < 0) _dragOffset = 0;
|
||||
if (_dragOffset > _maxDrag) _dragOffset = _maxDrag;
|
||||
});
|
||||
}
|
||||
|
||||
void _onDragEnd(DragEndDetails details) {
|
||||
if (_dragOffset >= _maxDrag * 0.8) {
|
||||
_controller.forward();
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
widget.onSlideComplete();
|
||||
});
|
||||
} else {
|
||||
_controller.reverse();
|
||||
setState(() {
|
||||
_dragOffset = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isSlided = _dragOffset >= _maxDrag * 0.8;
|
||||
final progress = _dragOffset / _maxDrag;
|
||||
|
||||
return SizedBox(
|
||||
height: 64,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Фон кнопки: темный → градиент
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
color: isSlided
|
||||
? Colors.transparent
|
||||
: const Color(0xFF141530),
|
||||
),
|
||||
),
|
||||
|
||||
// Градиентный фон (появляется при свайпе)
|
||||
if (isSlided)
|
||||
ShaderMask(
|
||||
shaderCallback: (Rect bounds) {
|
||||
return const LinearGradient(
|
||||
colors: [Color(0xFF67E8F9), Color(0xFF86EFAC)],
|
||||
).createShader(bounds);
|
||||
},
|
||||
blendMode: BlendMode.srcIn,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Текст «Забронировать» — виден только в начальном состоянии
|
||||
if (!isSlided)
|
||||
Center(
|
||||
child: Text(
|
||||
'Забронировать',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Три стрелки — справа от текста (только когда текст виден)
|
||||
if (!isSlided)
|
||||
Positioned(
|
||||
right: 40,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white),
|
||||
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white.withOpacity(0.6)),
|
||||
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white.withOpacity(0.3)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Круг с иконкой самоката — двигается слева направо
|
||||
Transform.translate(
|
||||
offset: Offset(_dragOffset, 0),
|
||||
child: SizedBox(
|
||||
width: 56,
|
||||
height: 56,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.electric_scooter,
|
||||
color: Color(0xFF141530),
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Иконка самоката справа — появляется при полном свайпе
|
||||
if (isSlided)
|
||||
Positioned(
|
||||
right: 16,
|
||||
child: SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.electric_scooter,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user