首先来看看酷安v10的个人资料页面,下图。

coolapk 个人资料 动图

再来看看我们使用 Flutter 实现的页面。

social project 个人资料页面 动图 (DEBUG 测试 v2)
social project 个人资料页面 动图 (DEBUG 测试 v2)

关于如何在Flutter内使用可伸缩appbar的具体方法,可以在百度上搜索,我在这里推荐此篇文章:https://blog.csdn.net/u011272795/article/details/82740389


如果你使用过 Appbar,那么想必一定知道 flexibleSpace。flexibleSpace 配合 expandedHeight 属性,可以实现 appbar 内嵌 widdget(一般用于内嵌图像)。

social project 个人资料卡 appbar 背景图

在这里,我们在 FlexibleSpaceBar 下的 background 属性传入一个 Stack() 用于叠加 widget。 (代码如下,使用时请调整相关属性,SliverAppBar 下调整一个属性 expandedHeight: 350.0) 代码我就不做解释了😂,大家都看得懂。

Stack(
            children: <Widget>[
              Image.network(
                "IMAGE.jpg",
                fit: BoxFit.cover,
                height: 430,
              ),
              Padding(
                padding: EdgeInsets.fromLTRB(20, 82, 20, 0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: <Widget>[
                        WpUserHeader(
                          radius: 45,
                          canClick: false,
                          userId: widget.wpUserId,
                          showUserName: false,
                          wpSource: WordPressRep.wpSource,
                        ),
                        Row(
                          mainAxisAlignment: MainAxisAlignment.end,
                          children: <Widget>[
                            RaisedButton(
                              padding: EdgeInsets.all(10.0),
                              shape: StadiumBorder(),
                              child: Text(
                                "  编辑个人资料  ",
                                style: TextStyle(color: Colors.white),
                              ),
                              color: Color.fromARGB(50, 255, 255, 255),
                              elevation: 0,
                              onPressed: () {},
                            ),
                            RaisedButton(
                              padding: EdgeInsets.all(10.0),
                              shape: CircleBorder(),
                              child: Icon(
                                Icons.camera,
                                color: Colors.white,
                              ),
                              color: Color.fromARGB(50, 255, 255, 255),
                              elevation: 0,
                              onPressed: () {},
                            ),
                          ],
                        ),
                      ],
                    ),
                    SizedBox(
                      height: 20,
                    ),
                    Text(_wpUser.name,
                        style: TextStyle(fontSize: 25, color: Colors.white)),
                    SizedBox(
                      height: 15,
                    ),
                    Text(
                      "user_mail@mail.mail",
                      style: TextStyle(color: Colors.white, fontSize: 15),
                    ),
                    SizedBox(
                      height: 15,
                    ),
                    Text(
                      "这里是个人签名。这里是个人签名。这里是个人签名。",
                      style: TextStyle(color: Colors.white, fontSize: 15),
                    ),
                    Text(
                      "这里是个人签名。这里是个人签名。这里是个人签名。",
                      style: TextStyle(color: Colors.white, fontSize: 15),
                    ),
                    Text(
                      "这里是个人签名。这里是个人签名。这里是个人签名。",
                      style: TextStyle(color: Colors.white, fontSize: 15),
                    ),
                  ],
                ),
              )
            ],
          )

添加完成后你会发现似乎就这么完成了(本贴终结?)。。。当然是不可能的。😂


我们可以看到在滑动时,酷安的个人资料是随着滑动而不做任何附带操作。而我们这个则是完全跟着背景图像,为此我们需要改变 FlexibleSpaceBar 。下面贴改过的代码

// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math' as math;
import 'dart:ui' as ui;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

class CustomFlexibleSpaceBar extends StatefulWidget {
  const CustomFlexibleSpaceBar({
    Key key,
    this.title,
    this.background,
    this.centerTitle,
    this.titlePadding,
    this.appbarColor,
    this.callBack,
    this.onImageTap,
    this.collapseMode = CollapseMode.parallax,
    this.stretchModes = const <StretchMode>[StretchMode.zoomBackground],
  })  : assert(collapseMode != null),
        super(key: key);

  final Widget title;

  final Color appbarColor;

  final Widget background;

  final bool centerTitle;

  final CollapseMode collapseMode;

  final List<StretchMode> stretchModes;

  final EdgeInsetsGeometry titlePadding;

