Json 데이터 예시

[
    {
        "uid" : 1,
        "thumbnail": "assets/images/1.jpg",
        "title" : "꼬북좌 이미지1",
        "description" : "남심 '저격'브레이브걸스 유정 근황"
    },
    {
        "uid" : 2,
        "thumbnail": "assets/images/2.jpg",
        "title" : "꼬북좌 이미지2",
        "description" : "브레이브걸스 꼬북좌 유정 사진짤 방출"
    },
    {
        "uid" : 3,
        "thumbnail": "assets/images/3.jpg",
        "title" : "꼬북좌 이미지3",
        "description" : "예상치 못한 인스타에 팬들 반응 난리났다."
    },
    {
        "uid" : 4,
        "thumbnail": "assets/images/4.jpg",
        "title" : "꼬북좌 이미지4",
        "description" : "꼬북좌 인스타 이미지"
    },
    {
        "uid" : 5,
        "thumbnail": "assets/images/5.jpg",
        "title" : "진짜 꼬부기",
        "description" : "진짜 꼬부기 이미지"
    }
]

GetxController를 상속 받아 Json 데이터를 가져오는 컨트롤러

class JsonLoader extends GetxController {
  static JsonLoader get to => Get.find();
  var list = <Map<String, dynamic>>[].obs;
  @override
  void onInit() async {
    super.onInit();
    _loadJsonFile();
  }

  void _loadJsonFile() async {
    if (Get.context != null) {
      String data = await DefaultAssetBundle.of(Get.context!)
          .loadString("assets/json/post.json");
      list(json.decode(data).cast<Map<String, dynamic>>().toList());
    } else {
      Future.delayed(Duration(milliseconds: 200), _loadJsonFile);
    }
  }
}

SingleChildScrollView로 GetX의 Obx로 Json 데이터 리스트 위젯 만들기

SingleChildScrollView(
  child: Obx(
    () => Column(
      children: List.generate(
        JsonLoader.to.list.length,
        (index) {
        // 부모에서 JsonLoader binding
        // 컨트롤러에서 만든 리스트를 post에 저장
          var post = JsonLoader.to.list[index]
              .map<String, String>((key, value) {
            return MapEntry(key.toString(), value.toString());
          });
          return PostWidget(
            uid: post['uid']!,
            thumbnail: post['thumbnail']!,
            title: post['title']!,
            description: post['description']!,
            callBack: () {
            // detail 페이지로 이동시 파라미터로 해당 위젯의 post 넘겨줌
              Get.toNamed('/detail', parameters: post);
            },
          );
        },
      ),
    ),
  ),
)

리스트의 아이템 만들기

typedef PostClickFunction = void Function();

class PostWidget extends StatelessWidget {
  final String uid;
  final String thumbnail;
  final String title;
  final String description;
  final PostClickFunction callBack;

  const PostWidget(
      {required this.uid,
      required this.thumbnail,
      required this.title,
      required this.description,
      required this.callBack});

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: callBack,
      child: Container(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Image.asset(thumbnail),
            Padding(
              padding: const EdgeInsets.only(
                  top: 10, bottom: 20, left: 10, right: 10),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  Text(
                    title,
                    style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
                  ),
                  Text(
                    description,
                    style: TextStyle(
                      fontSize: 12,
                    ),
                  )
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

callback을 통해 디테일 페이지로 이동

리스트 위젯의 callback 부분에 Get.toNamed로 최상단에 지정한 라우팅 주소 넣고, 파라미터로 데이터 넘겨주기

GetxController를 통해 파라미터 받아오기

class TextAnimation extends GetxController {
  Map<String, String?>? post;

  @override
  void onInit() {
    super.onInit();
    _loadData();
  }

  void _loadData() {
    post = Get.parameters;
  }
}

위에서 만든 GetxController 형식의 GetView를 상속받아 파라미터의 데이터 가져오기

class PostDetailView extends GetView<TextAnimation> {
  const PostDetailView();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(controller.post!['title']!),
      ),
      body: Column(
        children: [
          Image.asset(controller.post!['thumbnail']!),
          Column(
            children: [
              Text(
                controller.post!['title']!,
              ),
              Text(
                controller.post!['description']!,
              ),
            ],
          ),
        ],
      ),
    );
  }
}
728x90

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: animatedSwicher(),
    );
  }
}

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

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

