[Flutter] 위젯을 이미지로 찍어내기

2023. 3. 19. 14:37Developers 공간 [Shorts]/Frontend

728x90
반응형
<분류>
A. 수단
- OS/Platform/Tool : Linux, Kubernetes(k8s), Docker, AWS
- Package Manager : node.js, yarn, brew, 
- Compiler/Transpillar : React, Nvcc, gcc/g++, Babel, Flutter

- Module Bundler  : React, Webpack, Parcel

B. 언어
- C/C++, python, Javacsript, Typescript, Go-Lang, CUDA, Dart, HTML/CSS

C. 라이브러리 및 프레임워크 및 SDK
- OpenCV, OpenCL, FastAPI, PyTorch, Tensorflow, Nsight

 


1. What? (현상)

위젯 자체를 이미지로 찍어내는 방법에 대해 정리하고자 합니다.

 


2. Why? (원인)

  • X

3. How? (해결책)

  • 먼저 어떤 위젯을 선언할 때 원하는 위젯을 RepaintBoundary()로 감싼다
    • RepaintBoundary() : 분리된 Layer Tree, 즉 Display 리스트를 만들어 내기 위한 위젯입니다.
      (자세하게는 아래 더보기를 참조)
    • GlobalKey() : 나중에 어떤 object를 활용해 Rendering할지를 알기 위해 Global key를 지정해주고, RepainBoundary()의 key로 넣어줍니다.
더보기

-----------------------------------------------------------------------------------------------------

<Flutter에서 Rendering하는 단계에 대해서>

 

[출처 : https://terry1213.github.io/flutter/flutter-decoding-flutter-rendering/]
  1. User Input & Animation : user에게 Input을 새로받거나, 새로운 이벤트로 인해 build()가 불립니다.
  2. Build :Build가 불리면 Widget Tree를 구성하고, 이는 Element Tree와 Render Tree를 구성합니다.
     ===============Rendering!==================
  3. Layout : Render Tree의 Object(RenderObject)가 레이아웃을 구성합니다. 이 때 Constraints 혹은 BoxConstraints를 활용해 계산된 사이즈를 child에게 차례대로 건네 줍니다.
  4. Paint : Render Tree의 Object(RenderObject)는 말 그대로 pixel단위로 캔버스라는 곳에 페인팅을 진행합니다.
  5. Compositing : 합성이라는 뜻으로, 스크롤바 처럼 이어지는 동작의 경우 여러가지 Paint된 캔버스들을 합성하는 단계입니다. 이때 Layer tree라는 것을 활용하며, Layer Tree의 하나의 Object가 캔버스라고 할 수 있습니다.
    ** 이때 Flutter는 RenderObject 여러개를 하나의 레이어로 그룹화하므로, 레이어가 구분되어 불필요한 추가 페인팅 작업을 줄일 수 있습니다. 근데, 사용자가 RepaintBoundary() 위젯을 통해 자체적인 레이어를 구분해준다면, 해당 레이어를 기준으로 페인팅 작업이 이루어지기 때문에, 성능이 향상될 수도 있습니다.
  6. Rasterize : 이제 Layer Tree를 전달받아 프린트 가능한  픽셀 형태로 전환하는 작업을 진행합니다.

-----------------------------------------------------------------------------------------------------

List<Widget> tabNames = <Widget>[
    Page1(),
    Page2(),
    Page3(),
    Page4()
  ];
final gkeytemp = GlobalKey();
  
...
    child: PageView(
      scrollDirection: Axis.horizontal,
      controller: viewModel.pageController,
      children: List.generate(tabNames.length, (index) => RepaintBoundary(
          key:gkeytemp,
          child: tabNames[index],
      )),
      onPageChanged: (value) {
      },
    ),
...
  • 이제 RenderRepaintBoundary를 만들어 진행합니다.
    • join :임시 디렉토리의 임시파일에 저장할 것입니다.
    • RenderRepaintBoundary : Global key를 활용해 Render Object를 찾아 냅니다. 이 때 선언되는 RenderRepaintBoundary 클래스는 Render Object를 나타내는 클래스입니다.
      • 이를 활용해 toImage()함수로 Image를 얻어 냅니다.
      • MediaQuery.of(context).devicePixelRatio : toImage의 파라미터로 pixelRatio를 넣어주는데, 이는 Logical Pixel과 Output Image간의 배율을 의미합니다. 사실 MediaQuery에서 얻어지는 값을 굳이 줄 필요는 없지만, physical Pixel 개수를 넣어주기 위해 MediaQuery를 활용했습니다.
      • 이후에 Uint8 List로 바꾸어준다음에 File에 기록을 해주면 파일형태의 이미지가 완성됩니다.
import 'package:flutter/rendering.dart';
import 'package:flutter_native_image/flutter_native_image.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:ui' as ui;
import 'dart:io'
...
    final path = join(
      (await getTemporaryDirectory()).path,
      '${DateTime.now()}.png',
    );
    if(gkeytemp!=null){
      final RenderRepaintBoundary rojecet = gkeytemp.currentContext!.findRenderObject() as RenderRepaintBoundary;
      final ui.Image tempscreen = await rojecet.toImage(
      	pixelRatio:MediaQuery.of(gkeytemp.currentContext!).devicePixelRatio
      );
      final ByteData? byteData = await tempscreen.toByteData(format:ui.ImageByteFormat.png);
      final Uint8List png8Byttes =byteData!.buffer.asUint8List();
      final File file = File(path);
      await file.writeAsBytes(png8Byttes);
    }else{
      print("ERROR!!");
    }
...

 


https://www.educative.io/answers/how-to-convert-a-widget-to-an-image-in-flutter

https://api.flutter.dev/flutter/rendering/RenderRepaintBoundary/toImage.html

https://terry1213.github.io/flutter/flutter-decoding-flutter-rendering/

728x90
반응형