Creating Resizable View that resizes when pinch or drag from corners and sides in FLUTTER
Solution 1
Updated
I've made a simple prototype to show the idea.
- Draw size handlers and a container;
- Use GestureDetector to detect dragging;
- Refresh the main container size and coordinates.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Text Overflow Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
body: Demo(),
),
);
}
}
class Demo extends StatefulWidget {
@override
_DemoState createState() => _DemoState();
}
class _DemoState extends State<Demo> {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(60),
child: ResizebleWidget(
child: Text(
'''I've just did simple prototype to show main idea.
1. Draw size handlers with container;
2. Use GestureDetector to get new variables of sizes
3. Refresh the main container size.''',
),
),
);
}
}
class ResizebleWidget extends StatefulWidget {
ResizebleWidget({this.child});
final Widget child;
@override
_ResizebleWidgetState createState() => _ResizebleWidgetState();
}
const ballDiameter = 30.0;
class _ResizebleWidgetState extends State<ResizebleWidget> {
double height = 400;
double width = 200;
double top = 0;
double left = 0;
void onDrag(double dx, double dy) {
var newHeight = height + dy;
var newWidth = width + dx;
setState(() {
height = newHeight > 0 ? newHeight : 0;
width = newWidth > 0 ? newWidth : 0;
});
}
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
top: top,
left: left,
child: Container(
height: height,
width: width,
color: Colors.red[100],
child: widget.child,
),
),
// top left
Positioned(
top: top - ballDiameter / 2,
left: left - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var mid = (dx + dy) / 2;
var newHeight = height - 2 * mid;
var newWidth = width - 2 * mid;
setState(() {
height = newHeight > 0 ? newHeight : 0;
width = newWidth > 0 ? newWidth : 0;
top = top + mid;
left = left + mid;
});
},
),
),
// top middle
Positioned(
top: top - ballDiameter / 2,
left: left + width / 2 - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var newHeight = height - dy;
setState(() {
height = newHeight > 0 ? newHeight : 0;
top = top + dy;
});
},
),
),
// top right
Positioned(
top: top - ballDiameter / 2,
left: left + width - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var mid = (dx + (dy * -1)) / 2;
var newHeight = height + 2 * mid;
var newWidth = width + 2 * mid;
setState(() {
height = newHeight > 0 ? newHeight : 0;
width = newWidth > 0 ? newWidth : 0;
top = top - mid;
left = left - mid;
});
},
),
),
// center right
Positioned(
top: top + height / 2 - ballDiameter / 2,
left: left + width - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var newWidth = width + dx;
setState(() {
width = newWidth > 0 ? newWidth : 0;
});
},
),
),
// bottom right
Positioned(
top: top + height - ballDiameter / 2,
left: left + width - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var mid = (dx + dy) / 2;
var newHeight = height + 2 * mid;
var newWidth = width + 2 * mid;
setState(() {
height = newHeight > 0 ? newHeight : 0;
width = newWidth > 0 ? newWidth : 0;
top = top - mid;
left = left - mid;
});
},
),
),
// bottom center
Positioned(
top: top + height - ballDiameter / 2,
left: left + width / 2 - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var newHeight = height + dy;
setState(() {
height = newHeight > 0 ? newHeight : 0;
});
},
),
),
// bottom left
Positioned(
top: top + height - ballDiameter / 2,
left: left - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var mid = ((dx * -1) + dy) / 2;
var newHeight = height + 2 * mid;
var newWidth = width + 2 * mid;
setState(() {
height = newHeight > 0 ? newHeight : 0;
width = newWidth > 0 ? newWidth : 0;
top = top - mid;
left = left - mid;
});
},
),
),
//left center
Positioned(
top: top + height / 2 - ballDiameter / 2,
left: left - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var newWidth = width - dx;
setState(() {
width = newWidth > 0 ? newWidth : 0;
left = left + dx;
});
},
),
),
// center center
Positioned(
top: top + height / 2 - ballDiameter / 2,
left: left + width / 2 - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
setState(() {
top = top + dy;
left = left + dx;
});
},
),
),
],
);
}
}
class ManipulatingBall extends StatefulWidget {
ManipulatingBall({Key key, this.onDrag});
final Function onDrag;
@override
_ManipulatingBallState createState() => _ManipulatingBallState();
}
class _ManipulatingBallState extends State<ManipulatingBall> {
double initX;
double initY;
_handleDrag(details) {
setState(() {
initX = details.globalPosition.dx;
initY = details.globalPosition.dy;
});
}
_handleUpdate(details) {
var dx = details.globalPosition.dx - initX;
var dy = details.globalPosition.dy - initY;
initX = details.globalPosition.dx;
initY = details.globalPosition.dy;
widget.onDrag(dx, dy);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanStart: _handleDrag,
onPanUpdate: _handleUpdate,
child: Container(
width: ballDiameter,
height: ballDiameter,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.5),
shape: BoxShape.circle,
),
),
);
}
}
Solution 2
I did a discrete version (ie snaps every 50 units) of the code above if it helps anyone:
import 'package:flutter/material.dart';
class DiscreteResizableComponent extends StatefulWidget {
const DiscreteResizableComponent({Key key, this.child}):super(key:key);
final Widget child;
@override
_ResizebleWidgetState createState() => _ResizebleWidgetState();
}
const ballDiameter = 30.0;
const discreteStepSize = 50;
class _ResizebleWidgetState extends State<DiscreteResizableComponent> {
double height = 400;
double width = 200;
double top = 0;
double left = 0;
double cumulativeDy=0;
double cumulativeDx=0;
double cumulativeMid = 0;
void onDrag(double dx, double dy) {
var newHeight = height + dy;
var newWidth = width + dx;
setState(() {
height = newHeight > 0 ? newHeight : 0;
width = newWidth > 0 ? newWidth : 0;
});
}
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
top: top,
left: left,
child: Container(
height: height,
width: width,
color: Colors.red[100],
child: widget.child,
),
),
// top left
Positioned(
top: top - ballDiameter / 2,
left: left - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var mid = (dx + dy) / 2;
cumulativeMid -= 2*mid;
if(cumulativeMid>=discreteStepSize)
{
setState(() {
var newHeight = height+discreteStepSize;
height = newHeight > 0 ? newHeight : 0;
var newWidth = width +discreteStepSize;
width = newWidth > 0 ? newWidth : 0;
cumulativeMid=0;
});
}
else if(cumulativeMid<=-discreteStepSize)
{
setState(() {
var newHeight = height - discreteStepSize;
height = newHeight > 0 ? newHeight : 0;
var newWidth = width - discreteStepSize;
width = newWidth > 0 ? newWidth : 0;
cumulativeMid=0;
});
}
},
),
),
// top middle
Positioned(
top: top - ballDiameter / 2,
left: left + width / 2 - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
cumulativeDy -= dy;
if(cumulativeDy>=discreteStepSize)
{
setState(() {
var newHeight = height+discreteStepSize;
height = newHeight > 0 ? newHeight : 0;
cumulativeDy=0;
});
}
else if(cumulativeDy<=-discreteStepSize)
{
setState(() {
var newHeight = height - discreteStepSize;
height = newHeight > 0 ? newHeight : 0;
cumulativeDy=0;
});
}
},
),
),
// top right
Positioned(
top: top - ballDiameter / 2,
left: left + width - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var mid = (dx + (dy * -1)) / 2;
cumulativeMid += 2*mid;
if(cumulativeMid>=discreteStepSize)
{
setState(() {
var newHeight = height+discreteStepSize;
height = newHeight > 0 ? newHeight : 0;
var newWidth = width +discreteStepSize;
width = newWidth > 0 ? newWidth : 0;
cumulativeMid=0;
});
}
else if(cumulativeMid<=-discreteStepSize)
{
setState(() {
var newHeight = height - discreteStepSize;
height = newHeight > 0 ? newHeight : 0;
var newWidth = width - discreteStepSize;
width = newWidth > 0 ? newWidth : 0;
cumulativeMid=0;
});
}
},
),
),
// center right
Positioned(
top: top + height / 2 - ballDiameter / 2,
left: left + width - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
cumulativeDx += dx;
if(cumulativeDx>=discreteStepSize)
{
setState(() {
var newWidth = width+discreteStepSize;
width = newWidth > 0 ? newWidth : 0;
cumulativeDx=0;
});
}
else if(cumulativeDx<=-discreteStepSize)
{
setState(() {
var newWidth = width-discreteStepSize;
width = newWidth > 0 ? newWidth : 0;
cumulativeDx=0;
});
}
},
),
),
// bottom right
Positioned(
top: top + height - ballDiameter / 2,
left: left + width - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var mid = (dx + dy) / 2;
cumulativeMid += 2*mid;
if(cumulativeMid>=discreteStepSize)
{
setState(() {
var newHeight = height+discreteStepSize;
height = newHeight > 0 ? newHeight : 0;
var newWidth = width +discreteStepSize;
width = newWidth > 0 ? newWidth : 0;
cumulativeMid=0;
});
}
else if(cumulativeMid<=-discreteStepSize)
{
setState(() {
var newHeight = height - discreteStepSize;
height = newHeight > 0 ? newHeight : 0;
var newWidth = width - discreteStepSize;
width = newWidth > 0 ? newWidth : 0;
cumulativeMid=0;
});
}
},
),
),
// bottom center
Positioned(
top: top + height - ballDiameter / 2,
left: left + width / 2 - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
cumulativeDy += dy;
if(cumulativeDy>=discreteStepSize)
{
setState(() {
var newHeight = height+discreteStepSize;
height = newHeight > 0 ? newHeight : 0;
cumulativeDy=0;
});
}
else if(cumulativeDy<=-discreteStepSize)
{
setState(() {
var newHeight = height-discreteStepSize;
height = newHeight > 0 ? newHeight : 0;
cumulativeDy=0;
});
}
},
),
),
// bottom left
Positioned(
top: top + height - ballDiameter / 2,
left: left - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var mid = ((dx * -1) + dy) / 2;
cumulativeMid += 2*mid;
if(cumulativeMid>=discreteStepSize)
{
setState(() {
var newHeight = height+discreteStepSize;
height = newHeight > 0 ? newHeight : 0;
var newWidth = width +discreteStepSize;
width = newWidth > 0 ? newWidth : 0;
cumulativeMid=0;
});
}
else if(cumulativeMid<=-discreteStepSize)
{
setState(() {
var newHeight = height - discreteStepSize;
height = newHeight > 0 ? newHeight : 0;
var newWidth = width - discreteStepSize;
width = newWidth > 0 ? newWidth : 0;
cumulativeMid=0;
});
}
},
),
),
//left center
Positioned(
top: top + height / 2 - ballDiameter / 2,
left: left - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
cumulativeDx -= dx;
if(cumulativeDx>=discreteStepSize)
{
setState(() {
var newWidth = width+discreteStepSize;
width = newWidth > 0 ? newWidth : 0;
cumulativeDx=0;
});
}
else if(cumulativeDx<=-discreteStepSize)
{
setState(() {
var newWidth = width-discreteStepSize;
width = newWidth > 0 ? newWidth : 0;
cumulativeDx=0;
});
}
},
),
),
],
);
}
}
class ManipulatingBall extends StatefulWidget {
ManipulatingBall({Key key, this.onDrag});
final Function onDrag;
@override
_ManipulatingBallState createState() => _ManipulatingBallState();
}
class _ManipulatingBallState extends State<ManipulatingBall> {
double initX;
double initY;
_handleDrag(details) {
setState(() {
initX = details.globalPosition.dx;
initY = details.globalPosition.dy;
});
}
_handleUpdate(details) {
var dx = details.globalPosition.dx - initX;
var dy = details.globalPosition.dy - initY;
initX = details.globalPosition.dx;
initY = details.globalPosition.dy;
widget.onDrag(dx, dy);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanStart: _handleDrag,
onPanUpdate: _handleUpdate,
child: Container(
width: ballDiameter,
height: ballDiameter,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.5),
shape: BoxShape.circle,
),
),
);
}
}
![jazzbpn](https://i.stack.imgur.com/6uv0w.png?s=256&g=1)
jazzbpn
With 6+ years of experience, I have developed many iOS/android applications in different context, professional and oldest passion for computer programming began very early in my life. I've learned the social environment is as important as logic aspects of the developing approach then I appreciate very much to get in touch with positive and eager colleagues that involve me in new and exciting challenges. This is why I still want to get involved in new opportunities to improve my skillness.
Updated on December 19, 2022Comments
-
jazzbpn over 1 year
I am currently working on a ScreenView with features like draggable and resizable views with corners and sides like in the image above. The problem I have now is that I want to resize the view by touch gestures in the corners. Therefore, I thought of a Point which I add to a view on selection, which can be dragged to resize the selected view. Answer updated!!
Resizable-Widget ReactNative Demo: React Native PLUGIN example
Modified Workable Example:
import 'package:flutter/material.dart'; class ResizeWidget extends StatefulWidget { @override _ResizeWidgetState createState() => _ResizeWidgetState(); } class _ResizeWidgetState extends State<ResizeWidget> { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( backgroundColor: Colors.black, body: Container( // padding: EdgeInsets.only(top: 50), child: ResizebleWidget( child: Container( padding: EdgeInsets.all(10), child: Text( 'Waao!! you can really dance.', style: TextStyle( color: Colors.white, fontStyle: FontStyle.italic, fontSize: 18), ), ), ), ), ), ); } } class ResizebleWidget extends StatefulWidget { ResizebleWidget({this.child}); final Widget child; @override _ResizebleWidgetState createState() => _ResizebleWidgetState(); } const ballDiameter = 10.0; class _ResizebleWidgetState extends State<ResizebleWidget> { double height = 100; double width = 200; bool isCorner = false; double top = 0; double left = 0; @override Widget build(BuildContext context) { return Stack( children: <Widget>[ Positioned( top: top, left: left, child: Container( height: height, width: width, decoration: BoxDecoration( color: Colors.blueGrey, border: Border.all( width: 2, color: Colors.white70, ), borderRadius: BorderRadius.circular(0.0), ), // need tp check if draggable is done from corner or sides child: isCorner ? FittedBox( child: widget.child, ) : Center( child: widget.child, ), ), ), // top left Positioned( top: top - ballDiameter / 2, left: left - ballDiameter / 2, child: ManipulatingBall( onDrag: (dx, dy) { var mid = (dx + dy) / 2; var newHeight = height - 2 * mid; var newWidth = width - 2 * mid; setState(() { isCorner = true; height = newHeight > 0 ? newHeight : 0; width = newWidth > 0 ? newWidth : 0; top = top + mid; left = left + mid; }); }, handlerWidget: HandlerWidget.VERTICAL, ), ), // top middle Positioned( top: top - ballDiameter / 2, left: left + width / 2 - ballDiameter / 2, child: ManipulatingBall( onDrag: (dx, dy) { var newHeight = height - dy; setState(() { isCorner = false; height = newHeight > 0 ? newHeight : 0; top = top + dy; }); }, handlerWidget: HandlerWidget.HORIZONTAL, ), ), // top right Positioned( top: top - ballDiameter / 2, left: left + width - ballDiameter / 2, child: ManipulatingBall( onDrag: (dx, dy) { var mid = (dx + (dy * -1)) / 2; var newHeight = height + 2 * mid; var newWidth = width + 2 * mid; setState(() { isCorner = true; height = newHeight > 0 ? newHeight : 0; width = newWidth > 0 ? newWidth : 0; top = top - mid; left = left - mid; }); }, handlerWidget: HandlerWidget.VERTICAL, ), ), // center right Positioned( top: top + height / 2 - ballDiameter / 2, left: left + width - ballDiameter / 2, child: ManipulatingBall( onDrag: (dx, dy) { var newWidth = width + dx; setState(() { isCorner = false; width = newWidth > 0 ? newWidth : 0; }); }, handlerWidget: HandlerWidget.HORIZONTAL, ), ), // bottom right Positioned( top: top + height - ballDiameter / 2, left: left + width - ballDiameter / 2, child: ManipulatingBall( onDrag: (dx, dy) { var mid = (dx + dy) / 2; var newHeight = height + 2 * mid; var newWidth = width + 2 * mid; setState(() { isCorner = true; height = newHeight > 0 ? newHeight : 0; width = newWidth > 0 ? newWidth : 0; top = top - mid; left = left - mid; }); }, handlerWidget: HandlerWidget.VERTICAL, ), ), // bottom center Positioned( top: top + height - ballDiameter / 2, left: left + width / 2 - ballDiameter / 2, child: ManipulatingBall( onDrag: (dx, dy) { var newHeight = height + dy; setState(() { isCorner = false; height = newHeight > 0 ? newHeight : 0; }); }, handlerWidget: HandlerWidget.HORIZONTAL, ), ), // bottom left Positioned( top: top + height - ballDiameter / 2, left: left - ballDiameter / 2, child: ManipulatingBall( onDrag: (dx, dy) { var mid = ((dx * -1) + dy) / 2; var newHeight = height + 2 * mid; var newWidth = width + 2 * mid; setState(() { isCorner = true; height = newHeight > 0 ? newHeight : 0; width = newWidth > 0 ? newWidth : 0; top = top - mid; left = left - mid; }); }, handlerWidget: HandlerWidget.VERTICAL, ), ), //left center Positioned( top: top + height / 2 - ballDiameter / 2, left: left - ballDiameter / 2, child: ManipulatingBall( onDrag: (dx, dy) { var newWidth = width - dx; setState(() { isCorner = false; width = newWidth > 0 ? newWidth : 0; left = left + dx; }); }, handlerWidget: HandlerWidget.HORIZONTAL, ), ), // center center Positioned( top: top + height / 2 - ballDiameter / 2, left: left + width / 2 - ballDiameter / 2, child: ManipulatingBall( onDrag: (dx, dy) { setState(() { isCorner = false; top = top + dy; left = left + dx; }); }, handlerWidget: HandlerWidget.VERTICAL, ), ), ], ); } } class ManipulatingBall extends StatefulWidget { ManipulatingBall({Key key, this.onDrag, this.handlerWidget}); final Function onDrag; final HandlerWidget handlerWidget; @override _ManipulatingBallState createState() => _ManipulatingBallState(); } enum HandlerWidget { HORIZONTAL, VERTICAL } class _ManipulatingBallState extends State<ManipulatingBall> { double initX; double initY; _handleDrag(details) { setState(() { initX = details.globalPosition.dx; initY = details.globalPosition.dy; }); } _handleUpdate(details) { var dx = details.globalPosition.dx - initX; var dy = details.globalPosition.dy - initY; initX = details.globalPosition.dx; initY = details.globalPosition.dy; widget.onDrag(dx, dy); } @override Widget build(BuildContext context) { return GestureDetector( onPanStart: _handleDrag, onPanUpdate: _handleUpdate, child: Container( width: ballDiameter, height: ballDiameter, decoration: BoxDecoration( color: Colors.white, shape: this.widget.handlerWidget == HandlerWidget.VERTICAL ? BoxShape.circle : BoxShape.rectangle, ), ), ); } }
Output:
-
jazzbpn about 4 yearsThanks for the example. But the required result should have a feature like double tap zoom, pinch to zoom and zoom-in/out from sides and corner as well.
-
Kherel about 4 yearsImplementing a whole library would take more time. But there are existing libraries for pinch zoom. You can find it pub.com.
-
jazzbpn about 4 yearsOk. But, How to make the borders like in the react-native plugin? ManipulatingBall on four-sides and in middle-left and middle-right of the widget(like in the react-native plugin).
-
Kherel about 4 yearsyes, nine handlers. All of them has their own drag handlers. Corner handlers and a center handler react on x and y change. Side handlers react on x or y change. Main container has size and coordiante. Left and and top size handlers change coordinate and size. Right and bottom size handlers change size. The center handler change only coordinate.
-
jazzbpn about 4 yearsWhen dragging from the four-corners both height and width should increase/decrease in same ratio. It should behave like zoom-in/out. How to achieve that?
-
Kherel about 4 yearsI've created a chat chat.stackoverflow.com/rooms/info/210788/…
-
jazzbpn about 4 yearsSure I will upvote. But before that do you mind helping me with one more issue? Please check this video: drive.google.com/file/d/1IXi6D6P4hrD7qpPYHnwvABdQYlklqgL6/… 1. AutoMultiline TextView when dragging from sides (DONE) 2. Zoomable feature for Text or EditText when dragging from corner( Not-Done). The output should be like when the user zoom-in/out the widget will zoom-in/out without being in multiline. Could you please help me with this last issue? Thank you!!
-
Kherel about 4 years@jazzbpn check this: gist.github.com/kherel/19ecf4fc9818592fa2b37a6c69bbca55
-
AgentRed over 2 years@Kherel I've tried your code, but I have a problem when I use transform.rotate and rotated (for example) 45 degrees it doesn't behave the same way. is it because it now have different left and top position?