class _animatedSwicherState extends State<animatedSwicher> {
  // 바뀔 위젯을 변수처리
  // var 타입은 값이 한번 정해지면 고정
  // var mWidget = FirstWidget();
  // dynamic 또는 부모타입인 widget
  Widget mWidget = FirstWidget();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedSwitcher(
              duration: Duration(seconds: 3),
              child: mWidget,
            ),
            RaisedButton(
              onPressed: () {
                setState(() {
                  mWidget = SecondWidget();
                });
              },
              child: Text("버튼"),
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.blue,
      width: 100,
      height: 100,
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.orange,
      width: 100,
      height: 100,
    );
  }
}
728x90

import 'dart:async';
import 'dart:math';

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: customRotationTransition(),
    );
  }
}

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

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

class _customRotationTransitionState extends State<customRotationTransition>
    with SingleTickerProviderStateMixin {
  double radian = 0.0;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    // 지속적으로 무언가를 실행할땐 timer
    Timer.periodic(Duration(milliseconds: 100), (timer) {
      setState(() {
        radian = radian + pi / 9;
      });
      // setState가 재실행되기 때문에 랜더링이 계속 됨
    });
    // Future.delayed(Duration(seconds: 3),()=>{});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            RaisedButton(
              onPressed: () {
                setState(() {
                  radian = radian + pi / 2;
                });
              },
              child: Text("버튼"),
            ),
            Transform.rotate(
              // angle 만큼 회전
              // 단위 pi -> radian 개념 (반지름의 길이와 같은 원의 둘레)
              // radian이 되는 각도는 57도, 180도일때의 반지름과 3레디안의 차이는 0.14임
              // 원의 둘레는 radian으로 표현 => 원 둘레 = 5.x * radian
              // pi = 3 radian - 0.14
              // 원의 둘레 = pi * 2
              angle: radian,
              child: Container(
                width: 100,
                height: 100,
                color: Colors.blue,
                child: Text("왼쪽모서리"),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

참고

Timer를 통해 setState를 계속 호출하는건 현재 페이지의 랜더링을 계속하기 때문에 비효율적
버튼을 통해 애니메이션이 필요할때 위의 방법이 유용

728x90

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: positionedTransition(),
    );
  }
}

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

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

class _positionedTransitionState extends State<positionedTransition>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  @override
  void initState() {
    // TODO: implement initState
        // PositionedTransition의 컨트롤러를 컨트롤할 컨트롤러 
    _animationController =
        AnimationController(vsync: this, duration: Duration(seconds: 5));
    _animationController.repeat();
    super.initState();
    // 컨트롤러 초기화
    ;
  }

  @override
  Widget build(BuildContext context) {
        // 미디어쿼리로 디바이스 높이가져옴
    double height = MediaQuery.of(context).size.height;
        // PositionedTransition 컨트롤러 생성
    Animation<RelativeRect> _controller = RelativeRectTween(
            begin: RelativeRect.fromLTRB(0, height, 0, 0),
            end: RelativeRect.fromLTRB(0, 0, 0, 0))
        .animate(CurvedAnimation(
            parent: _animationController, curve: Curves.easeInOut));
    return Scaffold(
      body: Stack(
        children: [
                    // PositionedTransition은 스택에서만 작동
          PositionedTransition(
            rect: _controller,
            child: Container(color: Colors.blue),
          ),
        ],
      ),
    );
  }
}
728x90

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: scaleTransition(),
    );
  }
}

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

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

