Design-loving front-end engineer
Ryong
Design-loving front-end engineer
전체 방문자
였늘
μ–΄μ œ
    • Framework
    • React
      • Concept
      • Library
      • Hook
      • Component
      • Test
    • NodeJS
    • Android
      • Concept
      • Code
      • Sunflower
      • Etc
    • Flutter
      • Concept
      • Package
    • Web
    • Web
    • CSS
    • Language
    • JavaScript
    • TypeScript
    • Kotlin
    • Dart
    • Algorithm
    • Data Structure
    • Programmers
    • Management
    • Git
    • Editor
    • VSCode
    • Knowledge
    • Voice
Design-loving front-end engineer

Ryong

[ Flutter ] μ• λ‹ˆλ©”μ΄μ…˜
Flutter/Concept

[ Flutter ] μ• λ‹ˆλ©”μ΄μ…˜

2021. 12. 29. 12:30

Hero

πŸ’¬  ν™”λ©΄ μ „ν™˜μ‹œ μžμ—°μŠ€λŸ½κ²Œ μ—°κ²°λ˜λŠ” μ• λ‹ˆλ©”μ΄μ…˜μ„ 지원

πŸ’¬  이전 ν™”λ©΄μœΌλ‘œ λŒμ•„κ°ˆ λ•Œλ„ μžμ—°μŠ€λŸ½κ²Œ μ• λ‹ˆλ©”μ΄μ…˜μ΄ λ™μž‘ν•œλ‹€.

πŸ’¬  μ• λ‹ˆλ©”μ΄μ…˜ 효과의 λŒ€μƒμ΄ λ˜λŠ” μ–‘μͺ½ ν™”λ©΄μ˜ μœ„μ ―μ„ Hero μœ„μ ―μœΌλ‘œ 감싸고, tag ν”„λ‘œνΌν‹°λ₯Ό λ°˜λ“œμ‹œ λ™μΌν•˜κ²Œ μ§€μ •ν•΄μ•Ό ν•œλ‹€.

// 첫 번째 νŽ˜μ΄μ§€
class HeroPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Hero'),
      ),
      body: Center(
        child: GestureDetector(
          onTap: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => HeroDetailPage()),
            );
          },
          child: Hero(
            tag: 'image',  // μ—¬κΈ°μ„œ μž‘μ„±ν•œ νƒœκ·Έμ™€ 두 번째 νƒœκ·Έκ°€ 동일해야 함
            child: Image.asset(
              'assets/sample.jpg',
              width: 100,
              height: 100,
            ),
          ),
        ),
      ),
    );
  }
}

// 두 번째 νŽ˜μ΄μ§€
class HeroDetailPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Hero Detail'),
      ),
      body: Hero(
        tag: 'image',  // μ—¬κΈ°μ„œ μž‘μ„±ν•œ νƒœκ·Έμ™€ 첫 번째 νƒœκ·Έκ°€ 동일해야 함
        child: Image.asset('assets/sample.jpg'),
      ),
    );
  }
}

 

AnimatedContainer

πŸ’¬  ν•œ ν™”λ©΄ λ‚΄μ—μ„œ setState() ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•˜μ—¬ 화면을 μƒˆλ‘œ 그릴 λ•Œ λ³€κ²½λœ ν”„λ‘œνΌν‹°μ— μ˜ν•΄ μ• λ‹ˆλ©”μ΄μ…˜λœλ‹€.

πŸ’¬  μƒνƒœλ₯Ό λ³€κ²½μ‹œν‚€λ©΄μ„œ μ• λ‹ˆλ©”μ΄μ…˜μ΄ μ μš©λ˜λ―€λ‘œ StatefulWidget으둜 μž‘μ„±ν•œλ‹€.

AnimatedContainer(
  duration: Duration(seconds: 1),  // 1초 λ™μ•ˆ μ• λ‹ˆλ©”μ΄μ…˜ 적용
  width: 100.0,                    // κ°€λ‘œ 길이
  height: 150.0,                   // μ„Έλ‘œ 길이
  child: [μœ„μ ―],
  curve: Curves.fastoutSlowIn,     // 미리 μ •μ˜λœ μ• λ‹ˆλ©”μ΄μ…˜ 효과
),

πŸ’¬  100 x 100 크기의 이미지λ₯Ό νƒ­ν•˜λ©΄ 100 ~ 299 크기둜 λžœλ€ν•˜κ²Œ 크기가 λ³€κ²½λ˜μ–΄ μ• λ‹ˆλ©”μ΄μ…˜ λ˜λŠ” μ˜ˆμ‹œ

