Flutter

Flutter 신용카드 예제 Like 카카오페이 2편

흐성진 2025. 3. 5. 17:23
반응형

들어가기

2025.02.07 - [Flutter] - Flutter 신용카드 예제 Like 카카오페이 1편

 

Flutter 신용카드 예제 Like 카카오페이 1편

주제 선정 [Flutter] 그라데이션으로 만들어보는 광원 효과빛이 반사되는 효과는 어떻게 만들까?velog.io처음에 위의 글을 보고 애니메이션이 기깔난다고 생각을 하고있었다.문득 카카오페이를 들

sj-d.tistory.com

 

해당 글에 이어서 작성하는 내용이다.

우선 이전편에서 카드 생성, 토스카드 모양으로 그리기, 기타 애니메이션 등을 했었다.

 

이제 DB에 저장된 데이터를 받아와서 하단에서 카드를 보여주고, 상단영역에서는 카드 상세정보를 보여주면서

유저의 제스쳐에 따라서 카드의 각도가 변하면서 광원효과를 넣어보려고 한다.

 

카드 목록 보여주기

우선 카드 목록을 보여주기 위해서 데이터베이스에서 데이터를 가져와서 보여줘야한다.

데이터베이스 관련 내용은 저번에 다뤘기 때문에 생략하고

Carousel로 만들어서 상단에서 보여주려고 하려고한다.

https://pub.dev/packages/carousel_slider

 

carousel_slider | Flutter package

A carousel slider widget, support infinite scroll and custom child widget.

pub.dev

 

해당 라이브러리르 사용하면 carousel 을 쉽게 사용할 수 있다.

물론 공식지원하는 CarouselView 도 있고
https://api.flutter.dev/flutter/material/CarouselView-class.html

 

CarouselView class - material library - Dart API

A Material Design carousel widget. The CarouselView presents a scrollable list of items, each of which can dynamically change size based on the chosen layout. Material Design 3 introduced 4 carousel layouts: Multi-browse: This layout shows at least one lar

api.flutter.dev

 

이에 대응하는 방식인 PageView 도 있지만 세가지의 방법으로 구현해 봤을때 carousel_slider 가 가장 완성도가 높다고 판단하여 해당 라이브러리를 사용해서 진행하였다.

 

우선 우리가 스크롤 뷰를 사용하던, 탭바를 사용하던 컨트롤러를 지정해 주어야 한다.

carousel_slider 도 마찬가지로 컨트롤러를 만들어 주어야 한다.

final CarouselController controller = CarouselController(initialItem: 1);

 

다음은 CarouselSlider.builder 를 통해서 해당 _cards 의 리스트를 그려주도록 한다.

 

CarouselSlider

Stack(
  alignment: Alignment.center,
  children: [
    // Transform.translate를 사용하여 위치 조정
    Transform.translate(
      offset: const Offset(-75, 0), // x축으로 -75 픽셀 이동
      child: Container(
        height: 120,
        padding: const EdgeInsets.only(bottom: 20),
        child: CarouselSlider.builder(
          carouselController: _carouselController,
          itemCount: _cards.length,
          options: CarouselOptions(
            height: 100,
            viewportFraction: 0.8,
            enlargeCenterPage: true,
            enableInfiniteScroll: _cards.length > 1,
            initialPage: currentCardIndex,
            autoPlay: false,
            onPageChanged: (index, reason) {
              if (currentCardIndex != index) {
                setState(() {
                  currentCardIndex = index;
                });
              }
            },
            clipBehavior: Clip.none,
            padEnds: true,
            pageSnapping: true,
            disableCenter: false,
            enlargeStrategy: CenterPageEnlargeStrategy.scale,
          ),
          itemBuilder: (context, index, realIndex) {
            // 카드 색상 가져오기
            final cardColor = _getColorFromDynamic(_cards[index]['card_color']);
            
            return GestureDetector(
              onTap: () => _selectCard(index),
              child: CustomPaint(
                painter: CardPainter(
                  cardWidth: 150,
                  cardHeight: 90,
                  cardColor: currentCardIndex == index
                      ? cardColor
                      : cardColor.withValues(alpha: 0.5),
                ),
              ),
            );
          },
        ),
      ),
    ),
  ],
),

 

