Paint on Image after Image loaded from the network - dart

I'm loading the images from api call and then showing the image and some data in listview in flutter and drawing bounding box on image, but since my image is getting loaded from url my UI gets disturbed,How I ensure that the Canvas will paint after the image is loaded from the network url in flutter layout.Is there any callbacks which I can use.
class ListPage extends StatefulWidget {
ListPage({Key key, this.title}) : super(key: key);
final String title;
#override
_ListPageState createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
Future<List<ProcessedInference>> processInference;
#override
void initState() {
processInference = localInference();
super.initState();
}
#override
Widget build(BuildContext context) {
final toAppBar = AppBar(
elevation: 0.1,
backgroundColor: Color.fromRGBO(58, 66, 86, 1.0),
title: Text(widget.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.list),
onPressed: () {},
)
],
);
Card makeCard(ProcessedInference pro) => Card(
elevation: 2.0,
margin: new EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0),
child: Container(
child: Column(
children: <Widget>[
CustomPaint(
size: Size(640.0, 480.0),
foregroundPainter: RectPainter(pro.boundingBox),
child: Image.network(pro.frameUrl),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(
height: 8.0,
),
Text(
"Spotted: ${pro.peopleCount}",
style: TextStyle(fontSize: 16.0),
textAlign: TextAlign.start,
),
SizedBox(
height: 8.0,
),
Text(
"Spotted on : ${pro.timeStamp}",
style: TextStyle(fontSize: 16.0),
textAlign: TextAlign.start,
),
SizedBox(
height: 8.0,
),
],
),
],
),
),
);
final makeBody = Container(
child: FutureBuilder<List<ProcessedInference>>(
future: processInference,
builder: (context, snapshot) {
if (snapshot.hasData) {
print("Has data");
if (snapshot.data == null || snapshot.data.length <= 0) {
return Center(
child: Text("No results", textAlign: TextAlign.center));
} else {
return ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return makeCard(snapshot.data[index]);
});
}
} else if (snapshot.hasError) {
print("has error ");
return Text("${snapshot.error}");
}
return CircularProgressIndicator();
},
),
);
return Scaffold(
appBar: toAppBar,
body: makeBody,
);
}
}
class RectPainter extends CustomPainter {
final List<List<double>> boundingBox;
RectPainter(this.boundingBox);
#override
void paint(Canvas canvas, Size size) {
final paint = Paint();
paint.color = Colors.deepOrange;
paint.style = PaintingStyle.stroke;
paint.strokeWidth = 2.0;
final boxPaint = Paint();
boxPaint.color = Colors.amberAccent;
boxPaint.style = PaintingStyle.fill;
boxPaint.strokeWidth = 2.0;
for (var i = 0; i < boundingBox.length; i++) {
var confidence = boundingBox[i][0];
var left = boundingBox[i][1] * size.width;
var top = boundingBox[i][2] * size.height;
var right = boundingBox[i][3] * size.width;
var bottom = boundingBox[i][4] * size.height;
var rect = Rect.fromLTRB(left, top - 15, right, bottom);
canvas.drawRect(rect, paint);
TextSpan span = new TextSpan(
style: new TextStyle(color: Colors.red[600], fontSize: 10.0),
text: confidence.toStringAsFixed(2));
TextPainter tp = new TextPainter(
text: span,
textAlign: TextAlign.center,
textDirection: TextDirection.ltr);
tp.layout();
canvas.drawRect(
Rect.fromLTRB(
rect.left, rect.top, rect.left + tp.width, rect.top + tp.height),
boxPaint);
tp.paint(canvas, new Offset(rect.left, rect.top));
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}

Related

VideoEditorController breaks all VideoPlayers in parent widgets

I have a page where I crop and trim the video.
When I initialize a VideoEditorController and for example edit the video, or just navigate back to previous page. And then I get method channel error and all CachedVideoPlayer widgets show a black screen. if we rebuild the widget with CachedVideoPlayer it'll play. And if we try to repeat the same process again for some strange reason CachedVideoPlayers won't crash.
I'm testing on a real device(Iphone 12 mini).
Error:
MissingPluginException(No implementation found for method cancel on channel flutter.io/videoPlayer/videoEvents171)
How I initialize VideoEditorController
#override
void initState() {
super.initState();
_controller = VideoEditorController.file(widget.file,
maxDuration: const Duration(seconds: 30))
..initialize().then((_) => setState(() {
_controller.preferredCropAspectRatio = widget.aspectRatio;
showTrimmer = true;
}));
}
CropScreen
class CropScreen extends StatefulWidget {
const CropScreen({
Key? key,
required this.file,
required this.exportedFile,
required this.aspectRatio,
}) : super(key: key);
final File file;
final double aspectRatio;
final Function(File) exportedFile;
#override
State<CropScreen> createState() => _CropScreenState();
}
class _CropScreenState extends State<CropScreen> {
late VideoEditorController _controller;
final double height = 60;
bool showTrimmer = false;
final ValueNotifier<bool> _isLoadingNotifier = ValueNotifier(false);
void show() {
_isLoadingNotifier.value = true;
}
void hide() {
_isLoadingNotifier.value = false;
}
#override
void initState() {
super.initState();
_controller = VideoEditorController.file(widget.file,
maxDuration: const Duration(seconds: 30))
..initialize().then((_) => setState(() {
_controller.preferredCropAspectRatio = widget.aspectRatio;
showTrimmer = true;
}));
}
#override
void dispose() {
// TODO: implement dispose
super.dispose();
_controller.dispose();
}
#override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: _isLoadingNotifier,
builder: (context, value, child) {
return Stack(
children: [
Stack(
children: [
Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(30),
child: Column(children: [
Row(children: [
Expanded(
child: IconButton(
onPressed: () => _controller
.rotate90Degrees(RotateDirection.left),
icon: const Icon(
Icons.rotate_left,
color: white,
),
),
),
Expanded(
child: IconButton(
onPressed: () => _controller
.rotate90Degrees(RotateDirection.right),
icon: const Icon(
Icons.rotate_right,
color: white,
),
),
)
]),
const SizedBox(height: 15),
Expanded(
child: CropGridViewer(
controller: _controller, horizontalMargin: 60),
),
const SizedBox(
height: 15,
),
showTrimmer
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _trimSlider(context))
: SizedBox(height: 100),
const SizedBox(height: 15),
Row(children: [
Expanded(
child: IconButton(
onPressed: () {
Navigator.pop(context);
},
icon: Center(
child: Text(
"Cancel",
style: MainTheme.of(context)
.bodyText1
.copyWith(fontWeight: FontWeight.w600),
),
),
),
),
buildSplashTap("21:9", 21 / 9,
padding:
const EdgeInsets.symmetric(horizontal: 10)),
buildSplashTap("1:1", 1 / 1),
buildSplashTap("4:5", 4 / 5,
padding:
const EdgeInsets.symmetric(horizontal: 10)),
buildSplashTap("NO", null,
padding: const EdgeInsets.only(right: 10)),
Expanded(
child: IconButton(
onPressed: () async {
//2 WAYS TO UPDATE CROP
//WAY 1:
show();
_controller.updateCrop();
await _controller.exportVideo(
onCompleted: (file) {
print(file!.path);
widget.exportedFile(file);
hide();
Navigator.pop(context);
});
/*WAY 2:
controller.minCrop = controller.cacheMinCrop;
controller.maxCrop = controller.cacheMaxCrop;
*/
},
icon: Center(
child: Text(
"Crop",
style: MainTheme.of(context)
.bodyText1
.copyWith(fontWeight: FontWeight.w600),
),
),
),
),
]),
]),
),
),
),
],
),
if (value)
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 4.0, sigmaY: 4.0),
child: const Opacity(
opacity: 0.8,
child:
ModalBarrier(dismissible: false, color: Colors.black),
),
),
if (value)
Center(
child: FutureBuilder(
future: Future.delayed(Duration(milliseconds: 500)),
builder: (context, snapshot) {
return const SpinKitFadingCircle(
size: 60, color: primary);
},
),
),
],
);
});
}
Widget buildSplashTap(
String title,
double? aspectRatio, {
EdgeInsetsGeometry? padding,
}) {
return InkWell(
onTap: () => _controller.preferredCropAspectRatio = aspectRatio,
child: Padding(
padding: padding ?? EdgeInsets.zero,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.aspect_ratio, color: Colors.white),
Text(
title,
style: const TextStyle(fontWeight: FontWeight.bold, color: white),
),
],
),
),
);
}
List<Widget> _trimSlider(BuildContext context) {
return [
AnimatedBuilder(
animation: _controller.video,
builder: (_, __) {
final duration = _controller.video.value.duration.inSeconds;
final pos = _controller.trimPosition * duration;
final start = _controller.minTrim * duration;
final end = _controller.maxTrim * duration;
return Padding(
padding: EdgeInsets.symmetric(horizontal: height / 4),
child: Row(children: [
Text(
formatter(Duration(seconds: pos.toInt())),
style: MainTheme.of(context).bodyText1,
),
const Expanded(child: SizedBox()),
OpacityTransition(
visible: _controller.isTrimming,
child: Row(mainAxisSize: MainAxisSize.min, children: [
Text(
formatter(Duration(seconds: start.toInt())),
style: MainTheme.of(context).bodyText1,
),
const SizedBox(width: 10),
Text(
formatter(Duration(seconds: end.toInt())),
style: MainTheme.of(context).bodyText1,
),
]),
)
]),
);
},
),
Container(
width: MediaQuery.of(context).size.width,
margin: EdgeInsets.symmetric(vertical: height / 4),
child: TrimSlider(
controller: _controller,
height: height,
horizontalMargin: height / 4,
child: TrimTimeline(
controller: _controller,
margin: const EdgeInsets.only(top: 10))),
)
];
}
String formatter(Duration duration) => [
duration.inMinutes.remainder(60).toString().padLeft(2, '0'),
duration.inSeconds.remainder(60).toString().padLeft(2, '0')
].join(":");
}
To use VideoEditorController you need to add this package:
video_editor: ^1.4.1
Dart SDK:
sdk: ">=2.16.1 <3.0.0"

