å ¥é
å€ãã®ããã¹ããã£ãŒã«ããããç»é¢ããããŸãããããã®5ã€ãŸãã¯30ãååšããå¯èœæ§ãããããããã®éã«ããŸããŸãªãŠã£ãžã§ãããååšããå¯èœæ§ããããŸãã
ä»äº
- ããŒããŒãã®äžã«ãã[次ãž]ãã¿ã³ã§ãããã¯ãé 眮ããŠã次ã®ãã£ãŒã«ãã«åãæ¿ããŸãã
- ãã©ãŒã«ã¹ã倿Žãããšãã¯ããæ¬¡ãžããã¿ã³ã§ãã£ãŒã«ãããããã¯ãŸã§ã¹ã¯ããŒã«ããŸãã
åé¡
ãã¿ã³ã®ãããããã¯ãããã¹ããã£ãŒã«ãã«éãªã£ãŠããŸããããã¹ããã£ãŒã«ãã®éãªãåãã¹ããŒã¹ã®ãµã€ãºã«ããèªåã¹ã¯ããŒã«ãå®è£ ããå¿ èŠããããŸãã
ãœãªã¥ãŒã·ã§ã³ã®æºå
1.20ãã£ãŒã«ãã®ç»é¢ãèŠãŠã¿ãŸãããã
ã³ãŒãïŒ
List<String> list = List.generate(20, (index) => index.toString());
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: <Widget>[
for (String value in list)
TextField(
decoration: InputDecoration(labelText: value),
)
],
),
),
),
),
);
}
ããã¹ããã£ãŒã«ãã«ãã©ãŒã«ã¹ãåããããšã次ã®å³
ã衚瀺ãããŸãããã£ãŒã«ãã¯å®å šã«è¡šç€ºããããã¹ãŠãæ£åžžã§ãã
2.ãã¿ã³ã§ãããã¯ã远å ããŸãããªãŒããŒã¬ã€ã¯
ããããã¯ã衚瀺ããããã«äœ¿çšãããŸããããã«ãããã¹ã¿ãã¯ã©ãããŒã䜿çšããã«ãç»é¢äžã®ãŠã£ãžã§ãããšã¯ç¬ç«ããŠãã¬ãŒãã衚瀺ã§ããŸããåæã«ããã£ãŒã«ããšã次ãžããããã¯ã®éã«çŽæ¥ã®çžäºäœçšã¯ãããŸããããªãŒããŒã¬ã€ã«é¢ãã çŽ æŽãããèšäºã ã€ãŸãããªãŒããŒã¬ã€ã䜿çšãããšããªãŒããŒã¬ã€ã¹ã¿ãã¯ãä»ããŠããŠã£ãžã§ãããä»ã®ãŠã£ãžã§ããã®äžã«ãªãŒããŒã¬ã€ã§ããŸããOverlayEntryã䜿çšãããšã察å¿ãããªãŒããŒã¬ã€ãå¶åŸ¡ã§ããŸãã ã³ãŒãïŒ
bool _isShow = false;
OverlayEntry _overlayEntry;
KeyboardListener _keyboardListener;
@override
void initState() {
SchedulerBinding.instance.addPostFrameCallback((_) {
_overlayEntry = OverlayEntry(builder: _buildOverlay);
Overlay.of(context).insert(_overlayEntry);
_keyboardListener = KeyboardListener()
..addListener(onChange: _keyboardHandle);
});
super.initState();
}
@override
void dispose() {
_keyboardListener.dispose();
_overlayEntry.remove();
super.dispose();
}
Widget _buildOverlay(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
bottom: MediaQuery.of(context).viewInsets.bottom,
left: 0,
right: 0,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: _isShow ? 1.0 : 0.0,
child: NextBlock(
onPressed: () {},
isShow: _isShow,
),
),
),
],
);
void _keyboardHandle(bool isVisible) {
_isShow = isVisible;
_overlayEntry?.markNeedsBuild();
}
3.äºæ³ã©ããããããã¯ã¯ããŒãžã³ãšãªãŒããŒã©ããããŠããŸãã
ãœãªã¥ãŒã·ã§ã³ã®ã¢ã€ãã¢
1. ScrollControllerããç»é¢ã®çŸåšã®ã¹ã¯ããŒã«äœçœ®ãååŸãããã£ãŒã«ããŸã§ã¹ã¯ããŒã«ããŸãã
ãã£ãŒã«ãã®ãµã€ãºã¯äžæã§ããç¹ã«è€æ°è¡ã®å Žåã¯ããã£ãŒã«ããŸã§ã¹ã¯ããŒã«ãããšäžæ£ç¢ºãªçµæã«ãªããŸãã解決çã¯å®ç§ã§ãæè»ã§ããããŸããã
2.ãªã¹ãã®å€ã«ãŠã£ãžã§ããã®ãµã€ãºã远å ããã¹ã¯ããŒã«ãèæ ®ã«å ¥ããŸãã
ãŠã£ãžã§ãããåºå®ã®é«ãã«èšå®ãããšãã¹ã¯ããŒã«ã®äœçœ®ãšãŠã£ãžã§ããã®ãµã€ãºããããã°ãçŸåšå¯èŠãŸãŒã³ã«ãããã®ãšãç¹å®ã®ãŠã£ãžã§ããã衚瀺ããããã«ã¹ã¯ããŒã«ããå¿ èŠãããéãããããŸãã
çæïŒ
- ãªã¹ãå€ã®ãã¹ãŠã®ãŠã£ãžã§ãããèæ
®ã«å
¥ããŠãèšç®ã§äœ¿çšãããåºå®ãµã€ãºãèšå®ããå¿
èŠããããŸããããã¯ãã€ã³ã¿ãŒãã§ã€ã¹ã®å¿
èŠãªèšèšãšåäœã«åžžã«å¯Ÿå¿ããŠãããšã¯éããŸããã
- UIãç·šéãããšãèšç®ãä¿®æ£ãããŸãã
3.ãã£ãŒã«ãã®ç»é¢ãšã次ãžããããã¯ã«å¯ŸãããŠã£ãžã§ããã®äœçœ®ãååŸããéããèªã¿åããŸãã
ãã€ãã¹-ç®±ããåºããŠãã®ãããªå¯èœæ§ã¯ãããŸããã
4.ã¬ã³ããªã³ã°ã¬ã€ã€ãŒã䜿çšããŸããèšäºã«
åºã¥ããŠãFlutterã¯ãã®åå«ãããªãŒã«é 眮ããæ¹æ³ãç¥ã£ãŠããŸããã€ãŸãããã®æ å ±ãåŒãåºãããšãã§ããŸããRenderObjectã¯ã¬ã³ããªã³ã°ãæ åœããŸãããããã説æããŸããRenderBoxã«ã¯ããŠã£ãžã§ããã®å¹ ãšé«ãã瀺ããµã€ãºããã¯ã¹ããããŸãããããã¯ããŠã£ãžã§ããã®ã¬ã³ããªã³ã°æã«èšç®ãããŸãããªã¹ããã³ã³ãããŒãããã¹ããã£ãŒã«ãïŒè€æ°è¡ã®ãã®ãå«ãïŒãªã©ã§ãã ããªãã¯RenderBoxãä»ããŠååŸããããšãã§ããŸã
context context.findRenderObject() as RenderBox
GlobalKeyã䜿çšããŠããã£ãŒã«ãã®ã³ã³ããã¹ããååŸã§ããŸãã
ãã€ãã¹ïŒ
GlobalKeyã¯æãç°¡åãªããšã§ã¯ãããŸããããããŠããããã§ããã ãå°ãªãäœ¿ãæ¹ãè¯ãã§ãã
ãã°ããŒãã«ããŒãæã€ãŠã£ãžã§ããã¯ãããªãŒå ã®ããå Žæããå¥ã®å Žæã«ç§»åãããšãã«ãµãããªãŒãåæç»ããŸãããµãããªãŒãåæç»ããã«ã¯ããŠã£ãžã§ãããå€ãå Žæããåé€ãããã®ãšåãã¢ãã¡ãŒã·ã§ã³ãã¬ãŒã ã§ãããªãŒå ã®æ°ããå Žæã«å°éããå¿ èŠããããŸãã
ã°ããŒãã«ããŒã¯ãããã©ãŒãã³ã¹ã®ç¹ã§æ¯èŒçé«äŸ¡ã§ããäžèšã®æ©èœã®ããããå¿ èŠãªãå Žåã¯ãKeyãValueKeyãObjectKeyããŸãã¯UniqueKeyã®äœ¿çšãæ€èšããŠãã ããã
åãã°ããŒãã«ããŒãæã€ããªãŒã«2ã€ã®ãŠã£ãžã§ãããåæã«å«ããããšã¯ã§ããŸããããããå®è¡ããããšãããšãã©ã³ã¿ã€ã ãšã©ãŒãçºçããŸããããœãŒã¹ã
å®éã20åã®GlobalKeyãç»é¢ã«è¡šç€ºãããŸãŸã«ããŠãããšãã²ã©ãããšã¯äœãèµ·ãããŸããããå¿ èŠãªå Žåã«ã®ã¿äœ¿çšããããšããå§ãããŸãããã®ãããå¥ã®æ¹æ³ãæ¢ããŸãã
GlobalKeyã䜿çšããªããœãªã¥ãŒã·ã§ã³
ã¬ã³ããŒã¬ã€ã€ãŒã䜿çšããŸããæåã®ã¹ãããã¯ãRenderBoxããäœããåŒãåºãããšãã§ãããã©ããããããŠãããå¿ èŠãªããŒã¿ã§ãããã©ããã確èªããããšã§ãã
仮説ãã¹ãã³ãŒãïŒ
FocusNode get focus => widget.focus;
@override
void initState() {
super.initState();
Future.delayed(const Duration(seconds: 1)).then((_) {
// (1)
RenderBox rb = (focus.context.findRenderObject() as RenderBox);
//(3)
RenderBox parent = _getParent(rb);
//(4)
print('parent = ${parent.size.height}');
});
}
RenderBox _getParent(RenderBox rb) {
return rb.parent is RenderWrapper ? rb.parent : _getParent(rb.parent);
}
Widget build(BuildContext context) {
return Wrapper(
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
child: Center(
child: TextField(
focusNode: focus,
),
),
),
);
}
//(2)
class Wrapper extends SingleChildRenderObjectWidget {
const Wrapper({
Key key,
Widget child,
}) : super(key: key, child: child);
@override
RenderWrapper createRenderObject(BuildContext context) {
return RenderWrapper();
}
}
class RenderWrapper extends RenderProxyBox {
RenderWrapper({
RenderBox child,
}) : super(child);
}
ïŒ1ïŒãã£ãŒã«ããŸã§ã¹ã¯ããŒã«ããå¿ èŠãããããããã®ã³ã³ããã¹ããååŸãïŒããšãã°ãFocusNodeãä»ããŠïŒãRenderBoxãèŠã€ããŠãµã€ãºãååŸããå¿ èŠããããŸãããã ããããã¯ããã¹ãããã¯ã¹ã®ãµã€ãºã§ããã芪ãŠã£ãžã§ããïŒããã£ã³ã°ãªã©ïŒãå¿ èŠãªå Žåã¯ã芪ãã£ãŒã«ããä»ããŠèŠªRenderBoxãååŸããå¿ èŠããããŸãã
ïŒ2ïŒåœç€Ÿã¯ãåœç€ŸããRenderWrapperã¯ã©ã¹ãç¶æ¿SingleChildRenderObjectWidgetããŠäœæRenderProxyBoxããã®ããã«ã RenderProxyBoxã¯åã®ãã¹ãŠã®ããããã£ãã·ãã¥ã¬ãŒããããŠã£ãžã§ããããªãŒãã¬ã³ããªã³ã°ããããšãã«è¡šç€ºããŸãã
Flutterèªäœã¯ãSingleChildRenderObjectWidgetã®ç¶æ¿è ïŒ
AlignãAnimatedSizeãSizedBoxãOpacityãPaddingïŒããã䜿çšããŸãã
ïŒ3ïŒRenderWrapperã«ééãããŸã§ãããªãŒãä»ããŠèŠªãååž°çã«ãã©ããŒã¹ããŸãã
ïŒ4ïŒparent.size.heightãååŸããŸã-ããã«ããæ£ããé«ããåŸãããŸããããã¯æ£ããæ¹æ³ã§ãã
ãã¡ããããã®ãŸãŸã«ããããšã¯ã§ããŸããã
ããããååž°çã¢ãããŒãã«ã¯æ¬ ç¹ããããŸãã
- ååž°çãªããªãŒãã©ããŒãµã«ã¯ãæºåãã§ããŠããªãç¥å ã«ééããªãããšãä¿èšŒãããã®ã§ã¯ãããŸããã圌ã¯ã¿ã€ãã«åããªããããããŸããããããŠããã¯ããã ãã§ããã©ããããããããã¹ãäžã«RenderViewã«ééãããã¹ãŠãèœã¡ãŸããããã¡ãããäžé©åãªç¥å ãç¡èŠããããšã¯ã§ããŸãããããä¿¡é Œæ§ã®é«ãã¢ãããŒããå¿ èŠã§ãã
- ããã¯ç®¡çäžå¯èœã§ããããŸã æè»ãªãœãªã¥ãŒã·ã§ã³ã§ã¯ãããŸããã
RenderObjectã®äœ¿çš
ãã®ã¢ãããŒãã¯ãrender_metricsããã±ãŒãžã®çµæã§ãããã¢ããªã±ãŒã·ã§ã³ã®1ã€ã§é·ãé䜿çšãããŠããŸããã
æäœããžãã¯ïŒ
1ã察象ã®ãŠã£ãžã§ããïŒWidgetã¯ã©ã¹ã®åå«ïŒãRenderMetricsObjectã§ã©ããããŸãããã¹ããšã¿ãŒã²ãããŠã£ãžã§ããã¯é¢ä¿ãããŸããã
RenderMetricsObject(
child: ...,
)
2.æåã®ãã¬ãŒã ã®åŸããã®ã¡ããªãã¯ãå©çšå¯èœã«ãªããŸããç»é¢ã«å¯ŸãããŠã£ãžã§ããã®ãµã€ãºãŸãã¯äœçœ®ïŒçµ¶å¯ŸãŸãã¯ã¹ã¯ããŒã«äžïŒã®å Žåãã¡ããªãã¯ãååºŠèŠæ±ããããšãæ°ããããŒã¿ããããŸãã
3. RenderManagerã䜿çšããå¿ èŠã¯ãããŸãããã䜿çšããå Žåã¯ããŠã£ãžã§ããã®IDãæž¡ãå¿ èŠããããŸãã
RenderMetricsObject(
id: _text1Id,
manager: renderManager,
child: ...
4.ã³ãŒã«ããã¯ã䜿çšã§ããŸãã
- onMount-RenderObjectãäœæããŸããæž¡ãããIDïŒãŸãã¯æž¡ãããªãã£ãå Žåã¯nullïŒãšå¯Ÿå¿ããRenderMetricsBoxã€ã³ã¹ã¿ã³ã¹ãåŒæ°ãšããŠåãåããŸãã
- onUnMount-ããªãŒããã®åé€ã
ãã©ã¡ãŒã¿ã§ã¯ã颿°ã¯RenderMetricsObjectã«æž¡ãããIDãåãåããŸãããããã®é¢æ°ã¯ããããŒãžã£ãŒãäžèŠãªå ŽåããRenderObjectãäœæãããŠããªãŒããåé€ãããææãç¥ãå¿ èŠãããå Žåã«åœ¹ç«ã¡ãŸãã
RenderMetricsObject(
id: _textBlockId,
onMount: (id, box) {},
onUnMount: (box) {},
child...
)
5.ã¡ããªãã¯ãååŸããŸããRenderMetricsBoxã¯ã©ã¹ã¯ãlocalToGlobalãä»ããŠãã®ãã£ã¡ã³ã·ã§ã³ãååŸããããŒã¿ã²ãã¿ãŒãå®è£ ããŸããlocalToGlobalã¯ããã€ã³ãããã®RenderBoxã®ããŒã«ã«åº§æšç³»ãããç»é¢ãåºæºã«ããã°ããŒãã«åº§æšç³»ã«è«çãã¯ã»ã«ã§å€æããŸãã
A-ãŠã£ãžã§ããã®å¹ ãç»é¢ãåºæºã«ããŠåº§æšã®å³ç«¯ã®ãã€ã³ãã«å€æãããŸãã
B-é«ãã¯ãç»é¢ãåºæºã«ããæãäœã座æšç¹ã«å€æãããŸãã
class RenderMetricsBox extends RenderProxyBox {
RenderData get data {
Size size = this.size;
double width = size.width;
double height = size.height;
Offset globalOffset = localToGlobal(Offset(width, height));
double dy = globalOffset.dy;
double dx = globalOffset.dx;
return RenderData(
yTop: dy - height,
yBottom: dy,
yCenter: dy - height / 2,
xLeft: dx - width,
xRight: dx,
xCenter: dx - width / 2,
width: width,
height: height,
);
}
RenderMetricsBox({
RenderBox child,
}) : super(child);
}
6. RenderDataã¯ãåã ã®xå€ãšyå€ãdoubleãšããŠæäŸãã座æšç¹ãCoordsMetricsãšããŠæäŸããåãªãããŒã¿ã¯ã©ã¹ã§ãã
7. ComparisonDiff -2ã€ã®RenderDataãæžç®ãããšããããã®å·®ãå«ãComparisonDiffã€ã³ã¹ã¿ã³ã¹ãè¿ãããŸãããŸããæåã®ãŠã£ãžã§ããã®äžéšãš2çªç®ã®ãŠã£ãžã§ããã®äžéšã®éã®äœçœ®ã®éãïŒdiffBottomToTopïŒã®ã²ãã¿ãŒïŒdiffTopToBottomïŒãæäŸããŸããããããdiffLeftToRightãšdiffRightToLeftã
8. RenderParametersManagerã¯ãRenderManagerã®åå«ã§ãããŠã£ãžã§ããã¡ããªãã¯ãšãããã®éããååŸããŸãã
ã³ãŒãïŒ
class RenderMetricsScreen extends StatefulWidget {
@override
State<StatefulWidget> createState() => _RenderMetricsScreenState();
}
class _RenderMetricsScreenState extends State<RenderMetricsScreen> {
final List<String> list = List.generate(20, (index) => index.toString());
/// render_metrics
///
final _renderParametersManager = RenderParametersManager();
final ScrollController scrollController = ScrollController();
/// id ""
final doneBlockId = 'doneBlockId';
final List<FocusNode> focusNodes = [];
bool _isShow = false;
OverlayEntry _overlayEntry;
KeyboardListener _keyboardListener;
/// FocusNode,
FocusNode lastFocusedNode;
@override
void initState() {
SchedulerBinding.instance.addPostFrameCallback((_) {
_overlayEntry = OverlayEntry(builder: _buildOverlay);
Overlay.of(context).insert(_overlayEntry);
_keyboardListener = KeyboardListener()
..addListener(onChange: _keyboardHandle);
});
FocusNode node;
for(int i = 0; i < list.length; i++) {
node = FocusNode(debugLabel: i.toString());
focusNodes.add(node);
node.addListener(_onChangeFocus(node));
}
super.initState();
}
@override
void dispose() {
_keyboardListener.dispose();
_overlayEntry.remove();
focusNodes.forEach((node) => node.dispose());
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
controller: scrollController,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: <Widget>[
for (int i = 0; i < list.length; i++)
RenderMetricsObject(
id: focusNodes[i],
manager: _renderParametersManager,
child: TextField(
focusNode: focusNodes[i],
decoration: InputDecoration(labelText: list[i]),
),
),
],
),
),
),
),
);
}
Widget _buildOverlay(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
bottom: MediaQuery.of(context).viewInsets.bottom,
left: 0,
right: 0,
child: RenderMetricsObject(
id: doneBlockId,
manager: _renderParametersManager,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: _isShow ? 1.0 : 0.0,
child: NextBlock(
onPressed: () {},
isShow: _isShow,
),
),
),
),
],
);
}
VoidCallback _onChangeFocus(FocusNode node) => () {
if (!node.hasFocus) return;
lastFocusedNode = node;
_doScrollIfNeeded();
};
/// ,
/// .
void _doScrollIfNeeded() async {
if (lastFocusedNode == null) return;
double scrollOffset;
try {
/// id, data null
scrollOffset = await _calculateScrollOffset();
} catch (e) {
return;
}
_doScroll(scrollOffset);
}
///
void _doScroll(double scrollOffset) {
double offset = scrollController.offset + scrollOffset;
if (offset < 0) offset = 0;
scrollController.position.animateTo(
offset,
duration: const Duration(milliseconds: 200),
curve: Curves.linear,
);
}
/// .
///
/// ""
/// (/).
Future<double> _calculateScrollOffset() async {
await Future.delayed(const Duration(milliseconds: 300));
ComparisonDiff diff = _renderParametersManager.getDiffById(
lastFocusedNode,
doneBlockId,
);
lastFocusedNode = null;
if (diff == null || diff.firstData == null || diff.secondData == null) {
return 0.0;
}
return diff.diffBottomToTop;
}
void _keyboardHandle(bool isVisible) {
_isShow = isVisible;
_overlayEntry?.markNeedsBuild();
}
}
render_metricsã䜿çšããçµæ
çµæ
ãŠã£ãžã§ããã¬ãã«ãããæ·±ãæãäžããã¬ã³ããŒã¬ã€ã€ãŒã§ã®å°ããªæäœã®å©ããåããŠãããè€éãªUIãšããžãã¯ãèšè¿°ã§ãã䟿å©ãªæ©èœãæã«å ¥ããŸãããåçãŠã£ãžã§ããã®ãµã€ãºãšãã®äœçœ®ãç¥ã£ãããéè€ãããŠã£ãžã§ãããæ¯èŒãããããå¿ èŠãããå ŽåããããŸãããããŠããã®ã©ã€ãã©ãªã¯ãããéããããå¹ççãªåé¡è§£æ±ºã®ããã«ããããã¹ãŠã®æ©èœãæäŸããŸããèšäºã§ã¯ãåäœã®ã¡ã«ããºã ã説æããåé¡ã®äŸãšè§£æ±ºçã瀺ããŸãããç§ã¯å³æžé€šãèšäºããããŠããªãã®ãã£ãŒãããã¯ã®å©çãé¡ã£ãŠããŸãã