생각보다 구현하면서 CarouselOption 에서 많은걸 지정할 수 있었는데

기본적인 정의는 아래와 같다.

 

https://github.com/serenader2014/flutter_carousel_slider/blob/master/lib/carousel_options.dart

 

flutter_carousel_slider/lib/carousel_options.dart at master · serenader2014/flutter_carousel_slider

A flutter carousel widget, support infinite scroll, and custom child widget. - serenader2014/flutter_carousel_slider

github.com

 

여기서 내가 사용한 부분은

 

  • height
    • 높이
  • viewportFraction
    • 각 페이지가 사용하는 비율(기본값이 0.8 이라 사실 선언 안해줘도됨)
  • enlargeCenterPage
    • 현재 페이지를 더 크게 보여줌(기본값 false)
  • enableInfiniteScroll
    • 캐러셀 자체 슬라이드가 무한 반복 되어야 하는지 혹은 길이가 제한되어야 하는지
  • initialPage
    • 캐러셀이 생성될때 최초에 보여지는 화면
  • autoPlay
    • 자동 재생 여부(만약 true로 사용시 autoPlayInterval, autoPlayAnimationDuration, autoPlayCurve 등과 같이 사용 가능)
  • onPageChanged
    • 페이지가 변경되었을때의 동작
  • clipBehavior
    • 페이지에 대한 clipBehavior(기본값 Clip.hardEdge)
  • padEnds
    • 목록의 양끝에 패팅을 추가할지에 대한 여부
  • pageSnapping
    • 페이지에 대해서 스냅을 사용할지의 여부 false로 놓게되면 스크롤을 자유롭게 가능
  • disableCenter
    • Center 위젯을 비활성화 할지에 대한 여부
  • enlargeStrategy
    • 현재 위치한 인덱스의 확대 방법으로 CenterPageEnlargeStrategy.scale , .height, .zoom 을 사용가능하다

 

그리고 왜 Transform.translate 로 이동시켰는지 의문이 들 수 있는데 이상하게 나는 카드를 가운대로 보내고 싶은데 시작점이 가운데가 되는 현상이 있었고 이걸 해결하고자 위치를 강제적으로 이동시켜서 사용했다.

 

그리고 카드의 색상 같은 경우 SQFlite 에서는 별도의 색상관련 데이터를 지정할수 없어 TEXT 형식으로 받았기 때문에 변환해서 카드의 색상을 넣어주어야 했다.

 

Carousel Indicator

SizedBox(
  height: 50,
  child: Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: _cards.asMap().entries.map((entry) {
      return GestureDetector(
        onTap: () => _selectCard(entry.key),
        child: Container(
          width: 8,
          height: 8,
          margin: const EdgeInsets.symmetric(horizontal: 4),
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: currentCardIndex == entry.key
                ? Colors.white
                : Colors.white.withValues(alpha: 0.5),
          ),
        ),
      );
    }).toList(),
  ),
)

 

그리고 현재 탭의 위치에 대해서 인지를 시켜주기위해서 만들어주었다.

 

카드의 값에 따른 UI 구현

