Flutter中有意思的滚动效果 - Sliver系列

滚动效果:

 

出处原文链接:https://segmentfault.com/a/1190000019902201

由于原文代码对屏幕短的手机存在适配问题,本人进行修改,新代码如下:

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Welcome to Flutter',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  final double _headerCollapsedHeight = 40;
  final double _headerExpandedHeight = 300;

  @override
  Widget build(BuildContext context) {
    //final double height = MediaQuery.of(context).size.height;
    final MediaQueryData mqd = MediaQuery.of(context);
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverPersistentHeader(
            pinned: true,
            delegate: _MySliverHeaderDelegate(
              title: '哪吒之魔童降世',
              collapsedHeight: _headerCollapsedHeight,
              expandedHeight: _headerExpandedHeight,
              paddingTop: mqd.padding.top,
              coverImgUrl:
                  'https://img.zcool.cn/community/01c6615d3ae047a8012187f447cfef.jpg@1280w_1l_2o_100sh.jpg',
              coverImgName: null,
            ),
          ),
          SliverFillRemaining(
            hasScrollBody: false,
            child: FilmContent(
              minHeight:
                  (mqd.size.height - mqd.padding.top - _headerCollapsedHeight),
            ),
          ),
        ],
      ),
    );
  }
}

class _MySliverHeaderDelegate extends SliverPersistentHeaderDelegate {
  _MySliverHeaderDelegate({
    required this.collapsedHeight,
    required this.expandedHeight,
    required this.paddingTop,
    this.coverImgUrl,
    this.coverImgName,
    required this.title,
  });

  final double collapsedHeight;
  final double expandedHeight;
  final double paddingTop;
  final String? coverImgUrl;
  final String? coverImgName;
  final String title;
  String _statusBarMode = 'dark';

  @override
  double get minExtent => collapsedHeight + paddingTop;

