Pengantar
Ada layar dengan banyak bidang teks. Bisa ada 5 atau 30. Di antara mereka bisa ada berbagai widget.
Tugas
- Tempatkan blok dengan tombol "Berikutnya" di atas keyboard untuk beralih ke bidang berikutnya.
- Saat mengubah fokus, gulir bidang ke blok dengan tombol "Berikutnya".
Masalah
Blok dengan tombol tumpang tindih dengan bidang teks. Penting untuk menerapkan pengguliran otomatis dengan ukuran ruang yang tumpang tindih dari bidang teks.
Mempersiapkan solusi
1. Mari kita ambil layar 20 bidang.
Kode:
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),
)
],
),
),
),
),
);
}
Dengan fokus pada bidang teks, kita melihat gambar berikut:
Bidang terlihat sempurna dan semuanya teratur.
2. Tambahkan blok dengan tombol. Overlay
digunakan untuk menampilkan blok . Ini memungkinkan Anda untuk menampilkan pelat secara terpisah dari widget di layar dan tidak menggunakan pembungkus Stack. Pada saat yang sama, kami tidak memiliki interaksi langsung antara bidang dan blok "Berikutnya". Artikel bagus tentang Overlay. Singkatnya: Overlay memungkinkan Anda untuk menghamparkan widget di atas widget lain, melalui tumpukan overlay. OverlayEntry memungkinkan Anda mengontrol Overlay yang sesuai. Kode:
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. Seperti yang diharapkan, blok tumpang tindih dengan margin.
Ide Solusi
1. Ambil posisi gulir layar saat ini dari ScrollController dan gulir ke lapangan.
Ukuran bidang tidak diketahui, terutama jika multi-garis, maka menggulir ke sana akan memberikan hasil yang tidak akurat. Solusinya tidak akan sempurna atau fleksibel.
2. Tambahkan ukuran widget di luar daftar dan pertimbangkan pengguliran akun.
Jika Anda mengatur widget ke ketinggian tetap, maka, dengan mengetahui posisi scroll dan ukuran widget, Anda akan tahu apa yang sekarang ada di zona visibilitas dan seberapa banyak Anda perlu menggulir untuk menampilkan widget tertentu.
Kekurangan :
- Anda harus mempertimbangkan semua widget di luar daftar dan mengaturnya dengan ukuran tetap yang akan digunakan dalam penghitungan, yang tidak selalu sesuai dengan desain dan perilaku antarmuka yang diperlukan.
- Pengeditan UI akan menyebabkan revisi dalam penghitungan.
3. Ambil posisi widget relatif terhadap layar bidang dan blok "Berikutnya" dan baca perbedaannya.
Minus - tidak ada kemungkinan seperti itu di luar kotak.
4. Gunakan lapisan render.
Berdasarkan artikel tersebut , Flutter mengetahui cara mengatur turunannya di pohon, yang berarti informasi ini dapat ditarik. RenderObject bertanggung jawab untuk rendering , kami akan pergi ke sana. RenderBox memiliki kotak ukuran dengan lebar dan tinggi widget. Mereka dihitung saat merender widget: baik itu daftar, penampung, bidang teks (bahkan yang multi-baris), dll.
Anda bisa mendapatkan RenderBox melalui
context context.findRenderObject() as RenderBox
Anda dapat menggunakan GlobalKey untuk mendapatkan konteks bidang.
Minus :
GlobalKey bukan hal termudah. Dan lebih baik menggunakannya sesedikit mungkin.
“Widget dengan kunci global menggambar ulang subpohonnya saat berpindah dari satu lokasi di pohon ke pohon lainnya. Untuk menggambar ulang subpohonnya, widget harus tiba di lokasi barunya di pohon dalam bingkai animasi yang sama dengan tempat ia dihapus dari lokasi lama.
Kunci global relatif mahal dalam hal kinerja. Jika Anda tidak memerlukan fitur apa pun yang tercantum di atas, pertimbangkan untuk menggunakan Key, ValueKey, ObjectKey, atau UniqueKey.
Anda tidak dapat menyertakan dua widget di pohon pada saat yang sama dengan kunci global yang sama. Jika Anda mencoba melakukan ini, akan ada kesalahan runtime. " Sumber .
Faktanya, jika Anda menyimpan 20 GlobalKey di layar, tidak ada hal buruk yang akan terjadi, tetapi karena disarankan untuk menggunakannya hanya jika diperlukan, kami akan mencoba mencari cara lain.
Solusi tanpa GlobalKey
Kami akan menggunakan lapisan render. Langkah pertama adalah memeriksa apakah kita dapat menarik sesuatu dari RenderBox dan apakah ini data yang kita butuhkan.
Kode Pengujian Hipotesis:
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) Karena Anda perlu menggulir ke lapangan, Anda perlu mendapatkan konteksnya (misalnya, melalui FocusNode), temukan RenderBox dan ambil ukuran. Tapi ini adalah ukuran kotak teks, dan jika kita juga membutuhkan widget induk (misalnya, Padding), kita perlu mengambil RenderBox induk melalui bidang induk.
(2) Kami mewarisi kelas RenderWrapper kami dari SingleChildRenderObjectWidget dan membuat RenderProxyBox untuknya. RenderProxyBox mensimulasikan semua properti anak, menampilkannya ketika pohon widget dirender.
Flutter sendiri sering kali menggunakan pewaris SingleChildRenderObjectWidget:
Align, AnimatedSize, SizedBox, Opacity, Padding.
(3) Secara rekursif melintasi induk melalui pohon sampai kita menemukan RenderWrapper.
(4) Ambil parent.size.height - ini akan memberikan tinggi yang benar. Ini jalan yang benar.
Tentu saja, Anda tidak bisa pergi dengan cara ini.
Tetapi pendekatan rekursif juga memiliki kekurangan :
- Penjelajahan pohon berulang tidak menjamin bahwa kita tidak akan bertemu dengan nenek moyang yang belum kita siapkan. Dia mungkin tidak cocok dengan tipenya dan hanya itu. Entah bagaimana dalam pengujian saya menemukan RenderView dan semuanya jatuh. Anda dapat, tentu saja, mengabaikan leluhur yang tidak sesuai, tetapi Anda menginginkan pendekatan yang lebih dapat diandalkan.
- Ini adalah solusi yang tidak dapat diatur dan masih belum fleksibel.
Menggunakan RenderObject
Pendekatan ini adalah hasil dari paket render_metrics dan telah lama digunakan di salah satu aplikasi kami.
Logika operasi:
1. Gabungkan widget yang diinginkan (turunan dari kelas Widget) di RenderMetricsObject . Widget bersarang dan target tidak masalah.
RenderMetricsObject(
child: ...,
)
2. Setelah bingkai pertama, metriknya akan tersedia untuk kita. Jika ukuran atau posisi widget relatif terhadap layar (absolute atau di scrolling), maka saat metrik diminta kembali, akan ada data baru.
3. Tidak perlu menggunakan RenderManager , tetapi saat menggunakannya Anda harus meneruskan id untuk widget.
RenderMetricsObject(
id: _text1Id,
manager: renderManager,
child: ...
4. Anda dapat menggunakan panggilan balik:
- onMount - Buat RenderObject. Menerima id yang diteruskan (atau null, jika tidak diteruskan) dan instance RenderMetricsBox yang sesuai sebagai argumen.
- onUnMount - penghapusan dari pohon.
Dalam parameter, fungsi menerima id yang diteruskan ke RenderMetricsObject. Fungsi-fungsi ini berguna saat Anda tidak memerlukan pengelola dan / atau Anda perlu tahu kapan RenderObject dibuat dan dihapus dari pohon.
RenderMetricsObject(
id: _textBlockId,
onMount: (id, box) {},
onUnMount: (box) {},
child...
)
5. Mendapatkan metrik. Kelas RenderMetricsBox mengimplementasikan pengambil data, yang mengambil dimensinya melalui localToGlobal. localToGlobal mengonversi titik dari sistem koordinat lokal untuk RenderBox ini ke sistem koordinat global yang relatif ke layar dalam piksel logis.
A - lebar widget, dikonversi ke titik koordinat paling kanan yang relatif ke layar.
B - Ketinggian diubah ke titik koordinat terendah yang relatif terhadap layar.
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 hanyalah kelas data yang menyediakan nilai x dan y terpisah sebagai titik ganda dan koordinat sebagai CoordsMetrics .
7. ComparisonDiff - Pengurangan dua RenderData akan mengembalikan instance ComparisonDiff dengan perbedaan di antara keduanya. Ini juga menyediakan pengambil (diffTopToBottom) untuk perbedaan posisi antara bagian bawah widget pertama dan atas widget kedua, dan sebaliknya (diffBottomToTop). diffLeftToRight dan diffRightToLeft masing-masing.
8. RenderParametersManager adalah turunan dari RenderManager. Untuk mendapatkan metrik widget dan perbedaannya.
Kode:
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();
}
}
Hasil menggunakan render_metrics
Hasil
Menggali lebih dalam dari lapisan widget, dengan bantuan manipulasi kecil dengan lapisan render, kami mendapatkan fungsionalitas berguna yang memungkinkan Anda menulis UI dan logika yang lebih kompleks. Terkadang Anda perlu mengetahui ukuran widget dinamis, posisinya, atau membandingkan widget yang tumpang tindih. Dan pustaka ini menyediakan semua fitur ini untuk pemecahan masalah yang lebih cepat dan lebih efisien. Dalam artikel tersebut, saya mencoba menjelaskan mekanisme operasi, memberikan contoh masalah dan solusi. Saya berharap untuk manfaat perpustakaan, artikel dan tanggapan Anda.