child: CustomPaint(
  painter: CardPainter(
    cardWidth: 300,
    cardHeight: 200,
    cardColor: _getColorFromDynamic(_cards[currentCardIndex]['card_color']),
  ),
  child: SizedBox(
    width: 300,
    height: 200,
    child: Stack(
      children: [
        // 카드 번호
        Positioned(
          top: 120,
          left: 20,
          child: Text(
            _cards[currentCardIndex]['card_number'] ?? '',
            style: const TextStyle(
              color: Colors.white,
              fontSize: 16,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
        // 카드 소유자 이름
        Positioned(
          top: 160,
          left: 20,
          child: Text(
            _cards[currentCardIndex]['card_name'] ?? '',
            style: const TextStyle(
              color: Colors.white,
              fontSize: 16,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
        // 카드 만료일
        Positioned(
          top: 160,
          right: 20,
          child: Text(
            _cards[currentCardIndex]['card_expiration_date'] ?? '',
            style: const TextStyle(
              color: Colors.white,
              fontSize: 16,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      ],
    ),
  ),
),

 

현재 선택한 카드의 내용을 보여주기 위한 UI 이다.

기존에 1편에서 다뤘던 카드를 추가하는 부분이랑 큰 차이는 없으니 별다른 설명없이 넘어가겠다.

 

구현 결과는 다음과 같다.

 

 

드래그를 통해 움직이는 효과 주기

이렇게 카드를 슬라이드를 통해서 움직이게 만들어줬다.

그러면 이제 필요한건 내가 터치이벤트를 발생시켜서 드래그를 하면 그 방향으로 움직이는 부분을 만들어야 한다.

그러기 위해선

카드를 터치한다 -> 카드가 터치되었다는 신호를 받는다 -> 드래그를 하면 카드가 움직인다 -> 손을떼면 원래 위치로 돌아온다.

이렇게 구현해야 한다.

 

크게 어려운건 없고 카드 위젯 자체에 GestureDetector 로 감싸주고

 

이번에 처음 써보는 onPanStart, onPanUpdate, onPanEnd 를 사용했다.

해당 기능은 조이스틱과 같은 상황에서 많이 사용된다고 한다.

 

우선 onPanStart 를 통해서 드래그 이벤트가 일어날 준비를 한다

onPanStart: (_) {
	isCardTilting = true;
},

 

그다음 onPanUpdate 를 통해서 사용자의 드래그에 대해 업데이트 한다

onPanUpdate: (details) {
  _onPointerMove(details.localPosition, const Size(300, 200));
},

// 사용자의 터치 위치에 따라 기울기 값 조절
void _onPointerMove(Offset position, Size cardSize) {
  if (!isCardTilting) return;
  
  setState(() {
    // 카드 중심을 기준으로 기울기 계산
    final centerX = cardSize.width / 2;
    final centerY = cardSize.height / 2;
    
    // 터치 위치와 중심 간의 차이를 기반으로 기울기 계산
    tiltX = ((position.dy - centerY) / 10).clamp(-15.0, 15.0);
    tiltY = (-(position.dx - centerX) / 10).clamp(-15.0, 15.0);
  });
}

 

기울기 계산 로직은 사용자의 터치 위치와 카드 중심점 간의 거리를 기반으로 계산한다.

먼저 카드의 중심점을 계산해서 centerX, centerY 를 잡아주고,

 

tiltX 는 Y 축 방향의 터치 위치(position.dy)와 중심점 의 차이를 기반으로 계산된다. 여기서 -15도에서 15도로 제한한 이유는 사용자가 카드 위쪽을 터치하면 tiltX는 음즈가 되어 카드가 앞으로 기울어지고 아래쪽을 터치하면 양수가 뒤에 반대로 기울임을 주기 위함이다.

 

tiltY 는 X 축 방향의 터치 위치(position.dx)와 줌심점의 차이를 기반으로 계산된다. 여기서 Y는 - 를 사용한 이유는 직관적인 움직임을 위해서이다. 사용자가 카드의 오른쪽을 터치하면 음수가 되어 카드가 오른쪽으로 기울어지고 왼쪽을 터치하게되면 양수가 되어 카드가 왼쪽으로 기울어 지게 된다.

 

이렇게 계산된 tiltX 와 tiltY 값은 밑에 Matrix4에서 3D 회전에서 다시 사용하게 된다.

 

드래그가 종료되면 onPanEnd 를 통해서 다시 원래의 상태로 되돌린다.

onPanEnd: (_) => _resetTilt(),

// 손을 떼면 원래 위치로 돌아가는 애니메이션 적용
void _resetTilt() {
  isCardTilting = false;
  _animationController.forward(from: 0.0);
}

 

이전에 사용했던 Transform의 Martrix4 를 통해서 이동에 따른 원근감을 주면서 회전을 시켜준다.

child: Transform(
  transform: Matrix4.identity()
    ..setEntry(3, 2, 0.001) // 원근감 추가
    ..rotateX(tiltX * (pi / 180))
    ..rotateY(tiltY * (pi / 180)),

 

이렇게 사용하게 되면 사용자의 터치 드래그 이벤트를 받아오면서 카드를 회전시킬 수 있다.

 

원금감을 주기위해 Z축 방향으로 원근감을 추가해주고,

rotateX와 rotateY 메서드는 각각 X축과 Y축을 중심으로 회전 변환을 적용하기 위해서 pi / 180 을 곱해주게 되는데,

Martrix4는 라디안을 단위로 한다.

하지만 현재는 각도(degree)를 사용하기 떄문에 라디안(radian)으로 변환을 하기 위함이다.

움직임에 따라 광원 효과 주기

그럼 이제 해야되는건 움직임에 따라서 카드의 빛 반사를 만들어 줘야한다.

딱히 어려운건없고 Stack 에 Positioned 을 통해서 카드 위젯 위해 쌓아서 구현했다.

이전에 기울기를 통해 얻어오는 tilitX, tiltY 의 값을 통해서 opacity 를 조정하면서 만들어줬다.

 

opacity 값은 X축과 Y축 기울기의 절대값의 합(tiltX.abs() + tiltY.abs())를 30으로 나눈 값으로 설정했다.

이렇게 만들게 되면 카드가 더 많이 기울어 질수록 뚜렷하게 광원효과를 나타낼 수 있다.

 

그라데이션 자체의 시작을 왼쪽 상단 끝을 오른쪽 하단으로 줘서 왼쪽 상단에 광원이 있는것처럼 효과를 냈고 반투명한 흰색을 넣었다.

// 빛 반사 효과 (기울기에 따라 투명도 변경)
Positioned.fill(
  child: Opacity(
    opacity: (tiltX.abs() + tiltY.abs()) / 30,
    child: Container(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [
            Colors.white.withValues(alpha: 0.7),
            Colors.transparent,
          ],
        ),
        borderRadius: BorderRadius.circular(12),
      ),
    ),
  ),
),

 

이렇게 사용하게되면 사용자의 드래그에 따라서 카드가 회전하면서 광원효과를 받을 수 있다.

 

결론

사실 회사에서는 크게 애니메이션에 신경을 쓰고있지는 않는다.

그런데 자주 사용하던 GestureDetector 에서 onPanStart 와 같이 조이스틱을 사용할 수 있는 부분과,

Martrix4를 사용해서 원금감을 주고 회전을 시키고,

기울기를 이용해서 카드 자체에 광원효과를 주고 좋은 학습의 기회가 되었다.

RiverPod은 이제야 공부해서 돌아와야해서 조금 오래걸릴거같다.

 

소스 코드

https://github.com/Hsungjin/Flutter/tree/main/personal_study/creadit_card_example

 

Flutter/personal_study/creadit_card_example at main · Hsungjin/Flutter

Contribute to Hsungjin/Flutter development by creating an account on GitHub.

github.com

 

참고

https://velog.io/@locked/Flutter-%EA%B7%B8%EB%9D%BC%EB%8D%B0%EC%9D%B4%EC%85%98%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EB%8A%94-%EA%B4%91%EC%9B%90-%ED%9A%A8%EA%B3%BC

 

[Flutter] 그라데이션으로 만들어보는 광원 효과

빛이 반사되는 효과는 어떻게 만들까?

velog.io

 

https://velog.io/@gongd/Flutter-%EC%A1%B0%EC%9D%B4%EC%8A%A4%ED%8B%B1-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0

 

[Flutter] 조이스틱 만들어보기

🧑‍💻 플러터로 조이스틱을 구현해보자

velog.io

 

https://developer0524.tistory.com/77

 

기울어지는 카드 UI 만들기 ( Flutter )

Flutter에서는 다양한 애니메이션과 인터랙션을 쉽게 구현할 수 있습니다. 이번에는 사용자의 움직임(터치 또는 드래그)에 따라 이미지가 기울어지는 카드 효과를 만들어보겠습니다. 이 효과는

developer0524.tistory.com

 

 

반응형