Flutter 下拉图片放大 上滑出现标题

文章展示了如何在Flutter中创建一个自定义组件SliverFlexibleHeader,实现下拉时图片放大效果。同时,通过Stack、NotificationListener和AnimatedOpacity等组件,实现在上滑时显示标题栏的交互。代码示例详细解释了关键部分的逻辑和实现方法。
摘要由CSDN通过智能技术生成

效果

😒😒😒

一、先实现下拉图片放大,

仅copy了[  flukit (Flutter UI Kit)是一个Flutter Widget库 ]中的一些代码。在本示例中无需引用模块。flukit/sliver_flexible_header.dart at main · flutterchina/flukit · GitHub

 写组件

 extra_info_constraints.dart  (别管,复制就行)

import 'package:flutter/widgets.dart';

/// A box constraints with extra information.
///
/// See also:
///   * [SliverFlexibleHeader], which use [ExtraInfoBoxConstraints].
///   * [SliverPersistentHeaderToBox], which use [ExtraInfoBoxConstraints].
class ExtraInfoBoxConstraints<T> extends BoxConstraints {
  ExtraInfoBoxConstraints(
      this.extra,
      BoxConstraints constraints,
      ) : super(
    minWidth: constraints.minWidth,
    minHeight: constraints.minHeight,
    maxWidth: constraints.maxWidth,
    maxHeight: constraints.maxHeight,
  );

  /// extra information
  final T extra;

  BoxConstraints asBoxConstraints() => copyWith();

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is ExtraInfoBoxConstraints &&
        super == other &&
        other.extra == extra;
  }

  @override
  int get hashCode {
    return hashValues(super.hashCode, extra);
  }
}

 sliver_flexible_header.dart   (别管,复制就行)

import 'dart:math';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:Shura/utils/extra_info_constraints.dart';

typedef SliverFlexibleHeaderBuilder = Widget Function(
    BuildContext context,
    double maxExtent,
    ScrollDirection direction,
    );

/// A sliver to provide a flexible header that its height can expand when user continue
/// dragging over scroll . Typically as the first child  of [CustomScrollView].
class SliverFlexibleHeader extends StatelessWidget {
  const SliverFlexibleHeader({
    Key? key,
    this.visibleExtent = 0,
    required this.builder,
  }) : super(key: key);

  final SliverFlexibleHeaderBuilder builder;
  final double visibleExtent;

  @override
  Widget build(BuildContext context) {
    return _SliverFlexibleHeader(
      visibleExtent: visibleExtent,
      child: LayoutBuilder(
        builder: (BuildContext context, BoxConstraints constraints) {
          return builder(
            context,
            constraints.maxHeight,
            // 获取滑动方向
            (constraints as ExtraInfoBoxConstraints<ScrollDirection>).extra,
          );
        },
      ),
    );
  }
}

class _SliverFlexibleHeader extends SingleChildRenderObjectWidget {
  const _SliverFlexibleHeader({
    Key? key,
    required Widget child,
    this.visibleExtent = 0,
  }) : super(key: key, child: child);
  final double visibleExtent;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return _FlexibleHeaderRenderSliver(visibleExtent);
  }

  @override
  void updateRenderObject(context, _FlexibleHeaderRenderSliver renderObject) {
    renderObject.visibleExtent = visibleExtent;
  }
}

class _FlexibleHeaderRenderSliver extends RenderSliverSingleBoxAdapter {
  _FlexibleHeaderRenderSliver(double visibleExtent)
      : _visibleExtent = visibleExtent;
  double _lastOverScroll = 0;
  double _lastScrollOffset = 0;
  double _visibleExtent = 0;
  ScrollDirection _direction = ScrollDirection.idle;

  // 该变量用来确保Sliver完全离开屏幕时会通知child且只通知一次.
  bool _reported = false;

  // 是否需要修正scrollOffset. _visibleExtent 值更新后,
  // 为了防止突然的跳动,要先修正 scrollOffset。
  double? _scrollOffsetCorrection;