// vsync에 this를 넣기위해 tickerprovider를 mixin해줘야함
class _scaleTransitionState extends State<scaleTransition>
    with SingleTickerProviderStateMixin {
  // ScaleTransition의 controller
  late AnimationController _animationController;

  @override
  void initState() {
    // 컨트롤러 초기화
    _animationController = AnimationController(
      duration: Duration(seconds: 3),
      vsync: this,
    );
    // animation 형태
    _animationController.forward();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    // tween begin부터 end까지
    // animate 동안
    Animation<double> _animation =
        Tween(begin: 0.0, end: 1.0).animate(_animationController);

    return Scaffold(
      body: Center(
        child: ScaleTransition(
          // animation 구현할땐 Animation 객체가 필요
          scale: _animation,
          child: Container(
            width: 200,
            height: 200,
            color: Colors.blue,
          ),
        ),
      ),
    );
  }
}

AnimationController 문서에 required로 TickerProvider타입의 vsync 필요

  AnimationController({
    double? value,
    this.duration,
    this.reverseDuration,
    this.debugLabel,
    this.lowerBound = 0.0,
    this.upperBound = 1.0,
    this.animationBehavior = AnimationBehavior.normal,
    required TickerProvider vsync,
  }) : assert(lowerBound != null),
       assert(upperBound != null),
       assert(upperBound >= lowerBound),
       assert(vsync != null),
       _direction = _AnimationDirection.forward {
    _ticker = vsync.createTicker(_tick);
    _internalSetValue(value ?? lowerBound);
  }

TickerProvider 설명

하나의 애니메이션 : SingleTickerProviderStateMixin
여러 애니메이션 : TickerProviderStateMixin

/// An interface implemented by classes that can vend [Ticker] objects.
///
/// Tickers can be used by any object that wants to be notified whenever a frame
/// triggers, but are most commonly used indirectly via an
/// [AnimationController]. [AnimationController]s need a [TickerProvider] to
/// obtain their [Ticker]. If you are creating an [AnimationController] from a
/// [State], then you can use the [TickerProviderStateMixin] and
/// [SingleTickerProviderStateMixin] classes to obtain a suitable
/// [TickerProvider]. The widget test framework [WidgetTester] object can be
/// used as a ticker provider in the context of tests. In other contexts, you
/// will have to either pass a [TickerProvider] from a higher level (e.g.
/// indirectly from a [State] that mixes in [TickerProviderStateMixin]), or
/// create a custom [TickerProvider] subclass.

비슷한 애니메이션

FadeTransition

scale을 opacity로 변경, Tween을 0.0부터 1.0사이

728x90

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp()); // 비동기로 실행(이벤트 루프에 등록된다.)
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHome(),
    );
  }
}

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

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

class _MyHomeState extends State<MyHome> {
  late Size size;

  // 컨테이너 애니메이션
  bool isOpen = false;
  @override
  void initState() {
    super.initState();
    // error : context를 찾지 못함
    // size = MediaQuery.of(context).size;
  }

  @override
  Widget build(BuildContext context) {
    size = MediaQuery.of(context).size;
    return Scaffold(
      appBar: AppBar(
        actions: [
          // 아이콘을 버튼화
          InkWell(
            child: Icon(Icons.menu),
            onTap: () {
              setState(() {
                // 클릭할때마다 isOpen의 값이 반대로
                isOpen = !isOpen;
              });
            },
          ),
        ],
      ),
      body: Stack(
        children: [
          Center(
            child: Text("Animation screen"),
          ),
          AnimatedContainer(
            // 속도
            duration: Duration(seconds: 2),
            // animation 형태
            curve: Curves.easeInOut,
            height: double.infinity,
            // 컨테이너의 가로 사이즈
            width: size.width * (2 / 3),
            color: Colors.blue,
            // isOpen 값에 따라 가로 위치 이동
            transform: Matrix4.translationValues(
                isOpen ? size.width * (1 / 3) : size.width, 0, 0),
          )
        ],
      ),
    );
  }
}

728x90

+ Recent posts