Skip to content

Instantly share code, notes, and snippets.

@s0nerik
Last active August 26, 2020 21:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save s0nerik/b75438df6bf57039b40f77fdbf6a908e to your computer and use it in GitHub Desktop.
Save s0nerik/b75438df6bf57039b40f77fdbf6a908e to your computer and use it in GitHub Desktop.
A widget that mimics Android's `adjustPan` windowSoftInputMode
import 'dart:async';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
typedef AdjustPanChildBuilder = Widget Function(EdgeInsetsGeometry padding);
/// A widget that forces the same behavior as
/// Android's `adjustPan` windowSoftInputMode.
///
/// !!!IMPORTANT!!!
/// If used within [Scaffold] - `resizeToAvoidBottomInset: false` must be set.
class AdjustPan extends StatefulWidget {
const AdjustPan({
Key key,
@required this.builder,
this.additionalPadding = 24,
this.duration = const Duration(milliseconds: 200),
this.curve = Curves.easeInOut,
}) : assert(builder != null),
assert(additionalPadding != null),
super(key: key);
/// Child widget builder.
/// Make sure to use the provided padding for scrollable containers!
final AdjustPanChildBuilder builder;
/// Padding above the bottom inset and below the [FocusNode]
final double additionalPadding;
/// Scroll animation duration
final Duration duration;
/// Scroll animation curve
final Curve curve;
@override
_AdjustPanState createState() => _AdjustPanState();
}
class _AdjustPanState extends State<AdjustPan> {
var _bottomInsetHeight = 0.0;
bool _hasFocus;
FocusNode _primaryFocus;
final _adjustScrollStreamCtrl = StreamController<int>();
StreamSubscription _adjustScrollStreamSub;
Timer _scrollTimer;
@override
void initState() {
super.initState();
FocusManager.instance.addListener(_onFocusChange);
_adjustScrollStreamSub =
_adjustScrollStreamCtrl.stream.listen(_adjustScroll);
}
@override
void dispose() {
_scrollTimer?.cancel();
FocusManager.instance.removeListener(_onFocusChange);
_adjustScrollStreamSub?.cancel();
_adjustScrollStreamCtrl.close();
super.dispose();
}
void _onFocusChange() {
final primaryFocus = FocusManager.instance.primaryFocus;
if (primaryFocus != _primaryFocus) {
_primaryFocus = primaryFocus;
_scheduleScrollAdjustment();
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final bottomInsetHeight = MediaQuery.of(context).viewInsets.bottom;
if (bottomInsetHeight != _bottomInsetHeight) {
_bottomInsetHeight = bottomInsetHeight;
_scheduleScrollAdjustment();
}
}
void _scheduleScrollAdjustment() {
if (_hasFocus != true) {
return;
}
final t = DateTime.now().millisecondsSinceEpoch ~/ 100;
_adjustScrollStreamCtrl.add(t);
}
void _adjustScroll(_) {
if (_hasFocus != true) {
return;
}
final bottomInsetHeight = MediaQuery.of(context).viewInsets.bottom;
if (bottomInsetHeight > 0) {
final screenHeight = MediaQuery.of(context).size.height;
final focusNode = FocusManager.instance.primaryFocus;
_scrollToFocusNode(focusNode, bottomInsetHeight, screenHeight);
}
}
@override
Widget build(BuildContext context) {
return FocusScope(
onFocusChange: (focused) {
_hasFocus = focused;
},
child: LayoutBuilder(
builder: (context, constraints) {
final child = widget.builder(
EdgeInsets.only(bottom: _bottomInsetHeight),
);
if (child is ScrollView || child is SingleChildScrollView) {
return child;
}
return SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.only(
bottom: _bottomInsetHeight,
),
child: SizedBox(
height: constraints.maxHeight,
child: child,
),
);
},
),
);
}
void _scrollToFocusNode(
FocusNode focusNode,
double bottomInsetHeight,
double screenHeight,
) {
final RenderBox focusNodeRenderBox = focusNode?.context?.findRenderObject();
if (focusNodeRenderBox == null) {
return;
}
// Calculate the offset needed to show the object in the [ScrollView]
// so that its bottom touches the top of the keyboard.
final viewport = RenderAbstractViewport.of(focusNodeRenderBox);
final offset = viewport?.getOffsetToReveal(focusNodeRenderBox, 1)?.offset;
if (offset == null) {
return;
}
final pos = Scrollable.of(focusNode.context)?.position;
if (pos == null) {
return;
}
final target =
(offset + bottomInsetHeight + widget.additionalPadding).clamp(
pos.minScrollExtent,
pos.maxScrollExtent,
);
pos.animateTo(
target,
duration: widget.duration,
curve: widget.curve,
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment