# ใช้ Overlay ทำ Suggestion

#Dev#Flutter

การใช้ Overlay จะช่วยทำให้เราสามารถซ้อน Widget ได้อย่างอิสระ และผมจะเอาเจ้า Overlay นี่แหละมาทำ Suggestion
โดย Overlay จะอยู่ในเลเยอร์ชั้นที่สูงกว่า Widget อื่นๆ ซึ่งหลายคนอาจจะสงสัยว่าใช้ Stack Widget ก็ได้รึเปล่า
คำตอบคือได้ครับ แต่จะมีปัญหาบางอย่างถ้าเราเอามาใช้ทำ Suggestion

Header

# ลองสร้างโปรเจคกันก่อน

  1. รันคำสั่งใน Command line เพื่อสร้างโปรเจค Flutter
flutter create sample_overlay # สร้างโปรเจคชื่อ sample_overlay
  1. เปิดโปรเจคขึ้นมาแล้วแก้ไฟล์ lib/main.dart ตามนี้
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: GestureDetector(
        // ใส่ไว้เพื่อให้ Focus ที่ TextField หายเมื่อคลิกข้างนอก
        onTap: () {
          FocusScope.of(context).requestFocus(new FocusNode());
        },
        child: Home(),
      ),
    );
  }
}

class Home extends StatefulWidget {
  
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  FocusNode _focusNode = FocusNode(); // FocusNode เอาไว้ควบคุมการโฟกัส Widget
  bool showSuggestion = false;

  
  void initState() {
    _focusNode.addListener(() { // Event เมื่อมีการเปลี่ยนแปลงโฟกัสของ Widget ที่มี FocusNode นี้
      setState(() {
        this.showSuggestion = _focusNode.hasFocus;
      });
    });
    super.initState();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'Awesome Overlay',
        ),
      ),
      body: Container(
        color: Colors.green,
        padding: EdgeInsets.all(10.0),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            TextField(
              decoration: InputDecoration(
                filled: true,
                fillColor: Colors.white,
                focusColor: Colors.white,
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(5.0),
                ),
              ),
              focusNode: _focusNode,
            ),
            // เมื่อ TextField ถูกคลิกจะแสดง Suggestion ขึ้นมา
            if (this.showSuggestion) suggestionList(),
          ],
        ),
      ),
    );
  }
}

Widget suggestionList() {
  return Container(
    height: 100.0,
    color: Colors.white,
    child: ListView(
      children: <Widget>[
        ListTile(title: Text('This')),
        ListTile(title: Text('is')),
        ListTile(title: Text('very')),
        ListTile(title: Text('awesome')),
      ],
    ),
  );
}
Title
หน้าจอเริ่มต้น

จากโค้ดด้านบนเมื่อได้ลองแล้วก็จะพบว่า Container สีเขียวที่ครอบ Widget TextField และ Suggestion นั้นจะมีการเปลี่ยนขนาดเมื่อผมคลิกที่ตัว TextField และมี Suggestion แสดงออกมา ซึ่งนั่นไม่ใช่สิ่งที่ผมต้องการ ผมไม่ได้ต้องการให้มันเปลี่ยนขนาดไปๆมาๆ (ดูรูปตัวอย่างด้านล่าง เหมือนๆกันครับ)

ทีนี้ผมจะเปลี่ยนไปใช้ Stack แทน Column ตามนี้














 












 
 
 
 
 








...


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text(
        'Awesome Overlay',
      ),
    ),
    body: Container(
      color: Colors.green,
      padding: EdgeInsets.all(10.0),
      child: Stack(
        children: <Widget>[
          TextField(
            decoration: InputDecoration(
              filled: true,
              fillColor: Colors.white,
              focusColor: Colors.white,
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(5.0),
              ),
            ),
            focusNode: _focusNode,
          ),
          if (this.showSuggestion)
            Container(
              margin: EdgeInsets.only(top: 62.0), // ต้องใส่ Marginเพิ่มไม่งั้นมันจะทับกัน
              child: suggestionList(),
            ),
        ],
      ),
    ),
  );
}

...

ซึ่งปัญหาก็จะเหมือนๆเดิมก็คือขนาดของ Container ก็ยังขยายตามสิ่งที่อยู่ข้างในอยู่ดี แถมเกิดปัญหาใหม่อีกคือเราต้องคอย Margin ให้ตรง (ซึ่งผมจะไม่ทำ 🤣)

Title
เหมือนอันเริ่มต้นแหละครับ แต่ต้องใส่ Margin

# ใช้ Overlay แก้ปัญหา

ดังนั้นทางรอดของผมก็คือ เปลี่ยนไปใช้ Overlay แทน
โดยผมจะทำการย้ายส่วนของ TextField และ suggestionList ของผมไปอยู่ใน StatefulWidget อันใหม่เป็นครอบครัวเดียวกัน จะไปไหนก็ไปด้วยกันเลย






















 







 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 



...

class Home extends StatefulWidget {
  
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'Awesome Overlay',
        ),
      ),
      body: Container(
        color: Colors.green,
        padding: EdgeInsets.all(10.0),
        child: Stack(
          children: <Widget>[
            TextFieldWithSuggestion(),
          ],
        ),
      ),
    );
  }
}

class TextFieldWithSuggestion extends StatefulWidget {
  
  _TextFieldWithSuggestionState createState() =>
      _TextFieldWithSuggestionState();
}

