扑。RenderObject-测量和征服

大家好,我叫Dmitry Andriyanov。我是Surf的Flutter开发人员。要构建高效且高效的UI,主要的Flutter库就足够了。但是有时候您需要实施特定的案例,然后您必须进行更深入的研究。







介绍性



屏幕上有许多文本字段。它们可以是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.使用按钮添加一个块。覆盖图







用于显示块这使您可以独立于屏幕上的小部件显示板,而无需使用堆栈包装器。同时,字段与“ Next”块之间没有直接交互。关于叠加的 不错的文章 简而言之:叠加层允许您通过叠加层栈将窗口小部件叠加在其他窗口小部件之上OverlayEntry使您可以控制相应的Overlay。 代码:















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.取得窗口小部件相对于字段屏幕和“ Next”块的位置,并读取差异。



-开箱即用。



4.使用渲染层。



根据该文章,Flutter知道如何在树中安排其后代,这意味着可以提取此信息。RenderObject负责渲染,我们将继续进行。RenderBox有一个大小框,其中包含小部件的宽度和高度。它们是在渲染小部件时计算的:包括列表,容器,文本字段(甚至是多行字段)等。



您可以通过获得RenderBox

context context.findRenderObject() as RenderBox


您可以使用GlobalKey来获取字段的上下文。







GlobalKey不是最简单的事情。最好尽可能少地使用它。



“具有全局键的小部件在子树从树中的一个位置移动到另一位置时会重绘其子树。要重绘其子树,小部件必须在与从旧位置删除的动画帧相同的动画帧中到达树中的新位置。



就性能而言,全局密钥相对昂贵。如果不需要上面列出的任何功能,请考虑使用Key,ValueKey,ObjectKey或UniqueKey。



您不能使用相同的全局密钥同时在树中包含两个小部件。如果您尝试这样做,将会出现运行时错误。” 来源



实际上,如果在屏幕上保留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并取其大小。但这是文本框的大小,并且如果我们还需要父窗口小部件(例如Padding),则需要通过父字段获取父RenderBox。



(2)我们从SingleChildRenderObjectWidget继承RenderWrapper类,并为其创建一个RenderProxyBox。 RenderProxyBox模拟子项的所有属性,在渲染小部件树时显示该子项。

Flutter本身经常使用SingleChildRenderObjectWidget的继承者:

Align,AnimatedSize,SizedBox,Opacity,Padding。



(3)递归遍历树的父母,直到遇到RenderWrapper。



(4)取parent.size.height-这将给出正确的高度。这是正确的方法。



当然,您不能这样离开。



但是递归方法也有其缺点



  • 递归树遍历不能保证我们不会遇到我们还没有准备好的祖先。他可能不适合这种类型,仅此而已。在某种程度上,我遇到了RenderView的测试,一切都失败了。您当然可以忽略不合适的祖先,但是您想要一个更可靠的方法。
  • 这是一种难以管理且仍不灵活的解决方案。


使用RenderObject



这种方法是由render_metrics包产生的,长期以来一直在我们的应用程序中使用过。



操作逻辑:



1 .RenderMetricsObject中包装感兴趣的小部件(Widget类的后代)嵌套和目标小部件无关紧要。



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值作为CoordsMetrics的精度和坐标点



7. ComparisonDiff-减去两个RenderData会返回一个CompareDiff实例,它们之间存在差异。它还为第一个小部件的底部和第二个小部件的顶部之间的位置差提供了一个吸气剂(diffTopToBottom),反之亦然(diffBottomToTop)。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和逻辑。有时您需要了解动态窗口小部件的大小,它们的位置或比较重叠的窗口小部件。这个库提供了所有这些功能,可以更快,更有效地解决问题。在本文中,我试图解释操作机制,并举例说明了问题和解决方案。我希望从图书馆,文章和您的反馈中受益。



All Articles