在Flutter中,打開(kāi)鍵盤(pán)后,我們可能希望底部的內(nèi)容不被覆蓋。Flutter提供了一些方法來(lái)實(shí)現(xiàn)這一點(diǎn)。下面將從多個(gè)方面詳細(xì)闡述如何使用Flutter實(shí)現(xiàn)鍵盤(pán)頂起底部的效果。
一、使用SingleChildScrollView
使用SingleChildScrollView可以讓底部?jī)?nèi)容在鍵盤(pán)彈出時(shí)自動(dòng)滾動(dòng)到可見(jiàn)區(qū)域。我們只需要將底部?jī)?nèi)容包裹在SingleChildScrollView中,并在頁(yè)面初始化時(shí)獲得一個(gè)GlobalKey,然后在鍵盤(pán)彈出后通過(guò)該GlobalKey定位到bottom widget的位置,再通過(guò)動(dòng)畫(huà)滾動(dòng)到該位置。
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State {
final GlobalKey _bottomWidgetKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
// other widgets
Container(
key: _bottomWidgetKey,
child: // bottom widget
),
],
),
),
appBar: AppBar(
title: Text("Keyboard"),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Show the keyboard
FocusScope.of(context).requestFocus(FocusNode());
},
child: Icon(Icons.add),
),
);
}
@override
void initState() {
super.initState();
final BottomWidgetRenderBox =
_bottomWidgetKey.currentContext.findRenderObject() as RenderBox;
SchedulerBinding.instance!.addPostFrameCallback((_) {
Future.delayed(const Duration(milliseconds: 300)).then((_) {
final RenderBox keyboard =
context.findRenderObject() as RenderBox;
final keyboardHeight = keyboard.size.height;
final heightDiff =
keyboardHeight - (MediaQuery.of(context).viewInsets.bottom);
final double offsetY =
heightDiff > 0 ? -(BottomWidgetRenderBox.size.height + heightDiff) : 0;
if (offsetY != 0) {
_controller.animateTo(
_controller.offset + offsetY,
duration: new Duration(milliseconds: 300),
curve: Curves.easeOut);
}
});
});
}
}
二、使用ListView
如果你希望底部?jī)?nèi)容可以滾動(dòng),我們可以使用ListView。ListView將自動(dòng)在鍵盤(pán)彈出時(shí)滾動(dòng)到底部,并且可以讓用戶滾動(dòng)以查看所有內(nèi)容。
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Expanded(
child: ListView(
children: [
// other widgets
// bottom widget
],
),
),
// input widget
],
),
appBar: AppBar(
title: Text("ListView"),
),
);
}
}
三、使用Stack和AnimatedPositioned
使用Stack和AnimatedPositioned可以在鍵盤(pán)彈出時(shí)自動(dòng)調(diào)整底部?jī)?nèi)容的位置,使其不被鍵盤(pán)遮擋。我們可以將輸入框作為Stack的子元素,然后將底部?jī)?nèi)容作為Stack的第二個(gè)子元素。在鍵盤(pán)彈出時(shí),我們可以使用AnimatedPositioned調(diào)整第二個(gè)子元素的位置。
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State {
GlobalKey _globalKey = GlobalKey();
double _bottom = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text("Stack and AnimatedPositioned"),
),
body: Stack(
children: [
Positioned(
top: 0,
bottom: 0,
left: 0,
right: 0,
child: GestureDetector(
onTap: () => FocusScope.of(context).requestFocus(FocusNode()),
child: Container(
color: Colors.white,
child: SingleChildScrollView(
child: Column(
key: _globalKey,
children: [
// other widgets
Container(
height: 800,
),
],
),
),
),
),
),
AnimatedPositioned(
duration: Duration(milliseconds: 300),
bottom: _bottom,
left: 0,
right: 0,
child: Container(
color: Colors.lightBlue,
height: 140,
child: Center(
child: GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Text(
"Input",
style: TextStyle(
fontSize: 30,
color: Colors.white,
),
),
),
),
),
),
],
),
);
}
@override
void initState() {
super.initState();
final RenderBox renderBoxRed = _globalKey.currentContext.findRenderObject() as RenderBox;
WidgetsBinding.instance!.addPostFrameCallback((_) {
double height = renderBoxRed.size.height;
double screenHeight = MediaQuery.of(context).size.height;
double diff = screenHeight - height;
setState(() {
_bottom = diff;
});
});
}
}
四、結(jié)合SingleChildScrollView和Stack
我們也可以結(jié)合ScrollView和Stack來(lái)實(shí)現(xiàn)鍵盤(pán)頂起底部的效果。具體操作是將輸入框與底部?jī)?nèi)容放置在Stack中,并將Stack放置在SingleChildScrollView中。當(dāng)鍵盤(pán)彈出時(shí),可以與第一種方法類似地通過(guò)AnimationController將底部?jī)?nèi)容滑動(dòng)到屏幕中央。
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State
with SingleTickerProviderStateMixin {
double _offset = 0.0;
bool _isKeyboardShowing = false;
late final AnimationController _controller = AnimationController(
duration: const Duration(milliseconds: 200), vsync: this);
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text("Stack and SingleChildScrollView"),
),
body: SingleChildScrollView(
child: Stack(
children: [
Column(
children: [
Container(
height: 800,
),
],
),
Positioned(
bottom: _offset,
left: 0,
right: 0,
child: Container(
color: Colors.lightBlue,
height: 140,
child: Center(
child: GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Text(
"Input",
style: TextStyle(
fontSize: 30,
color: Colors.white,
),
),
),
),
),
),
],
),
),
);
}
@override
void initState() {
super.initState();
_controller.addListener(() {
setState(() {
_offset =
MediaQuery.of(context).viewInsets.bottom * _controller.value;
});
});
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
setState(() {
_isKeyboardShowing = true;
});
} else if (status == AnimationStatus.dismissed) {
setState(() {
_isKeyboardShowing = false;
});
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text("Stack and SingleChildScrollView"),
),
body: SingleChildScrollView(
child: Stack(
children: [
Column(
children: [
Container(
height: 800,
),
GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
}
child: // input widget
),
],
),
Positioned(
bottom: _offset,
left: 0,
right: 0,
child: Container(
color: Colors.lightBlue,
height: 140,
child: Center(
child: GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Text(
"Input",
style: TextStyle(
fontSize: 30,
color: Colors.white,
),
),
),
),
),
)
],
),
),
);
}
void _handleFocusChange() {
if (FocusScope.of(context).hasFocus != _isKeyboardShowing) {
_isKeyboardShowing = FocusScope.of(context).hasFocus;
_controller.animateTo(
_isKeyboardShowing ? 1.0 : 0.0,
duration: const Duration(milliseconds: 150),
curve: Curves.linear);
}
}
}
五、小結(jié)
本文介紹了Flutter中實(shí)現(xiàn)鍵盤(pán)頂起底部的幾種方法,包括使用SingleChildScrollView、ListView、Stack以及結(jié)合方法。在使用這些方法時(shí),我們需要考慮底部?jī)?nèi)容的特殊結(jié)構(gòu),確保鍵盤(pán)彈出時(shí),底部?jī)?nèi)容不會(huì)被遮擋。希望本文能對(duì)您在Flutter開(kāi)發(fā)中實(shí)現(xiàn)鍵盤(pán)頂起底部的需求提供幫助。