带有搜索栏的 Flutter 粘性标题
Posted
技术标签:
【中文标题】带有搜索栏的 Flutter 粘性标题【英文标题】:Flutter sticky header with search bar 【发布时间】:2022-01-08 18:22:09 【问题描述】:我想用搜索文本字段实现粘性标题。标题标题应该是最大到最小字体大小的动画(如补间动画),而滚动和搜索框应该在滚动时粘贴在标题标题下方。 I tried this as well but not got as I expected. 这就是我expected,但没有正确解释实现。有谁知道如何制作这种清单?如果你能帮助我,那就太好了。
【问题讨论】:
你试过this吗? @Sagar Stackapp 这是粘性标题列表视图。它会粘贴所有标题,而我想将标题与搜索框粘贴。 您能分享一下您的预期结果吗? @SagarStackapp 我已经更新了。请检查。 查看此代码示例:medium.com/@diegoveloper/… 【参考方案1】:请参考以下示例代码
class AnimatedAppBar extends StatefulWidget
const AnimatedAppBar(Key key) : super(key: key);
@override
_AnimatedAppBarState createState() => _AnimatedAppBarState();
class _AnimatedAppBarState extends State<AnimatedAppBar>
final TextEditingController stateController = TextEditingController();
final FocusNode stateFocus = FocusNode();
@override
Widget build(BuildContext context)
return SafeArea(
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innnerBoxIsScrolled)
return <Widget>[
SliverAppBar(
expandedHeight: 120.0,
floating: false,
pinned: true,
backgroundColor: Colors.grey,
automaticallyImplyLeading: false,
titleSpacing: 0.0,
toolbarHeight: 90.0,
centerTitle: false,
elevation: 0.0,
leadingWidth: 0.0,
title: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (innnerBoxIsScrolled != null &&
innnerBoxIsScrolled == true)
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 10.0,
),
Text(
"Search",
style: TextStyle(
color: Colors.black,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
autovalidateMode:
AutovalidateMode.onUserInteraction,
/* autovalidate is disabled */
controller: stateController,
inputFormatters: [
FilteringTextInputFormatter.deny(
RegExp(r"\s\s")),
FilteringTextInputFormatter.deny(RegExp(
r'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])')),
],
keyboardType: TextInputType.text,
maxLength: 160,
onChanged: (val) ,
maxLines: 1,
validator: (value) ,
focusNode: stateFocus,
autofocus: false,
decoration: InputDecoration(
errorMaxLines: 3,
counterText: "",
filled: true,
fillColor: Colors.white,
focusedBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Color(0xffE5E5E5),
),
),
disabledBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Color(0xffE5E5E5),
),
),
enabledBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Color(0xffE5E5E5),
),
),
border: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
),
),
errorBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Colors.red,
)),
focusedErrorBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Colors.red,
),
),
hintText: "Search" ?? "",
),
),
),
SizedBox(
height: 6.0,
)
],
),
],
),
// bottom: PreferredSize(
// preferredSize: Size.fromHeight(5.0),
// child: Text(''),
// ),
flexibleSpace: FlexibleSpaceBar(
background: Container(
width: MediaQuery.of(context).size.width,
child: Stack(
alignment: Alignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 10.0,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 8.0,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"Search",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 24.0,
),
),
CircleAvatar(
backgroundImage: NetworkImage(
"https://images.ctfassets.net/hrltx12pl8hq/2TRIFRwcjrTuNprkTQHVxs/088159eb8e811aaac789c24701d7fdb1/LP_image.jpg?fit=fill&w=632&h=354&fm=webp"), //NetworkImage
radius: 16.0,
),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
autovalidateMode:
AutovalidateMode.onUserInteraction,
/* autovalidate is disabled */
controller: stateController,
inputFormatters: [
FilteringTextInputFormatter.deny(
RegExp(r"\s\s")),
FilteringTextInputFormatter.deny(RegExp(
r'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])')),
],
keyboardType: TextInputType.text,
maxLength: 160,
onChanged: (val) ,
maxLines: 1,
validator: (value) ,
focusNode: stateFocus,
autofocus: false,
decoration: InputDecoration(
errorMaxLines: 3,
counterText: "",
filled: true,
fillColor: Colors.white,
focusedBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Color(0xffE5E5E5),
),
),
disabledBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Color(0xffE5E5E5),
),
),
enabledBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Color(0xffE5E5E5),
),
),
border: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
),
),
errorBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Colors.red,
)),
focusedErrorBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Colors.red,
),
),
hintText: "Search" ?? "",
),
),
),
],
),
],
),
),
),
),
];
,
body: Builder(
builder: (BuildContext context)
return SingleChildScrollView(
child: Column(
children: [
ListView.builder(
itemCount: 100,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (BuildContext context, int index)
return Padding(
padding: const EdgeInsets.all(4.0),
child: Text("Index value: $index"),
);
,
)
],
),
);
,
),
),
),
);
另外我们还可以添加动画
class AnimatedAppBar extends StatefulWidget
const AnimatedAppBar(Key key) : super(key: key);
@override
_AnimatedAppBarState createState() => _AnimatedAppBarState();
class _AnimatedAppBarState extends State<AnimatedAppBar>
with TickerProviderStateMixin
final TextEditingController stateController = TextEditingController();
final FocusNode stateFocus = FocusNode();
var animation;
var controller;
@override
Widget build(BuildContext context)
return SafeArea(
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innnerBoxIsScrolled)
if (innnerBoxIsScrolled)
/* Animation */
controller = AnimationController(
vsync: this,
duration: Duration(
seconds: 1,
),
);
animation = Tween(
begin: 0.0,
end: 1.0,
).animate(controller);
/* Animation */
controller.forward();
return <Widget>[
SliverAppBar(
expandedHeight: 120.0,
floating: false,
pinned: true,
backgroundColor: Colors.grey,
automaticallyImplyLeading: false,
titleSpacing: 0.0,
toolbarHeight: 90.0,
centerTitle: false,
elevation: 0.0,
leadingWidth: 0.0,
title: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (innnerBoxIsScrolled != null &&
innnerBoxIsScrolled == true)
FadeTransition(
opacity: animation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 10.0,
),
Text(
"Search",
style: TextStyle(
color: Colors.black,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
autovalidateMode:
AutovalidateMode.onUserInteraction,
/* autovalidate is disabled */
controller: stateController,
inputFormatters: [
FilteringTextInputFormatter.deny(
RegExp(r"\s\s")),
FilteringTextInputFormatter.deny(RegExp(
r'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])')),
],
keyboardType: TextInputType.text,
maxLength: 160,
onChanged: (val) ,
maxLines: 1,
validator: (value) ,
focusNode: stateFocus,
autofocus: false,
decoration: InputDecoration(
errorMaxLines: 3,
counterText: "",
filled: true,
fillColor: Colors.white,
focusedBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Color(0xffE5E5E5),
),
),
disabledBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Color(0xffE5E5E5),
),
),
enabledBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Color(0xffE5E5E5),
),
),
border: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
),
),
errorBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Colors.red,
)),
focusedErrorBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Colors.red,
),
),
hintText: "Search" ?? "",
),
),
),
SizedBox(
height: 6.0,
)
],
),
),
],
),
// bottom: PreferredSize(
// preferredSize: Size.fromHeight(5.0),
// child: Text(''),
// ),
flexibleSpace: FlexibleSpaceBar(
background: Container(
width: MediaQuery.of(context).size.width,
child: Stack(
alignment: Alignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 10.0,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 8.0,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"Search",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 24.0,
),
),
CircleAvatar(
backgroundImage: NetworkImage(
"https://images.ctfassets.net/hrltx12pl8hq/2TRIFRwcjrTuNprkTQHVxs/088159eb8e811aaac789c24701d7fdb1/LP_image.jpg?fit=fill&w=632&h=354&fm=webp"), //NetworkImage
radius: 16.0,
),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
autovalidateMode:
AutovalidateMode.onUserInteraction,
/* autovalidate is disabled */
controller: stateController,
inputFormatters: [
FilteringTextInputFormatter.deny(
RegExp(r"\s\s")),
FilteringTextInputFormatter.deny(RegExp(
r'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])')),
],
keyboardType: TextInputType.text,
maxLength: 160,
onChanged: (val) ,
maxLines: 1,
validator: (value) ,
focusNode: stateFocus,
autofocus: false,
decoration: InputDecoration(
errorMaxLines: 3,
counterText: "",
filled: true,
fillColor: Colors.white,
focusedBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Color(0xffE5E5E5),
),
),
disabledBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Color(0xffE5E5E5),
),
),
enabledBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Color(0xffE5E5E5),
),
),
border: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
),
),
errorBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Colors.red,
)),
focusedErrorBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
color: Colors.red,
),
),
hintText: "Search" ?? "",
),
),
),
],
),
],
),
),
),
),
];
,
body: Builder(
builder: (BuildContext context)
return SingleChildScrollView(
child: Column(
children: [
ListView.builder(
itemCount: 100,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (BuildContext context, int index)
return Padding(
padding: const EdgeInsets.all(4.0),
child: Text("Index value: $index"),
);
,
)
],
),
);
,
),
),
),
);
【讨论】:
非常感谢@Tejsaswini Dev。但是在第一次滚动标题和搜索框时没有显示。 请尝试减少动画持续时间 我尝试了毫秒,但在第一次滚动时遇到了同样的问题,之后它按预期工作。 请再试一次。我没有遇到这样的问题【参考方案2】:您可以使用堆栈来完成任务。
试试我的解决方案:
// ignore_for_file: avoid_print
import 'package:flutter/material.dart';
void main()
runApp(const MaterialApp(
title: 'Demo',
home: MaterialApp(
home: HomePage(),
),
));
class HomePage extends StatefulWidget
const HomePage(Key? key) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
class _HomePageState extends State<HomePage>
late ScrollController scrollController;
bool showHeader = false;
@override
void initState()
super.initState();
scrollController = ScrollController()
..addListener(()
double scrollOffset = scrollController.offset;
if (scrollOffset > 150)
if (!showHeader)
setState(()
showHeader = true;
);
else
if (showHeader)
setState(()
showHeader = false;
);
);
@override
Widget build(BuildContext context)
return Scaffold(
body: Stack(
children: [
ListView.builder(
padding: EdgeInsets.zero,
controller: scrollController,
itemBuilder: (_, index)
return Container(
height: 50,
color: Colors.red,
alignment: Alignment.center,
child: Text(
'$index',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
),
),
);
,
),
if (showHeader)
Container(
height: 150,
alignment: Alignment.center,
color: Colors.blue,
child: const Text(
'Fake SearchBar',
style: TextStyle(
color: Colors.white,
fontSize: 20,
),
),
),
],
),
);
结果:
你可以顺便添加自己的 alpha/resize 动画,希望对你有帮助:)
【讨论】:
以上是关于带有搜索栏的 Flutter 粘性标题的主要内容,如果未能解决你的问题,请参考以下文章