showBottomSheet Scaffold issue with context

Trying to implement showBottomSheet into my app, but it is throwing an error: Scaffold.of() called with a context that does not contain a Scaffold.
After searching the web a bit, I added a GlobalKey but that seems like it is not doing the trick. Does anyone have a suggestion?
class _DashboardState extends State<Dashboard> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
int _bottomNavBarCurrentIndex = 0;
dashboardViews.DashboardView dView = new dashboardViews.DashboardView();
List<Widget> _listOffers = new List<Widget>();
Widget _currentView;
void loadDashboardView() {
_currentView = (_bottomNavBarCurrentIndex == 0
? dView.getOfferView(_listOffers, _getOfferData)
: dView.getOrdersView());
}
_showInfoSheet() {
showBottomSheet(
context: _scaffoldKey.currentContext,
builder: (context) {
return Text('Hello');
});
}
Future _getOfferData() async {
loadDashboardView();
List<Widget> _resultsOffers = new List<Widget>();
SharedPreferences prefs = await SharedPreferences.getInstance();
String _token = prefs.getString('token');
final responseOffers =
await http.get(globals.apiConnString + 'GetActiveOffers?token=$_token');
if (responseOffers.statusCode == 200) {
List data = json.decode(responseOffers.body);
for (var i = 0; i < data.length; i++) {
_resultsOffers.add(GestureDetector(
onTap: () => _showInfoSheet(),
child: Card(
child: Padding(
padding: EdgeInsets.all(15),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Row(children: <Widget>[
Expanded(
flex: 5,
child: Text('${data[i]['Title']}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: globals.themeColor4))),
Expanded(
flex: 3,
child: Row(children: <Widget>[
Icon(Icons.access_time,
size: 15, color: Colors.grey),
Padding(
padding: EdgeInsets.fromLTRB(5, 0, 10, 0),
child: Text('11:30 PM',
style:
TextStyle(color: Colors.black))),
])),
Expanded(
flex: 1,
child: Row(children: <Widget>[
Icon(Icons.local_dining,
size: 15, color: Colors.grey),
Padding(
padding: EdgeInsets.fromLTRB(5, 0, 0, 0),
child: Text('${i.toString()}',
style:
TextStyle(color: Colors.black))),
])),
]),
Padding(padding: EdgeInsets.all(10)),
Row(children: <Widget>[
Text(
'Created May 2, 2019 at 2:31 PM',
style: TextStyle(color: Colors.grey[600]),
textAlign: TextAlign.start,
)
])
])))));
}
}
setState(() {
_listOffers = _resultsOffers;
});
loadDashboardView();
}
}
void _bottomNavBarTap(int index) {
setState(() {
_bottomNavBarCurrentIndex = index;
loadDashboardView();
});
}
void pullRefresh() {
_getOfferData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
backgroundColor: Colors.grey[200],
body: _currentView,
bottomNavigationBar: BottomNavigationBar(
onTap: _bottomNavBarTap,
currentIndex: _bottomNavBarCurrentIndex,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.near_me), title: Text('OFFERS')),
BottomNavigationBarItem(
icon: Icon(Icons.broken_image), title: Text('ORDERS'))
],
),
);
}
}
Hope someone can point me in the right direction, thanks!
EDIT: Here is one of the links I looked at with the same problem, and I tried setting the builder context to c as suggested but didn't work: https://github.com/flutter/flutter/issues/23234
EDIT 2: Adding screenshot that shows the variable contents when I'm getting the Scaffold.of() called with a context that does not contain a Scaffold error.
The reason you're getting this error is because you're calling Scaffold during its build process, and thus your showBottomSheet function can't see it. A workaround this problem is to provide a stateful widget with a Scaffold, assign the Scaffold a key , and then pass it to your Dashboard (I assumed it is the name of your stateful widget from your state class). You don't need to assign a key to the Scaffold inside the build of _DashboardState.
This is the class where you provide a Scaffold with key:
class DashboardPage extends StatefulWidget {
#override
_DashboardPageState createState() => _DashboardPageState() ;
}
class _DashboardPageState extends State<DashboardPage> {
final GlobalKey<ScaffoldState> scaffoldStateKey ;
#override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldStateKey,
body: Dashboard(scaffoldKey: scaffoldStateKey),
);
}
Your original Dashboard altered :
class Dashboard extends StatefulWidget{
Dashboard({Key key, this.scaffoldKey}):
super(key: key);
final GlobalKey<ScaffoldState> scaffoldKey ;
#override
_DashboardState createState() => new _DashboardState();
}
Your _DashboardState with changes outlined :
class _DashboardState extends State<Dashboard> {
int _bottomNavBarCurrentIndex = 0;
dashboardViews.DashboardView dView = new dashboardViews.DashboardView();
List<Widget> _listOffers = new List<Widget>();
Widget _currentView;
void loadDashboardView() {
_currentView = (_bottomNavBarCurrentIndex == 0
? dView.getOfferView(_listOffers, _getOfferData)
: dView.getOrdersView());
}
_showInfoSheet() {
showBottomSheet(
context: widget.scaffoldKey.currentContext, // referencing the key passed from [DashboardPage] to use its Scaffold.
builder: (context) {
return Text('Hello');
});
}
Future _getOfferData() async {
loadDashboardView();
List<Widget> _resultsOffers = new List<Widget>();
SharedPreferences prefs = await SharedPreferences.getInstance();
String _token = prefs.getString('token');
final responseOffers =
await http.get(globals.apiConnString + 'GetActiveOffers?token=$_token');
if (responseOffers.statusCode == 200) {
List data = json.decode(responseOffers.body);
for (var i = 0; i < data.length; i++) {
_resultsOffers.add(GestureDetector(
onTap: () => _showInfoSheet(),
child: Card(
child: Padding(
padding: EdgeInsets.all(15),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Row(children: <Widget>[
Expanded(
flex: 5,
child: Text('${data[i]['Title']}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: globals.themeColor4))),
Expanded(
flex: 3,
child: Row(children: <Widget>[
Icon(Icons.access_time,
size: 15, color: Colors.grey),
Padding(
padding: EdgeInsets.fromLTRB(5, 0, 10, 0),
child: Text('11:30 PM',
style:
TextStyle(color: Colors.black))),
])),
Expanded(
flex: 1,
child: Row(children: <Widget>[
Icon(Icons.local_dining,
size: 15, color: Colors.grey),
Padding(
padding: EdgeInsets.fromLTRB(5, 0, 0, 0),
child: Text('${i.toString()}',
style:
TextStyle(color: Colors.black))),
])),
]),
Padding(padding: EdgeInsets.all(10)),
Row(children: <Widget>[
Text(
'Created May 2, 2019 at 2:31 PM',
style: TextStyle(color: Colors.grey[600]),
textAlign: TextAlign.start,
)
])
])))));
}
}
setState(() {
_listOffers = _resultsOffers;
});
loadDashboardView();
}
void _bottomNavBarTap(int index) {
setState(() {
_bottomNavBarCurrentIndex = index;
loadDashboardView();
});
}
void pullRefresh() {
_getOfferData();
}
#override
Widget build(BuildContext context) {
// Scaffold key has been removed as there is no further need to it.
return Scaffold(
backgroundColor: Colors.grey[200],
body: _currentView,
bottomNavigationBar: BottomNavigationBar(
onTap: _bottomNavBarTap,
currentIndex: _bottomNavBarCurrentIndex,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.near_me), title: Text('OFFERS')),
BottomNavigationBarItem(
icon: Icon(Icons.broken_image), title: Text('ORDERS'))
],
),
);
}
}
}

