Changing the CameraPreview Aspect Ratio (Flutter) - flutter

I have an app where I have a Scaffold with an AppBar and a bottom Ads Banner.
In between, there is the CameraPreview from the camera plugin in Flutter.
As the CameraPreview is made to take the aspect ratio of the device/camera, the CameraPreview doesn't take the entire available space, leaving extra space on most devices.
I tried to crop the CameraPreview to show only whatever fits in the available space. It worked, but now the preview is stretched out
LayoutBuilder(
builder: (context, constraints) {
final cameraController = controller.cameraController!;
if(cameraController.value.previewSize != null) {
return ClipRect(
child: OverflowBox(
alignment: Alignment.center,
child: FittedBox(
fit: BoxFit.fitWidth,
child: SizedBox(
width: constraints.maxWidth,
height: constraints.maxWidth,
child: AspectRatio(
aspectRatio: cameraController.value.aspectRatio,
child: CameraPreview(cameraController),
),
),
),
),
);
} else {
return const SizedBox.shrink();
}
},
)
I tried other solutions like Transform.scale, but that only zooms into the preview, it doesn't change the ratio or the stretching.
Looking solutions in the package itself doesn't help either, most similar issues are stalling or already closed for stalling.
What am I supposed to do here? Am I supposed to manually clip the preview's value?

check this below code,
Use get screen size by MediaQuery & calculate scale for aspect ratio widget and add CameraPreview() to it like below
// get screen size
final size = MediaQuery.of(context).size;
// calculate scale for aspect ratio widget
var scale = cameraController.value.aspectRatio / size.aspectRatio;
// check if adjustments are needed...
if (cameraController.value.aspectRatio < size.aspectRatio) {
scale = 1 / scale;
}
return Transform.scale(
scale: scale,
child: Center(
child: AspectRatio(
aspectRatio: cameraController.value.aspectRatio,
child: CameraPreview(cameraController),
),
),
);
Complete code
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
if (controller != null && controller.value.isRecordingVideo) {
//stop video
}
},
child: Scaffold(
resizeToAvoidBottomInset: false,
key: _scaffoldKey,
body: Container(
child: Stack(
children: [
Positioned(
child: Container(
alignment: Alignment.center,
child: cameraScreen(),
),
),
Positioned(
child: Container(
alignment: Alignment.bottomCenter,
child: Container(
height: MediaQuery.of(context).size.height * .1,
color: Colors.black54,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
//can add Controls
],
),
),
),
)
],
),
),
),
);
}
Widget cameraScreen() {
final CameraController cameraController = controller;
if (cameraController == null || !cameraController.value.isInitialized) {
return Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.black,
child: Center(
child: Text(
"Loading Camera...",
style: CameraTextStyle.cameraUtilLoadingStyle(),
),
),
);
} else {
return cameraWidget(context, cameraController);
}
}
Widget cameraWidget(context, cameraController) {
// get screen size
final size = MediaQuery.of(context).size;
// calculate scale for aspect ratio widget
var scale = cameraController.value.aspectRatio / size.aspectRatio;
// check if adjustments are needed...
if (cameraController.value.aspectRatio < size.aspectRatio) {
scale = 1 / scale;
}
return Transform.scale(
scale: scale,
child: Center(
child: AspectRatio(
aspectRatio: cameraController.value.aspectRatio,
child: CameraPreview(cameraController),
),
),
);
}
Widget cameraSwitch() {
final CameraController cameraController = controller;
return Container(
child: InkWell(
onTap: () {
if (cameraController != null &&
cameraController.value.isInitialized &&
!cameraController.value.isRecordingVideo) {
if (cameras.isNotEmpty) {
if (selectedCamera == cameras[0]) {
selectedCamera = cameras[1];
onNewCameraSelected(selectedCamera);
} else {
selectedCamera = cameras[0];
onNewCameraSelected(selectedCamera);
}
}
}
setState(() {});
},
child: Icon(
Icons.switch_camera,
size: 30,
color: Colors.white,
),
),
);
}

Related

How can I capture youtube player state changes with the youtube_player_iframe library?

