seong

Flutter - SideMenu 본문

Flutter/Flutter

Flutter - SideMenu

hyeonseong 2023. 9. 12. 15:07

1. SideMenu Tile만들기

//Side_Menu_Tile의 변수 

  final RiveAsset menu;
  final VoidCallback press;
  final ValueChanged<Artboard> riveOnInIt;
  final bool isActive; // 선택된 아이템 활성화 유무 판단
  
//
Column(
      children: [
        const Padding(
          padding: EdgeInsets.only(left: 24),
          child: Divider(
            color: Colors.white24,
            height: 1,
          ),
        ),
        ListTile(
          onTap: press,
          leading: SizedBox(
            height: 34,
            width: 34,
            child: RiveAnimation.asset(
            //menu는 Rive asset의 model
              menu.src,
              artboard: menu.artboard,
              onInit: riveOnInIt,
            ),
          ),
          title: Text(
            menu.title,
            style: const TextStyle(color: Colors.white),
          ),
        ),
      ],
    );

2. menu가 클릭 되었는지 확인을 위해  SelectedMenu 선언

RiveAsset selectedMenu = sidebarMenus.first;

3. Side_Menu에서 Tile호출

sidebarMenus 는 List<RiveAsset>형태이다. 

map으로 요소 하나씩 SideMenuTile에 맞게 대입 해준다. 

...sidebarMenus.map(
                (menu) => SideMenuTile(
                  menu: menu,
                  riveOnInIt: (artboard) {
                    // 1. controller
                    StateMachineController controller = RiveUtils.getRiveController(
                      artboard,
                      stateMachineName: menu.stateMachineName,
                    );
                    // 2. active 넘겨주기
                    menu.input = controller.findSMI("active") as SMIBool;
                  },
                  press: () {
                    // 3. Animate 활성화
                    menu.input!.change(true);
                    // 4. 일정 시간 이후 Animate 비활성화
                    Future.delayed(const Duration(seconds: 1), () {
                      menu.input!.change(false);
                    });
                    // 5. 선택 메뉴 넘겨주기.
                    setState(() {
                      selectedMenu = menu;
                    });
                  },
                  isActive: selectedMenu == menu,
                ),
              ),

4. 선택된 Tile색 입히기.

다양한 방법이 있겠지만, Stack으로 구현되었다.

 width: isActive ? 288 : 0 // 메뉴가 선택되었다면 Container의 width가 288이 된다, 

이때 AnimatedPositioned를 사용해줌으로써 색이 부드럽게 입혀진다. 

 

결국 원리는

ListTile과 width,height만 똑같은 Color를 가진 Container를 만들고, 

선택되었다면 ListTile에 Container를 입혀주는 원리이다.

Container가 ListTile의 Text, Icon등을 가리지 않는이유
- Stack은 위젯을 아래에서 부터 쌓아올린다. 
- 마지막에 쌓인 위젯이 가장 위에 있기 때문에 Icon,Text는 가려지지 않는다.
Stack(
          children: [
            AnimatedPositioned(
              duration: const Duration(milliseconds: 300),
              height: 56,
              width: isActive ? 288 : 0, // SideMenuTile의 가로너비와 동일
              left: 0,
              child: Container(
                decoration: const BoxDecoration(
                  color: Color(0xFF6792FF),
                  borderRadius: BorderRadius.all(Radius.circular(10)),
                ),
              ),
            ),
            ListTile(
              onTap: press,
              leading: SizedBox(
                height: 34,
                width: 34,
                child: RiveAnimation.asset(
                  menu.src,
                  artboard: menu.artboard,
                  onInit: riveOnInIt,
                ),
              ),
              title: Text(
                menu.title,
                style: const TextStyle(color: Colors.white),
              ),
            ),
          ],
        ),

전체 코드

side_menu.dart

import 'package:animation_v1/components/side_menu_tile.dart';
import 'package:animation_v1/model/rive_asset.dart';
import 'package:animation_v1/utils/rive_utils.dart';
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';