How to show a menu at press/finger/mouse/cursor position in flutter

I have this piece of code which I got from Style clipboard in flutter
showMenu(
context: context,
// TODO: Position dynamically based on cursor or textfield
position: RelativeRect.fromLTRB(0.0, 600.0, 300.0, 0.0),
items: [
PopupMenuItem(
child: Row(
children: <Widget>[
// TODO: Dynamic items / handle click
PopupMenuItem(
child: Text(
"Paste",
style: Theme.of(context)
.textTheme
.body2
.copyWith(color: Colors.red),
),
),
PopupMenuItem(
child: Text("Select All"),
),
],
),
),
],
);
This code works great, except that the popup that is created is at a fixed position, how would I make it so that it pops up at the mouse/press/finger/cursor position or somewhere near that, kind of like when you want to copy and paste on your phone. (This dialog popup will not be used for copy and pasting)
I was able to solve a similar issue by using this answer:
https://stackoverflow.com/a/54714628/559525
Basically, I added a GestureDetector() around each ListTile and then you use onTapDown to store your press location and onLongPress to call your showMenu function. Here are the critical functions I added:
_showPopupMenu() async {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
await showMenu(
context: context,
position: RelativeRect.fromRect(
_tapPosition & Size(40, 40), // smaller rect, the touch area
Offset.zero & overlay.size // Bigger rect, the entire screen
),
items: [
PopupMenuItem(
child: Text("Show Usage"),
),
PopupMenuItem(
child: Text("Delete"),
),
],
elevation: 8.0,
);
}
void _storePosition(TapDownDetails details) {
_tapPosition = details.globalPosition;
}
}
And then here is the full code (you'll have to tweak a few things like the image, and filling in the list of devices):
import 'package:flutter/material.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'dart:core';
class RecentsPage extends StatefulWidget {
RecentsPage({Key key, this.title}) : super(key: key);
final String title;
#override
_RecentsPageState createState() => _RecentsPageState();
}
class _RecentsPageState extends State<RecentsPage> {
List<String> _recents;
var _tapPosition;
#override
void initState() {
super.initState();
_tapPosition = Offset(0.0, 0.0);
getRecents().then((value) {
setState(() {
_recents = value;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFFFFFFFF),
body: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(height: 25),
Stack(
children: <Widget>[
Container(
padding: EdgeInsets.only(left: 40),
child: Center(
child: AutoSizeText(
"Recents",
maxLines: 1,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 32),
),
),
),
Container(
padding: EdgeInsets.only(left: 30, top: 0),
child: GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Transform.scale(
scale: 2.0,
child: Icon(
Icons.chevron_left,
),
)),
),
],
),
Container(
height: 15,
),
Container(
height: 2,
color: Colors.blue,
),
Container(
height: 10,
),
Flexible(
child: ListView(
padding: EdgeInsets.all(15.0),
children: ListTile.divideTiles(
context: context,
tiles: _getRecentTiles(),
).toList(),
),
),
Container(height: 15),
],
),
),
),
);
}
List<Widget> _getRecentTiles() {
List<Widget> devices = List<Widget>();
String _dev;
String _owner = "John Doe";
if (_recents != null) {
for (_dev in _recents.reversed) {
if (_dev != null) {
_dev = _dev.toUpperCase().trim();
String serial = "12341234";
devices.add(GestureDetector(
onTapDown: _storePosition,
onLongPress: () {
print("long press of $serial");
_showPopupMenu();
},
child: ListTile(
contentPadding: EdgeInsets.symmetric(vertical: 20),
leading: Transform.scale(
scale: 0.8,
child: Image(
image: _myImage,
)),
title: AutoSizeText(
"$_owner",
maxLines: 1,
style: TextStyle(fontSize: 22),
),
subtitle: Text("Serial #: $serial"),
trailing: Icon(Icons.keyboard_arrow_right),
)));
}
}
} else {
devices.add(ListTile(
contentPadding: EdgeInsets.symmetric(vertical: 20),
title: AutoSizeText(
"No Recent Devices",
maxLines: 1,
style: TextStyle(fontSize: 20),
),
subtitle:
Text("Click the button to add a device"),
onTap: () {
print('add device');
},
));
}
return devices;
}
_showPopupMenu() async {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
await showMenu(
context: context,
position: RelativeRect.fromRect(
_tapPosition & Size(40, 40), // smaller rect, the touch area
Offset.zero & overlay.size // Bigger rect, the entire screen
),
items: [
PopupMenuItem(
child: Text("Show Usage"),
),
PopupMenuItem(
child: Text("Delete"),
),
],
elevation: 8.0,
);
}
void _storePosition(TapDownDetails details) {
_tapPosition = details.globalPosition;
}
}
Use gesture detector's onTapDown like this
GestureDetector(
onTapDown: (TapDownDetails details) {
showPopUpMenu(details.globalPosition);
},
then in this method we use tap down details to find position
Future<void> showPopUpMenu(Offset globalPosition) async {
double left = globalPosition.dx;
double top = globalPosition.dy;
await showMenu(
color: Colors.white,
//add your color
context: context,
position: RelativeRect.fromLTRB(left, top, 0, 0),
items: [
PopupMenuItem(
value: 1,
child: Padding(
padding: const EdgeInsets.only(left: 0, right: 40),
child: Row(
children: [
Icon(Icons.mail_outline),
SizedBox(
width: 10,
),
Text(
"Menu 1",
style: TextStyle(color: Colors.black),
),
],
),
),
),
PopupMenuItem(
value: 2,
child: Padding(
padding: const EdgeInsets.only(left: 0, right: 40),
child: Row(
children: [
Icon(Icons.vpn_key),
SizedBox(
width: 10,
),
Text(
"Menu 2",
style: TextStyle(color: Colors.black),
),
],
),
),
),
PopupMenuItem(
value: 3,
child: Row(
children: [
Icon(Icons.power_settings_new_sharp),
SizedBox(
width: 10,
),
Text(
"Menu 3",
style: TextStyle(color: Colors.black),
),
],
),
),
],
elevation: 8.0,
).then((value) {
print(value);
if (value == 1) {
//do your task here for menu 1
}
if (value == 2) {
//do your task here for menu 2
}
if (value == 3) {
//do your task here for menu 3
}
});
hope it works
Here is a reusable widget which does what you need. Just wrap your Text or other Widget with this CopyableWidget and pass in the onGetCopyTextRequested. It will display the Copy menu when the widget is long pressed, copy the text contents returned to the clipboard, and display a Snackbar on completion.
/// The text to copy to the clipboard should be returned or null if nothing can be copied
typedef GetCopyTextCallback = String Function();
class CopyableWidget extends StatefulWidget {
final Widget child;
final GetCopyTextCallback onGetCopyTextRequested;
const CopyableWidget({
Key key,
#required this.child,
#required this.onGetCopyTextRequested,
}) : super(key: key);
#override
_CopyableWidgetState createState() => _CopyableWidgetState();
}
class _CopyableWidgetState extends State<CopyableWidget> {
Offset _longPressStartPos;
#override
Widget build(BuildContext context) {
return InkWell(
highlightColor: Colors.transparent,
onTapDown: _onTapDown,
onLongPress: () => _onLongPress(context),
child: widget.child
);
}
void _onTapDown(TapDownDetails details) {
setState(() {
_longPressStartPos = details?.globalPosition;
});
}
void _onLongPress(BuildContext context) async {
if (_longPressStartPos == null)
return;
var isCopyPressed = await showCopyMenu(
context: context,
pressedPosition: _longPressStartPos
);
if (isCopyPressed == true && widget.onGetCopyTextRequested != null) {
var copyText = widget.onGetCopyTextRequested();
if (copyText != null) {
await Clipboard.setData(ClipboardData(text: copyText));
_showSuccessSnackbar(
context: context,
text: "Copied to the clipboard"
);
}
}
}
void _showSuccessSnackbar({
#required BuildContext context,
#required String text
}) {
var scaffold = Scaffold.of(context, nullOk: true);
if (scaffold != null) {
scaffold.showSnackBar(
SnackBar(
content: Row(
children: <Widget>[
Icon(
Icons.check_circle_outline,
size: 24,
),
SizedBox(width: 8),
Expanded(
child: Text(text)
)
],
)
)
);
}
}
}
Future<bool> showCopyMenu({
BuildContext context,
Offset pressedPosition
}) {
var x = pressedPosition.dx;
var y = pressedPosition.dy;
return showMenu<bool>(
context: context,
position: RelativeRect.fromLTRB(x, y, x + 1, y + 1),
items: [
PopupMenuItem<bool>(value: true, child: Text("Copy")),
]
);
}

Flutter custom range slider

I'm trying to create a range slider on top of a Row of Containers which should create an audio waveform, but I have no idea where to even start...
The main issue is that the range slider sits right on top of the row of containers and it should change their colors on the "selected" section.
Here's what I currently have:
The code to create the image and details.
class BeatLyricsPage extends StatefulWidget {
final Beat beat;
BeatLyricsPage(this.beat);
#override
_BeatLyricsPageState createState() => _BeatLyricsPageState(beat);
}
class _BeatLyricsPageState extends State<BeatLyricsPage> {
final Beat beat;
final _kPicHeight = 190.0;
// used in _buildPageHeading to add the beat key and beat bpm
Widget _buildBeatInfoItem(String text) => DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: MyColor.white, width: 1.0),
borderRadius: BorderRadius.circular(4.0),
),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 3.0, horizontal: 12.0),
child: Text(text, style: TextStyle(color: MyColor.white, fontSize: 10.0, fontWeight: FontWeight.w600)),
),
);
final _kAudioControlsWidth = 180.0;
final _kAudioControlsHeight = 36.0;
final _kAudioControlsMainButtonSize = 56.0;
Widget _buildAudioControls(BuildContext context) => Positioned(
left: (MediaQuery.of(context).size.width / 2) - (_kAudioControlsWidth / 2),
top: _kPicHeight - (_kAudioControlsHeight / 2),
child: Stack(
overflow: Overflow.visible,
children: [
Container(
width: _kAudioControlsWidth,
height: _kAudioControlsHeight,
decoration: BoxDecoration(color: MyColor.darkGrey, borderRadius: BorderRadius.circular(100.0)),
padding: EdgeInsets.symmetric(horizontal: LayoutSpacing.sm),
child: Row(
children: [
CButtonLike(beatId: beat.id),
Spacer(),
GestureDetector(
behavior: HitTestBehavior.opaque,
child: Icon(BeatPulseIcons.cart),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => LicenseOptionsPage(beat))),
),
],
),
),
// ****** MAIN BUTTON (Play/Pause) ******
Positioned(
left: (_kAudioControlsWidth / 2) - (_kAudioControlsMainButtonSize / 2),
top: (_kAudioControlsHeight - _kAudioControlsMainButtonSize) / 2,
child: Container(
height: _kAudioControlsMainButtonSize,
width: _kAudioControlsMainButtonSize,
decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, colors: [MyColor.primary, Color(0xFFf80d0a)]),
borderRadius: BorderRadius.circular(100.0)),
child: CButtonPlay(),
),
)
],
),
);
Widget _buildWaveForm() {
// creates a random list of doubles, "fake data"
var rng = Random();
final List waveFormData = [];
for (var i = 0; i < 90; i++) {
waveFormData.add(rng.nextInt(45).toDouble());
}
// player bloc
final playerBloc = BlocProvider.getPlayerBloc(context);
// renders
return Container(
height: _kPicHeight,
padding: EdgeInsets.symmetric(vertical: LayoutSpacing.xxxl),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// current playing second
StreamBuilder<double>(
stream: playerBloc.playingSecond,
initialData: 0.0,
builder: (_, playingSecondSnapshot) {
// current beat playing
return StreamBuilder<Beat>(
stream: playerBloc.playingBeat,
builder: (_, playingBeatSnapshot) {
final playingBeat = playingBeatSnapshot.data;
// if the beat playing is the same as the beat selected for the lyrics, show playing seconds
if (playingBeat?.id == beat.id)
return Text(secondsToTime(playingSecondSnapshot.data), style: MyFontStyle.sizeXxs);
// otherwise show 0:00
else
return Text(secondsToTime(0), style: MyFontStyle.sizeXxs);
},
);
},
),
SizedBox(width: LayoutSpacing.xs),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: waveFormData
.map((waveFormDataIndex) => Container(
height: waveFormDataIndex > 5.0 ? waveFormDataIndex : 5.0,
width: 2,
color: MyColor.white,
margin: EdgeInsets.only(right: 1),
))
.toList(),
),
SizedBox(width: LayoutSpacing.xs),
Text(secondsToTime(beat.length), style: MyFontStyle.sizeXxs),
],
),
);
}
Widget _buildPageHeading(BuildContext context, {#required String imageUrl}) => Stack(
children: [
Column(
children: [
Hero(
tag: MyKeys.makePlayerCoverKey(beat.id),
child: Opacity(
opacity: 0.3,
child: Container(
height: _kPicHeight,
decoration: BoxDecoration(
image: DecorationImage(image: CachedNetworkImageProvider(imageUrl), fit: BoxFit.cover),
),
),
),
),
Container(color: MyColor.background, height: LayoutSpacing.xl)
],
),
Padding(
padding: EdgeInsets.all(LayoutSpacing.xs),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildBeatInfoItem(beat.key),
SizedBox(width: 4.0),
_buildBeatInfoItem('${beat.bpm} BPM'),
],
),
),
_buildAudioControls(context),
_buildWaveForm(),
],
);
}
To create a custom range slider, you can use the GestureRecognizer and save the position of each slider in variable inside a StatefulWidget. To decide wether a bar with the index i is inside the range, you can divide the pixel position of the limiter(bar1&bar2 in the source below) by the width of bars and compare it to i.
Sadly I couldn't work with your code example. Instead I created a bare minimum example as you can see below. If you take a minute to read into, I'm sure you can transfer it to your application.
import 'dart:math';
import 'package:flutter/material.dart';
List<int> bars = [];
void main() {
// generate random bars
Random r = Random();
for (var i = 0; i < 50; i++) {
bars.add(r.nextInt(200));
}
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
);
}
}
class Home extends StatefulWidget {
#override
State<StatefulWidget> createState() => HomeState();
}
class HomeState extends State<Home> {
static const barWidth = 5.0;
double bar1Position = 60.0;
double bar2Position = 180.0;
#override
Widget build(BuildContext context) {
int i = 0;
return Scaffold(
body: Center(
child: Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: bars.map((int height) {
Color color =
i >= bar1Position / barWidth && i <= bar2Position / barWidth
? Colors.deepPurple
: Colors.blueGrey;
i++;
return Container(
color: color,
height: height.toDouble(),
width: 5.0,
);
}).toList(),
),
Bar(
position: bar2Position,
callback: (DragUpdateDetails details) {
setState(() {
bar2Position += details.delta.dx;
});
},
),
Bar(
position: bar1Position,
callback: (DragUpdateDetails details) {
setState(() {
bar1Position += details.delta.dx;
});
},
),
],
),
),
);
}
}
class Bar extends StatelessWidget {
final double position;
final GestureDragUpdateCallback callback;
Bar({this.position, this.callback});
#override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(left: position >= 0.0 ? position : 0.0),
child: GestureDetector(
onHorizontalDragUpdate: callback,
child: Container(
color: Colors.red,
height: 200.0,
width: 5.0,
),
),
);
}
}
in order to have a wave slider :
class WaveSlider extends StatefulWidget {
final double initialBarPosition;
final double barWidth;
final int maxBarHight;
final double width;
WaveSlider({
this.initialBarPosition = 0.0,
this.barWidth = 5.0,
this.maxBarHight = 50,
this.width = 60.0,
});
#override
State<StatefulWidget> createState() => WaveSliderState();
}
class WaveSliderState extends State<WaveSlider> {
List<int> bars = [];
double barPosition;
double barWidth;
int maxBarHight;
double width;
int numberOfBars;
void randomNumberGenerator() {
Random r = Random();
for (var i = 0; i < numberOfBars; i++) {
bars.add(r.nextInt(maxBarHight - 10) + 10);
}
}
_onTapDown(TapDownDetails details) {
var x = details.globalPosition.dx;
print("tap down " + x.toString());
setState(() {
barPosition = x;
});
}
#override
void initState() {
super.initState();
barPosition = widget.initialBarPosition;
barWidth = widget.barWidth;
maxBarHight = widget.maxBarHight.toInt();
width = widget.width;
if (bars.isNotEmpty) bars = [];
numberOfBars = width ~/ barWidth;
randomNumberGenerator();
}
#override
Widget build(BuildContext context) {
int barItem = 0;
return Scaffold(
backgroundColor: Colors.grey[900],
body: Center(
child: GestureDetector(
onTapDown: (TapDownDetails details) => _onTapDown(details),
onHorizontalDragUpdate: (DragUpdateDetails details) {
setState(() {
barPosition = details.globalPosition.dx;
});
},
child: Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.start,
children: bars.map((int height) {
Color color = barItem + 1 < barPosition / barWidth
? Colors.white
: Colors.grey[600];
barItem++;
return Row(
children: <Widget>[
Container(
width: .1,
height: height.toDouble(),
color: Colors.black,
),
Container(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(1.0),
topRight: const Radius.circular(1.0),
),
),
height: height.toDouble(),
width: 4.8,
),
Container(
width: .1,
height: height.toDouble(),
color: Colors.black,
),
],
);
}).toList(),
),
),
),
),
);
}
}
and use it like :
WaveSlider(
initialBarPosition: 180.0,
barWidth: 5.0,
maxBarHight: 50,
width: MediaQuery.of(context).size.width,
)