import 'dart:math';  // Random 클래슀 μ‚¬μš©μ— ν•„μš”

...

class _AnimatedContainerPageState extends State<AnimatedContainerPage> {
  var _size = 100.0;
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedContainer'),
      ),
      body: Center(
        child: GestureDetector(
          onTap: () {
            final random = Random();  // Random 클래슀 μ‚¬μš© μ€€λΉ„
            setState(() {
              // 클릭할 λ•Œλ§ˆλ‹€ 100.0~299.0 μ‚¬μ΄μ˜ μ‹€μˆ˜λ₯Ό λžœλ€ν•˜κ²Œ μ–»κΈ°
              _size = random.nextInt(200).toDouble() + 100;
            });
          },
          child: AnimatedContainer(
            duration: Duration(seconds: 1),
            width: _size,   // λžœλ€ν•œ 값을 적용
            height: _size,  // λžœλ€ν•œ 값을 적용
            child: Image.asset('assets/sample.jpg'),
            curve: Curves.fastOutSlowIn,
          ),
        ),
      ),
    );
  }
}

  β–«  setState() ν•¨μˆ˜μ— μ˜ν•΄ λ‹€μ‹œ 그렀질 λ•Œ AnimatedContainer μœ„μ ―μ€ 이전 _size κ°’μ—μ„œ μƒˆλ‘œ κ°±μ‹ λœ _size κ°’κΉŒμ§€ 1초 λ™μ•ˆ fastOutSlowIn에 μ •μ˜λœ νš¨κ³Όκ°€ 적용되며 μ• λ‹ˆλ©”μ΄μ…˜λœλ‹€.

 

SliverAppBar와 SliverFillRemaining

πŸ’¬  SliverAppBar와 SliverFillRemaining은 ν™”λ©΄ 헀더λ₯Ό λ™μ μœΌλ‘œ ν‘œν˜„ν•˜λŠ” μœ„μ ―μ΄λ‹€.

πŸ’¬  헀더λ₯Ό μœ„λ‘œ μŠ€ν¬λ‘€ν•˜λ©΄ 헀더 뢀뢄이 μž‘μ•„μ§€λ©΄μ„œ 헀더 ν•˜λ‹¨μ— 있던 정적인 λ‚΄μš©λ§Œ λ³΄μ΄λŠ” AppBar ν˜•νƒœλ‘œ μ• λ‹ˆλ©”μ΄μ…˜λ˜λ©°, μ΄λŸ¬ν•œ 효과λ₯Ό Sliver 효과라고 ν•œλ‹€.

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: CustomScrollView(
      slivers: <Widget>[
        SliverAppBar(                       // 헀더 μ˜μ—­
          pinned: true,                     // μΆ•μ†Œμ‹œ 상단에 AppBarκ°€ κ³ μ •λ˜λŠ”μ§€ μ„€μ •
          expandedHeight: 180.0,            // ν—€λ”μ˜ μ΅œλŒ€ 높이
          flexibleSpace: FlexibleSpaceBar(  // λŠ˜μ–΄λ‚˜λŠ” μ˜μ—­μ˜ UI μ •μ˜
            title: Text('Sliver'),
            background: Image.asset(
              'assets/sample.jpg',
              fit: BoxFit.cover,
            ),
          ),
        ),
        SliverFillRemaining(                // λ‚΄μš© μ˜μ—­
          child: Center(
            child: Text('center'),
          ),
        ),
      ],
    ),
  );
}

β–«  Scaffold의 appBarλ₯Ό μ§€μ •ν•˜μ§€ μ•Šκ³  body에 CustomScrollView의 μΈμŠ€ν„΄μŠ€λ₯Ό μ§€μ •ν–ˆλ‹€.

β–«  CustomScrollView의 slivers ν”„λ‘œνΌν‹°μ— SliverAppBar와 SliverFillRemaining μœ„μ ―μ„ μ„€μ •ν•œλ‹€.

β–«  SliverAppBar μœ„μ ―μ˜ Sliver 효과λ₯Ό μœ„ν•œ μ΅œμ†Œν•œμ˜ ν”„λ‘œνΌν‹°

  β–«  pinned : μΆ•μ†Œλ  λ•Œ 상단에 AppBarκ°€ 고정될지 μ‚¬λΌμ§ˆμ§€ μ„€μ •ν•œλ‹€.

  β–«  expandedHeight : ν™•λŒ€λ  λ•Œμ˜ μ΅œλŒ€ 높이λ₯Ό μ •ν•œλ‹€.

  β–«  flexibleSpace : ν™•λŒ€/μΆ•μ†Œλ˜λŠ” μ˜μ—­μ˜ UIλ₯Ό μž‘μ„±ν•œλ‹€.

