Проблема с ограничениями размера виджета при изменении размера окна.
Я новичок в разработке Flutter и для начала следую учебным пособиям и лабораториям кода, рекомендованным на flutter.dev. В качестве первого упражнения я решил воссоздать страницу приветствия Spotify Desktop, стремясь обеспечить максимально точное представление с точки зрения отзывчивости и размеров элементов. Я считаю, что это отличный способ по-настоящему глубоко изучить фреймворк.
В настоящее время я сосредоточен на нижней панели приложения. Однако я столкнулся с трудностями в достижении желаемого поведения нижней панели при изменении размера окна приложения. Чтобы лучше проиллюстрировать проблему, я создал короткую гифку, демонстрирующую ожидаемое поведение (реальная запись настольного приложения Spotify Linux): .
В частности, я изо всех сил пытаюсь точно изменить размер ползунка плеера, чтобы он соответствовал минимальному и максимальному размерам, найденным в приложении Spotify. Когда я сжимаю окно приложения, размер ползунка не прекращается, когда он достигает установленного мной минимального размера. Я ожидаю, что виджет переполнится с предупреждением, когда я уменьшу размер окна ниже минимального размера виджета.
Вот код моей текущей реализации нижней панели. Обратите внимание, что еще не все настроено правильно, поскольку работа все еще продолжается, и я в основном сосредоточился на втором фрагменте кода ниже, в котором используется только виджет слайдера.
import 'package:flutter/material.dart';
import 'dart:developer' as developer;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Spotify Clone',
theme: ThemeData(
primarySwatch: Colors.green,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: AppBottomBar(),
);
}
}
class AppBottomBar extends StatefulWidget {
@override
_AppBottomBarState createState() => _AppBottomBarState();
}
class _AppBottomBarState extends State<AppBottomBar> {
@override
Widget build(BuildContext context) {
return Material(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// This correctly centers the middle element.
// However, when resizing, both left and right elements are considered to be equal in width
// and the some overlapping appears before for the right element.
// Also, in the Spotify app, the middle element is supposed to be the one being expanded, no the other way around.
children: [
Expanded(child: BottomBarSongOverview()),
MusicController(),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
AppController(),
],
)),
],
),
);
}
}
String formatDuration(int totalSeconds) {
final duration = Duration(seconds: totalSeconds);
final hours = duration.inHours;
var minutes = duration.inMinutes.remainder(60).toString();
final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
if (hours > 0) {
minutes = minutes.padLeft(2, '0');
return '$hours:$minutes:$seconds';
} else {
return '$minutes:$seconds';
}
}
class MusicController extends StatefulWidget {
@override
_MusicControllerState createState() => _MusicControllerState();
}
class _MusicControllerState extends State<MusicController> {
double duration = 340;
var currentTime = 0.0;
@override
Widget build(BuildContext context) {
return Center(
child: Container(
constraints: const BoxConstraints(
minWidth: 300,
maxWidth: 700,
),
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () => {developer.log('Enable shuffle')},
icon: const Icon(
Icons.shuffle,
semanticLabel: "Enable shuffle",
)),
IconButton(
onPressed: () => {developer.log('Previous')},
icon: const Icon(
Icons.skip_previous,
semanticLabel: "Previous",
)),
IconButton(
onPressed: () => {developer.log('Play')},
icon: const Icon(Icons.play_arrow, semanticLabel: "Play")),
IconButton(
onPressed: () => {developer.log('Next')},
icon: const Icon(Icons.skip_next, semanticLabel: "Next")),
IconButton(
onPressed: () => {developer.log('Enable repeat')},
icon: const Icon(Icons.repeat, semanticLabel: "Repeat")),
],
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(formatDuration(currentTime.ceil())),
Expanded(
child: Slider.adaptive(
value: currentTime,
min: 0.0,
max: duration,
onChanged: (value) {
setState(() {
currentTime = value;
});
},
),
),
Text(formatDuration(duration.ceil())),
],
)
],
),
),
);
}
}
class BottomBarSongOverview extends StatefulWidget {
@override
State<BottomBarSongOverview> createState() => _BottomBarSongOverviewState();
}
class _BottomBarSongOverviewState extends State<BottomBarSongOverview> {
var songName = "Please help me understand Flutter";
var artistName = "MetaHG";
var isFavorite = false;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Image.network(
'https://picsum.photos/200',
width: 50,
height: 50,
),
const SizedBox(width: 8),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(songName),
const SizedBox(height: 8),
Text(artistName),
],
),
const SizedBox(width: 8),
IconButton(
onPressed: () => {developer.log('Save to your library')},
icon: isFavorite
? const Icon(Icons.favorite)
: const Icon(Icons.favorite_border))
],
),
);
}
}
class AppController extends StatefulWidget {
@override
_AppControllerState createState() => _AppControllerState();
}
class _AppControllerState extends State<AppController> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.mic, semanticLabel: "Lyrics"),
onPressed: () => {developer.log("Lyrics")},
),
IconButton(
icon: const Icon(Icons.queue_music, semanticLabel: "Queue"),
onPressed: () => {developer.log("Queue")},
),
IconButton(
icon: const Icon(Icons.phonelink,
semanticLabel: "Connect to a device"),
onPressed: () => {developer.log("Connect to a device")},
),
SoundController(),
IconButton(
icon: const Icon(Icons.fullscreen, semanticLabel: "Full screen"),
onPressed: () => {developer.log("Full screen")},
),
],
),
);
}
}
class SoundController extends StatefulWidget {
@override
_SoundControllerState createState() => _SoundControllerState();
}
class _SoundControllerState extends State<SoundController> {
var volume = 0.5;
var isMute = false;
IconData getVolumeIcon(double value) {
if (isMute) {
return Icons.volume_off;
}
if (value == 0) {
return Icons.volume_mute;
} else if (value <= 0.5) {
return Icons.volume_down;
} else {
return Icons.volume_up;
}
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: Icon(
getVolumeIcon(volume),
semanticLabel: isMute ? 'Unmute' : 'Mute',
),
onPressed: () {
developer.log("Mute");
setState(() {
isMute = !isMute;
});
},
),
SizedBox(
height: 24.0,
child: Slider.adaptive(
value: volume,
onChanged: (value) {
setState(() {
volume = value;
});
}),
),
],
);
}
}
Кроме того, я выделил код виджета-слайдера только для того, чтобы лучше понять ограничения на размер виджета. Моя попытка ограничить размер виджета была сосредоточена главным образом на этом фрагменте кода.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Spotify Clone',
theme: ThemeData(
primarySwatch: Colors.green,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Material(child: SliderTest()),
);
}
}
class SliderTest extends StatefulWidget {
@override
_SliderTestState createState() => _SliderTestState();
}
class _SliderTestState extends State<SliderTest> {
double duration = 340;
var currentTime = 0.0;
@override
Widget build(BuildContext context) {
return Center(
child: Container(
padding: EdgeInsets.all(8.0),
constraints: const BoxConstraints(
minWidth: 300,
maxWidth: 700,
),
child: Row(
children: [
Text(currentTime.ceil().toString()),
Expanded(
child: SizedBox(
child: Slider.adaptive(
value: currentTime,
min: 0.0,
max: duration,
onChanged: (value) {
setState(() {
currentTime = value;
});
},
),
),
),
Text(duration.ceil().toString()),
],
),
),
);
}
}
Теперь о моих вопросах:
- Чего мне не хватает в отношении ограничений? Насколько я понимаю, ползунок адаптируется к максимальному размеру родительского виджета, который меньше указанного минимального размера виджета ползунка. Может ли это быть проблемой? И если да, то почему в этом случае родитель имеет приоритет над ребенком?
- Как я могу заставить мой средний виджет вести себя правильно с точки зрения минимального и максимального размеров?
Я был бы очень признателен за любые идеи или предложения о том, как преодолеть эту проблему и добиться желаемого поведения. Заранее спасибо за вашу помощь!
С наилучшими пожеланиями,
МетаХГ