[Flutter] card_list_page_view_example

김영진·2021년 12월 27일
1

Flutter 앱 개발 일기

목록 보기
21/31
post-custom-banner

목적

신규앱 카드 애니메이션 개발을 위한 애니메이션 예제 분석

설명이빈약합니다. 하단에 소스코드 있어용

내용

결과물

프로젝트 구조

lib
├── contact.dart
├── main.dart
├── model
│   └── contact.dart
├── my_app.dart
└── ui
    ├── contact_card.dart
    ├── home_page.dart
    └── perspective_list_view.dart

소스코드

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

import 'my_app.dart';

void main() {
  SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(statusBarColor: Colors.transparent));
  runApp(const MyApp());
}
my_app.dart
import 'package:animation/ui/home_page.dart';
import 'package:flutter/material.dart';

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: const Color(0xff23202a),
        appBarTheme: AppBarTheme(
            centerTitle: true, color: Colors.deepPurple[400], iconTheme: const IconThemeData(color: Colors.white70)),
        textTheme: const TextTheme(bodyText2: TextStyle(color: Color(0xff303030))),
        iconTheme: const IconThemeData(color: Colors.grey),
      ),
      home: const ContactListPage(),
    );
  }
}
model/contact.dart
class Contact {
  final String name;
  final String role;
  final String address;
  final String phone;
  final String email;
  final String website;

  const Contact(this.name, this.role, this.address, this.phone, this.email, this.website);

  static const contacts = [
    Contact('김영진', '개발자', '서울', '010-1111-1111', 'aaa@aaa.aa', 'https://www.aa.com'),
    Contact('김영진', '개발자', '서울', '010-1111-1111', 'aaa@aaa.aa', 'https://www.aa.com'),
    Contact('김영진', '개발자', '서울', '010-1111-1111', 'aaa@aaa.aa', 'https://www.aa.com'),
    Contact('김영진', '개발자', '서울', '010-1111-1111', 'aaa@aaa.aa', 'https://www.aa.com'),
    Contact('김영진', '개발자', '서울', '010-1111-1111', 'aaa@aaa.aa', 'https://www.aa.com'),
    Contact('김영진', '개발자', '서울', '010-1111-1111', 'aaa@aaa.aa', 'https://www.aa.com'),
    Contact('김영진', '개발자', '서울', '010-1111-1111', 'aaa@aaa.aa', 'https://www.aa.com'),
    ...
  ];
}
home_page.dart
import 'package:animation/ui/contact_card.dart';
import 'package:animation/ui/perspective_list_view.dart';
import 'package:flutter/material.dart';

import '../model/contact.dart';

class ContactListPage extends StatelessWidget {
  const ContactListPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('템플릿 겔러리'),
        leading: IconButton(onPressed: () {}, icon: const Icon(Icons.menu)),
        actions: [
          IconButton(onPressed: () {}, icon: const Icon(Icons.search)),
        ],
        shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(20))),
      ),
      body: PerspectiveListView(
	// 보여지는 카드 개수
        visualizedItems: 10,
        // 카드 높이
        itemExtent: MediaQuery.of(context).size.height * 0.33,
        // 시작 인덱스
        initialIndex: 7,
        // 쉐도우
        backItemsShadowColor: Theme.of(context).scaffoldBackgroundColor,
        // 패딩
        padding: const EdgeInsets.all(10),
        // 카드 리스트
        children: List.generate(Contact.contacts.length, (index) {
          final contact = Contact.contacts[index];
          final borderColor = Colors.accents[index % Colors.accents.length];
          return ContactCard(borderColor: borderColor, contact: contact);
        }),
      ),
    );
  }
}
contact_card.dart
import 'package:animation/model/contact.dart';
import 'package:flutter/material.dart';

class ContactCard extends StatelessWidget {
  const ContactCard({Key? key, required this.borderColor, required this.contact}) : super(key: key);