  static Widget createSettings({
    double toolbarOpacity,
    double minExtent,
    double maxExtent,
    @required double currentExtent,
    @required Widget child,
  }) {
    assert(currentExtent != null);
    return FlexibleSpaceBarSettings(
      toolbarOpacity: toolbarOpacity ?? 1.0,
      minExtent: minExtent ?? currentExtent,
      maxExtent: maxExtent ?? currentExtent,
      currentExtent: currentExtent,
      child: child,
    );
  }

  final Function(double opacity) callBack;
  final Function onImageTap;

  @override
  _CustomFlexibleSpaceBarState createState() => _CustomFlexibleSpaceBarState();
}

class _CustomFlexibleSpaceBarState extends State<CustomFlexibleSpaceBar> {
  bool _getEffectiveCenterTitle(ThemeData theme) {
    if (widget.centerTitle != null) return widget.centerTitle;
    assert(theme.platform != null);
    switch (theme.platform) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        return false;
      case TargetPlatform.iOS:
        return true;
    }
    return null;
  }

  Alignment _getTitleAlignment(bool effectiveCenterTitle) {
    if (effectiveCenterTitle) return Alignment.bottomCenter;
    final TextDirection textDirection = Directionality.of(context);
    assert(textDirection != null);
    switch (textDirection) {
      case TextDirection.rtl:
        return Alignment.bottomRight;
      case TextDirection.ltr:
        return Alignment.bottomLeft;
    }
    return null;
  }

  double _getCollapsePadding(double t, FlexibleSpaceBarSettings settings) {
    switch (widget.collapseMode) {
      case CollapseMode.pin:
        return -(settings.maxExtent - settings.currentExtent);
      case CollapseMode.none:
        return 0.0;
      case CollapseMode.parallax:
        final double deltaExtent = settings.maxExtent - settings.minExtent;
        return -Tween<double>(begin: 0.0, end: deltaExtent / 4.0).transform(t);
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
        builder: (BuildContext context, BoxConstraints constraints) {
      final FlexibleSpaceBarSettings settings = context
          .dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
      assert(
        settings != null,
        'A FlexibleSpaceBar must be wrapped in the widget returned by FlexibleSpaceBar.createSettings().',
      );

      final List<Widget> children = <Widget>[];

      final double deltaExtent = settings.maxExtent - settings.minExtent;

      // 0.0 -> Expanded
      // 1.0 -> Collapsed to toolbar
      final double t =
          (1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent)
              .clamp(0.0, 1.0);

      // background
      if (widget.background != null) {
        final double fadeStart =
            math.max(0.0, 1.0 - kToolbarHeight / deltaExtent);
        const double fadeEnd = 1.0;
        assert(fadeStart <= fadeEnd);
        final double opacity = 1.0 - Interval(fadeStart, fadeEnd).transform(t);
        if (opacity > 0.0) {
          double height = settings.maxExtent;

          // StretchMode.zoomBackground
          if (widget.stretchModes.contains(StretchMode.zoomBackground) &&
              constraints.maxHeight > height) {
            height = constraints.maxHeight;
          }

          final Stack bg = widget.background;

          final double deltaExtent = settings.maxExtent - settings.minExtent;

          var top =
              -Tween<double>(begin: 0.0, end: deltaExtent / 4.0).transform(t);

//              children.add(
//                Positioned(
//                  top: top,
//                  left: 0.0,
//                  right: 0.0,
//                  height: height,
//                  child: bg.children[0],
//                ),
//              );
//
//              children.add(
//                Opacity(
//                  opacity: Tween<double>(begin: 0.4, end: 0.8).transform(t),
//                  child: Container(
//                    color: Colors.black,
//                  ),
//                ),
//              );
//
//              children.add(
//                Positioned(
//                  top: -(settings.maxExtent - settings.currentExtent),
//                  left: 0.0,
//                  right: 0.0,
//                  height: height,
//                  child: Opacity(
//                    opacity: Tween<double>(begin: 1, end: 0).transform(t),
//                    child: bg.children[1],
//                  ),
//                ),
//              );

          widget.callBack(t);

          children.add(Stack(
            children: <Widget>[
//              Positioned(
//                top: _getCollapsePadding(t, settings),
//                left: 0.0,
//                right: 0.0,
//                height: height,
//                child: Opacity(
//                  opacity: opacity,
//                  child: widget.background,
//                ),
//              )

              Positioned(
                top: top,
                left: 0.0,
                right: 0.0,
                height: height,
                child: Opacity(
                  opacity: opacity,
                  child: Stack(
                    children: <Widget>[
                      bg.children[0],
                      InkWell(
                        child: Container(
                          color: Color.fromARGB(
                              Tween<double>(begin: 150.0, end: 230.0)
                                  .transform(t)
                                  .round(),
                              0,
                              0,
                              0),
                        ),
                        onTap: widget.onImageTap,
                      ),
                    ],
                  ),
                ),
              ),

              Positioned(
                top: -(settings.maxExtent - settings.currentExtent),
                left: 0.0,
                right: 0.0,
                height: height,
                child: Opacity(
                  opacity: Tween<double>(begin: 1, end: 0).transform(t),
                  child: bg.children[1],
                ),
              ),
            ],
          ));

          // StretchMode.blurBackground
          if (widget.stretchModes.contains(StretchMode.blurBackground) &&
              constraints.maxHeight > settings.maxExtent) {
            final double blurAmount =
                (constraints.maxHeight - settings.maxExtent) / 10;
            children.add(Positioned.fill(
                child: BackdropFilter(
                    child: Container(
                      color: Colors.transparent,
                    ),
                    filter: ui.ImageFilter.blur(
                      sigmaX: blurAmount,
                      sigmaY: blurAmount,
                    ))));
          }
        }
      }

      // title
      if (widget.title != null) {
        final ThemeData theme = Theme.of(context);

        Widget title;
        switch (theme.platform) {
          case TargetPlatform.iOS:
            title = widget.title;
            break;
          case TargetPlatform.fuchsia:
          case TargetPlatform.android:
            title = Semantics(
              namesRoute: true,
              child: widget.title,
            );
        }

        // StretchMode.fadeTitle
        if (widget.stretchModes.contains(StretchMode.fadeTitle) &&
            constraints.maxHeight > settings.maxExtent) {
          final double stretchOpacity = 1 -
              ((constraints.maxHeight - settings.maxExtent) / 100)
                  .clamp(0.0, 1.0);
          title = Opacity(
            opacity: stretchOpacity,
            child: title,
          );
        }

        final double opacity = settings.toolbarOpacity;
        if (opacity > 0.0) {
          TextStyle titleStyle = theme.primaryTextTheme.title;
          titleStyle =
              titleStyle.copyWith(color: titleStyle.color.withOpacity(opacity));
          final bool effectiveCenterTitle = _getEffectiveCenterTitle(theme);
          final EdgeInsetsGeometry padding = widget.titlePadding ??
              EdgeInsetsDirectional.only(
                start: effectiveCenterTitle ? 0.0 : 72.0,
                bottom: 16.0,
              );
          final double scaleValue =
              Tween<double>(begin: 1.5, end: 1.0).transform(t);
          final Matrix4 scaleTransform = Matrix4.identity()
            ..scale(scaleValue, scaleValue, 1.0);
          final Alignment titleAlignment =
              _getTitleAlignment(effectiveCenterTitle);
          children.add(Container(
            padding: padding,
            child: Transform(
              alignment: titleAlignment,
              transform: scaleTransform,
              child: Align(
                alignment: titleAlignment,
                child: DefaultTextStyle(
                  style: titleStyle,
                  child: title,
                ),
              ),
            ),
          ));
        }
      }

      return ClipRect(child: Stack(children: children));
    });
  }
}