  set visibleExtent(double value) {
    // 可视长度发生变化,更新状态并重新布局
    if (_visibleExtent != value) {
      _lastOverScroll = 0;
      _reported = false;
      // 计算修正值
      _scrollOffsetCorrection = value - _visibleExtent;
      _visibleExtent = value;
      markNeedsLayout();
    }
  }

  @override
  void performLayout() {
    // _visibleExtent 值更新后,为了防止突然的跳动,先修正 scrollOffset
    if (_scrollOffsetCorrection != null) {
      geometry = SliverGeometry(
        //修正
        scrollOffsetCorrection: _scrollOffsetCorrection,
      );
      _scrollOffsetCorrection = null;
      return;
    }

    if (child == null) {
      geometry = SliverGeometry(scrollExtent: _visibleExtent);
      return;
    }

    //当已经完全滑出屏幕时
    if (constraints.scrollOffset > _visibleExtent) {
      geometry = SliverGeometry(scrollExtent: _visibleExtent);
      // 通知 child 重新布局,注意,通知一次即可,如果不通知,滑出屏幕后,child 在最后
      // 一次构建时拿到的可用高度可能不为 0。因为使用者在构建子节点的时候,可能会依赖
      // "当前的可用高度是否为0" 来做一些特殊处理,比如记录是否子节点已经离开了屏幕,
      // 因此,我们需要在离开屏幕时确保LayoutBuilder的builder会被调用一次(构建子组件)。
      if (!_reported) {
        _reported = true;
        child!.layout(
          ExtraInfoBoxConstraints(
            _direction, //传递滑动方向
            constraints.asBoxConstraints(maxExtent: 0),
          ),
          //我们不会使用自节点的 Size, 关于此参数更详细的内容见本书后面关于layout原理的介绍
          parentUsesSize: false,
        );
      }
      return;
    }

    //子组件回到了屏幕中,重置通知状态
    _reported = false;

    // 下拉过程中overlap会一直变化.
    double overScroll = constraints.overlap < 0 ? constraints.overlap.abs() : 0;
    var scrollOffset = constraints.scrollOffset;
    _direction = ScrollDirection.idle;

    // 根据前后的overScroll值之差确定列表滑动方向。注意,不能直接使用 constraints.userScrollDirection,
    // 这是因为该参数只表示用户滑动操作的方向。比如当我们下拉超出边界时,然后松手,此时列表会弹回,即列表滚动
    // 方向是向上,而此时用户操作已经结束,ScrollDirection 的方向是上一次的用户滑动方向(向下),这时便有问题。
    var distance = overScroll > 0
        ? overScroll - _lastOverScroll
        : _lastScrollOffset - scrollOffset;
    _lastOverScroll = overScroll;
    _lastScrollOffset = scrollOffset;

    if (constraints.userScrollDirection == ScrollDirection.idle) {
      _direction = ScrollDirection.idle;
      _lastOverScroll = 0;
    } else if (distance > 0) {
      _direction = ScrollDirection.forward;
    } else if (distance < 0) {
      _direction = ScrollDirection.reverse;
    }

    // 在Viewport中顶部的可视空间为该 Sliver 可绘制的最大区域。
    // 1. 如果Sliver已经滑出可视区域则 constraints.scrollOffset 会大于 _visibleExtent,
    //    这种情况我们在一开始就判断过了。
    // 2. 如果我们下拉超出了边界,此时 overScroll>0,scrollOffset 值为0,所以最终的绘制区域为
    //    _visibleExtent + overScroll.
    double paintExtent = _visibleExtent + overScroll - constraints.scrollOffset;
    // 绘制高度不超过最大可绘制空间
    paintExtent = min(paintExtent, constraints.remainingPaintExtent);

    //对子组件进行布局,子组件通过 LayoutBuilder可以拿到这里我们传递的约束对象(ExtraInfoBoxConstraints)
    child!.layout(
      ExtraInfoBoxConstraints(
        _direction, //传递滑动方向
        constraints.asBoxConstraints(maxExtent: paintExtent),
      ),
      parentUsesSize: false,
    );

    //最大为_visibleExtent,最小为 0
    double layoutExtent = min(_visibleExtent, paintExtent);

    //设置geometry,Viewport 在布局时会用到
    geometry = SliverGeometry(
      scrollExtent: _visibleExtent,
      paintOrigin: -overScroll,
      paintExtent: paintExtent,
      maxPaintExtent: paintExtent,
      layoutExtent: layoutExtent,
    );
  }
}

