# ใช้ Overlay ทำ Suggestion
#Dev#Flutterการใช้ Overlay จะช่วยทำให้เราสามารถซ้อน Widget ได้อย่างอิสระ และผมจะเอาเจ้า Overlay นี่แหละมาทำ Suggestion
โดย Overlay จะอยู่ในเลเยอร์ชั้นที่สูงกว่า Widget อื่นๆ ซึ่งหลายคนอาจจะสงสัยว่าใช้ Stack Widget ก็ได้รึเปล่า
คำตอบคือได้ครับ แต่จะมีปัญหาบางอย่างถ้าเราเอามาใช้ทำ Suggestion
# ลองสร้างโปรเจคกันก่อน
- รันคำสั่งใน Command line เพื่อสร้างโปรเจค Flutter
flutter create sample_overlay # สร้างโปรเจคชื่อ sample_overlay
- เปิดโปรเจคขึ้นมาแล้วแก้ไฟล์
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')),
],
),
);
}
จากโค้ดด้านบนเมื่อได้ลองแล้วก็จะพบว่า 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 ให้ตรง (ซึ่งผมจะไม่ทำ 🤣)
# ใช้ 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 แทนก็ได้ครับ
คำเตือน
ภายใต้ 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) ของผมได้ครับ