下面说下改动:

  • final Color appbarColor;( appbarColor 传入 Color ,为了与 appbar 背景色保持一致, )
  • final Function(double opacity) callBack; (图片 opacity 不透明度发生改变时回调)
  • final Function onImageTap; (背景图被点击回调)

上面是字段,一下是代码的变动:

      final List<Widget> children = <Widget>[];

      final double deltaExtent = settings.maxExtent - settings.minExtent;

      // 0.0 -> Expanded
      // 1.0 -> Collapsed to toolbar
      final double t =
          (1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent)
              .clamp(0.0, 1.0);

      // background
      if (widget.background != null) {
        final double fadeStart =
            math.max(0.0, 1.0 - kToolbarHeight / deltaExtent);
        const double fadeEnd = 1.0;
        assert(fadeStart <= fadeEnd);
        final double opacity = 1.0 - Interval(fadeStart, fadeEnd).transform(t);
        if (opacity > 0.0) {
          double height = settings.maxExtent;

          // StretchMode.zoomBackground
          if (widget.stretchModes.contains(StretchMode.zoomBackground) &&
              constraints.maxHeight > height) {
            height = constraints.maxHeight;
          }

          final Stack bg = widget.background;

          final double deltaExtent = settings.maxExtent - settings.minExtent;
          var top =
              -Tween<double>(begin: 0.0, end: deltaExtent / 4.0).transform(t);

//              children.add(
//                Positioned(
//                  top: top,
//                  left: 0.0,
//                  right: 0.0,
//                  height: height,
//                  child: bg.children[0],
//                ),
//              );
//
//              children.add(
//                Opacity(
//                  opacity: Tween<double>(begin: 0.4, end: 0.8).transform(t),
//                  child: Container(
//                    color: Colors.black,
//                  ),
//                ),
//              );
//
//              children.add(
//                Positioned(
//                  top: -(settings.maxExtent - settings.currentExtent),
//                  left: 0.0,
//                  right: 0.0,
//                  height: height,
//                  child: Opacity(
//                    opacity: Tween<double>(begin: 1, end: 0).transform(t),
//                    child: bg.children[1],
//                  ),
//                ),
//              );

          widget.callBack(t);

          children.add(Stack(
            children: <Widget>[
//              Positioned(
//                top: _getCollapsePadding(t, settings),
//                left: 0.0,
//                right: 0.0,
//                height: height,
//                child: Opacity(
//                  opacity: opacity,
//                  child: widget.background,
//                ),
//              )

              Positioned(
                top: top,
                left: 0.0,
                right: 0.0,
                height: height,
                child: Opacity(
                  opacity: opacity,
                  child: bg.children[0],
                ),
              ),

              InkWell(
                child: Opacity(
                  opacity: opacity,
                  child: Container(
                    color: Color.fromARGB(
                        Tween<double>(begin: 150.0, end: 230.0)
                            .transform(t)
                            .round(),
                        0,
                        0,
                        0),
                  ),
                ),
                onTap: widget.onImageTap,
              ),

              Positioned(
                top: -(settings.maxExtent - settings.currentExtent),
                left: 0.0,
                right: 0.0,
                height: height,
                child: Opacity(
                  opacity: Tween<double>(begin: 1, end: 0).transform(t),
                  child: bg.children[1],
                ),
              ),
            ],
          ));
// .......
        }
      }

