Created
March 18, 2020 10:40
-
-
Save GAM3RG33K/0e42e32a623666a2a161fc2061ae1577 to your computer and use it in GitHub Desktop.
A Floating Widget in Flutter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
/// Enum for selecting floating widget's initial alignment | |
enum WidgetAlignment { | |
/// The widget will be positioned of top left corner of the parent | |
topLeft, | |
/// The widget will be positioned of top right corner of the parent | |
topRight, | |
/// The widget will be positioned of bottom left corner of the parent | |
bottomLeft, | |
/// The widget will be positioned of bottom right corner of the parent | |
bottomRight, | |
} | |
/// This widget is a container widget which can make the provided child [Widget] | |
/// into a Floating widget, which user can drag anywhere within the parent widgets bounds | |
/// | |
/// This widget takes a [child] and an [initialPosition]. | |
/// | |
/// The [child] is a required inout and it must not be null. | |
/// The [initialPosition] is the Offset containing the co-ordinates where | |
/// the floating widget should be displayed inside the parent widgets bounds | |
/// | |
/// | |
/// Note: | |
/// - Use stack widget when adding a floating widget, other wise this widget will not | |
/// be able to move from the place it is placed in by default | |
/// - Place the Widget over the biggest visible widget in the current widget tree | |
/// like, in PortraitMode's main container widget or Landscape mode's main stack | |
/// - Do not give the initialPosition which might be out of bounds of the parent | |
/// widget | |
class FloatingWidget extends StatefulWidget { | |
/// This is the child widget which is to be used as a floating widget | |
final Widget child; | |
/// This is the initial position of the widget, if not provided | |
/// it is considered to be [Offset.zero] | |
final Offset initialPosition; | |
/// This is build context of the parent widget, which used for getting the parent's | |
/// position and size | |
final BuildContext buildContext; | |
///[experimental feature] Takes a widget alignment value instead of the initial position | |
final WidgetAlignment alignment; | |
/// Public constructor | |
const FloatingWidget({ | |
Key key, | |
@required this.child, | |
@required this.buildContext, | |
this.initialPosition = Offset.zero, | |
this.alignment, | |
}) : assert(child != null), | |
assert(buildContext != null), | |
super(key: key); | |
@override | |
_FloatingWidgetState createState() => _FloatingWidgetState(); | |
} | |
class _FloatingWidgetState extends State<FloatingWidget> { | |
double xPosition; | |
double yPosition; | |
Offset parentPosition; | |
Size parentSize; | |
@override | |
void initState() { | |
super.initState(); | |
WidgetsBinding.instance.addPostFrameCallback(initBoundValues); | |
} | |
Offset get initialPosition { | |
return widget.initialPosition; | |
} | |
Offset get positionFromAlignment { | |
return getPosition(widget.alignment); | |
} | |
double get parentLeft => parentPosition?.dx ?? 0; | |
double get parentRight => parentLeft + parentSize?.width ?? parentLeft; | |
double get parentTop => parentPosition?.dy ?? 0; | |
double get parentBottom => parentTop + parentSize?.height ?? parentTop; | |
double get parentWidth => parentSize?.width ?? 0; | |
double get parentHeight => parentSize?.height ?? 0; | |
@override | |
Widget build(BuildContext context) { | |
if ((xPosition == null && yPosition == null)) { | |
if (positionFromAlignment != null) { | |
// TODO(hjoshi): alignment code not working right now. check feasibility and fix it | |
xPosition = positionFromAlignment.dx; | |
yPosition = positionFromAlignment.dy; | |
} else { | |
xPosition = initialPosition.dx; | |
yPosition = initialPosition.dy; | |
} | |
} | |
return Stack( | |
children: <Widget>[ | |
///use following widget to see the parent bounds | |
Positioned( | |
top: yPosition, | |
left: xPosition, | |
child: GestureDetector( | |
behavior: HitTestBehavior.opaque, | |
onPanUpdate: (DragUpdateDetails panUpdateDetails) { | |
setState(() { | |
final double deltaX = panUpdateDetails.delta.dx; | |
final double deltaY = panUpdateDetails.delta.dy; | |
xPosition += deltaX; | |
yPosition += deltaY; | |
//This allows restriction of movement out side of the parent widget | |
// only applicable on left and top of the widget | |
if (xPosition <= parentLeft) { | |
xPosition = parentLeft; | |
} | |
if (yPosition < parentTop) { | |
yPosition = parentTop; | |
} | |
//This allows restriction of movement out side of the parent widget | |
// only applicable on right and bottom of the widget | |
// Since the child widget is not inflated yet, we can not find it's | |
// size hence this solution is temporary, and should be improved | |
if (xPosition >= parentRight * 0.8) { | |
xPosition = parentRight * 0.8; | |
} | |
if (yPosition > parentBottom * 0.8) { | |
yPosition = parentBottom * 0.8; | |
} | |
}); | |
}, | |
child: widget.child, | |
), | |
), | |
], | |
); | |
} | |
void initBoundValues(Duration timeStamp) { | |
parentPosition = getWidgetPosition(widget.buildContext); | |
parentSize = getWidgetSize(widget.buildContext); | |
} | |
Offset getPosition(WidgetAlignment alignment) { | |
if (parentPosition != null) { | |
final Offset position = parentPosition; | |
switch (alignment) { | |
case WidgetAlignment.topLeft: | |
return Offset(position.dx, position.dy); | |
case WidgetAlignment.topRight: | |
return Offset(position.dx + parentWidth * 0.7, position.dy); | |
case WidgetAlignment.bottomLeft: | |
return Offset(position.dx, position.dy + parentHeight); | |
case WidgetAlignment.bottomRight: | |
return Offset( | |
position.dx + parentWidth * 0.7, position.dy + parentHeight); | |
} | |
} | |
return widget.initialPosition; | |
} | |
} | |
/// This method is used to get the render box property of the widget | |
/// | |
/// A render box has various properties related to widget;'s render | |
/// that is position and size of the widget | |
RenderBox getRenderBox(BuildContext context) { | |
final RenderBox renderObject = context.findRenderObject(); | |
return renderObject; | |
} | |
/// This method is used for getting widget's position using given context | |
/// The resulting position is the top left point of the widget. | |
Offset getWidgetPosition(BuildContext context) => | |
getRenderBox(context).localToGlobal(Offset.zero); | |
/// This method is used for getting widget's size using given context | |
Size getWidgetSize(BuildContext context) => getRenderBox(context).size; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment