In my application, I am showing a video on the screen.
When application starts, it show full screen of the video, overlay with some information about the video.
This screen is called glass screen.
When the user click a button 3/4 of the screen is adjusted to show the video and bottom part show some other information.
The screen height adjustment is done with "Expanded" flex set to 3/4, by the parent widget and it is working fine.
In both cases the video must fill the height of the screen and user can zoom and pan to see parts outside the screen. I am using "InteractiveViewer" for that.
"WidgetSize" allow me to get the size of the area a Widget occupies.
I have several factors to consider area size video going to display and size of the video
I need to maintain the aspect ratio of the video.
When the glass door is closed, it seems the video is displaying correctly.
But when it is closed (3/4 area) I can't figure out how to find out the correct scale for the video and how to adjust the position of it with "InteractiveViewer".
This is just a test to figure out how to use it
_viewTransformationController.value.setIdentity();
_viewTransformationController.value.setEntry(0, 0, 2);
_viewTransformationController.value.setEntry(1, 1, 2);
_viewTransformationController.value.setEntry(2, 2, 2);
_viewTransformationController.value.setEntry(1, 3, -75);
zoom level 2 is not correct value. -75 is to adjust the Y position (not really sure about this)
Following is the full code.
ScaleVideoController _scaleController;
final _viewTransformationController =
TransformationController(Matrix4.identity());
#override
void initState() {
super.initState();
_scaleController = widget.controller;
this._scaleController.videoSizeChanged = (double width, double height) {
this._videoSize = Size(width, height);
this._updateScale();
};
this._scaleController.glassDoorOpenTriggered = () {
this._glassDoorClosed = false;
this._updateScale();
};
}
_updateScale() {
final physicalScreenSize = window.physicalSize;
if (this._glassDoorClosed) {
this._displayVideoHeight = this._areaSize.height;
this._displayVideoWidth =
this._areaSize.width * this._displayVideoHeight / _areaSize.height;
this._scale = physicalScreenSize.height / this._displayVideoHeight;
} else {
_viewTransformationController.value.setIdentity();
_viewTransformationController.value.setEntry(0, 0, 2);
_viewTransformationController.value.setEntry(1, 1, 2);
_viewTransformationController.value.setEntry(2, 2, 2);
_viewTransformationController.value.setEntry(1, 3, -75);
}
setState(() {});
}
Widget _buildScaledRemoteVideo() {
return WidgetSize(
onChange: (Size s) {
this._areaSize = s;
this._updateScale();
},
child: InteractiveViewer(
transformationController: _viewTransformationController,
scaleEnabled: !_glassDoorClosed,
minScale: 0.1,
constrained: false,
child: Transform.scale(
scale: _glassDoorClosed ? _scale : 1,
child: Container(
width: this._displayVideoWidth,
height: this._displayVideoHeight,
child: widget.remoteVideoRenderer(),
),
),
),
);
}
This is how it should look like,
The read area is the video..
Can anyone provide some advice on how to adjust the screen when it
Related
I'm using this character animation in my Flutter app (with the animated background):
https://rive.app/community/1201-5354-lumberjack-walk-cycle/
(if you hit download on that link you will only get the character animation without the background.
to get the character plus background you have to open it in editor and hit download in there)
Now in Flutter I can run the animation forever like this:
RiveAnimation.asset(
'assets/animations/lumberjack.riv',
),
The Character and the background is animated. Now I want to customize the animation speed. For that I looked at this example: https://github.com/rive-app/rive-flutter/blob/master/example/lib/custom_controller.dart
So I put in this class:
class SpeedController extends SimpleAnimation {
final double speedMultiplier;
SpeedController(
String animationName, {
double mix = 1,
this.speedMultiplier = 1,
}) : super(animationName, mix: mix);
#override
void apply(RuntimeArtboard artboard, double elapsedSeconds) {
if (instance == null || !instance!.keepGoing) {
isActive = false;
}
instance!
..animation.apply(instance!.time, coreContext: artboard, mix: mix)
..advance(elapsedSeconds * speedMultiplier);
}
}
and added the controller to my animation:
RiveAnimation.asset(
animations: const ['idle'],
controllers: [SpeedController('curves', speedMultiplier: 5)],
'assets/animations/lumberjack.riv',
),
Not only is the animation speed not changing (character is animated in default speed), but also is the background not animated at all anymore.
Instead of direct download, open in rive
Now make sure to download on V7
The walk animation is on a different artBoard,
You can find it by
late SpeedController walkController = SpeedController(
'walk',
speedMultiplier: .1,
);
SizedBox(
height: 200,
child: RiveAnimation.asset(
animations: ["walk"],
artboard: "Lumberjack",
controllers: [walkController],
fileLoc,
),
),
Also you need to change the animation name according to rive animation LumberingLumberjack
late SpeedController _controller =
SpeedController('LumberingLumberjack', speedMultiplier: .3);
RiveAnimation.asset(
animations: ["LumberingLumberjack"],
controllers: [_controller],
fileLoc,
),
I am currently using a SpriteAnimationWidget from Flame, and I want to have different animations by clicking buttons.
To test out if this works, I am changing the animation parameter of the SpriteAnimationWidget with a button click. After 1 second, the widget has to return to its original animation.
Here is my code:
Future<void> playReaction() async {
setState(() {
isPlaying = true;
state = 'reaction';
});
await Future.delayed(Duration(milliseconds: 1000));
setState(() {
isPlaying = false;
state = 'idle';
});
}
And inside the build, I am returning:
if(state == 'reaction') {
spriteAnimation = reactionSprite!.createAnimation(row: 0, stepTime: 0.1, from: 0, to: 10);
print('reaction');
}
else if(state == 'idle'){
spriteAnimation = sprite!.createAnimation(row: 1, stepTime: 0.2, from: 0, to: 4);
print('idle');
}
return Container(
height: widget.size,
child: Stack(
fit: StackFit.expand,
children: [
Positioned.fill(
child: SpriteAnimationWidget(
animation: spriteAnimation!,
anchor: Anchor.center,
)
),
]
),
);
The variables sprite and reactionSprite are sprite sheets that I preload using SpriteSheet.fromColumnsAndRows().
The problem is that, although my app successfully runs playReaction() and reaches print('reaction'), the SpriteAnimationWidget is still playing the idle animation instead of the reaction animation. I know that it's being rebuilt somehow because I can see that there is a slight unnatural discontinuity in the animation when I hit the button (plus the print messages).
Why is setState() not updating the SpriteAnimationWidget with a new animation parameter?
I am relatively new to Flutter and Flame, so I would love some help or advice.
ps. I tried using the same sprite sheet for the change with a different row (instead of using a completely different sprite sheet as in the code above), but the result was the same.
This was actually a bug, we are fixing it here.
Meanwhile, as a workaround, you can change the key of the widget every time that you change the animation and it should update the widget.
I'm trying to build a Flutter Web version of the threejs Periodic Table Helix view (see here: https://mrdoob.com/lab/javascript/threejs/css3d/periodictable/ and click on "Helix")
Currently, I place all the element tiles in a stack, and position and rotate them with matrix transformations. See below code:
#override
Widget build(BuildContext context) {
double initialRotateX = (widget.atomicNumber - 1) *
(math.pi / Constants.numberOfElementsPerHalfCircle);
return Transform(
transform: Matrix4.identity()
..translate(
math.sin((widget.atomicNumber - 1) *
(math.pi / Constants.numberOfElementsPerHalfCircle)) *
Constants.radius,
widget.atomicNumber * 4,
-math.cos((widget.atomicNumber - 1) *
(math.pi / Constants.numberOfElementsPerHalfCircle)) *
Constants.radius,
)
..translate(Constants.elementCardHalfSize)
..rotateY(-initialRotateX)
..translate(-Constants.elementCardHalfSize),
child: Container(
decoration: BoxDecoration(
color: Constants.elementCardColor,
border: Border.all(width: 1, color: Colors.white.withOpacity(0.3))),
child: SizedBox(
width: Constants.elementCardWidth.toDouble(),
height: Constants.elementCardHeight.toDouble(),
child: ElementText()
),
),
);
All these cards are then placed inside a stack widget in another class, and that widget is rotated, like so:
return AnimatedBuilder(
animation: widget.animationControllerX,
builder: (context, _) {
double rotateX =
widget.animationControllerX.value / Constants.turnResolution;
double rotateY =
widget.animationControllerY.value / Constants.turnResolution;
return Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(-rotateX)
..rotateX(rotateY),
alignment: Alignment.center,
child: Container(
child: Stack(
children: List<Widget>.generate(
Constants.numberOfElements,
(index) => ElementCard(
atomicNumber:
Constants.elements[(index + 1).toString()].atomicNumber,
),
),
),
),
);
},
);
As you can see, there is a "..rotateY" transformation applied, which makes a card appear to move forward when the stack is rotated.
However, when rotating, cards that should be in the back are smaller, yet paint over the cards that should be in the front. (There is no doubt that the cards are in the right positions, since I can rotate them along the x axis and see that for myself, but when painting, the card on the back is painted over the text in the card on the front. I believe this is because a stack always positions the elevation of its widgets according to the order they're provided in, and the order is fixed in the list. Is there any way I can fix this?
Screenshots to explain what I mean:
I can change the opacity of the tiles to make the issue more obvious:
As you can see, the larger cards are closer to the screen, since that's how they're transformed, but they're painting behind the smaller cards. Any ideas on how to make this happen?
So I found a solution. It's not perfect, but it works so far.
The issue is that Flutter paints widgets in the order that they appear. Since the larger cards are higher up on the list, even though they have a closer z coordinate, they are painted first, and the smaller card is painted on top of them.
The solution is to use a Flow() widget to dynamically change the order in which the cards are painted. In the children widgets, where I'd normally build them, I instead stored their transformations in a map. Then, in the flow delegate function, access these transformations, calculate the z coordinate of each widget after transforming (row 3, column 3 (1 indexed) in the transformation matrix). Sort the widgets by z coordinate, and then paint them on screen in that order.
Here is the code for the same:
The part with the root Flow widget:
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: widget.animationControllerX,
builder: (context, _) {
double rotateX =
widget.animationControllerX.value / Constants.turnResolution;
double rotateY =
widget.animationControllerY.value / Constants.turnResolution;
return Container(
transform: Matrix4.identity()
..translate(
MediaQuery.of(context).size.width / 2,
),
child: Flow(
clipBehavior: Clip.none,
delegate: CustomFlowDelegate(rotateY: -rotateX, rotateX: rotateY),
children: List<Widget>.generate(
Constants.numberOfElements,
(index) => ElementCard(
atomicNumber:
Constants.elements[(index + 1).toString()].atomicNumber,
),
),
),
);
},
);
}
The FlowDelegate for the widget:
class CustomFlowDelegate extends FlowDelegate {
CustomFlowDelegate({this.rotateX, this.rotateY});
final rotateY, rotateX;
#override
void paintChildren(FlowPaintingContext context) {
Map<int, double> zValue = {};
for (int i = 0; i < context.childCount; i++) {
var map = Constants.transformationsMap[i + 1];
Matrix4 transformedMatrix = Matrix4.identity()
..setEntry(3, 2, 0.001)
..setEntry(3, 1, 0.001)
..rotateY(this.rotateY)
..rotateX(this.rotateX)
..translate(
map['translateOneX'], map['translateOneY'], map['translateOneZ'])
..translate(map['translateTwoX'])
..rotateY(map['rotateThreeY'])
..translate(map['translateFourX']);
zValue[i + 1] = transformedMatrix.getRow(2)[2];
}
var sortedKeys = zValue.keys.toList(growable: false)
..sort((k1, k2) => zValue[k1].compareTo(zValue[k2]));
LinkedHashMap sortedMap = new LinkedHashMap.fromIterable(sortedKeys,
key: (k) => k, value: (k) => zValue[k]);
for (int key in sortedMap.keys) {
var map = Constants.transformationsMap[key];
context.paintChild(
key - 1,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..setEntry(3, 1, 0.001)
..rotateY(this.rotateY)
..rotateX(this.rotateX)
..translate(
map['translateOneX'], map['translateOneY'], map['translateOneZ'])
..translate(map['translateTwoX'])
..rotateY(map['rotateThreeY'])
..translate(map['translateFourX']),
);
}
}
#override
bool shouldRepaint(covariant FlowDelegate oldDelegate) {
return true;
}
}
Here's what the result looks like:
And with the opacity turned up a bit so you can really see the fix in action:
And yes, I know storing the transformations in a map and then sorting them from another page isn't best practice.
Hope this helps someone.
My question essentially duplicates the previously asked question: Flutter: How to set a max/min scale using matrix_gesture_detector
I have a MatrixGestureDetector:
MatrixGestureDetector(
onMatrixUpdate: (m, tm, sm, rm) {
setState(() {
transform = m;
});
},
child: Transform(
transform: transform,
child: Image.asset("assets/maps/map.gif"),
),
),
The picture can be zoomed in and out indefinitely, but I want to limit the minimum and maximum scale.
The solution suggested in the link above is not correct for me. The scale is really limited, but when it reaches its limits, gestures start to be handled incorrectly. The picture starts moving very quickly when you try to zoom in or out when the limits are reached.
Is there any solution to this problem? Maybe there are other packages to solve my problem? PhotoViev did not satisfy me for the reason described here: Flutter PhotoView - Rotation about the point between the fingers
Flutter announced a new version(1.20) and it came with a new widget called InteractiveViewer.
You can use the InteractiveViewer to achieve what you want by setting a minScale and maxScale.
This release introduces a new widget, the InteractiveViewer. The InteractiveViewer is designed for building common kinds of interactivity into your app, like pan, zoom, and drag ānā drop
Read more about the release here: Flutter 1.20 Release notes
I added a demo:
NOTE: You can only use this widget if you have upgraded to the latest Flutter version (1.20)
class ZoomImage extends StatelessWidget {
final transformationController = TransformationController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: InteractiveViewer(
transformationController: transformationController,
onInteractionEnd: (details) {
setState(() {
// unzoom when interaction has ended
transformationController.toScene(Offset.zero);
});
},
// set the boundary margin for the image
boundaryMargin: EdgeInsets.all(20.0),
// set min scale here
minScale: 0.1,
// set maximum scall here
maxScale: 1.6,
child: ClipRRect(
borderRadius: BorderRadius.circular(18.0),
child: Image.asset('assets/maps/map.gif'),
),
),
);
}
}
I want to clip a widget and use this clipped image in a layout and the bounderies should be the visible part of the imgage when clipped.
Using this custom clipper
#override
Rect getClip(Size size) {
Rect rect = Rect.fromLTRB(25,0,size.width - 25, size.height);
return rect;
}
#override
bool shouldReclip(CustomRect oldClipper) {
return true;
}
}
results in 25 px blank space left of the clipped image and 25 px blank space right of the clipped image.
And at least we ant to copy specific areas of an image and scale/position it exactly in the app... -> more complex desired result:
You need to add a transform to translate the widget over to the newly empty space. Just be aware that the widget itself will still occupy the same width in this example - so if there is some sibling in a row, for example, it will still get "pushed over" by the same amount of space. If you need to change that you'll need to add a SizedBox to the final part of this example so that you can trim down the size of the widget to the portion you've clipped.
Also note that this is not a very good practice - ideally you should be fetching the image you actually want to display. Flutter will still need to load your entire image into memory and then do some non-trivial work to add the clip you want. That takes up plenty of extra CPU and memory. But sometimes you don't have a choice I guess.
This example shows just displaying an image, followed by applying a custom clip, followed by applying a translation, which is what OP is looking for.
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart';
void main() {
final Widget image = Image.network(
'https://via.placeholder.com/300x60?text=This is just a placeholder');
const double widthAmount = 100;
runApp(MaterialApp(
home: Scaffold(
body: Center(
child: Column(
children: <Widget>[
Spacer(),
image,
Spacer(),
ClipRect(
clipper: CustomRect(widthAmount),
child: image,
),
Spacer(),
Transform(
transform: Matrix4.translation(Vector3(-widthAmount, 0.0, 0.0)),
child: ClipRect(
clipper: CustomRect(widthAmount),
child: image,
),
),
Spacer(),
],
),
),
),
));
}
class CustomRect extends CustomClipper<Rect> {
CustomRect(this.widthAmount);
final double widthAmount;
#override
Rect getClip(Size size) {
Rect rect =
Rect.fromLTRB(widthAmount, 0, size.width - widthAmount, size.height);
return rect;
}
#override
bool shouldReclip(CustomRect oldClipper) {
return oldClipper.widthAmount != widthAmount;
}
}
Okay, the solution above is not really working as if we translate the clipped image we get some free space (the amount that we translate).
Isn't there any option in Flutter to just define an area of an widget and get this area as a new Widget/Image with exactly the width and height of the defined area to be able to position it (without any paddings/white space around),to scale it, ...?