Flattern. RenderObject - Messen und Erobern

Hallo allerseits, mein Name ist Dmitry Andriyanov. Ich bin ein Flutter-Entwickler bei Surf. Die Hauptbibliothek von Flutter reicht aus, um eine effiziente und produktive Benutzeroberfläche zu erstellen. Aber es gibt Zeiten, in denen Sie bestimmte Fälle implementieren müssen und dann tiefer graben müssen.







Einleitend



Es gibt einen Bildschirm mit vielen Textfeldern. Es können 5 oder 30 von ihnen sein. Zwischen ihnen können verschiedene Widgets sein.







Aufgabe



  • Platzieren Sie einen Block mit der Schaltfläche "Weiter" über der Tastatur, um zum nächsten Feld zu wechseln.
  • Scrollen Sie beim Ändern des Fokus mit der Schaltfläche "Weiter" durch das Feld zum Block.


Problem



Der Block mit der Schaltfläche überlappt das Textfeld. Es ist erforderlich, das automatische Scrollen anhand der Größe des überlappenden Bereichs des Textfelds zu implementieren.







Vorbereitung für eine Lösung



1. Nehmen wir einen Bildschirm mit 20 Feldern.



Der Code:



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),
               )
           ],
         ),
       ),
     ),
   ),
 );
}


Mit Fokus im Textfeld sehen wir folgendes Bild: Das







Feld ist perfekt sichtbar und alles ist in Ordnung.



2. Fügen Sie einen Block mit einer Schaltfläche hinzu. Überlagerung wird







verwendet, um den Block anzuzeigen . Auf diese Weise können Sie die Platte unabhängig von den Widgets auf dem Bildschirm anzeigen und nicht die Stapelverpackungen verwenden. Gleichzeitig haben wir keine direkte Interaktion zwischen den Feldern und dem Block "Weiter". Schöner Artikel über Overlay. Kurz gesagt: Mit Overlay können Sie Widgets über den Overlay- Stapel über andere Widgets legen . Mit OverlayEntry können Sie das entsprechende Overlay steuern. Der Code:















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. Wie erwartet überlappt der Block den Rand.



Lösungsideen



1. Nehmen Sie die aktuelle Bildlaufposition des Bildschirms vom ScrollController und scrollen Sie zum Feld.

Die Größe des Feldes ist unbekannt, insbesondere wenn es mehrzeilig ist. Wenn Sie dann dorthin scrollen, erhalten Sie ein ungenaues Ergebnis. Die Lösung wird nicht perfekt oder flexibel sein.



2. Fügen Sie die Größe der Widgets außerhalb der Liste hinzu und berücksichtigen Sie das Scrollen.

Wenn Sie die Widgets auf eine feste Höhe einstellen, wissen Sie anhand der Position des Bildlaufs und der Größe der Widgets, was sich jetzt im Sichtbarkeitsbereich befindet und wie viel Sie scrollen müssen, um ein bestimmtes Widget anzuzeigen.



Nachteile :



  • Sie müssen alle Widgets außerhalb der Liste berücksichtigen und feste Größen festlegen, die für Berechnungen verwendet werden, die nicht immer dem erforderlichen Design und Schnittstellenverhalten entsprechen.

  • Änderungen an der Benutzeroberfläche führen zu Änderungen in den Berechnungen.


3. Nehmen Sie die Position der Widgets relativ zum Bildschirm des Feldes und zum Block "Weiter" und lesen Sie den Unterschied.



Minus - es gibt keine solche Möglichkeit sofort.



4. Verwenden Sie eine Renderebene.



Basierend auf dem Artikel weiß Flutter, wie man seine Nachkommen im Baum anordnet, was bedeutet, dass diese Informationen abgerufen werden können. RenderObject ist für das Rendern verantwortlich , wir werden darauf zugreifen . Die RenderBox verfügt über ein Größenfeld mit der Breite und Höhe des Widgets. Sie werden beim Rendern für Widgets berechnet: Listen, Container, Textfelder (auch mehrzeilige) usw.



Sie können die RenderBox durch bekommen

context context.findRenderObject() as RenderBox


Mit dem GlobalKey können Sie den Kontext eines Felds abrufen.



Minus :



GlobalKey ist nicht die einfachste Sache. Und es ist besser, es so wenig wie möglich zu verwenden.



„Widgets mit globalen Schlüsseln zeichnen ihre Teilbäume neu, wenn sie von einem Ort im Baum zu einem anderen wechseln. Um seinen Teilbaum neu zu zeichnen, muss das Widget an seiner neuen Position im Baum in demselben Animationsrahmen ankommen, in dem es von der alten Position entfernt wurde.



Globale Schlüssel sind in Bezug auf die Leistung relativ teuer. Wenn Sie keine der oben aufgeführten Funktionen benötigen, sollten Sie Key, ValueKey, ObjectKey oder UniqueKey verwenden.



Sie können nicht zwei Widgets gleichzeitig mit demselben globalen Schlüssel in einen Baum aufnehmen. Wenn Sie dies versuchen, tritt ein Laufzeitfehler auf. " Quelle .



Wenn Sie 20 GlobalKey auf dem Bildschirm behalten, passiert nichts Schlimmes. Da jedoch empfohlen wird, es nur bei Bedarf zu verwenden, werden wir versuchen, nach einem anderen Weg zu suchen.



Lösung ohne GlobalKey



Wir werden eine Renderebene verwenden. Der erste Schritt besteht darin, zu überprüfen, ob wir etwas aus der RenderBox herausziehen können und ob dies die Daten sind, die wir benötigen.



