Flutter vs 前端 杂谈SliverAppBar的弹性背景的显隐效果使用Html+JS怎么实现
1. 一些AppBar效果
在 Flutter 中,最简单的 appbar 就是 Appbar 组件,它没有任何难点,任何刚刚入门的开发着在 Flutter 脚手架创建的计数器应用中就使用了它。但是现实的开发场景中,Appbar 组件往往难以适应复杂的需求场景。
比如以下是 “王者营地”APP (即王者荣耀官方的社区应用) 的 Appbar,这个据说也是 Flutter 实现的:
这种向下滚动时,AppBar出现,向上滚到顶AppBar逐渐隐藏的效果还是比较简单,可以直接使用SliverAppBar。
与之相比,下面这个高德地图滚动方向与王者营地是相反的,并且还带有一个相遇于下面内容部分似乎在向下跑的图片:
这些效果当然不是使用 Appbar 组件做的。
在 Flutter 中,最简单的随着滚动带有显影效果的appbar可以使用 SliverAppBar 组件实现。
但是实际上appbar仅仅是一个应用顶部导航的效果不仅仅局限于 Flutter 原生的 Appbar 和 SliverAppBar 。实际上,为了实现更加灵活的 appbar,还可以考虑基于 Sliver 协议 实现外观类似的组件,将它放在页面的顶部,着很好理解,因为在 写 Web 的时候就可以这样干(事实上我就是这样干过)。因此先从一个类似的 Web 中手写的例子看起。
2. 一个Web移动端上的复杂AppBar例子
先看效果吧(其实就是模仿上面的高德地图的大概效果):
(附:感谢图片来源地址,我在网络随便拿的,仅仅用于此示例,祝愿贵App、贵店铺生意红火。)
这个Appbar以及相关其它动画效果,本质上都是与滚动相关的。总结起来,我们要实现的效果如下:
- 页面上方有一个固定的Appbar,背景颜色为蓝色,内部包含一个输入框;
- 页面的上半部分有一个背景图像,通过
#bg-item
元素实现,并且这个元素在最下层; - 页面的下半部分分为两部分:
#scroll-item
:一个滚动元素,包含一张图片,它会随着页面的滚动而滚动,但在背景元素之上。#content
:一个内容区域,包含一个标题,它也会随着页面的滚动而滚动,但在滚动元素之上。
- 使用JavaScript监听页面的滚动事件,根据滚动距离动态改变以下效果:
- Appbar的背景颜色透明度,使其在页面滚动时逐渐变为透明。
- Appbar内文字的颜色透明度,同样逐渐变为透明。
- Appbar内输入框的透明度,使其在页面滚动时逐渐变为透明。
- 滚动元素的位置,随着页面滚动而向上移动,实现视差效果。
- 内容区域的位置,随着页面滚动而向上移动,也实现视差效果。
- 使用CSS的变量
--my-height
定义了滚动元素的初始高度,以便在JavaScript中使用。
在 Web 中,要实现总体思路是通过 JavaScript 监听页面滚动事件,根据滚动距离动态改变页面元素的样式,从而实现Appbar背景颜色和文字颜色的渐变效果,以及滚动元素和内容区域的视差滚动效果。这种交互设计可以提升页面的视觉吸引力和用户体验。
Web代码如下:
<!DOCTYPE html> <html> <head> <!-- 作者信息 --> <!-- Author: 李俊才 --> <!-- Email: 291149494@163.com --> <!-- 许可证信息 --> <!-- LICENSE: MIT --> <style> body { margin: 0; padding: 0; --my-height: 460px; // 定义一个CSS变量,表示滚动元素的初始高度 } #appbar { position: fixed; top: 0; width: 100%; height: 50px; background: rgba(0, 123, 255, 1); color:white; font-size: large; transition: background 0.3s; display: flex; align-items: center; justify-content: space-between; padding: 0 10px; padding-right: 20px; z-index: 3; // 设置appbar在最上层 } #appbar-input { padding: 5px; border-radius: 5px; border: 1px solid white; margin-right: 20px; } #appbar-input::placeholder { color: white; } #bg-item { position: fixed; top: 0; width: 100%; height: var(--my-height); z-index: 0; // 设置背景元素在最下层 } #bg-item img { width: 100%; height: auto; object-fit: cover; } #scroll-item { position: absolute; top: var(--my-height); width: 100%; z-index: 1; // 设置滚动元素在背景元素之上 } #scroll-item img { width: 100%; height: auto; object-fit: cover; } #content { position: absolute; top: var(--my-height); height: 610px; width: 100%; background-color: #ececec; z-index: 2; // 设置内容元素在滚动元素之上 } </style> </head>
从CSS部分就可以看出,实际上归纳起来,我把页面拆分为了 appbar、背景图层、滚动图层、内容层,通过 z-index
属性来控制层级关系(可以结合下面html部分)。代码接上:
<!-- 作者信息 --> <!-- Author: 李俊才 --> <!-- Email: 291149494@163.com --> <!-- 许可证信息 --> <!-- LICENSE: MIT --> <body> <div id="appbar"> <div>我是appbar</div> <input id="appbar-input" type="text" placeholder="我是输入框"> </div> <div id="bg-item"> <img src="https://gw.alicdn.com/imgextra/i4/2212013333132/O1CN01DetIjE1Z0VMx4155t_!!2212013333132.jpg_Q75.jpg_.webp" alt="Image"> </div> <div id="scroll-item"> <img src="https://gitee.com/jacklee1995/example-pictures/raw/master/piano/jonathanvasquez8950_piano_795a8e31-a910-48aa-9eae-45b1602f7cba.png" alt="Image"/> </div> <div id="content"> <h1>我是内容区域</h1> </div> <script> // 获取 appbar 以及appbar内的输入框元素节点 const appbar = document.getElementById('appbar'); const appbarInput = document.getElementById('appbar-input'); // 获取滚动项节点,这是一个与内容节点差速滚动的元素 const scrollItem = document.getElementById('scroll-item'); // 获取内容节点 const content = document.getElementById('content'); // 定义页面滚动的最大距离,在这个距离内appbar的背景颜色和文字颜色会发生变化 const maxScroll = 280; window.addEventListener("scroll", function() { let scrollTop = window.pageYOffset || document.documentElement.scrollTop; let opacity = (scrollTop / maxScroll); opacity = opacity < 0 ? 0 : opacity; // 根据滚动距离动态改变appbar的背景颜色透明度 appbar.style.background = `rgba(0, 123, 255, ${opacity})`; // 根据滚动距离动态改变appbar内文字的颜色透明度 appbar.style.color = `rgba(255, 255, 255, ${opacity})`; appbarInput.style.opacity = `${opacity}`; // 根据滚动距离动态改变滚动项的位置 scrollItem.style.top = `calc(var(--my-height) - ${scrollTop}px)`; // 根据滚动距离动态改变内容的位置 content.style.top = `calc(var(--my-height) - ${scrollTop / 2}px)`; }); </script> </body> </html>
所有的控制逻辑我在 scroll 监听中完成的,实际上就是对各个层的控制。
3. Flutter小试:我就不用SliverAppBar了
其实我的意思就是想自定义与滚动效果相关的appbar。既然需要滚动控制,而且使用 SliverAppBar 套参数也不是那么方便,即使这样的效果无非是控制一个盒子的透明度变化以及其它的位置移动。说来说去, SliverAppBar 也可以通过其它的组件实现,最后转为 Sliver 协议放入CustomScrollView不就可以了。
整体思路和 Web 中差不多,由于是分层,需要使用 Stack 与 Positioned 组件(相比于上一小节在Web中我们使用的是html+CSS的z-index,然后在 JS 代码中动态调整opacity)。
整体思路完全一样。于是,可以将上一个小节的 Web 代码 改用Flutter实现一下:
import 'package:flutter/material.dart'; // Author: 李俊才 // Email: 291149494@163.com // https://blog.csdn.net/qq_28550263/article/details/134149018 class WebAppBarScaffold extends StatefulWidget { const WebAppBarScaffold({Key? key}) : super(key: key); @override State<WebAppBarScaffold> createState() => _WebAppBarScaffoldState(); } class _WebAppBarScaffoldState extends State<WebAppBarScaffold> { double _opacity = 0.0; double _offsetImage = 0.0; double _offsetContent = 0.0; @override Widget build(BuildContext context) { return Scaffold( body: NotificationListener<ScrollNotification>( onNotification: (ScrollNotification scrollInfo) { if (scrollInfo is ScrollUpdateNotification) { setState(() { _opacity = scrollInfo.metrics.pixels / 280; _opacity = _opacity.clamp(0.0, 1.0); _offsetImage = scrollInfo.metrics.pixels * 1.5; // 修改这里 _offsetContent = scrollInfo.metrics.pixels / 2; // 修改这里 }); } return true; }, child: Stack( children: <Widget>[ Positioned( top: 0, child: Image.network( 'https://gw.alicdn.com/imgextra/i4/2212013333132/O1CN01DetIjE1Z0VMx4155t_!!2212013333132.jpg_Q75.jpg_.webp', width: MediaQuery.of(context).size.width, fit: BoxFit.cover, ), ), Positioned( top: 460 - _offsetImage, child: Image.network( 'https://gitee.com/jacklee1995/example-pictures/raw/master/piano/jonathanvasquez8950_piano_795a8e31-a910-48aa-9eae-45b1602f7cba.png', width: MediaQuery.of(context).size.width, fit: BoxFit.cover, ), ), Positioned( top: 460 - _offsetContent, child: Container( color: Colors.grey[200], width: MediaQuery.of(context).size.width, height: 610, child: const Center(child: Text('我是内容区域')), ), ), Positioned( top: 0, child: Container( width: MediaQuery.of(context).size.width, height: 50, color: Colors.blue.withOpacity(_opacity), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Padding( padding: const EdgeInsets.only(left: 10), child: Text( '我是appbar', style: TextStyle( color: Colors.white.withOpacity(_opacity)), ), ), Padding( padding: const EdgeInsets.only(right: 20), child: Opacity( opacity: _opacity, child: const SizedBox( width: 200, // 限定宽度 child: TextField( decoration: InputDecoration( hintText: '我是输入框', hintStyle: TextStyle(color: Colors.white), ), ), ), ), ), ], ), ), ), ListView.builder( itemCount: 1, itemBuilder: (context, index) { return Container( height: MediaQuery.of(context).size.height * 2); }, ), ], ), ), ); } }
这里其实有一个小缺陷,就是滚动过头我没去做处理了。这里是一点小数学问题,就是计算中间层的图片相对于内容层图片滚动的位移值恰好为图片的高度时,让中间滚动图片层和内容层一起滚动,就可以避免看到中间滚动图层相比于内容层越来越远。读者可以尝试修改一下代码。
4. 结论
其实本文主要目的还是比较。可以看到,使用Web事件监听处理滚动事件,其实和Flutter中使用滚动控制差不多。对于一些复杂的效果,没有必要拘束于现有的组件,可以基于一些更加基础的部件构成复杂的效果。