  @override
  double get maxExtent => expandedHeight + paddingTop;

  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
    return true;
  }

  void updateStatusBarBrightness(shrinkOffset) {
    if (shrinkOffset > 50 && _statusBarMode == 'dark') {
      _statusBarMode = 'light';
      SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
        statusBarBrightness: Brightness.light,
        statusBarIconBrightness: Brightness.light,
      ));
    } else if (shrinkOffset <= 50 && _statusBarMode == 'light') {
      _statusBarMode = 'dark';
      SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
        statusBarBrightness: Brightness.dark,
        statusBarIconBrightness: Brightness.dark,
      ));
    }
  }

  Color makeStickyHeaderBgColor(shrinkOffset) {
    final int alpha =
        (shrinkOffset / (maxExtent - minExtent) * 255).clamp(0, 255).toInt();
    return Color.fromARGB(alpha, 255, 255, 255);
  }

  Color makeStickyHeaderTextColor(shrinkOffset, isIcon) {
    if (shrinkOffset <= 50) {
      return isIcon ? Colors.white : Colors.transparent;
    } else {
      final int alpha =
          (shrinkOffset / (maxExtent - minExtent) * 255).clamp(0, 255).toInt();
      return Color.fromARGB(alpha, 0, 0, 0);
    }
  }

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    updateStatusBarBrightness(shrinkOffset);
    return SizedBox(
      height: maxExtent,
      width: MediaQuery.of(context).size.width,
      child: Stack(
        fit: StackFit.expand,
        children: [
          coverImgUrl != null
              ? Image.network(coverImgUrl!, fit: BoxFit.cover)
              : coverImgName != null
                  ? Image.asset(coverImgName!, fit: BoxFit.cover)
                  : Container(
                      color: Colors.orangeAccent,
                    ),
          Positioned(
            left: 0,
            top: maxExtent / 2,
            right: 0,
            bottom: 0,
            child: Container(
              decoration: const BoxDecoration(
                gradient: LinearGradient(
                  begin: Alignment.topCenter,
                  end: Alignment.bottomCenter,
                  colors: [
                    Color(0x00000000),
                    Color(0x90000000),
                  ],
                ),
              ),
            ),
          ),
          Positioned(
            left: 0,
            right: 0,
            top: 0,
            child: Container(
              color: makeStickyHeaderBgColor(shrinkOffset),
              child: SafeArea(
                bottom: false,
                child: SizedBox(
                  height: collapsedHeight,
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      IconButton(
                        icon: Icon(
                          Icons.arrow_back_ios,
                          color: makeStickyHeaderTextColor(shrinkOffset, true),
                        ),
                        onPressed: () => Navigator.pop(context),
                      ),
                      Text(
                        title,
                        style: TextStyle(
                          fontSize: 20,
                          fontWeight: FontWeight.w500,
                          color: makeStickyHeaderTextColor(shrinkOffset, false),
                        ),
                      ),
                      IconButton(
                        icon: Icon(
                          Icons.share,
                          color: makeStickyHeaderTextColor(shrinkOffset, true),
                        ),
                        onPressed: () {},
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class FilmContent extends StatelessWidget {
  const FilmContent({super.key, required this.minHeight});

  final double minHeight;

  @override
  Widget build(BuildContext context) {
    return Container(
      constraints: BoxConstraints(minHeight: minHeight),
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Container(
                width: 130,
                height: 180,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(6),
                  // color: Colors.orangeAccent,
                ),
                clipBehavior: Clip.hardEdge,
                child: Image.network(
                  'https://img1.gamersky.com/image2019/07/20190725_ll_red_136_2/gamersky_07small_14_201972510258D0.jpg',
                  width: 130,
                  height: 180,
                  fit: BoxFit.cover,
                ),
              ),
              const SizedBox(width: 10),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    _buildText(
                      '哪吒之魔童降世',
                      fontSize: 25,
                      fontWeight: FontWeight.bold,
                    ),
                    const SizedBox(height: 10),
                    _buildText(
                      '动画/中国大陆/110分钟',
                      color: const Color(0xFF999999),
                    ),
                    const SizedBox(height: 8),
                    _buildText(
                      '2019-07-26 08:00 中国大陆上映',
                      color: const Color(0xFF999999),
                    ),
                    const SizedBox(height: 8),
                    _buildText(
                      '32.1万人想看/大V推荐度95%',
                      color: const Color(0xFF999999),
                    ),
                  ],
                ),
              ),
            ],
          ),
          const Divider(height: 32),
          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              _buildText(
                '剧情简介',
                fontSize: 25,
                fontWeight: FontWeight.bold,
              ),
              const SizedBox(height: 10),
              _buildText(
                '天地灵气孕育出一颗能量巨大的混元珠,元始天尊将混元珠提炼成灵珠和魔丸,灵珠投胎为人,助周伐纣时可堪大用;而魔丸则会诞出魔王,为祸人间。元始天尊启动了天劫咒语,3年后天雷将会降临,摧毁魔丸。太乙受命将灵珠托生于陈塘关李靖家的儿子哪吒身上。然而阴差阳错,灵珠和魔丸竟然被掉包。本应是灵珠英雄的哪吒却成了混世大魔王。调皮捣蛋顽劣不堪的哪吒却徒有一颗做英雄的心。然而面对众人对魔丸的误解和即将来临的天雷的降临,哪吒是否命中注定会立地成魔?他将何去何从?',
                color: const Color(0xFF999999),
                textAlign: TextAlign.justify,
              ),
            ],
          ),
        ],
      ),
    );
  }

  _buildText(
    String data, {
    double fontSize = 15,
    FontWeight? fontWeight,
    Color? color = const Color(0xFF333333),
    TextAlign? textAlign,
  }) {
    return Text(
      data,
      style: TextStyle(
        fontSize: fontSize,
        fontWeight: fontWeight,
        color: color,
        height: 1,
      ),
      textAlign: textAlign,
      overflow: TextOverflow.clip,
    );
  }
}

你可能感兴趣的:(IT_前端开发_Flutter,flutter,android,ios)