然后使用

这里已经实现了下拉图像放大的功能,但在CustomScrollView这个Widget中怎么都没弄出来标题栏的效果。所以只好用NotificationListener包裹,重新设计。

// 这个 Widget 已经是  下拉图像的最简版    
   CustomScrollView(
            physics: const BouncingScrollPhysics(
                parent: AlwaysScrollableScrollPhysics()),
            slivers: [
              SliverFlexibleHeader(
                visibleExtent: 200.0,
                builder: (context, availableHeight, direction) {
                  return GestureDetector(
                    onTap: () => debugPrint('tap'),
                    child: LayoutBuilder(builder: (context, cons) {
                      return Image(
                        image: const NetworkImage(
                            "https://w.wallhaven.cc/full/zy/wallhaven-zygeko.jpg"),
                        height: availableHeight,
                        fit: BoxFit.cover,
                      );
                    }),
                  );
                },
              ),
            ],
          ),

二、上滑显示标题

先解释要点:

  1. 用 Stack 的特性来避免层级问题,否则标题会在底下看不到
  2. NotificationListener 监听滑动
  3. Positioned 定位组件
  4. AnimatedOpacity动画组件

看完整代码

直接复制去编译看看理解更深

import 'dart:ui';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:logger/logger.dart';
import '../components/sliver_flexible_header.dart';

class HomeMyScreen extends StatefulWidget {
  const HomeMyScreen({Key? key}) : super(key: key);

  @override
  State<HomeMyScreen> createState() => _HomeMyScreen();
}

class _HomeMyScreen extends State<HomeMyScreen> {
  int trans = 0;
  final double headerImgHeight = 400.0;

