フラッタヌ。RenderObject-枬定しお埁服する

みなさん、こんにちは。私の名前はドミトリヌ・アンドリダノフです。私はSurfのFlutter開発者です。メむンのFlutterラむブラリは、効率的で生産的なUIを構築するのに十分です。ただし、特定のケヌスを実装する必芁があり、それからさらに深く掘り䞋げる必芁がある堎合がありたす。







入門



倚くのテキストフィヌルドがある画面がありたす。それらの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ずロゞックを蚘述できる䟿利な機胜を手に入れたした。動的りィゞェットのサむズずその䜍眮を知ったり、重耇するりィゞェットを比范したりする必芁がある堎合がありたす。そしお、このラむブラリは、より速く、より効率的な問題解決のためにこれらすべおの機胜を提䟛したす。蚘事では、動䜜のメカニズムを説明し、問題の䟋ず解決策を瀺したした。私は図曞通、蚘事、そしおあなたのフィヌドバックの利益を願っおいたす。



All Articles