Horizontally scrollable cards with Snap effect in flutter

I want to create a list of cards scrolling horizontally with snap to fit effect when swiped either from left or right.
Each card has some spacing between them and fit to screen similar to below image
Apart from that these horizontally scrollable list elements should be contained inside a vertically scrollable list.
I all I am able to achieve is only displaying a list of horizontal scrolling cards after following example in flutter docs.
class SnapCarousel extends StatelessWidget {
#override
Widget build(BuildContext context) {
final title = 'Horizontal List';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Container(
margin: EdgeInsets.symmetric(vertical: 20.0),
height: 200.0,
child: ListView(
scrollDirection: Axis.horizontal,
children: <Widget>[
Container(
width: 160.0,
color: Colors.red,
),
Container(
width: 160.0,
color: Colors.blue,
),
Container(
width: 160.0,
color: Colors.green,
),
Container(
width: 160.0,
color: Colors.yellow,
),
Container(
width: 160.0,
color: Colors.orange,
),
],
),
),
),
);
}
}
Use PageView and ListView:
import 'package:flutter/material.dart';
main() => runApp(MaterialApp(home: MyHomePage()));
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Carousel in vertical scrollable'),
),
body: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 16.0),
itemBuilder: (BuildContext context, int index) {
if(index % 2 == 0) {
return _buildCarousel(context, index ~/ 2);
}
else {
return Divider();
}
},
),
);
}
Widget _buildCarousel(BuildContext context, int carouselIndex) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Carousel $carouselIndex'),
SizedBox(
// you may want to use an aspect ratio here for tablet support
height: 200.0,
child: PageView.builder(
// store this controller in a State to save the carousel scroll position
controller: PageController(viewportFraction: 0.8),
itemBuilder: (BuildContext context, int itemIndex) {
return _buildCarouselItem(context, carouselIndex, itemIndex);
},
),
)
],
);
}
Widget _buildCarouselItem(BuildContext context, int carouselIndex, int itemIndex) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 4.0),
child: Container(
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
),
);
}
}
Screenshot:
If you don't want to use any 3rd party packages, you can simply try this:
class _HomePageState extends State<HomePage> {
int _index = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: SizedBox(
height: 200, // card height
child: PageView.builder(
itemCount: 10,
controller: PageController(viewportFraction: 0.7),
onPageChanged: (int index) => setState(() => _index = index),
itemBuilder: (_, i) {
return Transform.scale(
scale: i == _index ? 1 : 0.9,
child: Card(
elevation: 6,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Center(
child: Text(
"Card ${i + 1}",
style: TextStyle(fontSize: 32),
),
),
),
);
},
),
),
),
);
}
}
this is an old question, and I arrived here looking for something else ;-), but what WitVault was lookig is done easy with this package: https://pub.dev/packages/flutter_swiper
The implementation:
Put the dependencies in pubsec.yaml:
dependencies:
flutter_swiper: ^1.1.6
Import it in the page where you need it:
import 'package:flutter_swiper/flutter_swiper.dart';
In the layout:
new Swiper(
itemBuilder: (BuildContext context, int index) {
return new Image.network(
"http://via.placeholder.com/288x188",
fit: BoxFit.fill,
);
},
itemCount: 10,
viewportFraction: 0.8,
scale: 0.9,
)
To achieve the snap effect via ListView, just set the physics to PageScrollPhysics
const List<Widget> children = [
ContainerCard(),
ContainerCard(),
ContainerCard(),
];
ListView.builder(
scrollDirection: Axis.horizontal,
physics: const PageScrollPhysics(), // this for snapping
itemCount: children.length,
itemBuilder: (_, index) => children[index],
)
Advanced Snap List
If you are looking for advanced usages, such as dynamic item sizes, configurable snap points, visualization of items, and essential control (such as scrollToIndex, animate) you should use the native-based SnappyListView with way more features.
SnappyListView(
itemCount: Colors.accents.length,
itemBuilder: (context, index) {
return Container(
height: 100,
color: Colors.accents.elementAt(index),
child: Text("Index: $index"),
),
);
I believe the answer solution from CopsOnRoad is better and simple for someone who don't want to use a 3rd party library. However, since there is no animation, I add the scale animation when the card is viewed (expand) and the previous card is swiped (shrink) using index. So what happened is whenever the first time the page load, 1st and 2nd card won't have any animation, and when the card is swiped, only the previous and current card have the scale animation. So this is my implementation:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int currentIndex = -1, previousIndex = 0;
double getAnimationValue(int currentIndex, int widgetIndex, int previousIndex,
{bool begin = true}) {
if (widgetIndex == currentIndex) {
return begin ? 0.9 : 1;
} else {
return begin ? 1 : 0.9;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizedBox(
height: 200, // card height
child: PageView.builder(
itemCount: 10,
controller: PageController(viewportFraction: 0.7),
onPageChanged: (int index) {
setState(() {
if (currentIndex != -1) {
previousIndex = currentIndex;
}
currentIndex = index;
});
},
itemBuilder: (_, widgetIndex) {
return (currentIndex != -1 &&
(previousIndex == widgetIndex ||
widgetIndex == currentIndex))
? TweenAnimationBuilder(
duration: const Duration(milliseconds: 400),
tween: Tween<double>(
begin: getAnimationValue(
currentIndex,
widgetIndex,
previousIndex,
),
end: getAnimationValue(
currentIndex,
widgetIndex,
previousIndex,
begin: false,
),
),
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Card(
elevation: 6,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Card${widgetIndex + 1}",
style: const TextStyle(fontSize: 30),
),
Text(
"$widgetIndex >> Widget Index << $widgetIndex",
style: const TextStyle(fontSize: 22),
),
Text(
"$currentIndex >> Current Index << $currentIndex",
style: const TextStyle(fontSize: 22),
),
Text(
"$previousIndex >> Previous Index << $previousIndex",
style: const TextStyle(fontSize: 22),
),
],
),
),
);
},
)
: Transform.scale(
// this is used when you want to disable animation when initialized the page
scale:
(widgetIndex == 0 && currentIndex == -1) ? 1 : 0.9,
child: Card(
elevation: 6,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Card${widgetIndex + 1}",
style: const TextStyle(fontSize: 30),
),
Text(
"$widgetIndex >> Widget Index << $widgetIndex",
style: const TextStyle(fontSize: 22),
),
Text(
"$currentIndex >> Init Index << $currentIndex",
style: const TextStyle(fontSize: 22),
),
Text(
"$previousIndex >> Previous Index << $previousIndex",
style: const TextStyle(fontSize: 22),
),
],
),
),
);
},
),
),
],
),
);
}
}
I used TweenAnimationBuilder for this animation and hardcoded the widget. You can use method for your widget or use package flutter_animate for easy animation whenever necessary.

Resources