Abhishek Raj
Abhishek Raj

Reputation: 11

How can we implement something like this in flutter?

I want to build something like this in flutter.

snap

I have tried using slivers but was not able to get the desired output as the animation goes to the top for tapped widget .

Key ideal features are followings.

1.Behave like list view, but cards should be stacked at the top of screen.

2.The tapped list should be selected and displayed as main card as in the attached image.

import 'dart:math' as math;

class FlashCards extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
          appBar: AppBar(
            title: Text("Flash Cards"),
            backgroundColor: Colors.black,
          ),
          body: StackedList()),
    );
  }
}

class StackedList extends StatefulWidget {
  @override
  _StackedListState createState() => _StackedListState();
}

class _StackedListState extends State<StackedList> {
  static const _minHeight = 16.0;
  //var _maxHeight = 120.0;
  bool _tapped = false;
  final List<Color> _colors = Colors.primaries;

  @override
  Widget build(BuildContext context) => CustomScrollView(
        slivers: _colors
            .map(
              (color) => StackedListChild(
                minHeight: _minHeight,
                maxHeight: _tapped
                    ? (_colors.indexOf(color) == _colors.length - 1
                        ? MediaQuery.of(context).size.height
                        : 500)
                    : (_colors.indexOf(color) == _colors.length - 1
                        ? MediaQuery.of(context).size.height
                        : 120.0),
                pinned: true,
                child: GestureDetector(
                  child: Container(
                    color: _colors.indexOf(color) == 0
                        ? Colors.black
                        : _colors[_colors.indexOf(color) - 1],
                    child: Container(
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.vertical(
                            top: Radius.circular(_minHeight)),
                        color: color,
                      ),
                    ),
                  ),
                  onTap: () => setState(() {
                    _tapped = !_tapped;
                  }),
                  //print(_colors.indexOf(color).toString() + "Tapped"),
                ),
              ),
            )
            .toList(),
      );
}

class StackedListChild extends StatelessWidget {
  final double minHeight;
  final double maxHeight;
  final bool pinned;
  final bool floating;
  final Widget child;

  SliverPersistentHeaderDelegate get _delegate => _StackedListDelegate(
      minHeight: minHeight, maxHeight: maxHeight, child: child);

  const StackedListChild({
    Key key,
    @required this.minHeight,
    @required this.maxHeight,
    @required this.child,
    this.pinned = false,
    this.floating = false,
  })  : assert(child != null),
        assert(minHeight != null),
        assert(maxHeight != null),
        assert(pinned != null),
        assert(floating != null),
        super(key: key);

  @override
  Widget build(BuildContext context) => SliverPersistentHeader(
      key: key, pinned: pinned, floating: floating, delegate: _delegate);
}

class _StackedListDelegate extends SliverPersistentHeaderDelegate {
  final double minHeight;
  final double maxHeight;
  final Widget child;

  _StackedListDelegate({
    @required this.minHeight,
    @required this.maxHeight,
    @required this.child,
  });

  @override
  double get minExtent => minHeight;

  @override
  double get maxExtent => math.max(maxHeight, minHeight);

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return new SizedBox.expand(child: child);
  }

  @override
  bool shouldRebuild(_StackedListDelegate oldDelegate) {
    return maxHeight != oldDelegate.maxHeight ||
        minHeight != oldDelegate.minHeight ||
        child != oldDelegate.child;
  }
}

As in animation the tapped widget goes to the top and only that get expanded.

Upvotes: 1

Views: 912

Answers (1)

Kherel
Kherel

Reputation: 16185

To make design you want need to solve several tasks:

  1. Upper card has fixed position.
  2. Card have transparent corners and we can see previous card there.

How to solve it?

  1. Normally to do first task, you need to create your own sliver and change sliver geometry. Watch The boring show about slivers: https://www.youtube.com/watch?v=Mz3kHQxBjGg&t=1606s.

Problem with that is that SliverList is pretty complicated, especially if you are only beginning to learn flutter.

So my solution Is make a bit hacky.

We could use scroll position and get index of top visible card. After that hide it in the sliver list, but show it in stack widget. It would have same visible affect.

  1. In your code you are wrapping card with extra container to use color of previous card as background color, but you will have a problem using this approach when you will need to show two conners close to each over. The solution for it is to use SizedOverflowBox. SizedOverflowBox widget help you to render bigger widget in a place of smaller widget with visible overflow.

enter image description here

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

void main() => runApp(FlashCards());

class FlashCards extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
          appBar: AppBar(
            title: Text("Flash Cards"),
            backgroundColor: Colors.black,
          ),
          backgroundColor: Colors.black,
          body: StackedList()),
    );
  }
}

class StackedList extends StatefulWidget {
  @override
  _StackedListState createState() => _StackedListState();
}

const double closedCardSize = 100;
const double roundRadiusSize = 16;

class _StackedListState extends State<StackedList> {
  final List<Color> colors = Colors.primaries;
  ScrollController controller = ScrollController();
  int firstVisibleCardIndex = 0;

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  void initState() {
    controller.addListener(() {
      setState(
        () {
          firstVisibleCardIndex =
              (controller.offset / closedCardSize).floor();
        },
      );
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Container(
          height: closedCardSize + roundRadiusSize,
          decoration: BoxDecoration(
            borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
            color: colors[firstVisibleCardIndex],
          ),
        ),
        CustomScrollView(
          physics: ClampingScrollPhysics(),
          controller: controller,
          slivers: [
            SliverList(
              delegate: SliverChildBuilderDelegate(
                  (context, index) => StackedListChild(
                        istTransparent: index <= firstVisibleCardIndex,
                        color: colors[index],
                        key: ObjectKey(colors[index]),
                      ),
                  childCount: colors.length - (firstVisibleCardIndex + 1)),
            )
          ],
        )
      ],
    );
  }
}


class StackedListChild extends StatelessWidget {
  const StackedListChild({
    Key key,
    this.color,
    this.istTransparent = false,
  }) : super(key: key);

  final Color color;
  final bool istTransparent;

  @override
  Widget build(BuildContext context) {
    if (istTransparent) {
      return SizedBox(
        height: closedCardSize,
      );
    }
    return Transform.translate(
      offset: Offset(0, roundRadiusSize / 2),
      child: SizedOverflowBox(
        size: Size(double.infinity, closedCardSize),
        child: Container(
          height: closedCardSize + roundRadiusSize,
          child: Container(
            decoration: BoxDecoration(
              borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
              color: color,
            ),
          ),
        ),
      ),
    );
  }
}

Upvotes: 3

Related Questions