Using the youtube_player_iframe library I was able to capture some video status data (playing, paused, with/without audio) for xAPI statemetns, but from external buttons, not from the player itself.
My custom buttons
Row optionButtons(BuildContext context) {
return Row(
children: [
YoutubeValueBuilder(
builder: (context, value) {
return IconButton(
icon: Icon(
value.playerState == PlayerState.playing
? Icons.pause
: Icons.play_arrow,
),
onPressed: () {
if (value.playerState == PlayerState.playing) {
context.ytController.pauseVideo();
pausePlay(
"Pause", "WatchTemplateVideo", context.ytController);
} else {
context.ytController.playVideo();
pausePlay("Play", "WatchTemplateVideo", context.ytController);
}
},
);
},
)
],
);
}
Video player
Widget oneVideo(BuildContext context, double heigth, double width) {
return SingleChildScrollView(
child: Column(
children: [
SizedBox(
height: heigth,
width: width,
child: YoutubePlayer(
controller: _controller,
backgroundColor: Colors.black,
),
),
],
));}
Widget build(BuildContext context) {
double heigth = MediaQuery.of(context).size.height * 0.7;
double width = MediaQuery.of(context).size.width * 0.7;
return YoutubePlayerScaffold(
controller: _controller,
builder: (context, player) {
return YoutubePlayerControllerProvider(
controller: _controller,
child: Column(
children: [
oneVideo(context, heigth, width),
optionButtons(context)
],
),
);
});
player's image
However I have not been able to get it to capture the data from the internal controls of the player

Camera preview not matching device | flutter

My camera preview is distorted. It appears far too zoomed in, and also stretched.
What am I doing wrong? How can I fix?
final size = MediaQuery.of(context).size;
final deviceRatio = size.width / size.height;
return Stack(children: <Widget>[
Center(
child: Transform.scale(
scale: controller.value.aspectRatio / deviceRatio,
child: new AspectRatio(
aspectRatio: controller.value.aspectRatio,
child: new CameraPreview(controller),
),
),
),
]);
}
Note: Should work for iOS and Android.
I am using camera package: camera: ^0.7.0+2
Here full page
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
import 'dart:io';
class Camera extends StatefulWidget {
Function setData;
Camera({Key key, this.setData}) : super(key: key);
#override
_CameraScreenState createState() => _CameraScreenState();
}
class _CameraScreenState extends State<Camera> {
CameraController controller;
List cameras;
int selectedCameraIndex;
String imgPath;
var image;
Future _openGallery() async {
image = await controller.takePicture();
if (widget.setData != null) {
widget.setData(File(image.path));
}
}
#override
void initState() {
super.initState();
availableCameras().then((availableCameras) {
cameras = availableCameras;
if (cameras.length > 0) {
setState(() {
selectedCameraIndex = 0;
});
_initCameraController(cameras[selectedCameraIndex]).then((void v) {});
} else {
print('No camera available');
}
}).catchError((err) {
print('Error :${err.code}Error message : ${err.message}');
});
}
Future _initCameraController(CameraDescription cameraDescription) async {
if (controller != null) {
await controller.dispose();
}
controller = CameraController(cameraDescription, ResolutionPreset.high);
controller.addListener(() {
if (mounted) {
setState(() {});
}
if (controller.value.hasError) {
print('Camera error ${controller.value.errorDescription}');
}
});
try {
await controller.initialize();
} on CameraException catch (e) {}
if (mounted) {
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
flex: 1,
child: _cameraPreviewWidget(),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 120,
width: double.infinity,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[_cameraControlWidget(context), Spacer()],
),
),
)
],
),
),
),
);
}
Widget _cameraPreviewWidget() {
if (controller == null || !controller.value.isInitialized) {
return const Text(
'Loading',
style: TextStyle(
color: Colors.white,
fontSize: 20.0,
fontWeight: FontWeight.w900,
),
);
}
final size = MediaQuery.of(context).size;
final deviceRatio = size.width / size.height;
return Stack(children: <Widget>[
Center(
child: Transform.scale(
scale: controller.value.aspectRatio / deviceRatio,
child: new AspectRatio(
aspectRatio: controller.value.aspectRatio,
child: new CameraPreview(controller),
),
),
),
]);
}
Widget _cameraControlWidget(context) {
return Expanded(
child: Align(
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
FloatingActionButton(
child: Icon(
Icons.camera,
color: Colors.black,
),
backgroundColor: Colors.white,
onPressed: () {
_openGallery();
Navigator.pop(context);
},
)
],
),
),
);
}
}
Edit based on answer:
final size = MediaQuery.of(context).size;
final deviceRatio = size.width / size.height;
return Stack(children: <Widget>[
Center(
child: Transform.scale(
scale: 16 / 9,
child: Center(
child: AspectRatio(
aspectRatio: 1,
child: Camera(),
),
),
),
),
]);
}
Had the same problem using camerawesome some times ago.
Had to use Transform and an Aspect Ratio:
Transform.scale(
scale: 16 / 9,
child: Center(
child: AspectRatio(
aspectRatio: 1,
child: Camera(),
),
),
),
Use Matrix4.diagonal3Values for scaling, as we can then control the X, Y, Z axis. X is the horizontal, Y is the vertical and Z is for the ones that want to go into other dimensions 🚀.
final size = MediaQuery.of(context).size;
final deviceRatio = size.width / size.height;
final xScale = cameraController.value.aspectRatio / deviceRatio;
// Modify the yScale if you are in Landscape
final yScale = 1;
return Container(
child: AspectRatio(
aspectRatio: deviceRatio,
child: Transform(
alignment: Alignment.center,
transform: Matrix4.diagonal3Values(xScale, yScale, 1),
child: CameraPreview(cameraController),
),
),
);
If you are working with a camera that rotates and supports Landscape, you will most likely need to scale up the Y axis, and skip the X.
The Lightsnap app is locked in Portrait so we don’t need to re-scale the camera preview when the phone rotates. Just a note that you may need to do this if you are supporting landscape.
Thanks to Lightsnap(More Details here)