final Stack bg = widget.background; 类型转换。 FlexibleSpaceBar 最终返回的 (return ClipRect(child: Stack(children: children));
)就是一个Stack,其中所有 widget 都在 children 里面。所以我们在原来处理 background 属性的地方进行了修改。由于时间问题等各种原因(懒)我们就把 background 转为 Stack,其中限制为两个子 Widget

  • A. 背景图 (Image.asset 等)
  • B. 个人资料布局 (Stack, Column 等)

当我们在把 background 加入 children 中,我们在上述 A、B 两块 Widget 中加入一个纯色背景并设置不透明度,覆盖在背景图之上,以便于调整对比度,是的个人资料布局观感更好。

InkWell(
child: Opacity(
opacity: opacity,
child: Container(
color: Color.fromARGB(
Tween<double>(begin: 150.0, end: 230.0)
.transform(t)
.round(),
0,
0,
0),
),
),
onTap: widget.onImageTap,
),

现在,background 内的 Widget 情况:

  • A. 背景图 (Image.asset 等)
  • B. 半透明纯色(半透明黑)
  • C. 个人资料布局 (Stack, Column 等)

解释完布局之后,再说说滑动时的动画。

位移:(parallax)

      final FlexibleSpaceBarSettings settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();      
      final double deltaExtent = settings.maxExtent - settings.minExtent;
      // 0.0 -> Expanded
      // 1.0 -> Collapsed to toolbar
      final double t = (1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent).clamp(0.0, 1.0);
      final double deltaExtent = settings.maxExtent - settings.minExtent;
      var top = -Tween<double>(begin: 0.0, end: deltaExtent / 4.0).transform(t);

位移:(pin)

final FlexibleSpaceBarSettings settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();    
var top = -(settings.maxExtent - settings.currentExtent);