  final Color borderColor;
  final Contact contact;

  
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // Card tab
        Align(
          heightFactor: .9,
          alignment: Alignment.centerLeft,
          child: Container(
            height: 30,
            width: 70,
            decoration:
                BoxDecoration(color: borderColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(10))),
            child: const Icon(
              Icons.add,
              color: Colors.white,
            ),
          ),
        ),
        // Card Body
        Expanded(
          child: Container(
            width: double.infinity,
            padding: const EdgeInsets.all(15),
            decoration: BoxDecoration(
                color: borderColor,
                borderRadius: const BorderRadius.only(
                    bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20), topRight: Radius.circular(20))),
            child: Container(
              decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10)),
              padding: const EdgeInsets.all(10),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // Name and Role
                  Row(
                    children: [
                      Icon(Icons.person_outline, size: 40),
                      const SizedBox(width: 10),
                      Flexible(child: Text.rich(TextSpan(text: '\n${contact.role}', style: TextStyle())))
                    ],
                  )
                ],
              ),
            ),
          ),
        )
      ],
    );
  }
}
perspective_list_view.dart
class PerspectiveListView extends StatefulWidget {
  const PerspectiveListView({
    Key? key,
    required this.children,
    required this.itemExtent,
    required this.visualizedItems,
    this.initialIndex = 0,
    this.padding = const EdgeInsets.all(0),
    this.onTapFrontItem,
    this.onChangeItem,
    this.backItemsShadowColor = Colors.black,
  }) : super(key: key);

  final List<Widget> children;
  final double itemExtent;
  final int visualizedItems;
  final int initialIndex;
  final EdgeInsetsGeometry padding;
  final ValueChanged<int>? onTapFrontItem;
  final ValueChanged<int>? onChangeItem;
  final Color backItemsShadowColor;

  
  State<PerspectiveListView> createState() => _PerspectiveListViewState();
}
Perspective_list_view_state 선언부
class _PerspectiveListViewState extends State<PerspectiveListView> {
  late PageController _pageController;
  late double _pagePercent;
  late int _currentIndex;
Perspective_list_view_state initState

  void initState() {
    // 시작 인덱스 등록
    _pageController = PageController(initialPage: widget.initialIndex, viewportFraction: 1 / widget.visualizedItems);
    // 현재 인덱스 등록
    _currentIndex = widget.initialIndex;
    // 뭐하는 파라미터인지 아직 모르겠음
    _pagePercent = 0.0;
    _pageController.addListener(_pageListener);
    super.initState();
  }
dispose
 
