Skip to content

Commit 90cc421

Browse files
committed
[SAFE AREA] Properly centers focused widgets when the keyboard appears
1 parent ff6e737 commit 90cc421

File tree

1 file changed

+114
-7
lines changed

1 file changed

+114
-7
lines changed
Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,135 @@
1+
import 'dart:math';
2+
13
import 'package:flutter/widgets.dart';
24
import '../models/figma_frame_model.dart';
35
import 'figma_frame.dart';
6+
import 'stateful_figma_node.dart';
47

5-
class FigmaSafeArea extends FigmaFrame {
8+
class FigmaSafeArea extends StatefulFigmaNode<FigmaFrameModel> {
69
const FigmaSafeArea({
710
required super.model,
811
super.key
912
});
1013

1114
static FigmaSafeArea fromJson(Map<String, dynamic> json) {
12-
return FigmaFrame.fromJson(json, FigmaSafeArea.new, FigmaFrameModel.fromJson);
15+
return FigmaFrame.fromJson(json, FigmaSafeArea.new);
16+
}
17+
18+
@override
19+
StatefulFigmaNodeState createState() => FigmaSafeAreaState();
20+
}
21+
22+
class FigmaSafeAreaState extends StatefulFigmaNodeState<FigmaSafeArea>
23+
with SingleTickerProviderStateMixin {
24+
late AnimationController controller;
25+
double bottomInset = 0.0;
26+
double yOffset = 0.0;
27+
double oldYOffset = 0.0;
28+
FocusNode? focusNode;
29+
double focusNodeY = 0.0;
30+
31+
void updateFocusNode(FocusNode? node, double screenHeight) {
32+
if (node == focusNode) {
33+
return;
34+
}
35+
focusNode = node;
36+
if (focusNode != null) {
37+
BuildContext? context = focusNode!.context;
38+
if (context != null) {
39+
RenderBox box = context.findRenderObject() as RenderBox;
40+
Offset position = box.localToGlobal(Offset(box.size.width * 0.5, box.size.height * 0.5));
41+
focusNodeY = position.dy + yOffset;
42+
if (bottomInset != 0.0) {
43+
double center = (screenHeight - bottomInset) * 0.5;
44+
double newOffset = min(max(focusNodeY - center, 0.0), bottomInset);
45+
animateOffset(yOffset, newOffset);
46+
}
47+
}
48+
} else {
49+
focusNodeY = 0.0;
50+
}
51+
}
52+
53+
void animateOffset(double from, double to) {
54+
controller.addListener(() {
55+
double t = Curves.easeOutSine.transform(controller.value);
56+
setState(() {
57+
yOffset = from + (to - from) * t;
58+
oldYOffset = yOffset;
59+
});
60+
});
61+
62+
controller.forward();
63+
}
64+
65+
void updateBottomInset(double bottom, double screenHeight) {
66+
if (bottom != bottomInset) {
67+
if (focusNode == null) {
68+
if (yOffset != 0.0) {
69+
yOffset = oldYOffset * (bottom / bottomInset);
70+
}
71+
if (bottom == 0.0) {
72+
yOffset = 0.0;
73+
oldYOffset = 0.0;
74+
bottomInset = bottom;
75+
}
76+
} else {
77+
bottomInset = bottom;
78+
double center = (screenHeight - bottomInset) * 0.5;
79+
yOffset = min(max(focusNodeY - center, 0.0), bottomInset);
80+
oldYOffset = yOffset;
81+
}
82+
}
83+
}
84+
85+
void handleFocusChange() {
86+
if (context.mounted) {
87+
double screenHeight = MediaQuery.of(context).size.height;
88+
updateFocusNode(
89+
FocusScope.of(context, createDependency: false).focusedChild,
90+
screenHeight,
91+
);
92+
}
93+
}
94+
95+
@override
96+
void initState() {
97+
super.initState();
98+
controller = AnimationController(
99+
duration: const Duration(milliseconds: 250),
100+
vsync: this,
101+
);
102+
103+
controller.addStatusListener((status) {
104+
if(status == AnimationStatus.completed) {
105+
controller.clearListeners(); // we could also save all listeners and remove them one by one
106+
controller.reset();
107+
}
108+
});
109+
110+
FocusManager.instance.addListener(handleFocusChange);
111+
}
112+
113+
@override
114+
void dispose() {
115+
FocusManager.instance.removeListener(handleFocusChange);
116+
controller.dispose();
117+
super.dispose();
13118
}
14119

15120
@override
16121
Widget buildFigmaNode(BuildContext context) {
17-
Matrix4 bottomInset = Matrix4.identity();
18-
bottomInset.translate(0.0, -MediaQuery.of(context).viewInsets.bottom * 0.7, 0.0);
122+
double screenHeight = MediaQuery.of(context).size.height;
123+
updateBottomInset(MediaQuery.of(context).viewInsets.bottom, screenHeight);
124+
125+
Matrix4 offset = Matrix4.identity()..translate(0.0, -yOffset, 0.0);
19126

20127
return Transform(
21-
transform: bottomInset,
128+
transform: offset,
22129
child: SafeArea(
23130
maintainBottomViewPadding: true,
24-
child: super.buildFigmaNode(context),
131+
child: FigmaFrame.buildFigmaFrame(context, widget.model),
25132
),
26133
);
27134
}
28-
}
135+
}

0 commit comments

Comments
 (0)