  void setTrans(marginTop) {
    setState(() {
      trans = marginTop;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Stack(
      children: [
        NotificationListener(
          onNotification: (ScrollNotification note) {
            setTrans(note.metrics.pixels.toInt());
            return true;
          },
          child: CustomScrollView(
            physics: const BouncingScrollPhysics(
                parent: AlwaysScrollableScrollPhysics()),
            slivers: [
              SliverFlexibleHeader(
                visibleExtent: (headerImgHeight + 36.0),
                builder: (context, availableHeight, direction) {
                  return GestureDetector(
                    child: LayoutBuilder(builder: (context, cons) {
                      return Stack(
                        children: [
                          Container(
                            padding: const EdgeInsets.only(bottom: 36),
                            child: Image(
                              image: const NetworkImage(
                                  "https://w.wallhaven.cc/full/zy/wallhaven-zygeko.jpg"),
                              height: availableHeight,
                              width: MediaQuery.of(context).size.width,
                              fit: BoxFit.cover,
                            ),
                          ),
                          Positioned(
                            left: 0,
                            bottom: 36,
                            child: Container(
                              width: MediaQuery.of(context).size.width,
                              height: 48,
                              decoration: BoxDecoration(
                                  gradient: LinearGradient(
                                      begin: Alignment.topCenter,
                                      end: Alignment.bottomCenter,
                                      stops: [
                                    0.0,
                                    1.0
                                  ], //[渐变起始点, 渐变结束点]
                                      //渐变颜色[始点颜色, 结束颜色]
                                      colors: [
                                    Color.fromRGBO(15, 15, 15, 0),
                                    Color.fromRGBO(15, 15, 15, 0.8)
                                  ])),
                            ),
                          ),
                          Positioned(
                              left: 12,
                              bottom: 0,
                              child: Row(
                                children: [
                                  Container(
                                      width: 72,
                                      height: 72,
                                      //设置了 decoration 就不能设置color,两者只能存在一个
                                      decoration: const BoxDecoration(
                                          boxShadow: [
                                            BoxShadow(
                                                color: Colors.black26,
                                                offset: Offset(0, 0.0),
                                                //阴影y轴偏移量
                                                blurRadius: 2,
                                                //阴影模糊程度
                                                spreadRadius: 1 //阴影扩散程度
                                                )
                                          ],
                                          image: DecorationImage(
                                              image: AssetImage(
                                                  "assets/images/cha4.jpg")),
                                          //设置图片
                                          borderRadius: BorderRadius.all(
                                              Radius.circular(6)))),
                                  Padding(
                                    padding: const EdgeInsets.only(left: 12),
                                    child: SizedBox(
                                      height: 72,
                                      child: Column(
                                        crossAxisAlignment:
                                            CrossAxisAlignment.start,
                                        mainAxisAlignment:
                                            MainAxisAlignment.spaceAround,
                                        children: const [
                                          Text("吸鼠霸王",
                                              style: TextStyle(
                                                  fontWeight: FontWeight.w600,
                                                  fontSize: 18,
                                                  color: Colors.white)),
                                          Text("UID:hahaha_666",
                                              style: TextStyle(
                                                  fontWeight: FontWeight.w600,
                                                  fontSize: 12,
                                                  color: Colors.orange))
                                        ],
                                      ),
                                    ),
                                  )
                                ],
                              )),
                        ],
                      );
                    }),
                  );
                },
              ),
              SliverList(
                  delegate: SliverChildBuilderDelegate((context, index) {
                return Container(
                  child: Padding(
                    padding: const EdgeInsets.only(left: 10.0, right: 10),
                    child: Row(
                      children: [
                        CircleAvatar(
                          radius: (index + 1) * 10,
                          backgroundImage: NetworkImage("https://w.wallhaven.cc/full/zy/wallhaven-zygeko.jpg"),
                        ),
                      ],
                    ),
                  ),
                );
              }, childCount: 12)),
            ],
          ),
        ),
        Positioned(
            top: 0,
            child: AnimatedOpacity(
                opacity: trans > headerImgHeight
                    ? ((trans - headerImgHeight) / 40 > 1
                        ? 1
                        : (trans - headerImgHeight) / 40)
                    : 0,
                duration: const Duration(milliseconds: 400),
                child: ClipRRect(
                  child: BackdropFilter(
                    filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),

                    ///整体模糊度
                    child: Container(
                      padding: EdgeInsets.only(
                          top: MediaQueryData.fromWindow(window).padding.top),
                      height: 80,
                      width: MediaQuery.of(context).size.width,
                      alignment: Alignment.center,
                      decoration: const BoxDecoration(
                        color: Color.fromRGBO(15, 15, 15, 0.88),

                        ///背景透明
                      ),
                      child: Row(
                        children: [
                          Container(
                            padding: const EdgeInsets.only(left: 12, right: 12),
                            child: const CircleAvatar(
                              backgroundImage:
                                  AssetImage('assets/images/cha4.jpg'),
                            ),
                          ),
                          const Text(
                            '吸鼠霸王',
                            style: TextStyle(
                                color: Colors.white,
                                fontWeight: FontWeight.w500),
                          )
                        ],
                      ),
                    ),
                  ),
                ))),
        Positioned(
            right: 16,
            top: (MediaQueryData.fromWindow(window).padding.top)+10,
            child: CircleAvatar(
                radius: 16,
                backgroundColor: Colors.black54,
                child: IconButton(
                    onPressed: () {debugPrint('setting');},
                    color: Colors.white,
                    iconSize: 16,
                    icon: Icon(Icons.settings_rounded))))
      ],
    ));
  }
}

现在的效果💃,也弄了蛮久,气死我了

 

 

本地图片用网络图片代替或者用你的本地图像