  void dispose() {
    _pageController.removeListener(_pageListener);
    _pageController.dispose();
    super.dispose();
  }
_pageListener
void _pageListener() {
// 현재 페이지는 페이지컨트롤러의 페이지값을 내림함
// 페이지 컨트롤러는 0.0000000~xx.000000000의 실수임
// 따라서 내림하면 첫페이지는 0 두번째페이지는 1 이렇게 감
// pagePercent는 페이지값에서 현재 인덱스를 뺀값의 절댓값
// 페이지가 얼마나 이동했는지를 알려주는 값이네
    _currentIndex = _pageController.page!.floor();
    _pagePercent = (_pageController.page! - _currentIndex).abs();
    setState(() {});
  }
build

LayoutBuilder로 최대높이로 빌드
Stack을 활용하여 shadow 효과 주기


  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, constrains) {
      final height = constrains.maxHeight;
      return Stack(
        children: [
          // 예정된 페이지
          Padding(
            padding: widget.padding,
            child: _PerspectiveItems(
            // 화면세로크기의 0.33
              heightItem: widget.itemExtent,
              // 현재 인덱스
              currentIndex: _currentIndex,
              // 자식들
              children: widget.children,
              // 생성되는 아이템
              generateItems: widget.visualizedItems - 1,
              pagePercent: _pagePercent,
            ),
          ),
          // 뒤에있는 아이템 그림자
          Positioned.fill(
              child: DecoratedBox(
            decoration: BoxDecoration(
                gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [
              widget.backItemsShadowColor.withOpacity(0.8),
              widget.backItemsShadowColor.withOpacity(0)
            ])),
          )),

          // 비어있는 페이지
          PageView.builder(
            scrollDirection: Axis.vertical,
            controller: _pageController,
            physics: const BouncingScrollPhysics(),
            onPageChanged: (value) {
              if (widget.onChangeItem != null) {
                widget.onChangeItem!(value);
              }
            },
            itemBuilder: (context, index) {
              return const SizedBox();
            },
          ),
          // 아이템 탭하는 지역
          Positioned.fill(
            top: height - widget.itemExtent,
            child: GestureDetector(
              onTap: () {
                if (widget.onTapFrontItem != null) {
                  widget.onTapFrontItem!(_currentIndex);
                }
              },
            ),
          )
        ],
      );
    });
  }
}
_PerspectiveItems

  Widget build(BuildContext context) {
  // 높이 최대로 하고
    return LayoutBuilder(builder: (context, constrains) {
      final height = constrains.maxHeight;
      // expand 주고
      return Stack(
        fit: StackFit.expand,
        children: List.generate(generateItems, (index) {
        // 거꾸로된 인덱스 = 보여지는 아이템길이 - 처음꺼 - 나중꺼 - 현재인덱스
          final invertedIndex = (generateItems - 2) - index;
          // 인덱스 앞에꺼
          final indexPlus = index + 1;
          // 애니메이션 위치
          final positionPercent = indexPlus / generateItems;
          // 끝나는 애니메이션 위치
          final endPositionPercent = index / generateItems;
          // 현재 인덱스가 0 이면 
          return (currentIndex > invertedIndex)
              ? _TransformedItem(
                  child: children[currentIndex - (invertedIndex + 1)],
                  heightItem: heightItem,
                  factorChange: pagePercent,
                  scale: lerpDouble(0.5, 1.0, positionPercent)!,
                  endScale: lerpDouble(0.5, 1.0, endPositionPercent)!,
                  translateY: (height - heightItem) * positionPercent,
                  endTranslateY: (height - heightItem) * endPositionPercent,
                )
              : const SizedBox();
        })
          // 아래 아이템 숨기기(그냥 빡 뜨는게 아니라 아래에서 자연스럽게 올라옴
          ..add((currentIndex < children.length - 1)
              ? _TransformedItem(
                  child: children[currentIndex + 1],
                  heightItem: heightItem,
                  factorChange: pagePercent,
                  translateY: height + 20,
                  endTranslateY: height - heightItem,
                )
              : const SizedBox())
          // 상단 카드 고정하기
          ..insert(
              0,
              currentIndex > generateItems - 1
                  ? _TransformedItem(
                      child: children[currentIndex - generateItems],
                      heightItem: heightItem,
                      factorChange: 1.0,
                      endScale: 0.5,
                    )
                  : const SizedBox()),
      );
    });
  }

perspective_list_view.dart

class _TransformedItem extends StatelessWidget {
  const _TransformedItem({
    Key? key,
    required this.child,
    required this.heightItem,
    required this.factorChange,
    this.scale = 1.0,
    this.endScale = 1.0,
    this.translateY = 0.0,
    this.endTranslateY = 0.0,
  }) : super(key: key);

  final Widget child;
  final double heightItem;
  final double factorChange;
  final double scale;
  final double endScale;
  final double translateY;
  final double endTranslateY;

  
  Widget build(BuildContext context) {
    return Transform(
      alignment: Alignment.topCenter,
      transform: Matrix4.identity()
        ..scale(lerpDouble(scale, endScale, factorChange))
        ..translate(0.0, lerpDouble(translateY, endTranslateY, factorChange)!, 0.0),
      child: Align(
        alignment: Alignment.topCenter,
        child: SizedBox(
          height: heightItem,
          width: double.infinity,
          child: child,
        ),
      ),
    );
  }
}

결론

레이아웃 빌더 안에
스택 넣고
1. pageController.page값을 리슨해서 카드 스택 구성하고
2. pageView에 비어있는 위젯 넣어서 페이지 리슨만 시킴

소스코드

그 후

내가 만든 슬라이더 위젯!!!

핵심 소스만 첨부합니다!

import 'package:flutter/material.dart';
import 'package:withapp_did/certificate/model/cert_card.dart';

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

  
  State<CertCardListView> createState() => _CertCardListViewState();
}