不透明度:(滑动到一定程度开始慢慢变透明)

final FlexibleSpaceBarSettings settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();      
final double deltaExtent = settings.maxExtent - settings.minExtent;
// 0.0 -> Expanded
// 1.0 -> Collapsed to toolbar
final double t = (1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent).clamp(0.0, 1.0);
final double fadeStart = math.max(0.0, 1.0 - kToolbarHeight / deltaExtent);
const double fadeEnd = 1.0;
assert(fadeStart <= fadeEnd);
final double opacity = 1.0 - Interval(fadeStart, fadeEnd).transform(t);

不透明度:(随着滑动慢慢变得透明)

final double opacity = Tween<double>(begin: 1, end: 0).transform(t);

上述代码都有些共同字段:

  • settings
  • deltaExtent
  • t

阅读源码可知 setting 由 appbar 派发下来 (提供尺寸和不透明度信息。

deltaExtent 表明 FlexibleSpaceBar的最大大小

t注释表明这是一个折叠是的变量,在值为 0 时为展开式,在为1时为折叠。


先让我们来看看 FlexibleSpaceBar 的属性 collapseMode 设置为 parallax 和 pin的效果:

实现这一个现象的方法就是 _FlexibleSpaceBarState 类下的 _getCollapsePadding。

double _getCollapsePadding(double t, FlexibleSpaceBarSettings settings) {
switch (widget.collapseMode) {
case CollapseMode.pin:
return -(settings.maxExtent - settings.currentExtent);
case CollapseMode.none:
return 0.0;
case CollapseMode.parallax:
final double deltaExtent = settings.maxExtent - settings.minExtent;
return -Tween<double>(begin: 0.0, end: deltaExtent / 4.0).transform(t);
}
return null;
}

它根据参数 t、settings (上述已经解释过了)和 widget.collapseMode 来返回一个浮点数,以便控制 background 的运动。

我们发现在我们想要实现的效果中,这两种模式似乎都需要,这就是我们为什么在 children 下分 A B C 三个 Widget 。

  • A. 背景图 parallax
  • B. 半透明纯色 parallax (我们在这没有对它进行位置移动,懒得改前面文字了(我在下面改了), 如果你们需要可以自行变更,或者将这个半透明纯色与背景图叠加(内部再套个 Stack, 省的重写位置移动))
  • C. 个人资料布局 pin

下面说说不透明度,先列出三个 widget 的不透明度变化方式:

  • A. 背景图与半透明纯色 :滑动到一定程度开始慢慢变透明 (此处必须变透明,否在在滑动时会导致appbar 与 FlexibleSpaceBar 过渡生硬,如果有更好的方法还请留言探讨😂)
  • B. 个人资料布局:慢慢变透明
// A 和 B (背景图和半透明纯色)
// 在这里我把 A 和 B 叠加在一起,(为了方便位置移动,共用一个 Positioned )
Positioned(
  top: top, // pin
  left: 0.0,
  right: 0.0,
  height: height,
  child: Opacity(
    opacity: opacity, // 参上述不透明度相关代码
    child: Stack(
      children: <Widget>[
        bg.children[0],
        InkWell(
          child: Container(
            color: Color.fromARGB(
                Tween<double>(begin: 150.0, end: 230.0)
                    .transform(t)
                    .round(),
                0,
                0,
                0),
          ),
          onTap: widget.onImageTap,
        ),
      ],
    ),
  ),
),

// C (个人资料布局)
Positioned(
  top: -(settings.maxExtent - settings.currentExtent), // pin
  left: 0.0,
  right: 0.0,
  height: height,
  child: Opacity(
    opacity: Tween<double>(begin: 1, end: 0).transform(t),  // 参上述不透明度相关代码 
    child: bg.children[1],
  ),
),


August

Android 开发者、影视后期内容(包装)制作者、Unity 2D游戏开发者

2 条评论

Royal CBD · 2020年5月15日 3:09 上午

Hi there! This is my 1st comment here so I
just wanted to give a quick shout out and say I really enjoy reading your posts.
Can you recommend any other blogs/websites/forums that cover the same topics?
Many thanks!

    August · 2020年5月15日 9:06 上午

    Thank you for your support, please indicate the original address in the article

发表评论

电子邮件地址不会被公开。 必填项已用*标注