AppBar在屏幕滚动时变换透明度

1,334 阅读2分钟

AppBar在屏幕滚动时变换透明度

如gif所示,闲鱼的个人页再向上滚动时,设置图标所在header栏,慢慢从不可见变成可见

方法一

通过stack结合Notification,在滚动时改变Opacity

在Notification回调中,通过比较notification.metrics.pixels与header的高度[height]

height就是全部显示与半透明显示的分界线

onNotification: (ScrollNotification notification) {
  double progress = notification.metrics.pixels / height;
  //重新构建
  setState(() {
    opacity = notification.metrics.pixels >= height ? 1 : progress >= 0 ? progress : 0;
  });
  return false;
  //return true; //放开此行注释后,进度条将失效
},

完整代码

import 'package:flutter/material.dart';
​
class ProfilePage extends StatefulWidget {
  const ProfilePage({Key? key}) : super(key: key);
​
  @override
  State<ProfilePage> createState() => _ProfilePageState();
}
​
/// 闲鱼的截图量出来,安全top是40,但是在模拟器中47;
/// 闲鱼的header高40,icon尺寸18,共80高
class _ProfilePageState extends State<ProfilePage> {
  double opacity = 0;
  @override
  Widget build(BuildContext context) {
    const height = 40.0;
    return SafeArea(
      //进度条
      // 监听滚动通知
      child: NotificationListener<ScrollNotification>(
        onNotification: (ScrollNotification notification) {
          double progress = notification.metrics.pixels / height;
          //重新构建
          setState(() {
            opacity = notification.metrics.pixels >= height
                ? 1
                : progress >= 0
                    ? progress
                    : 0;
          });
          return false;
          //return true; //放开此行注释后,进度条将失效
        },
        child: Stack(
          children: [
            MainBody(),
            Opacity(
              opacity: opacity,
              child: Container(
                height: height,
                color: Colors.red,
                padding:
                    const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
                child: Row(
                  children: [
                    Text("${opacity.toStringAsFixed(2)}%"),
                    const Expanded(child: Center(child: Text("cn1001wang"))),
                    const Icon(Icons.settings, size: 18),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
​

占据位置用于滚动

class MainBody extends StatelessWidget {
  const MainBody({Key? key}) : super(key: key);
​
  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: [          
          Container(height: 700, color: Colors.amber),
          Container(height: 1000, color: Colors.green)
        ],
      ),
    );
  }
}

效果

ScrollOpacity方法一.gif

方法二

在方法一上改进,不再依靠stack,而是直接使用AppBar,Scaffold有一个新属性extendBodyBehindAppBar,设置为true之后可以允许body堆叠在appBar后面,此时只要将AppBar背景颜色设为透明,在滚动时设置为不透明,一样能实现上述效果

class ProfilePage2 extends StatefulWidget {
  const ProfilePage2({Key? key}) : super(key: key);
​
  @override
  State<ProfilePage2> createState() => _ProfilePage2State();
}
​
/// 闲鱼的截图量出来,安全top是40,但是在模拟器中47;
/// 闲鱼的header高40,icon尺寸18,共80高
class _ProfilePage2State extends State<ProfilePage2> {
  final double height = 40.0;
  bool _scrollNotificationCb(ScrollNotification notification) {
    double progress = notification.metrics.pixels / height;
    //重新构建
    setState(() {
      opacity = notification.metrics.pixels >= height
          ? 1
          : progress >= 0
              ? progress
              : 0;
    });
    return false;
    //return true; //放开此行注释后,进度条将失效
  }
​
  double opacity = 0;
  @override
  Widget build(BuildContext context) {
    final MediaQueryData data = MediaQuery.of(context);
    EdgeInsets padding = data.padding;
​
    const height = 40.0;
    return Scaffold(
      extendBodyBehindAppBar: true,// 注意此行
      backgroundColor: Colors.green[200],
      appBar: PreferredSize(
        preferredSize: const Size.fromHeight(height),// 为了实现40px高度,使用了PreferredSize
        child: AppBar(
          titleSpacing: 0,
          elevation: 0,
          backgroundColor: Colors.white.withOpacity(opacity),// 通过改变backgroundColor达到透明到不透明的变化
          title: HeaderBar(height: height, opacity: opacity),
          leadingWidth: 0,
        ),
      ),
      body: NotificationListener<ScrollNotification>(
        onNotification: _scrollNotificationCb,
        child: Padding(
          padding: EdgeInsets.only(top: padding.top),
          child: const MainBody(),
        ),
      ),
    );
  }
}
class HeaderBar extends StatelessWidget {
  const HeaderBar({Key? key, required this.opacity, required this.height})
      : super(key: key);
​
  final double opacity;
  final double height;
​
  Color get _colorByOpacity => Colors.black.withOpacity(opacity);
​
  @override
  Widget build(BuildContext context) {
    return Container(
      height: height,
      // color: Colors.blue,
      padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
      child: DefaultTextStyle(
        style: TextStyle(
          color: _colorByOpacity,
          fontSize: 12,
        ),
        child: Row(
          children: [
            Text("${opacity.toStringAsFixed(2)}%"),
            const Expanded(child: Center(child: Text("cn1001wang"))),
            Icon(
              Icons.settings,
              size: 18,
              color: _colorByOpacity,
            ),
          ],
        ),
      ),
    );
  }
}

方法三

将setState改造成Animation,优化性能

增加AnimationController来构建ColorTween,HeaderBar组件不再接收opacit ,改为接收Color

class ProfilePage3 extends StatefulWidget {
  const ProfilePage3({Key? key}) : super(key: key);
​
  @override
  State<ProfilePage3> createState() => _ProfilePage3State();
}
​
/// 闲鱼的截图量出来,安全top是40,但是在模拟器中47;
/// 闲鱼的header高40,icon尺寸18,共80高
class _ProfilePage3State extends State<ProfilePage3>
    with TickerProviderStateMixin {
  final double height = 40.0;
  bool _scrollNotificationCb(ScrollNotification scrollInfo) {
    if (scrollInfo.metrics.axis == Axis.vertical) {
      _colorAnimationController.animateTo(scrollInfo.metrics.pixels / height);
      return true;
    }
    return false;
  }
​
  late AnimationController _colorAnimationController;
​
  late Animation _colorTween, _textColorTween;
  @override
  void initState() {
    super.initState();
​
    _colorAnimationController =
        AnimationController(vsync: this, duration: const Duration(seconds: 0));
    _colorTween =
        ColorTween(begin: Colors.transparent, end: const Color(0xffffffff))
            .animate(_colorAnimationController);
    _textColorTween = ColorTween(begin: Colors.transparent, end: Colors.black)
        .animate(_colorAnimationController);
  }
​
  double opacity = 0;
  @override
  Widget build(BuildContext context) {
    final MediaQueryData data = MediaQuery.of(context);
    EdgeInsets padding = data.padding;
​
    const height = 40.0;
    return Scaffold(
      extendBodyBehindAppBar: true,
      backgroundColor: Colors.green[200],
      appBar: PreferredSize(
        preferredSize: const Size.fromHeight(height),
        child: AnimatedBuilder(
          animation: _colorAnimationController,
          builder: (context, child) {
            return AppBar(
              titleSpacing: 0,
              elevation: 0,
              backgroundColor: _colorTween.value,
              title: HeaderBar(
                height: height,
                textColor: _textColorTween.value,
              ),
              leadingWidth: 0,
            );
          },
        ),
      ),
      body: NotificationListener<ScrollNotification>(
        onNotification: _scrollNotificationCb,
        child: Padding(
          padding: EdgeInsets.only(top: padding.top),
          child: const MainBody(),
        ),
      ),
    );
  }
}

效果

ScrollOpacity方法三.gif

keyword

flutter,AppBar,Opacity,scroll

Changes the transparency of the AppBar as the screen scrolls.