#flutter #flutter-web
#Flutter #Flutter-web
Вопрос:
Сложный вопрос, надеюсь, у кого-то есть хорошая мысль.
Я вызываю showMenu(...)
свою текстовую кнопку в flutter, которая работает хорошо. Однако иногда во время изменения размера экрана меню застревает где-то на экране (в стороне от его предполагаемого положения). Иногда он следует за своим привязанным положением. Очень странно, и я заметил такое поведение и в выпадающем меню.
Вот мой пример кода. Я хочу либо перемещать меню всегда вместе с экраном, либо, в худшем случае, скрыть меню при изменении размера экрана. событие. Любые мысли о том, как это сделать, были бы замечательными!
class ZHomeMenuBar extends StatefulWidget {
const ZHomeMenuBar({Key? key}) : super(key: key);
@override
State<ZHomeMenuBar> createState() => _ZHomeMenuBarState();
}
class _ZHomeMenuBarState extends State<ZHomeMenuBar> {
final GlobalKey _mesKey = GlobalKey();
final GlobalKey _accKey = GlobalKey();
@override
Widget build(BuildContext context) {
final zuser = Provider.of<ZUser?>(context);
return Container(
height: 66,
decoration: BoxDecoration(color: context.backgroundColor),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Text(zuser == null ? "" : zuser.displayName ?? ""),
const Spacer(),
ZTextButton(text: "Portfolio", onPressed: () => {}),
context.sh,
ZTextButton(
key: _mesKey,
text: "Messages",
onPressed: () {
_showMessage(context);
}),
context.sh,
ZTextButton(key: _accKey, text: "Account", onPressed: () {}),
context.sh,
],
),
);
}
_showMessage(context) {
final RenderBox renderBox =
_mesKey.currentContext?.findRenderObject() as RenderBox;
final Size size = renderBox.size;
final Offset offset = renderBox.localToGlobal(Offset.zero);
showMenu(
context: context,
position: RelativeRect.fromLTRB(offset.dx, offset.dy size.height,
offset.dx size.width, offset.dy size.height),
items: [
PopupMenuItem<String>(child: const Text('menu option 1'), value: '1'),
PopupMenuItem<String>(child: const Text('menu option 2'), value: '2'),
PopupMenuItem<String>(child: const Text('menu option 3'), value: '3'),
]);
}
}
Ответ №1:
Чтобы скрыть меню при изменении размера экрана, вы можете использовать объект window в dart: html, чтобы добавить прослушиватель событий для изменения размера браузера и открыть контекст меню.
Вот обновленный код для вашего ZHomeMenuBar
виджета:
class ZHomeMenuBar extends StatefulWidget {
...
}
class _ZHomeMenuBarState extends State<ZHomeMenuBar> {
...
Timer? timer;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (kIsWeb) {
final int debounceDuration = 100;
html.window.addEventListener("resize", (_) {
final modalRoute = ModalRoute.of(context);
if (modalRoute == null) {
return;
}
if (!modalRoute.isCurrent) {
Navigator.pop(context);
if (timer != null amp;amp; timer!.isActive) {
timer!.cancel();
}
timer = Timer(Duration(milliseconds: 100), () {
_showMessage(context);
});
}
});
}
}
@override
void dispose() {
super.dispose();
timer?.cancel();
}
@override
Widget build(BuildContext context) {
...
}
_showMessage(context) {
...
}
}
Итак, что делает код в didChangeDependencies
методе, так это:
- Окно прослушивает событие изменения размера.
ModalRoute
Получается контекст.- Метод возвращает значение
ModalRoute
null isCurrent
СвойствоModalRoute
проверено. Если значение равно false, это означаетModalRoute
, что маршрут не является самым верхним в навигаторе, и это означает, что на экране есть диалоговое окно.- Поэтому, если
isCurrent
значение равно false, мы открываем контекст (это удаляет диалоговое окно) и устанавливаем короткий таймер сdebounceDuration
помощью, а затем показываем диалоговое окно. При этом используются недавно пересчитанные размеры и диалоговое окно отображается в правильном положении. - Если таймер активен во время работы другого таймера, мы отменяем предыдущий таймер и присваиваем переменной новый
timer
.
Вы можете обновить его по debounceDuration
мере необходимости.
Комментарии:
1. Я действительно ценю ваши усилия здесь, и я вижу, к чему вы стремитесь. Интересно, есть ли в flutter более плавный способ сделать это, но, возможно, нет..
2. Кроме того, как нам получить контекст здесь?
3.
didChangeDependencies
имеет доступ к контексту. См api.flutter.dev/flutter/widgets/State/initState.html
Ответ №2:
Я использую flutter всего несколько месяцев, так что моя методология может быть совершенно неправильной, но я делаю две вещи, когда позиционирую что-либо во flutter.
- Я всегда определяю размер виджетов в соответствии с математическим выражением. Так, например, у меня есть этот вспомогательный класс, который я нашел где-то еще в stackoverflow, который позволит мне получить ширину и высоту экрана. Чтобы использовать его, вы просто помещаете следующее в начало метода сборки любого виджета:
SizeConfig.init(context);
Опять же, я не могу поставить себе в заслугу этот вспомогательный класс, я взял его отсюда, где-то здесь. Если я смогу его найти, я свяжу его.
import 'package:flutter/material.dart';
class SizeConfig {
static late MediaQueryData _mediaQueryData;
static double screenWidth = 0;
static double screenHeight = 0;
static double blockSizeHorizontal = 0;
static double blockSizeVertical = 0;
static double _safeAreaHorizontal = 0;
static double _safeAreaVertical = 0;
static double safeBlockHorizontal = 0;
static double safeBlockVertical = 0;
void init(BuildContext context){
_mediaQueryData = MediaQuery.of(context);
screenWidth = _mediaQueryData.size.width;
screenHeight = _mediaQueryData.size.height;
blockSizeHorizontal = screenWidth/100;
blockSizeVertical = screenHeight/100;
_safeAreaHorizontal = _mediaQueryData.padding.left _mediaQueryData.padding.right;
_safeAreaVertical = _mediaQueryData.padding.top _mediaQueryData.padding.bottom kToolbarHeight;
safeBlockHorizontal = (screenWidth - _safeAreaHorizontal)/100;
safeBlockVertical = (screenHeight - _safeAreaVertical)/100;
}
}
Если вы используете этот класс и размер в соответствии с математическим выражением, выражение будет повторно вычисляться при каждом изменении размера окна. Так, например, это всегда будет устанавливать высоту на 1/4 от безопасной доступной высоты (безопасная доступная высота соответствует стандартной панели приложений). При изменении размера окна это значение пересчитывается. Этот метод предназначен для масштабирования размера виджета.
height: SizeConfig.safeBlockVertical * 25,
- Затем для позиционирования я использую контейнеры, строки, столбцы, отступы, центр и т. Д. Мне никогда не приходилось использовать «позиционирование» или смещения, и это вытекает из моего следующего пункта…
- Я всегда начинаю с виджета стека в качестве основы, если страница будет занята, это обеспечивает гибкость в том смысле, что, определяя размеры всего в соответствии с математическим выражением, я могу расположить два виджета непосредственно рядом друг с другом, но на разных «слоях» стека, и никто не сможет сказать.
- Бонус: Если вы столкнулись с ситуацией, когда вам нужно полностью переработать виджет или его размер с учетом определенного условия, хотя во многих случаях это плохая практика, для простого условного рендеринга может быть действительно хорошей функцией возврата общего виджета. Или может быть полезна общая функция, учитывающая ваши конкретные параметры, которая просто возвращает другую высоту или ширину.
Возьмем, к примеру, этот код макета. В зависимости от того, используете ли вы настольную платформу или мобильную платформу, я оформляю свои маршруты по-разному. Для этого я использую пакет vrouter…
VNesterBase(
widgetBuilder: (child) => getValueByPlatform(
desktop: DesktopLayout(
key: UniqueKey(),
child: child,
),
fallback: BaseLayout(
key: UniqueKey(),
child: child,
),
),
nestedRoutes: [] //nested routes
РЕДАКТИРОВАТЬ: я протестировал ваш класс, удалив кнопки и заменив их текстовыми виджетами, и они, похоже, правильно перемещаются при изменении размера окна.
Ответ №3:
Вы можете использовать WidgetsBindingObserver
mixin, который можно использовать для прослушивания изменения Widget
размера, чтобы вы могли pop
просматривать отображаемое меню. Это будет работать на всех доступных платформах.
Пример кода:
import 'package:flutter/material.dart';
class TestView extends StatefulWidget {
const TestView({Key? key}) : super(key: key);
@override
_TestViewState createState() => _TestViewState();
}
class _TestViewState extends State<TestView> with WidgetsBindingObserver {
late Size _lastSize;
@override
void initState() {
super.initState();
_lastSize = WidgetsBinding.instance!.window.physicalSize;
WidgetsBinding.instance!.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
@override
void didChangeMetrics() {
setState(() {
_lastSize = WidgetsBinding.instance!.window.physicalSize;
});
Navigator.of(context).popUntil((route) {
return route is MaterialPageRoute;
});
}
void _select(String choice) {}
@override
Widget build(BuildContext context) {
List<String> choices = ["1", "2", "3"];
return Scaffold(
appBar: AppBar(
title: const Text('test'),
actions: <Widget>[
PopupMenuButton(
onSelected: _select,
padding: EdgeInsets.zero,
// initialValue: choices[_selection],
itemBuilder: (BuildContext context) {
return choices.map((String choice) {
return PopupMenuItem<String>(
value: choice,
child: Text(choice),
);
}).toList();
},
)
],
),
body: Center(
child: Column(
children: [
Text('Current size: $_lastSize'),
Padding(
padding: const EdgeInsets.all(16),
child: DropdownButton<ThemeMode>(
value: null,
onChanged: (tm) {
print(_lastSize);
},
items: const [
DropdownMenuItem(
value: ThemeMode.system,
child: Text('System Theme'),
),
DropdownMenuItem(
value: ThemeMode.light,
child: Text('Light Theme'),
),
DropdownMenuItem(
value: ThemeMode.dark,
child: Text('Dark Theme'),
)
],
),
),
],
),
),
);
}
}
вместо:
Navigator.of(context).popUntil((route) {
return route is MaterialPageRoute;
});
вы также можете использовать Navigator.of(context).popUntil(ModalRoute.withName('YourPageName'));
если вы используете именованные маршруты.