Skip to content

Instantly share code, notes, and snippets.

@GAM3RG33K
Created March 18, 2020 10:40
Show Gist options
  • Save GAM3RG33K/0e42e32a623666a2a161fc2061ae1577 to your computer and use it in GitHub Desktop.
Save GAM3RG33K/0e42e32a623666a2a161fc2061ae1577 to your computer and use it in GitHub Desktop.
A Floating Widget in Flutter
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