본문 바로가기
Flutter

[Flutter] Animation을 사용하여 ListView 선택 효과 만들기(AnimatedPositioned)

by 아마도개발자 2024. 7. 21.

몇 번의 리스트를 만들어 봤지만, UI를 고려할 때, Row의 간격이나 선택 시 색깔변경 등 단순하게 보여지는 부분외에 큰 고민을 해본 적이 없는 것 같아 이번에는 나름대로 Animation을 활용한 list를 만들어 보기로 하였다.

 

내가 원하는 효과는 리스트 아이템 클릭을 하면, 해당 아이템이 최상단으로 올라가고, 아래쪽에 선택한 아이템에 대한 디테일이 나오게 하는 것이다. 효과를 만들기 위해 검색을해보니 Flutter codelabs에 유사한 기능을 소개하고 있었다.

 

Container Transformation Example by Flutter.dev

 

Animation 패키지의 OpenContainer를 활용하는 방법인데, 위젯 하나로 손쉽게 위와 같은 효과들을 만들어 낼 수 있었다. 이 위젯을 조금만 다듬으면 내가 원한 효과를 만들 수 있을 것이라 생각하고 조금 만져봤는데 문제가 있었다.

 

 

OpenContainer위젯은 ClosedContainer와 OpenContainer 속성에 내가 원하는 위젯을 넣어주면 자동으로 closed => open 위젯으로 화면을 보여주는 형식인데, 페이지가 라우트 되다보니 풀 스크린으로 동작을 한다는 점이었다. 내가 원한 것은 특정 위젯 내에서 변화를 보여주는 것이었기 때문에 사용하기가 적절하지 않았다. 조금 서치해보니 key를 활용해 페이지 라우트 시에도 일부 위젯에만 변화를 줄 수 있다는 부분이 있었는데, 그 정도 수고를 할 필요가 있을까? 하는 생각이 들었다. 사이드 프로젝트였다면 공부한다고 생각하며 추가를 했을 텐데, 실제 서비스에 넣을 생각이 있다보니, 잘 모르는 부분을 추가하고 싶지 않은 마음이 컸다.

 

그래서 생각한 방법이 Stack과 AnimatedPosition을 활용하는 방법이었다. 리스트 아이템을 AnimatedPosition으로 만들고, stack안에서 선택에 따라 top의 값을 바꿔주면 내가 원하는 효과를 만들 수 있을 것이라 생각했다.

 

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyListView(),
    );
  }
}

class MyListView extends StatefulWidget {
  @override
  _MyListViewState createState() => _MyListViewState();
}

class _MyListViewState extends State<MyListView> {
  
  // 선택된 아이템의 인덱스를 저장할 변수
  int _selectedIndex = -1;
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('List View Example'),
      ),
      body: Container(
        color: Colors.blue,
        width: 700,
        height: 700,
        child: Stack(
          children: List.generate(ls.length, (index) {
            bool isSelected = (_selectedIndex == -1) ||
                (_selectedIndex != -1 && _selectedIndex == index);

            return AnimatedPositioned(
              left: 0,
              top: _selectedIndex == index ? 0 : 60 * index.toDouble(),
              width: 50,
              height: isSelected ? 50 : 0,
              duration: isSelected
                  ? Duration(milliseconds: 500)
                  : Duration(milliseconds: 0),
              curve: Curves.easeInOut,
              child: GestureDetector(
                onTap: () {
                  setState(() {
                    _selectedIndex = index; // 아
                  });
                },
                child: Container(
                  color: Colors.red,
                  child: Center(
                    child: Text(
                      'Item ${index + 1}',
                      style: TextStyle(color: Colors.white),
                    ),
                  ),
                ),
              ),
            );
          }),
        ),
      ),
    );
  }
}

 

 

 

얼추 내가 원하던 효과가 만들어졌다. 이제 해야할일은 해당하는 item에 대한 디테일 페이지를 올려주는 것이다. 이것 또한 리스트 아래에 AnimatedPostiion과 AnimatedOpacity를 활용하여 해결할 수 있었다.

 

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyListView(),
    );
  }
}

class MyListView extends StatefulWidget {
  @override
  _MyListViewState createState() => _MyListViewState();
}

class _MyListViewState extends State<MyListView> {
  
  // 선택된 아이템의 인덱스를 저장할 변수
  int _selectedIndex = -1;
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('List View Example'),
      ),
      body: Container(
        color: Colors.blue,
        width: 700,
        height: 700,
        child: Stack(children: [
          ...List.generate(ls.length, (index) {
            bool isSelected = (_selectedIndex == -1) ||
                (_selectedIndex != -1 && _selectedIndex == index);

            return AnimatedPositioned(
              left: 0,
              top: _selectedIndex == index ? 0 : 60 * index.toDouble(),
              width: 50,
              height: isSelected ? 50 : 0,
              duration: isSelected
                  ? Duration(milliseconds: 500)
                  : Duration(milliseconds: 0),
              curve: Curves.easeInOut,
              child: GestureDetector(
                onTap: () {
                  setState(() {
                    _selectedIndex = index; // 아
                  });
                },
                child: Container(
                  color: Colors.red,
                  child: Center(
                    child: Text(
                      'Item ${index + 1}',
                      style: TextStyle(color: Colors.white),
                    ),
                  ),
                ),
              ),
            );
          }),
          AnimatedPositioned(
            top: 60,
            duration: Duration(seconds: 5),
            child: AnimatedOpacity(
                opacity: _selectedIndex != -1 ? 1.0 : 0.0,
                duration: Duration(seconds: 1),
                child: DetailPage()),
          )
        ]),
      ),
    );
  }
}

class DetailPage extends StatefulWidget {
  const DetailPage({super.key});

  @override
  State<DetailPage> createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 400,
      child: Column(
        children: [Text("Some Details")],
      ),
    );
  }
}

 

 

 

효과는 만들었으니 이제 제대로 된 리스트 내용을 채워주기만 하면 끝!