AspectRatio problem with Flutter camera and image layer

I have an AspectRatio problem with my Flutter camera (Plugin "camera_camera) and an image that I put on top of it with transparency as a layer.
I send you a screenshot of the problem. In the screenshot you can see the open camera and above it the picture I took right in front of it. Unfortunately you can see at different places that it does not match.
How do I get the camera to show exactly the same proportions as I photographed it from exactly the same position before?
If this helps: I recorded also a video with the issue:
https://danielederosa.de/downloads/flutter_issue.mp4
My Code
#override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (!controller.value.isInitialized) {
return Container(
color: theme.colorScheme.onPrimary,
child: Center(child: CircularProgressIndicator()));
}
return Scaffold(
appBar: CupertinoNavigationBar(
backgroundColor: theme.colorScheme.primary,
border: Border.symmetric(
vertical: BorderSide.none, horizontal: BorderSide.none),
automaticallyImplyLeading: false,
leading: IconButton(
icon: Icon(
Icons.chevron_left,
size: 30,
color: theme.colorScheme.onPrimary,
),
onPressed: () => Navigator.pop(context),
),
middle: Text("Memories",
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: theme.textTheme.headline3.fontSize)),
),
body: Container(
child: Column(
children: [
Expanded(
child: Camera(
mode: CameraMode.normal,
imageMask: lastPicture != null
? new Positioned.fill(
child: new Opacity(
opacity: 0.3,
child: RotatedBox(
quarterTurns: 1,
child: new Image.file(
File(lastPicture),
fit: BoxFit.cover,
),
),
),
)
: Container(),
onFile: (File file) {
_workWithImage(file);
},
),
),
],
),
),
);
}
I also tried to wrap the Camera widget into an AspectRatio widget with aspectRatio: 3/4 because my saved image are saved in this aspectRatio. But without success.
Do you have any idea to solve this issue?
I found a solution and got it to work.
Example code
#override
Widget build(BuildContext context) {
final theme = Theme.of(context);
var deviceSize = MediaQuery.of(context).size;
var sizeWidth = MediaQuery.of(context).size.width;
final deviceRatio = deviceSize.width / deviceSize.height;
var isPortrait = MediaQuery.of(context).orientation == Orientation.portrait;
return Scaffold(
backgroundColor: theme.colorScheme.primary,
appBar: CupertinoNavigationBar(
backgroundColor: theme.colorScheme.primary,
border: Border.symmetric(
vertical: BorderSide.none, horizontal: BorderSide.none),
automaticallyImplyLeading: false,
leading: IconButton(
icon: Icon(
Icons.chevron_left,
size: 30,
color: theme.colorScheme.onPrimary,
),
onPressed: () => Navigator.pop(context),
),
middle: Text(APP_NAME,
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: theme.textTheme.headline3.fontSize)),
),
body: NativeDeviceOrientationReader(
useSensor: true,
builder: (context) {
NativeDeviceOrientation orientation =
NativeDeviceOrientationReader.orientation(context);
return Stack(children: [
FutureBuilder<void>(
future: _initializeControllerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// If the Future is complete, display the preview.
return MeasureSize(
onChange: (size) {
setState(() {
cameraSize = size;
});
},
child: Transform.scale(
scale: cameraController.value.aspectRatio / deviceRatio,
child: Center(
child: AspectRatio(
aspectRatio: cameraController.value.aspectRatio,
child: ClipRect(
child: OverflowBox(
alignment: Alignment.center,
child: FittedBox(
fit: BoxFit.fitWidth,
child: Container(
width: sizeWidth,
height: sizeWidth /
cameraController.value.aspectRatio,
child: CameraPreview(
cameraController), // this is my CameraPreview
),
),
),
),
),
),
),
);
} else {
// Otherwise, display a loading indicator.
return Center(child: CircularProgressIndicator());
}
},
),
helpMode == true
? Transform.scale(
scale: cameraController.value.aspectRatio / deviceRatio,
child: Center(
child: Opacity(
opacity: .3,
child: orientation ==
NativeDeviceOrientation.landscapeLeft ||
orientation ==
NativeDeviceOrientation.landscapeRight
? RotatedBox(
quarterTurns: orientation ==
NativeDeviceOrientation.landscapeLeft
? 1
: 3,
child: Image.file(
File(lastPicture),
height: cameraSize.width,
fit: BoxFit.contain,
))
: Image.file(
File(lastPicture),
width: cameraSize.width,
height: cameraSize.height,
fit: BoxFit.contain,
),
),
),
)
: Container(),
]);
},
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
floatingActionButton: Container(
transform: Matrix4.translationValues(0.0, -8.0, 0.0),
child: FloatingActionButton(
backgroundColor: theme.colorScheme.primary,
child: Icon(
Icons.camera_alt,
color: theme.colorScheme.onPrimary,
),
// Provide an onPressed callback.
onPressed: () async {
// Take the Picture in a try / catch block. If anything goes wrong,
// catch the error.
try {
// Ensure that the camera is initialized.
//await _initializeControllerFuture;
// Construct the path where the image should be saved using the path
// package.
final path = join(
// Store the picture in the temp directory.
// Find the temp directory using the `path_provider` plugin.
(await getTemporaryDirectory()).path,
'${DateTime.now()}.png',
);
// Attempt to take a picture and log where it's been saved.
await cameraController.takePicture(path);
_workWithImage(File(path));
} catch (e) {
// If an error occurs, log the error to the console.
print(e);
}
},
),
),
);
}
}
typedef void OnWidgetSizeChange(Size size);
class MeasureSize extends StatefulWidget {
final Widget child;
final OnWidgetSizeChange onChange;
const MeasureSize({
Key key,
#required this.onChange,
#required this.child,
}) : super(key: key);
#override
_MeasureSizeState createState() => _MeasureSizeState();
}
class _MeasureSizeState extends State<MeasureSize> {
#override
Widget build(BuildContext context) {
SchedulerBinding.instance.addPostFrameCallback(postFrameCallback);
return Container(
key: widgetKey,
child: widget.child,
);
}
var widgetKey = GlobalKey();
var oldSize;
void postFrameCallback(_) {
var context = widgetKey.currentContext;
if (context == null) return;
var newSize = context.size;
if (oldSize == newSize) return;
oldSize = newSize;
widget.onChange(newSize);
}
}
I created the MeasureSize class. With this class I get the dimensions of a child widget. In this case I need the width and height from the camera. (Transform.scale) After I got this I had only to set this dimensions for the image overlay:
Image.file(
File(lastPicture),
width: cameraSize.width,
height: cameraSize.height,
fit: BoxFit.contain,
),
Now the image overlay fits to this what the camera displays.