class _CertCardListViewState extends State<CertCardListView> {
  late final PageController pageController;
  int currentPage = 0;
  late double percent = 0.0;
  String status = 'stacked';

  
  void initState() {
    pageController = PageController(viewportFraction: 1 / 3, initialPage: 0);
    pageController.addListener(pageListener);
    super.initState();
  }

  
  void dispose() {
    pageController.removeListener(pageListener);
    pageController.dispose();
    super.dispose();
  }

  void pageListener() {
    currentPage = pageController.page!.floor();
    percent = pageController.page! - currentPage.toDouble();
    setState(() {});
  }

  
  Widget build(BuildContext context) {
    return Expanded(
      child: Padding(
        padding: const EdgeInsets.only(bottom: 36),
        child: LayoutBuilder(builder: (context, constrains) {
          final cardWidth = constrains.maxWidth * 0.66;
          final cardHeight = constrains.maxHeight * 0.92;
          final cardTranslateX = (constrains.maxWidth - cardWidth) / 3;
          final cardTranslateY = (constrains.maxHeight - cardHeight) / 2;
          return Stack(
            fit: StackFit.expand,
            children: [
              Stack(
                fit: StackFit.loose,
                children: [
                  currentPage + percent + 3 < CertCard.certCards.length
                      ? Container(
                          margin: EdgeInsets.only(left: 24 + cardTranslateX * 2, top: cardTranslateY * 2),
                          width: cardWidth,
                          height: cardHeight,
                          decoration: BoxDecoration(
                            gradient:
                                const LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [
                              Color.fromRGBO(0, 196, 135, 1),
                              Color.fromRGBO(0, 90, 62, 1),
                            ]),
                            border: Border.all(color: const Color.fromRGBO(0, 155, 137, 1), width: 2),
                            borderRadius: const BorderRadius.all(Radius.circular(20)),
                          ),
                          child: Column(
                            children: [
                              Container(
                                padding: const EdgeInsets.only(left: 17, top: 11),
                                height: 140,
                                width: cardWidth,
                                child: Column(
                                  crossAxisAlignment: CrossAxisAlignment.stretch,
                                  children: const [
                                    Text(
                                      '팀 인증서',
                                      style: TextStyle(
                                          fontFamily: '210_MGothic',
                                          fontWeight: FontWeight.bold,
                                          fontSize: 16,
                                          letterSpacing: 0.8,
                                          color: Colors.black54),
                                    ),
                                    Text(
                                      '프로젝트 위드',
                                      style: TextStyle(
                                          fontFamily: '210_MGothic',
                                          fontWeight: FontWeight.w700,
                                          fontSize: 24,
                                          letterSpacing: -1.2,
                                          height: 1.5,
                                          color: Colors.black),
                                    ),
                                  ],
                                ),
                              ),
                              Expanded(
                                child: Stack(
                                  fit: StackFit.loose,
                                  alignment: Alignment.center,
                                  children: [
                                    Container(
                                      padding: const EdgeInsets.symmetric(vertical: 36),
                                      decoration: const BoxDecoration(
                                          gradient: SweepGradient(colors: [
                                            Color.fromRGBO(0, 90, 62, 1),
                                            Color.fromRGBO(0, 196, 135, 1),
                                            Color.fromRGBO(0, 90, 62, 1),
                                            Color.fromRGBO(0, 196, 135, 1),
                                            Color.fromRGBO(0, 90, 62, 1),
                                            Color.fromRGBO(0, 196, 135, 1),
                                            Color.fromRGBO(0, 90, 62, 1),
                                            Color.fromRGBO(0, 196, 135, 1),
                                            Color.fromRGBO(0, 90, 62, 1),
                                            Color.fromRGBO(0, 196, 135, 1),
                                            Color.fromRGBO(0, 90, 62, 1),
                                          ]),
                                          borderRadius: BorderRadius.vertical(
                                            bottom: Radius.circular(20),
                                          )),
                                      width: cardWidth,
                                    ),
                                    LayoutBuilder(builder: (context, constrains) {
                                      return Container(
                                        height: constrains.maxHeight / 2,
                                        width: constrains.maxHeight / 2,
                                        padding: EdgeInsets.all(constrains.maxHeight / 15),
                                        decoration: BoxDecoration(
                                            shape: BoxShape.circle,
                                            border: Border.all(color: Colors.black, width: 1.5)),
                                        child: Container(
                                          decoration: const BoxDecoration(
                                            color: Colors.black,
                                            shape: BoxShape.circle,
                                          ),
                                          child: const Icon(
                                            Icons.qr_code_rounded,
                                            size: 30,
                                          ),
                                        ),
                                      );
                                    })
                                  ],
                                ),
                              )
                            ],
                          ),
                        )
                      : const SizedBox(),
                  ...List.generate(2, (index) {
                    return currentPage + index < CertCard.certCards.length - 1
                        ? Transform.translate(
                            offset: Offset(cardTranslateX * (index) + cardTranslateX * (1 - percent),
                                cardTranslateY * (index) + cardTranslateY * (1 - percent)),
                            child: Container(
                              margin: const EdgeInsets.only(left: 24),
                              width: cardWidth,
                              height: cardHeight,
                              decoration: BoxDecoration(
                                gradient: const LinearGradient(
                                    begin: Alignment.topCenter,
                                    end: Alignment.bottomCenter,
                                    colors: [
                                      Color.fromRGBO(0, 196, 135, 1),
                                      Color.fromRGBO(0, 90, 62, 1),
                                    ]),
                                border: Border.all(color: const Color.fromRGBO(0, 155, 137, 1), width: 2),
                                borderRadius: const BorderRadius.all(Radius.circular(20)),
                              ),
                              child: Column(
                                children: [
                                  Container(
                                    padding: const EdgeInsets.only(left: 17, top: 11),
                                    height: 140,
                                    width: cardWidth,
                                    child: Column(
                                      crossAxisAlignment: CrossAxisAlignment.stretch,
                                      children: [
                                        Text(
                                          '팀 인증서',
                                          style: TextStyle(
                                              fontFamily: '210_MGothic',
                                              fontWeight: FontWeight.bold,
                                              fontSize: 16,
                                              letterSpacing: 0.8,
                                              color: Colors.black54),
                                        ),
                                        Text(
                                          CertCard.certCards[currentPage + 1].name,
                                          style: TextStyle(
                                              fontFamily: '210_MGothic',
                                              fontWeight: FontWeight.w700,
                                              fontSize: 24,
                                              letterSpacing: -1.2,
                                              height: 1.5,
                                              color: Colors.black),
                                        ),
                                      ],
                                    ),
                                  ),
                                  Expanded(
                                    child: Stack(
                                      fit: StackFit.loose,
                                      alignment: Alignment.center,
                                      children: [
                                        Container(
                                          padding: const EdgeInsets.symmetric(vertical: 36),
                                          decoration: const BoxDecoration(
                                              gradient: SweepGradient(colors: [
                                                Color.fromRGBO(0, 90, 62, 1),
                                                Color.fromRGBO(0, 196, 135, 1),
                                                Color.fromRGBO(0, 90, 62, 1),
                                                Color.fromRGBO(0, 196, 135, 1),
                                                Color.fromRGBO(0, 90, 62, 1),
                                                Color.fromRGBO(0, 196, 135, 1),
                                                Color.fromRGBO(0, 90, 62, 1),
                                                Color.fromRGBO(0, 196, 135, 1),
                                                Color.fromRGBO(0, 90, 62, 1),
                                                Color.fromRGBO(0, 196, 135, 1),
                                                Color.fromRGBO(0, 90, 62, 1),
                                              ]),
                                              borderRadius: BorderRadius.vertical(
                                                bottom: Radius.circular(20),
                                              )),
                                          width: cardWidth,
                                        ),
                                        LayoutBuilder(builder: (context, constrains) {
                                          return Container(
                                            height: constrains.maxHeight / 2,
                                            width: constrains.maxHeight / 2,
                                            padding: EdgeInsets.all(constrains.maxHeight / 15),
                                            decoration: BoxDecoration(
                                                shape: BoxShape.circle,
                                                border: Border.all(color: Colors.black, width: 1.5)),
                                            child: Container(
                                              decoration: const BoxDecoration(
                                                color: Colors.black,
                                                shape: BoxShape.circle,
                                              ),
                                              child: const Icon(
                                                Icons.qr_code_rounded,
                                                size: 30,
                                              ),
                                            ),
                                          );
                                        })
                                      ],
                                    ),
                                  )
                                ],
                              ),
                            ),
                          )
                        : const SizedBox();
                  }).reversed,
                  Transform.translate(
                    offset: Offset(percent * -300, 0),
                    child: Container(
                      margin: const EdgeInsets.only(left: 24),
                      width: cardWidth,
                      height: cardHeight,
                      decoration: BoxDecoration(
                        gradient:
                            const LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [
                          Color.fromRGBO(0, 196, 135, 1),
                          Color.fromRGBO(0, 90, 62, 1),
                        ]),
                        border: Border.all(color: const Color.fromRGBO(0, 155, 137, 1), width: 2),
                        borderRadius: const BorderRadius.all(Radius.circular(20)),
                      ),
                      child: Column(
                        children: [
                          Container(
                            padding: const EdgeInsets.only(left: 17, top: 11),
                            height: 140,
                            width: cardWidth,
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.stretch,
                              children: [
                                Text(
                                  '팀 인증서',
                                  style: TextStyle(
                                      fontFamily: '210_MGothic',
                                      fontWeight: FontWeight.bold,
                                      fontSize: 16,
                                      letterSpacing: 0.8,
                                      color: Colors.black54),
                                ),
                                Text(
                                  CertCard.certCards[currentPage].name,
                                  style: TextStyle(
                                      fontFamily: '210_MGothic',
                                      fontWeight: FontWeight.w700,
                                      fontSize: 24,
                                      letterSpacing: -1.2,
                                      height: 1.5,
                                      color: Colors.black),
                                ),
                              ],
                            ),
                          ),
                          Expanded(
                            child: Stack(
                              fit: StackFit.loose,
                              alignment: Alignment.center,
                              children: [
                                Container(
                                  padding: const EdgeInsets.symmetric(vertical: 36),
                                  decoration: const BoxDecoration(
                                      gradient: SweepGradient(colors: [
                                        Color.fromRGBO(0, 90, 62, 1),
                                        Color.fromRGBO(0, 196, 135, 1),
                                        Color.fromRGBO(0, 90, 62, 1),
                                        Color.fromRGBO(0, 196, 135, 1),
                                        Color.fromRGBO(0, 90, 62, 1),
                                        Color.fromRGBO(0, 196, 135, 1),
                                        Color.fromRGBO(0, 90, 62, 1),
                                        Color.fromRGBO(0, 196, 135, 1),
                                        Color.fromRGBO(0, 90, 62, 1),
                                        Color.fromRGBO(0, 196, 135, 1),
                                        Color.fromRGBO(0, 90, 62, 1),
                                      ]),
                                      borderRadius: BorderRadius.vertical(
                                        bottom: Radius.circular(20),
                                      )),
                                  width: cardWidth,
                                ),
                                LayoutBuilder(builder: (context, constrains) {
                                  return Container(
                                    height: constrains.maxHeight / 2,
                                    width: constrains.maxHeight / 2,
                                    padding: EdgeInsets.all(constrains.maxHeight / 15),
                                    decoration: BoxDecoration(
                                        shape: BoxShape.circle, border: Border.all(color: Colors.black, width: 1.5)),
                                    child: Container(
                                      decoration: const BoxDecoration(
                                        color: Colors.black,
                                        shape: BoxShape.circle,
                                      ),
                                      child: const Icon(
                                        Icons.qr_code_rounded,
                                        size: 30,
                                      ),
                                    ),
                                  );
                                })
                              ],
                            ),
                          )
                        ],
                      ),
                    ),
                  ),
                ],
              ),
              PageView(
                  controller: pageController,
                  physics: const BouncingScrollPhysics(),
                  children: List.generate(CertCard.certCards.length, (index) => const SizedBox())),
            ],
          );
        }),
      ),
    );
  }
}
profile
2021.05.03) Flutter, BlockChain, Sports, StartUp
post-custom-banner

0개의 댓글