β–«  flexibleSpace μœ„μ ―μ€ titleκ³Ό background ν”„λ‘œνΌν‹°λ₯Ό ν™œμš©ν•˜μ—¬ 적절히 AppBar μ˜μ—­μ΄ ν™•μž₯λ˜μ—ˆμ„ λ•Œμ˜ UIλ₯Ό μž‘μ„±ν•œλ‹€.

β–«  SliverFillRemaining μœ„μ ―μ—λŠ” 슀크둀 μ˜μ—­μ— ν‘œμ‹œλ  화면을 μ •μ˜ν•œλ‹€.

  β–«  child에 μž‘μ„±ν•œ λ‚΄μš©μ˜ 크기가 μž‘μ•„λ„ SliverAppBar 뢀뢄이 μΆ•μ†Œλ  λ•Œ λ”± ν•˜λ‚˜μ˜ 크기가 μ•Œμ•„μ„œ κ²°μ •λœλ‹€.

 

SliverAppBar와 SliverList

πŸ’¬  ListViewλ₯Ό μ‚¬μš©ν•˜μ—¬ Sliver 효과λ₯Ό μ£Όκ³  μ‹Άλ‹€λ©΄, ListView λŒ€μ‹  SliverListλ₯Ό μ‚¬μš©ν•΄μ•Ό ν•œλ‹€.

// 'No.0'μ—μ„œ 'No.49'κΉŒμ§€ ν‘œμ‹œν•˜λŠ” ListTile을 담은 리슀트
final _items = List.generate(50, (i) => ListTile(title: Text('No.$i')));

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: CustomScrollView(
      slivers: <Widget>[
        SliverAppBar(
          pinned: true,
          expandedHeight: 180.0,
          flexibleSpace: FlexibleSpaceBar(
            title: Text('Sliver'),
            background: Image.asset(
              'assets/sample.jpg',
              fit: BoxFit.cover,
            ),
          ),
        ),
        SliverList(
          // μƒμ„±μžμ— ν‘œμ‹œν•  μœ„μ ― 리슀트(_items)λ₯Ό 인수둜 전달
          delegate: SliverChildListDelegate(_items),
        ),
      ],
    ),
  );
}

 

Staggered Animations

πŸ’¬  μœ„μ ―μ˜ Opacity, Width, Height, Padding, BorderRadius, Color 등을 이전 κ°’κ³Ό 이후 κ°’μœΌλ‘œ interval에 λ§žμΆ°μ„œ λ³€κ²½ν•˜λŠ” μ• λ‹ˆλ©”μ΄μ…˜

https://youtu.be/0fFvnZemmh8

κ΅¬ν˜„λΆ€

import 'package:flutter/material.dart';