顶部appbar

        Positioned(
            top: 0,
            child: AnimatedOpacity(
                opacity: trans > headerImgHeight
                    ? ((trans - headerImgHeight) / 40 > 1
                        ? 1
                        : (trans - headerImgHeight) / 40)
                    : 0,
                duration: const Duration(milliseconds: 400),
                child: ClipRRect(
                  child: BackdropFilter(
                    filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),

                    ///整体模糊度
                    child: Container(
                      padding: EdgeInsets.only(
                          top: MediaQueryData.fromWindow(window).padding.top),
                      height: 80,
                      width: MediaQuery.of(context).size.width,
                      alignment: Alignment.center,
                      decoration: const BoxDecoration(
                        color: Color.fromRGBO(15, 15, 15, 0.88),

                        ///背景透明
                      ),
                      child: Row(
                        children: [
                          Container(
                            padding: const EdgeInsets.only(left: 12, right: 12),
                            child: const CircleAvatar(
                              backgroundImage:
                                  AssetImage('assets/images/cha4.jpg'),
                            ),
                          ),
                          const Text(
                            '吸鼠霸王',
                            style: TextStyle(
                                color: Colors.white,
                                fontWeight: FontWeight.w500),
                          )
                        ],
                      ),
                    ),
                  ),
                ))),

图像下拉

SliverFlexibleHeader(
                visibleExtent: (headerImgHeight + 36.0),
                builder: (context, availableHeight, direction) {
                  return GestureDetector(
                    child: LayoutBuilder(builder: (context, cons) {
                      return Stack(
                        children: [
                          Container(
                            padding: const EdgeInsets.only(bottom: 36),
                            child: Image(
                              image: const NetworkImage(
                                  "https://w.wallhaven.cc/full/zy/wallhaven-zygeko.jpg"),
                              height: availableHeight,
                              width: MediaQuery.of(context).size.width,
                              fit: BoxFit.cover,
                            ),
                          ),
                          Positioned(
                            left: 0,
                            bottom: 36,
                            child: Container(
                              width: MediaQuery.of(context).size.width,
                              height: 48,
                              decoration: BoxDecoration(
                                  gradient: LinearGradient(
                                      begin: Alignment.topCenter,
                                      end: Alignment.bottomCenter,
                                      stops: [
                                    0.0,
                                    1.0
                                  ], //[渐变起始点, 渐变结束点]
                                      //渐变颜色[始点颜色, 结束颜色]
                                      colors: [
                                    Color.fromRGBO(15, 15, 15, 0),
                                    Color.fromRGBO(15, 15, 15, 0.8)
                                  ])),
                            ),
                          ),
                          Positioned(
                              left: 12,
                              bottom: 0,
                              child: Row(
                                children: [
                                  Container(
                                      width: 72,
                                      height: 72,
                                      //设置了 decoration 就不能设置color,两者只能存在一个
                                      decoration: const BoxDecoration(
                                          boxShadow: [
                                            BoxShadow(
                                                color: Colors.black26,
                                                offset: Offset(0, 0.0),
                                                //阴影y轴偏移量
                                                blurRadius: 2,
                                                //阴影模糊程度
                                                spreadRadius: 1 //阴影扩散程度
                                                )
                                          ],
                                          image: DecorationImage(
                                              image: AssetImage(
                                                  "assets/images/cha4.jpg")),
                                          //设置图片
                                          borderRadius: BorderRadius.all(
                                              Radius.circular(6)))),
                                  Padding(
                                    padding: const EdgeInsets.only(left: 12),
                                    child: SizedBox(
                                      height: 72,
                                      child: Column(
                                        crossAxisAlignment:
                                            CrossAxisAlignment.start,
                                        mainAxisAlignment:
                                            MainAxisAlignment.spaceAround,
                                        children: const [
                                          Text("吸鼠霸王",
                                              style: TextStyle(
                                                  fontWeight: FontWeight.w600,
                                                  fontSize: 18,
                                                  color: Colors.white)),
                                          Text("UID:hahaha_666",
                                              style: TextStyle(
                                                  fontWeight: FontWeight.w600,
                                                  fontSize: 12,
                                                  color: Colors.orange))
                                        ],
                                      ),
                                    ),
                                  )
                                ],
                              )),
                        ],
                      );
                    }),
                  );
                },
              ),

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值