Code zum Testen von Hypothesen:



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) Da Sie zum Feld scrollen müssen, müssen Sie dessen Kontext abrufen (z. B. über FocusNode), die RenderBox suchen und die Größe übernehmen. Dies ist jedoch die Größe des Textfelds. Wenn wir auch übergeordnete Widgets benötigen (z. B. Auffüllen), müssen wir die übergeordnete RenderBox durch das übergeordnete Feld führen.



(2) Wir erben unsere RenderWrapper-Klasse von SingleChildRenderObjectWidget und erstellen eine RenderProxyBox dafür. RenderProxyBox simuliert alle Eigenschaften des untergeordneten Elements und zeigt sie an, wenn der Widget-Baum gerendert wird.

Flutter selbst verwendet häufig Erben von SingleChildRenderObjectWidget:

Align, AnimatedSize, SizedBox, Opacity, Padding.



(3) Durchlaufen Sie die Eltern rekursiv durch den Baum, bis wir auf einen RenderWrapper stoßen.



(4) Nehmen Sie parent.size.height - dies ergibt die richtige Höhe. Das ist der richtige Weg.



Natürlich können Sie diesen Weg nicht verlassen.



Der rekursive Ansatz hat aber auch seine Nachteile :



  • Die rekursive Baumdurchquerung garantiert nicht, dass wir nicht auf einen Vorfahren stoßen, für den wir nicht bereit sind. Er passt möglicherweise nicht zum Typ und das war's. Irgendwie bin ich bei Tests auf RenderView gestoßen und alles ist gefallen. Sie können den ungeeigneten Vorfahren natürlich ignorieren, aber Sie möchten einen zuverlässigeren Ansatz.
  • Dies ist eine unüberschaubare und immer noch nicht flexible Lösung.


Verwenden von RenderObject



Dieser Ansatz ist das Ergebnis des Pakets render_metrics und wird seit langem in einer unserer Anwendungen verwendet.



Operationslogik:



1. Brechen Sie das interessierende Widget (ein Nachkomme der Widget-Klasse) in RenderMetricsObject ein . Verschachtelung und Ziel-Widget spielen keine Rolle.



RenderMetricsObject(
 child: ...,
)


2. Nach dem ersten Frame stehen uns die Metriken zur Verfügung. Wenn die Größe oder Position des Widgets relativ zum Bildschirm (absolut oder scrollend) ist, werden beim erneuten Anfordern der Metriken neue Daten angezeigt.



3. Es ist nicht erforderlich, den RenderManager zu verwenden , aber wenn Sie ihn verwenden, müssen Sie die ID für das Widget übergeben.



RenderMetricsObject(
 id: _text1Id,
 manager: renderManager,
 child: ...


4. Sie können Rückrufe verwenden:



  • onMount - RenderObject erstellen. Erhält die übergebene ID (oder null, falls nicht übergeben) und die entsprechende RenderMetricsBox-Instanz als Argumente.
  • onUnMount - Entfernung aus dem Baum.


In den Parametern empfängt die Funktion die an RenderMetricsObject übergebene ID. Diese Funktionen sind nützlich, wenn Sie keinen Manager benötigen und / oder wissen müssen, wann ein RenderObject erstellt und aus dem Baum entfernt wurde.



RenderMetricsObject(
 id: _textBlockId,
 onMount: (id, box) {},
 onUnMount: (box) {},
 child...
)


5. Abrufen von Metriken. Die RenderMetricsBox- Klasse implementiert einen Daten-Getter, in dem er seine Dimensionen über localToGlobal übernimmt. localToGlobal konvertiert den Punkt aus dem lokalen Koordinatensystem für diese RenderBox in das globale Koordinatensystem relativ zum Bildschirm in logischen Pixeln.







A - Die Breite des Widgets, konvertiert in den Koordinatenpunkt ganz rechts relativ zum Bildschirm.



B - Die Höhe wird in den niedrigsten Koordinatenpunkt relativ zum Bildschirm konvertiert.



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 ist einfach eine Datenklasse, die einzelne x- und y-Werte als Doppel- und Koordinatenpunkte wie CoordsMetrics bereitstellt .



7. ComparisonDiff - Durch Subtrahieren von zwei RenderData wird eine ComparisonDiff-Instanz mit der Differenz zwischen ihnen zurückgegeben. Es bietet auch einen Getter (diffTopToBottom) für den Positionsunterschied zwischen der Unterseite des ersten Widgets und der Oberseite des zweiten und umgekehrt (diffBottomToTop). diffLeftToRight bzw. diffRightToLeft.



8. RenderParametersManager ist ein Nachkomme von RenderManager. Um Widget-Metriken und den Unterschied zwischen ihnen zu erhalten.



Der Code:



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();
 }
}


Ergebnis mit render_metrics







Ergebnis



Mit Hilfe kleiner Manipulationen an der Renderebene haben wir nützliche Funktionen erhalten, mit denen Sie komplexere Benutzeroberflächen und Logik schreiben können. Manchmal müssen Sie die Größe dynamischer Widgets und ihre Position kennen oder überlappende Widgets vergleichen. Diese Bibliothek bietet all diese Funktionen für eine schnellere und effizientere Problemlösung. In dem Artikel habe ich versucht, den Funktionsmechanismus zu erklären, ein Beispiel für ein Problem und eine Lösung gegeben. Ich hoffe zum Nutzen der Bibliothek, der Artikel und Ihres Feedbacks.



All Articles