Flutter overflowed positioned Button is not clickable

I have a stack widget parenting a Positioned widget like this:
Stack(
overflow: Overflow.visible,
children: [
Container(
width: 150,
height: 150,
),
Positioned(
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
print('FAB tapped!');
},
backgroundColor: Colors.blueGrey,
),
right: 0,
left: 0,
bottom: -26,
),
],
),
That part of the fab which is placed outside the container is not clickable, what is the solution?
and here is a screenshot:
try this :
Stack(
overflow: Overflow.visible,
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>
[
Container(width: 150, height: 150, color: Colors.yellow),
Container(width: 150, height: 28, color: Colors.transparent),
],
),
Positioned(
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
print('FAB tapped!');
},
backgroundColor: Colors.blueGrey,
),
right: 0,
left: 0,
bottom: 0,
),
],
)
you should keep button inside of stack if you want it to stay clickable
Providing an updated answer since overflow specification is deprecated after v1.22.0-12.0.pre. clipBehavior is the replacing property:
Stack(
clipBehavior: Clip.none,
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>
[
Container(width: 150, height: 150, color: Colors.yellow),
Container(width: 150, height: 28, color: Colors.transparent),
],
),
Positioned(
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
print('FAB tapped!');
},
backgroundColor: Colors.blueGrey,
),
right: 0,
left: 0,
bottom: 0,
),
],
)
Note: credits to #Amir's answer
The problem is when a child overflows on Stack that has Clip.none behavior, the part that is outside of Stack would not be recognized to be clicked.
Solution :
Wrap the Stack with Column and add the space you want to be outside of Stack :
final _clipSpace = 30;
Stack(
clipBehavior: Clip.none,
children: [
Column(
children: [
DecoratedBox(
decoration: const BoxDecoration(// decorate the box //
),
child: Column(
children: [
// column's children
],
)
],
),
),
// clip space
const SizedBox(height: _clipSpace,)
],
),
const Positioned(
child: _ActionButton(),
left: 0,
right: 0,
bottom: 0,
),
],
);
Container(
width: 150,
height: 180,
child: Stack(
children: [
Container(
width: double.infinity,
height: 150,
child: Image.asset('assets/images/image.jpg', fit: BoxFit.cover,)
),
Container(
alignment: Alignment.bottomCenter,
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
print('FAB tapped!');
},
backgroundColor: Colors.blueGrey,
),
),
],
),
),
Fab button is not clickable because it renders outside of stack as you have given -ve bottom, Ideally, you should have parent container and inside it has all stack widget you should render it.
Here I have used hardcoded values, but you should use media query as per your requirement
Like:
Container(
width: MediaQuery.of(context).size.width * 0.3,
height: MediaQuery.of(context).size.height * 0.3,
child: Stack(
children: [
Container(
width: double.infinity,
height: MediaQuery.of(context).size.height * 0.26,
child: Image.asset('assets/images/jitesh.jpg', fit: BoxFit.cover,)
),
Container(
alignment: Alignment.bottomCenter,
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
print('FAB tapped!');
},
backgroundColor: Colors.blueGrey,
),
),
],
),
),
up until now, there is now solution from Flutter, I should make a simple trick to solve this issue, I need to make a layout like this
the workaround is by adding a SizedBox below your background widget, the height of the SizedBox should be the same as the height of the overlaping widget.
like this
Stack(
clipBehavior: Clip.none,
children: [
Column( // wrap the background in a column
children: [
const _HeaderBackground(),
SizedBox(height: 100), // add the SizedBox with height = 100.0
],
),
Positioned(
bottom: 16,
left: 4,
right: 4,
child: _ReferralCodeSection(customer), // the height of this widget is 100
),
],
),
You have to put the button in the last place of the Stack's children
Stack(children: [...., buttonWidget ])
Flutter does not officially plan to solve this problem, so we can only use some hacking methods.
Here is my resolution with an example, you can use the following OverflowWithHitTest Widget directlly:
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// Creates a widget that can check its' overflow children's hitTest
///
/// [overflowKeys] is must, and there should be used on overflow widget's outermost widget those' sizes cover the overflow child, because it will [hitTest] its' children, but not [hitTest] its' parents. And i cannot found a way to check RenderBox's parent in flutter.
///
/// The [OverflowWithHitTest]'s size must contains the overflow widgets, so you can use it as outer as possible.
///
/// This will not reduce rendering performance, because it only overcheck the given widgets marked by [overflowKeys].
///
/// Demo:
///
/// class _MyPageStore extends State<MyPage> {
///
/// var overflowKeys = <GlobalKey>[GlobalKey()];
///
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: OverflowWithHitTest(
///
/// overflowKeys: overflowKeys,
///
/// child: Container(
/// height: 50,
/// child: UnconstrainedBox(
/// child: Container(
/// width: 200,
/// height: 50,
/// color: Colors.red,
/// child: OverflowBox(
/// alignment: Alignment.topLeft,
/// minWidth: 100,
/// maxWidth: 200,
/// minHeight: 100,
/// maxHeight: 200,
/// child: GestureDetector(
/// key: overflowKeys[0],
/// behavior: HitTestBehavior.translucent,
/// onTap: () {
/// print('==== onTap;');
/// },
/// child: Container(
/// color: Colors.blue,
/// height: 200,
/// child: Text('aaaa'),
/// ),
/// ),
/// ),
/// ),
/// ),
/// ),
/// ),
/// );
/// }
/// }
///
///
class OverflowWithHitTest extends SingleChildRenderObjectWidget {
const OverflowWithHitTest({
required this.overflowKeys,
Widget? child,
Key? key,
}) : super(key: key, child: child);
final List<GlobalKey> overflowKeys;
#override
_OverflowWithHitTestBox createRenderObject(BuildContext context) {
return _OverflowWithHitTestBox(overflowKeys: overflowKeys);
}
#override
void updateRenderObject(
BuildContext context, _OverflowWithHitTestBox renderObject) {
renderObject.overflowKeys = overflowKeys;
}
#override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(
DiagnosticsProperty<List<GlobalKey>>('overflowKeys', overflowKeys));
}
}
class _OverflowWithHitTestBox extends RenderProxyBoxWithHitTestBehavior {
_OverflowWithHitTestBox({required List<GlobalKey> overflowKeys})
: _overflowKeys = overflowKeys,
super(behavior: HitTestBehavior.translucent);
/// Global keys of overflow children
List<GlobalKey> get overflowKeys => _overflowKeys;
List<GlobalKey> _overflowKeys;
set overflowKeys(List<GlobalKey> value) {
var changed = false;
if (value.length != _overflowKeys.length) {
changed = true;
} else {
for (var ind = 0; ind < value.length; ind++) {
if (value[ind] != _overflowKeys[ind]) {
changed = true;
}
}
}
if (!changed) {
return;
}
_overflowKeys = value;
markNeedsPaint();
}
#override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
if (hitTestOverflowChildren(result, position: position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
bool hitTarget = false;
if (size.contains(position)) {
hitTarget =
hitTestChildren(result, position: position) || hitTestSelf(position);
if (hitTarget || behavior == HitTestBehavior.translucent)
result.add(BoxHitTestEntry(this, position));
}
return hitTarget;
}
bool hitTestOverflowChildren(BoxHitTestResult result,
{required Offset position}) {
if (overflowKeys.length == 0) {
return false;
}
var hitGlobalPosition = this.localToGlobal(position);
for (var child in overflowKeys) {
if (child.currentContext == null) {
continue;
}
var renderObj = child.currentContext!.findRenderObject();
if (renderObj == null || renderObj is! RenderBox) {
continue;
}
var localPosition = renderObj.globalToLocal(hitGlobalPosition);
if (renderObj.hitTest(result, position: localPosition)) {
return true;
}
}
return false;
}
}

How to drag elements inside a zoomable content widget?

I'm trying to create an editable node diagram in a draggable/zoomable viewport (kind of like the node system in Blender). A user should be able to edit and drag the nodes.
I can't get the PhotoView (which I use as a viewport) to stay still when I actually want to drag a node within it. All of the PhotoView's children are moving when only one widget - the node - should do so.
I've tried placing boxes listening to pointer events (to make them draggable) inside a PhotoView, but somehow, anything outside a centered area the size of the screen doesn't receive any touches.
Minimal code so far:
Creating the PhotoViewController (inside a State):
double scale = 1;
PhotoViewController controller;
#override
void initState() {
super.initState();
controller = PhotoViewController()
..outputStateStream.listen(listener);
}
void listener(PhotoViewControllerValue value) {
setState(() {
// store the scale in a local variable to drag widgets in relation to the zoom
scale = value.scale;
});
}
Building the viewport (also part of the State):
Offset position = Offset(0, 0);
#override
Widget build(BuildContext context) {
return Center(
child: PhotoView.customChild(
child: Stack(
children: <Widget>[
// vertical line
Center(
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.black
),
child: Container(
width: 1,
height: 1000,
),
),
),
// horizontal line
Center(
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.black
),
child: Container(
width: 1000,
height: 1,
),
),
),
// box to debug the initial screen size
Center(
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.black12
),
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
),
),
),
// stack containing all nodes (only one in this case)
Stack(
children: <Widget>[
// build a node...
Center(
child: Transform.translate( // offset the node
offset: position,
child: Listener( // make it a listener
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.red
),
child: Container(
width: 130,
height: 100,
child: Column(
children: <Widget>[
Text("Node",
style: TextStyle(
color: Colors.white,
fontSize: 18.0
),
)
],
),
),
),
// make the node listen to touch movements and eventually change its position
onPointerMove: (event) =>
{
setState(() =>
{
position +=
event.delta.scale(1 / scale, 1 / scale)
})
},
)
)
),
]
),
],
),
childSize: Size(10000, 10000),
backgroundDecoration: BoxDecoration(color: Colors.white),
initialScale: 1.0,
controller: controller,
),
);
}
At the moment, I kind of fixed the unintentional viewport drag by toggling a bool (whenever a node is touched/released) and overwriting the PhotoView's value.
Switching from the photo_view package to pskink's matrix_gesture_detector solved my issues. Though I now follow a different system: for every node, a new matrix is created (deriving from a main matrix, translated by the node's position). Matrices may be used inside a Transform widget to transform its child.
I achieved building a translatable viewport in which there is a draggable node carrying a clickable checkbox.
Some working minimal code:
import 'package:matrix_gesture_detector/matrix_gesture_detector.dart';
import 'package:vector_math/vector_math_64.dart' as vector;
...
// following code is executed inside a State
Matrix4 matrix = Matrix4.identity();
ValueNotifier<int> notifier = ValueNotifier(0);
vector.Vector3 nodePosition = vector.Vector3(50, 0, 0);
bool check = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Node Diagram Demo'),
),
body: LayoutBuilder(
builder: (ctx, constraints) {
return MatrixGestureDetector(
shouldRotate: false,
onMatrixUpdate: (m, tm, sm, rm) {
matrix = MatrixGestureDetector.compose(matrix, tm, sm, null);
notifier.value++;
},
child: Container(
width: double.infinity,
height: double.infinity,
alignment: Alignment.topLeft,
color: Color(0xff444444),
child: AnimatedBuilder(
animation: notifier,
builder: (ctx, child) {
return Container(
width: double.infinity,
height: double.infinity,
child: Stack( // a stack in which all nodes are built
children: <Widget>[
buildCenter(),
buildNode()
],
)
);
},
),
),
);
},
),
);
}
// I made this into a method so the transformed matrix can be calculated at runtime
Widget buildNode() {
// create a clone of the main matrix and translate it by the node's position
Matrix4 ma = matrix.clone();
ma.translate(nodePosition.x, nodePosition.y);
return Transform(
transform: ma, // transform the node using the new (translated) matrix
child: MatrixGestureDetector(
shouldRotate: false,
shouldScale: false,
onMatrixUpdate: (m, tm, sm, rm) {
Matrix4 change = tm;
// move the node (in relation to the viewport zoom) when it's being dragged
double sc = MatrixGestureDetector.decomposeToValues(matrix).scale;
nodePosition += change.getTranslation() / sc;
notifier.value++; // refresh view
},
// design a node holding a bool variable ('check')...
child: Container(
decoration: BoxDecoration(
color: Colors.blue
),
child: Container(
width: 200,
height: 100,
child: Column(
children: <Widget>[
Text("Node",
style: TextStyle(
color: Colors.white,
fontSize: 18.0
),
),
Checkbox(
onChanged: (v) =>
{
check = v,
notifier.value++ // refresh view
},
value: check,
)
],
),
)
)
)
);
}
// build two lines to indicate a matrix origin
Widget buildCenter() {
return Transform(
transform: matrix,
child: Stack(
children: <Widget>[
// vertical line
Center(
child: Container(
width: 1,
height: 250,
decoration: BoxDecoration(color: Colors.white)
)
),
// horizontal line
Center(
child: Container(
width: 250,
height: 1,
decoration: BoxDecoration(color: Colors.white)
)
),
],
)
);
}