import 'info_card.dart';

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

  @override
  State<SideMenu> createState() => _SideMenuState();
}

class _SideMenuState extends State<SideMenu> {
  RiveAsset selectedMenu = sidebarMenus.first;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        width: 288,
        height: double.infinity,
        color: const Color(0xFF17203A),
        child: SafeArea(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const InfoCard(
                name: "HyeonSeongLee",
                profession: "Mobile Developer",
              ),
              Padding(
                padding: const EdgeInsets.only(left: 24, top: 32, bottom: 16),
                child: Text(
                  "Browse".toUpperCase(), // 대문자
                  style: Theme.of(context).textTheme.titleMedium!.copyWith(color: Colors.white70),
                ),
              ),
              ...sidebarMenus.map(
                (menu) => SideMenuTile(
                  menu: menu,
                  riveOnInIt: (artboard) {
                    // 1. controller
                    StateMachineController controller = RiveUtils.getRiveController(
                      artboard,
                      stateMachineName: menu.stateMachineName,
                    );
                    // 2. active 넘겨주기
                    menu.input = controller.findSMI("active") as SMIBool;
                  },
                  press: () {
                    // 3. Animate 활성화
                    menu.input!.change(true);
                    // 4. 일정 시간 이후 Animate 비활성화
                    Future.delayed(const Duration(seconds: 1), () {
                      menu.input!.change(false);
                    });
                    // 5. 선택 메뉴 넘겨주기.
                    setState(() {
                      selectedMenu = menu;
                    });
                  },
                  isActive: selectedMenu == menu,
                ),
              ),
              Padding(
                padding: const EdgeInsets.only(left: 24, top: 32, bottom: 16),
                child: Text(
                  "History".toUpperCase(), // 대문자
                  style: Theme.of(context).textTheme.titleMedium!.copyWith(color: Colors.white70),
                ),
              ),
              ...sidebarMenus2.map(
                (menu2) => SideMenuTile(
                  menu: menu2,
                  press: () {
                    menu2.input!.change(true);
                    Future.delayed(const Duration(microseconds: 300), () {
                      menu2.input!.change(false);
                    });
                    setState(() {
                      selectedMenu = menu2;
                    });
                  },
                  riveOnInIt: (artboard) {
                    StateMachineController controller = RiveUtils.getRiveController(
                      artboard,
                      stateMachineName: menu2.stateMachineName,
                    );
                    menu2.input = controller.findSMI("active") as SMIBool;
                  },
                  isActive: selectedMenu == menu2,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

side_menu_tile.dart

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

import '../model/rive_asset.dart';

class SideMenuTile extends StatelessWidget {
  const SideMenuTile({
    super.key,
    required this.menu,
    required this.press,
    required this.riveOnInIt,
    required this.isActive,
  });

  final RiveAsset menu;
  final VoidCallback press;
  final ValueChanged<Artboard> riveOnInIt;
  final bool isActive;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Padding(
          padding: EdgeInsets.only(left: 24),
          child: Divider(
            color: Colors.white24,
            height: 1,
          ),
        ),
        Stack(
          children: [
            AnimatedPositioned(
              duration: const Duration(milliseconds: 300),
              height: 56,
              width: isActive ? 288 : 0, // SideMenuTile의 가로너비와 동일\
              left: 0,
              child: Container(
                decoration: const BoxDecoration(
                  color: Color(0xFF6792FF),
                  borderRadius: BorderRadius.all(Radius.circular(10)),
                ),
              ),
            ),
            ListTile(
              onTap: press,
              leading: SizedBox(
                height: 34,
                width: 34,
                child: RiveAnimation.asset(
                  menu.src,
                  artboard: menu.artboard,
                  onInit: riveOnInIt,
                ),
              ),
              title: Text(
                menu.title,
                style: const TextStyle(color: Colors.white),
              ),
            ),
          ],
        ),
      ],
    );
  }
}