如何在 SliverAppBar 和 SliverList 上堆叠 Circle Avatar? (推特可滚动标题动画)

问题描述 投票:0回答:1


我想实现上面的设计

我用条子做的有点像这个


import 'package:flutter/material.dart';

class ProfilePage extends StatefulWidget {
  @override
  _ProfilePageState createState() => _ProfilePageState();
}

class _ProfilePageState extends State<ProfilePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
              child: DefaultTabController(
          length: 4,
          child: NestedScrollView(
            body: TabBarView(children: [
             //pages of tabBar
            ]),
            headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
              return [
              SliverAppBar(
//for the pinned appBar
               
                                elevation: 0.0,
                                backgroundColor: Colors.red,
                                leading:   Padding(
                                  padding: const EdgeInsets.all(8.0),
                                  child: CircleAvatar(backgroundColor: Colors.red.withOpacity(0.4),
                                  child: Icon(Icons.arrow_back,color: Colors.white,),
                                  ),
                                ),
                pinned: true,
                flexibleSpace: FlexibleSpaceBar(
                      background: Image.network(
                    "https://images.pexels.com/photos/396547/pexels-photo-396547.jpeg?auto=compress&cs=tinysrgb&h=350",
                    fit: BoxFit.cover,
                  ),
                
                ),
                expandedHeight: 150,
              ),
            
                SliverList(delegate: SliverChildListDelegate([
                 // for bio and other stuff
                ])),
                SliverAppBar(
                  toolbarHeight: 0.0,
                  primary: false,
                  pinned: true,
                  bottom: TabBar(tabs: [
                  // tab bars
                  ],),
                )
              ];
            },
          ),
        ),
      ),
    );
  }
}


我尝试在我的条子列表中添加堆栈,但 Circle Avatar 显示在应用栏后面。
我们如何才能实现这样的设计,或者实现它的替代方法是什么。

flutter flutter-animation flutter-sliver sliverappbar
1个回答
0
投票

我通过使用 SliverPersistentHeader + 裁剪标题图像来完成设计。

import 'dart:ui';

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

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

const redOrange = Color(0xffb93303);

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: CustomScrollView(
        physics: const ClampingScrollPhysics(),
        slivers: [
          SliverPersistentHeader(
            pinned: true,
            delegate: ExampleAppBar(),
          ),
          SliverFillRemaining(
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 20),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: const [
                  SizedBox(height: 20),
                  Text(
                    'NASA',
                    style: TextStyle(
                      color: Colors.white70,
                      fontSize: 20,
                      fontWeight: FontWeight.w800,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class ExampleAppBar extends SliverPersistentHeaderDelegate {
  final bottomHeight = 60;
  final extraRadius = 5;
  @override
  Widget build(context, shrinkOffset, overlapsContent) {
    final imageTop =
        -shrinkOffset.clamp(0.0, maxExtent - minExtent - bottomHeight);

    final double clowsingRate = (shrinkOffset == 0
            ? 0.0
            : (shrinkOffset / (maxExtent - minExtent - bottomHeight)))
        .clamp(0, 1);

    final double opacity = shrinkOffset == minExtent
        ? 0
        : 1 - (shrinkOffset.clamp(minExtent, minExtent + 30) - minExtent) / 30;

    return Stack(
      children: [
        Positioned(
          bottom: 0,
          right: 20,
          left: 45,
          child: Row(
            children: [
              Transform.scale(
                scale: 1.9 - clowsingRate,
                alignment: Alignment.bottomCenter,
                child: const _Avatar(),
              ),
              const Spacer(),
              const _Button(),
            ],
          ),
        ),
        Positioned(
          top: imageTop,
          left: 0,
          right: 0,
          child: ClipPath(
            clipper: InvertedCircleClipper(
              radius: (1.9 - clowsingRate) * bottomHeight / 2 + extraRadius,
              offset: Offset(
                bottomHeight / 2 + 45,
                (maxExtent - bottomHeight + extraRadius / 2) +
                    clowsingRate * bottomHeight / 2,
              ),
            ),
            child: SizedBox(
              height: maxExtent - bottomHeight,
              child: ColoredBox(
                color: redOrange,
                child: Opacity(
                  opacity: opacity,
                  child: Image.network(
                    'https://i.ibb.co/RNCkztc/PIA24343-03-Mars-Perseverance-Landing-Graphics-Orange-Rover.webp',
                    fit: BoxFit.cover,
                  ),
                ),
              ),
            ),
          ),
        ),
        Positioned(
            top: MediaQuery.of(context).padding.top + 5,
            left: 10,
            right: 10,
            child: Row(
              children: const [
                _IconButton(
                  icon: Icons.arrow_back,
                ),
                Spacer(),
                _IconButton(
                  icon: Icons.more_vert,
                ),
              ],
            )),
      ],
    );
  }

  @override
  double get maxExtent => 250;

  @override
  double get minExtent => 100;

  @override
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
      true;
}

class _IconButton extends StatelessWidget {
  const _IconButton({
    required this.icon,
  });

  final IconData icon;

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: redOrange.withOpacity(0.5),
      ),
      padding: const EdgeInsets.all(4),
      child: Icon(
        icon,
        color: Colors.white,
      ),
    );
  }
}

class _Button extends StatelessWidget {
  const _Button();

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(50),
        border: Border.all(color: Colors.blueAccent, width: 2),
      ),
      child: const Text(
        'Follow',
        style: TextStyle(
          color: Colors.blueAccent,
          fontWeight: FontWeight.w700,
        ),
      ),
    );
  }
}

class InvertedCircleClipper extends CustomClipper<Path> {
  const InvertedCircleClipper({
    required this.offset,
    required this.radius,
  });
  final Offset offset;
  final double radius;

  @override
  Path getClip(size) {
    return Path()
      ..addOval(Rect.fromCircle(
        center: offset,
        radius: radius,
      ))
      ..addRect(Rect.fromLTWH(0.0, 0.0, size.width, size.height))
      ..fillType = PathFillType.evenOdd;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) => true;
}

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

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 60,
      width: 60,
      decoration: BoxDecoration(
        border: Border.all(
          color: Colors.blueAccent,
          width: 1.5,
        ),
        borderRadius: BorderRadius.circular(50),
      ),
      padding: const EdgeInsets.all(2),
      child: Image.network(
        'https://i.ibb.co/YdTh4Xx/logo.png',
      ),
    );
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.