Я изучаю флаттер и пытаюсь реализовать экран с фиксированным компонентом, а остальные можно прокручивать. Класс SingleChildScrollView выглядит так, как мне нужно, но я не могу заставить его работать. Это проект личного обучения, в котором я пытаюсь разработать систему управления запасами с использованием штрих-кодов с пакетом mobile_scanner. Я тестирую на своем телефоне Android, и в приведенном ниже примере есть дублирующиеся поля, чтобы усугубить проблему: я хочу, чтобы окно/предварительный просмотр сканера камеры было зафиксировано в верхней части экрана, а все остальные компоненты (должны ли они быть в форме? ), чтобы его можно было прокручивать. Например, экран получения инвентаря может иметь несколько штрих-кодов, позволяющих точно знать, где хранится товар (штрих-код на товаре, штрих-код на лотке, штрих-код на полке, штрих-код на шкафу). Экран удаления инвентаря может быть просто штрих-кодом товара. Мой код всегда прокручивает весь экран независимо от того, какие виджеты я использую.
Возможно, это будет еще один вопрос, но я упомяну его здесь. Лучшей реализацией было бы использовать чтение штрих-кода в качестве нового виджета/экрана, но я еще недостаточно понимаю управление состоянием. Я использую пакет auto_route и попробую с этим примером кулинарной книги. Я предполагаю, что моя проблема с исправлением и прокруткой произойдет и здесь.
main.dart
import 'package:flutter/material.dart';
import 'package:example/example_screen.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: ExampleScreen(),
),
),
);
}
}
example_screen.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// import 'package:auto_route/auto_route.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
// @RoutePage()
class ExampleScreen extends StatefulWidget {
ExampleScreen();
@override
State<ExampleScreen> createState() => _ExampleScreenState();
}
class _ExampleScreenState extends State<ExampleScreen> with WidgetsBindingObserver {
final MobileScannerController controller = MobileScannerController(
autoStart: false,
torchEnabled: false,
useNewCameraSelector: true,
);
final String tag = 'EXAMPLE';
Barcode? _barcode;
int activeBarcodeNum = 0;
StreamSubscription<Object?>? _subscription;
String? tmpDisplay;
final textController1 = TextEditingController();
final textController2 = TextEditingController();
void _handleBarcode(BarcodeCapture barcodes) {
Barcode? tmpCode = barcodes.barcodes.firstOrNull;
final rawVal = tmpCode?.rawValue.toString();
// TODO: do some logic
// if (rawVal!.contains('TYPE')) {
// check current values
setState(() {
_barcode = tmpCode;
controller.stop();
activeBarcodeNum = 0;
});
// }
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_subscription = controller.barcodes.listen(_handleBarcode);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (!controller.value.isInitialized) {
return;
}
switch (state) {
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
return;
case AppLifecycleState.resumed:
_subscription = controller.barcodes.listen(_handleBarcode);
// auto start controller on app resume
// unawaited(controller.start());
case AppLifecycleState.inactive:
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(controller.stop());
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Example screen"),
backgroundColor: Colors.deepOrange[400],
),
body: SingleChildScrollView(
child: IntrinsicHeight(
child: Column(
children: [
// scanner box
SizedBox(
height: 500,
child: MobileScanner(
controller: controller,
),
),
Expanded(
child: Column(
children: [
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton(
onPressed: () async {
// start widget
setState(() {
activeBarcodeNum = 1;
});
controller.start();
},
child: const Text(
'Scan',
style: TextStyle(color: Colors.black, fontSize: 20),
),
)
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Tag: ${_barcode == null ? 'N/A' : _barcode!.rawValue.toString()}'),
),
),
],
),
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton(
onPressed: () async {
// start widget
setState(() {
activeBarcodeNum = 1;
});
controller.start();
},
child: const Text(
'Scan',
style: TextStyle(color: Colors.black, fontSize: 20),
),
)
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Tag: ${_barcode == null ? 'N/A' : _barcode!.rawValue.toString()}'),
),
),
],
),
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton(
onPressed: () async {
// start widget
setState(() {
activeBarcodeNum = 1;
});
controller.start();
},
child: const Text(
'Scan',
style: TextStyle(color: Colors.black, fontSize: 20),
),
)
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Tag: ${_barcode == null ? 'N/A' : _barcode!.rawValue.toString()}'),
),
),
],
),
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton(
onPressed: () async {
// start widget
setState(() {
activeBarcodeNum = 4;
});
controller.start();
},
child: const Text(
'Scan',
style: TextStyle(color: Colors.black, fontSize: 20),
),
)
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Tag: ${_barcode == null ? 'N/A' : _barcode!.rawValue.toString()}'),
),
),
],
),
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton(
onPressed: () async {
// start widget
setState(() {
activeBarcodeNum = 4;
});
controller.start();
},
child: const Text(
'Scan',
style: TextStyle(color: Colors.black, fontSize: 20),
),
)
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Tag: ${_barcode == null ? 'N/A' : _barcode!.rawValue.toString()}'),
),
),
],
),
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton(
onPressed: () async {
// start widget
setState(() {
activeBarcodeNum = 4;
});
controller.start();
},
child: const Text(
'Scan',
style: TextStyle(color: Colors.black, fontSize: 20),
),
)
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Tag: ${_barcode == null ? 'N/A' : _barcode!.rawValue.toString()}'),
),
),
],
),
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton(
onPressed: () async {
// start widget
setState(() {
activeBarcodeNum = 4;
});
controller.start();
},
child: const Text(
'Scan',
style: TextStyle(color: Colors.black, fontSize: 20),
),
)
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Tag: ${_barcode == null ? 'N/A' : _barcode!.rawValue.toString()}'),
),
),
],
),
Padding(
//padding: const EdgeInsets.only(left:15.0,right: 15.0,top:0,bottom: 0),
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
child: TextField(
controller: textController1,
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
], // Only numbers can be entered
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Quantity',
hintText: 'Quantity'
),
),
),
Padding(
padding: const EdgeInsets.all(15.0),
child: TextField(
controller: textController2,
maxLines: 5,
decoration: InputDecoration(
hintText: "Enter notes here",
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.grey),
borderRadius: BorderRadius.circular(10),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: Colors.black,
width: 2,
),
borderRadius: BorderRadius.circular(15),
),
errorBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: Colors.red,
width: 2,
),
borderRadius: BorderRadius.circular(10),
),
),
),
),
Container(
height: 70,
width: 250,
decoration: BoxDecoration(
color: Colors.green, borderRadius: BorderRadius.circular(20)),
child: TextButton(
onPressed: () {
final quantStr = textController1.text;
if (quantStr.isNotEmpty) {
// TODO: do stuff with the values
}
// back to home screen
// AutoRouter.of(context).popAndPush(HomeRoute());
},
child: const Text(
'SUBMIT',
style: TextStyle(color: Colors.white, fontSize: 25),
),
)
)
],
),
),
],
),
),
),
);
}
@override
Future<void> dispose() async {
WidgetsBinding.instance.removeObserver(this);
unawaited(_subscription?.cancel());
_subscription = null;
super.dispose();
await controller.dispose();
}
}
Если вы хотите, чтобы первый виджет был зафиксирован/закреплен на экране, вы можете просто извлечь SingleChildScrollView в один виджет ниже в дереве.
В настоящее время у вас есть SingleChildScrollView
в качестве корневого виджета:
Scaffold(
appBar: AppBar(...),
body: SingleChildScrollView( //--> SingleChildScrollView as top level
child: IntrinsicHeight(
child: Column(
children: [
SizedBox(
height: 500,
child: MobileScanner(...),
),
Expanded(
child: Column(
children: [
_buildScanButton(),
_buildScanButton(),
...
],
),
),
],
),
),
),
)
что делает весь экран прокручиваемым.
Вместо этого переместите SingleChildScrollView
из корневого виджета вниз к следующему Column
:
Scaffold(
appBar: AppBar(...),
body: Column(
children: [
SizedBox( // --> No `SingleChildScrollView`, it's moved down the tree
height: 300,
child: MobileScanner(
controller: controller,
),
),
Expanded(
child: SingleChildScrollView( // --> add the `SingleChildScrollView` here.
child: Column(
children: [
_buildScanButton(),
_buildScanButton(),
_buildScanButton(),
_buildScanButton(),
_buildScanButton(),
_buildScanButton(),
),
],
),
Что касается вашего второго вопроса о создании кода в новом виджете/экране:
Вы можете прочитать раздел return-data документации.
Это работает в два этапа:
result
переменной: // Navigator.push returns a Future that completes after calling
// Navigator.pop on the Selection Screen.
final result = await Navigator.push(
context,
// Create the SelectionScreen in the next step.
MaterialPageRoute(builder: (context) => const SelectionScreen()),
);
// Close the screen and return "Yep!" as the result.
Navigator.pop(context, 'Yep!');
Вот полный работоспособный пример:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: ExampleScreen(),
),
),
);
}
}
class ExampleScreen extends StatefulWidget {
const ExampleScreen({super.key});
@override
State<ExampleScreen> createState() => _ExampleScreenState();
}
class _ExampleScreenState extends State<ExampleScreen>
with WidgetsBindingObserver {
final MobileScannerController controller = MobileScannerController(
autoStart: false,
torchEnabled: false,
useNewCameraSelector: true,
);
Barcode? _barcode;
StreamSubscription<Object?>? _subscription;
final textController1 = TextEditingController();
final textController2 = TextEditingController();
void _handleBarcode(BarcodeCapture barcodes) {
final Barcode? tmpCode = barcodes.barcodes.firstOrNull;
setState(() {
_barcode = tmpCode;
controller.stop();
});
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_subscription = controller.barcodes.listen(_handleBarcode);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (!controller.value.isInitialized) {
return;
}
switch (state) {
case AppLifecycleState.resumed:
_subscription = controller.barcodes.listen(_handleBarcode);
break;
case AppLifecycleState.inactive:
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(controller.stop());
break;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Example Screen"),
backgroundColor: Colors.deepOrange[400],
),
body: Column(
children: [
SizedBox(
height: 300,
child: MobileScanner(
controller: controller,
),
),
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
_buildScanButton(),
_buildScanButton(),
_buildScanButton(),
_buildScanButton(),
_buildScanButton(),
_buildScanButton(),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15, vertical: 10),
child: TextField(
controller: textController1,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Quantity',
hintText: 'Quantity',
),
),
),
Padding(
padding: const EdgeInsets.all(15.0),
child: TextField(
controller: textController2,
maxLines: 5,
decoration: InputDecoration(
hintText: "Enter notes here",
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.grey),
borderRadius: BorderRadius.circular(10),
),
focusedBorder: OutlineInputBorder(
borderSide:
const BorderSide(color: Colors.black, width: 2),
borderRadius: BorderRadius.circular(15),
),
errorBorder: OutlineInputBorder(
borderSide:
const BorderSide(color: Colors.red, width: 2),
borderRadius: BorderRadius.circular(10),
),
),
),
),
Container(
height: 70,
width: 250,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(20),
),
child: TextButton(
onPressed: () {
final quantStr = textController1.text;
if (quantStr.isNotEmpty) {
// TODO: do stuff with the values
}
},
child: const Text(
'SUBMIT',
style: TextStyle(color: Colors.white, fontSize: 25),
),
),
),
],
),
),
),
],
),
);
}
Widget _buildScanButton() {
return Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton(
onPressed: () {
setState(() {
controller.start();
});
},
child: const Text(
'Scan',
style: TextStyle(color: Colors.black, fontSize: 20),
),
),
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text(
'Tag: ${_barcode == null ? 'N/A' : _barcode!.rawValue.toString()}',
),
),
),
],
);
}
@override
Future<void> dispose() async {
WidgetsBinding.instance.removeObserver(this);
unawaited(_subscription?.cancel());
_subscription = null;
super.dispose();
await controller.dispose();
}
}
поместите SingleChildScrollView в столбец, который необходимо прокручивать. здесь вы применяете его к каждому ребенку в теле леса.
При использовании виджета SingleChildScrollView весь контент попадает под дочерние элементы и становится прокручиваемым. Поскольку вы все еще учитесь, я думаю, это может вам помочь. Кажется, вы использовали тот же виджет Row для тестирования. Вынесите внутренний виджет Column наружу и поместите фиксированное/непрокручиваемое содержимое в его дочерние элементы. Когда вам нужно прокрутить, используйте SingleChildScrollView и оберните прокручиваемое содержимое внутри своих дочерних элементов. (Обертка SingleChildScrollView контейнером может позволить вам внести некоторые изменения).
body: Column(
children: [
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton(
onPressed: () async {
// start widget
setState(() {
activeBarcodeNum = 1;
});
controller.start();
},
child: const Text(
'Scan',
style: TextStyle(color: Colors.black, fontSize: 20),
),
)
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Tag: ${_barcode == null ? 'N/A' : _barcode!.rawValue.toString()}'),
),
),
],
),
SingleChildScrollView(
children: [
**...content for scrolling..**
]