class StaggerAnimation extends StatelessWidget {
  StaggerAnimation({Key? key, required this.controller})
      :      
        opacity = Tween<double>(
          begin: 0.0,  // μ• λ‹ˆλ©”μ΄μ…˜ μ‹œμž‘
          end: 1.0,    // μ• λ‹ˆλ©”μ΄μ…˜ 끝
        ).animate(
          CurvedAnimation(
            parent: controller,
            curve: const Interval(  // 0μ—μ„œ 1κΉŒμ§€ μ• λ‹ˆλ©”μ΄μ…˜ 적용 μ‹œμ 
              0.0,
              0.100,
              curve: Curves.ease,
            ),
          ),
        ),
        width = Tween<double>(
          begin: 50.0,
          end: 150.0,
        ).animate(
          CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.125,
              0.250,
              curve: Curves.ease,
            ),
          ),
        ),
        height = Tween<double>(begin: 50.0, end: 150.0).animate(
          CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.250,
              0.375,
              curve: Curves.ease,
            ),
          ),
        ),
        padding = EdgeInsetsTween(
          begin: const EdgeInsets.only(bottom: 16.0),
          end: const EdgeInsets.only(bottom: 75.0),
        ).animate(
          CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.250,
              0.375,
              curve: Curves.ease,
            ),
          ),
        ),
        borderRadius = BorderRadiusTween(
          begin: BorderRadius.circular(4.0),
          end: BorderRadius.circular(75.0),
        ).animate(
          CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.375,
              0.500,
              curve: Curves.ease,
            ),
          ),
        ),
        color = ColorTween(
          begin: Colors.indigo[100],
          end: Colors.orange[400],
        ).animate(
          CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.500,
              0.750,
              curve: Curves.ease,
            ),
          ),
        ),
        super(key: key);

  final Animation<double> controller;
  final Animation<double> opacity;
  final Animation<double> width;
  final Animation<double> height;
  final Animation<EdgeInsets> padding;
  final Animation<BorderRadius?> borderRadius;
  final Animation<Color?> color;

  Widget _buildAnimation(BuildContext context, Widget? child) {
    return Container(
      padding: padding.value,                  // μ• λ‹ˆλ©”μ΄μ…˜ 적용
      alignment: Alignment.bottomCenter,
      child: Opacity(
        opacity: opacity.value,                // μ• λ‹ˆλ©”μ΄μ…˜ 적용
        child: Container(
          width: width.value,                  // μ• λ‹ˆλ©”μ΄μ…˜ 적용
          height: height.value,                // μ• λ‹ˆλ©”μ΄μ…˜ 적용
          decoration: BoxDecoration(
            color: color.value,                // μ• λ‹ˆλ©”μ΄μ…˜ 적용
            border: Border.all(
              color: Colors.indigo[300]!,
              width: 3.0,
            ),
            borderRadius: borderRadius.value,  // μ• λ‹ˆλ©”μ΄μ…˜ 적용
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      builder: _buildAnimation,
      animation: controller,
    );
  }
}

μ‹€ν–‰λΆ€

import 'dart:async';

import 'package:flutter/scheduler.dart' show timeDilation;

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

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

class _StaggerDemoState extends State<StaggerDemo>
    with TickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
  }

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

  Future<void> _playAnimation() async {
    try {
      await _controller.forward().orCancel;  // μ• λ‹ˆλ©”μ΄μ…˜ μ •λ°©ν–₯ μ‹€ν–‰
      await _controller.reverse().orCancel;  // μ• λ‹ˆλ©”μ΄μ…˜ μ—­λ°©ν–₯ μ‹€ν–‰
    } on TickerCanceled {
      // the animation got canceled, probably because we were disposed
    }
  }

  @override
  Widget build(BuildContext context) {
    timeDilation = 10.0;  // 1.0 is normal animation speed.
    return Scaffold(
      appBar: AppBar(
        title: const Text('Staggered Animation'),
      ),
      body: GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: () {
          _playAnimation();
        },
        child: Center(
          child: Container(
            width: 300.0,
            height: 300.0,
            decoration: BoxDecoration(
              color: Colors.black.withOpacity(0.1),
              border: Border.all(
                color: Colors.black.withOpacity(0.5),
              ),
            ),
            // μ• λ‹ˆλ©”μ΄μ…˜ 적용 : μ• λ‹ˆλ©”μ΄μ…˜ + μœ„μ ―
            child: StaggerAnimation(controller: _controller.view),
          ),
        ),
      ),
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: StaggerDemo(),
    ),
  );
}

 

μ €μž‘μžν‘œμ‹œ (μƒˆμ°½μ—΄λ¦Ό)

'Flutter > Concept' μΉ΄ν…Œκ³ λ¦¬μ˜ λ‹€λ₯Έ κΈ€

[ Flutter ] ν™”λ©΄ 이동  (0) 2021.12.29
[ Flutter ] 쿠퍼티노 λ””μžμΈ  (0) 2021.12.29
[ Flutter ] λ‹€μ΄μ–Όλ‘œκ·Έ  (0) 2021.12.29
[ Flutter ] λ²„νŠΌ  (0) 2021.12.28
[ Flutter ] ν™”λ©΄ 배치  (0) 2021.12.28
    'Flutter/Concept' μΉ΄ν…Œκ³ λ¦¬μ˜ λ‹€λ₯Έ κΈ€
    • [ Flutter ] ν™”λ©΄ 이동
    • [ Flutter ] 쿠퍼티노 λ””μžμΈ
    • [ Flutter ] λ‹€μ΄μ–Όλ‘œκ·Έ
    • [ Flutter ] λ²„νŠΌ
    Design-loving front-end engineer
    Design-loving front-end engineer
    λ””μžμΈμ— 관심이 λ§Žμ€ λͺ¨λ°”일 μ•± μ—”μ§€λ‹ˆμ–΄ Ryongμž…λ‹ˆλ‹€.

    ν‹°μŠ€ν† λ¦¬νˆ΄λ°”