class _TextFieldWithSuggestionState extends State<TextFieldWithSuggestion> {
  FocusNode _focusNode = FocusNode(); // FocusNode เอาไว้ควบคุมการโฟกัส Widget
  OverlayEntry _overlay;

  
  void initState() {
    _focusNode.addListener(() {
      if (_focusNode.hasFocus) {
        _overlay = _createSuggestionOverlay();
        Overlay.of(context).insert(_overlay);
      } else {
        _overlay.remove();
      }
    });
    super.initState();
  }

  OverlayEntry _createSuggestionOverlay() {
    RenderBox renderBox =
        context.findRenderObject(); // พิกัด และขนาดการ Render ของ Widget นี้
    var size = renderBox.size; // ขนาดของ Widget
    var offset = renderBox
        .localToGlobal(Offset.zero); // พิกัด X,Y ที่ Widget นี้แสดงอยู่

    return OverlayEntry(
      builder: (context) => Positioned(
        left: offset.dx,
        top: offset.dy + size.height + 5.0,
        width: size.width,
        child: Material(
          elevation: 2.0,
          child: _suggestionList(),
        ),
      ),
    );
  }

  Widget _suggestionList() {
    return Container(
      height: 100.0,
      color: Colors.white,
      child: ListView(
        children: <Widget>[
          ListTile(title: Text('This')),
          ListTile(title: Text('is')),
          ListTile(title: Text('very')),
          ListTile(title: Text('awesome')),
        ],
      ),
    );
  }

  
  Widget build(BuildContext context) {
    return TextField(
      decoration: InputDecoration(
        filled: true,
        fillColor: Colors.white,
        focusColor: Colors.white,
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(5.0),
        ),
      ),
      focusNode: _focusNode,
    );
  }
}

...

ผลที่ได้ก็คือพื้นหลังไม่ขยายออกแล้ว เย่ 😁
จากตัวอย่างผมใช้ ListView แบบตรงๆ เราสามารถเปลี่ยนไปใช้ ListView.builder แทนก็ได้ครับ

Title
Suggestion Overylay ลอยอยู่เหนือทุกสิ่ง

คำเตือน

ภายใต้ OverlayEntry จะต้องใส่ Material Widget ครอบไว้ด้วย เพราะหลายๆ Widget ต้องใช้
และ Overlay ไม่ได้ใส่มาให้ด้วย (ในที่นี้ผมใส่ elevation ไว้ด้วยให้ Suggestion เราดูเหมือนลอยๆอยู่)








 
 
 
 





...

return OverlayEntry(
  builder: (context) => Positioned(
    left: offset.dx,
    top: offset.dy + size.height + 5.0,
    width: size.width,
    child: Material(
      elevation: 2.0,
      child: suggestionList(),
    ),
  ),
);

...

# ทำให้ Overlay ตามติด TextField

ทั้งนี้การใช้ Overlay ก็ยังอาจจะมีปัญหาได้อีกถ้า TextField ของเราขยับ/เปลี่ยนตำแหน่ง เจ้าตัว Overlay ของเราในตอนนี้มันจะไม่ขยับตามไปด้วยครับ ต้องใช้ CompositedTransformFollower และ CompositedTransformTarget ในการทำให้ Widget 2 ตัวติดตามกันโดยการเปลี่ยน





 












 
 
 













 
 
















...

class _TextFieldWithSuggestionState extends State<TextFieldWithSuggestion> {
  FocusNode _focusNode = FocusNode(); // FocusNode เอาไว้ควบคุมการโฟกัส Widget
  LayerLink _layerLink = LayerLink(); // ตัวเชื่อมระว่าง Widget
  OverlayEntry _overlay;

  ...

  OverlayEntry _createSuggestionOverlay() {
    RenderBox renderBox =
        context.findRenderObject(); // พิกัด และขนาดการ Render ของ Widget นี้
    var size = renderBox.size; // ขนาดของ Widget

    return OverlayEntry(
      builder: (context) => Positioned(
        width: size.width,
        child: CompositedTransformFollower(
          link: _layerLink,
          offset: Offset(0.0, size.height + 5.0), 
          child: Material(
            elevation: 2.0,
            child: _suggestionList(),
          ),
        ),
      ),
    );
  }

  ...

  
  Widget build(BuildContext context) {
    return CompositedTransformTarget(
      link: _layerLink,
      child: TextField(
        decoration: InputDecoration(
          filled: true,
          fillColor: Colors.white,
          focusColor: Colors.white,
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(5.0),
          ),
        ),
        focusNode: _focusNode,
      ),
    );
  }

...

ลอง Restart ดูก็จะพบว่าได้ผลเหมือนเดิม แต่สิ่งที่ต่างออกไปคือ Widget ทั้งสองตัวจะเคลื่อนที่ตามๆกันครับ ถ้า Widget ที่เป็น Target มีการเปลี่ยนตำแหน่ง

ดาวน์โหลด Source Code ที่นี่ (opens new window)

# สรุป

เอาจริงๆถ้าเป็นคนที่เคยเขียนเว็บมาก่อนอาจจะมองภาพเป็น css position: absolute ก็ได้อยู่
และ Overlay ยังสามารถเอาไปใช้ประยุกต์ได้อีกหลายอย่าง
ถ้าหากมีข้อสงสัยอะไรสามารถติดต่อผ่าน Facebook Page (opens new window) ของผมได้ครับ


อัปเดตเมื่อ: 1 ปีที่แล้ว