관리 메뉴

Bull

[Flutter::Widget] Netiveview(ImagePicker)에서 상태 변경으로 이미지가 렌더링될 때 스크롤 하는 방법 본문

Software Framework/Flutter

[Flutter::Widget] Netiveview(ImagePicker)에서 상태 변경으로 이미지가 렌더링될 때 스크롤 하는 방법

Bull_ 2024. 9. 1. 11:09

내가 하고 있는 아르바이트의 수강생의 질문이었다. ImagePicker에서 이미지를 로드하고 상태 변화에 따라 위젯에 사진을 추가하는 로직인데 이 때 사진의 높이가 크면 스크롤뷰를 통해서 아래 구간에 공간을 추가하고 스크롤을 내려서 확인할 수 있는 위젯이다.

 

이때 scrollToBottom 메소드를 호출해서 스크롤을 아래쪽으로 자동 모션을 진행한다. 하지만 그래서 버튼 부분에 await을 통해 chooseImage를 호출하고 scrollToBottom을 호출하는 형식으로 진행했다.

 

하지만 이 이론에서 버그가 생겼다. 이미지 로드 -> 상태 변화 감지 -> 자동 스크롤 -> 재렌더링으로 일어나기 때문이다. 이렇게 되면 사진을 추가해도 재렌더링되기 전의 상태에서 scrollToBottom을 호출하기 때문에 스크롤을 내려도 변화가 없다. 이후에 재렌더링이 되기 때문에 원하는 방식을 어느 부분에서 호출을 하여도 이룰 수 없었다.

 

여러 클래스를 적용하던 중 Image.File에 있는 frameBuilder 프로퍼티를 찾았다. 이 빌더를 통해 이미지 프레임이 빌드된 이후의 상태를 체크할 수 있다. 그러면 이 스코프안에 scrollToBottom을 호출하면 우리가 원했던 방향으로 나아갈 수 있다.

frameBuilder: (BuildContext context, Widget child,
    int? frame, bool wasSynchronouslyLoaded) {
  if (frame != null || wasSynchronouslyLoaded) {
    widget.scrollToBottom();
  }
  return child;
}

wasSynchronouslyLoaded : 이미지가 동기적으로 로드되었는지 확인할 수 있다.
frame : 이미지가 몇 번째 프레임되고 있는지 확인할 수 있다. 일반적으로 gif처럼 움짤이 아니므로 단일 프레임으로 사용되기 때문에 null이 아님을 체크해주면 된다.

import 'dart:io';

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

void main() {
  runApp(MaterialApp(
    home: ParentWidget(),
  ));
}

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  final ScrollController _scrollController = ScrollController();

  void scrollToBottom() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _scrollController.animateTo(
        _scrollController.position.maxScrollExtent,
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeOut,
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Scroll Test"),
      ),
      body: SingleChildScrollView(
        controller: _scrollController,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              ChildWidget(scrollToBottom: scrollToBottom),
            ],
          ),
        ),
      ),
    );
  }
}


class ChildWidget extends StatefulWidget {
  final VoidCallback scrollToBottom;

  const ChildWidget({required this.scrollToBottom, Key? key}) : super(key: key);

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

class _ChildWidgetState extends State<ChildWidget> {
  File? imageFile;

  Future<void> chooseImage(ImageSource source) async {
    try {
      final XFile? pickedImage = await ImagePicker().pickImage(source: source);

      if (pickedImage != null) {
        setState(() {
          imageFile = File(pickedImage.path);
        });
      }
    } catch (errorMsg) {
      print(errorMsg.toString());
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        ElevatedButton(
          onPressed: () {
            showDialog(
              context: context,
              builder: (BuildContext context) {
                return SimpleDialog(
                  title: Text('Select Image'),
                  children: [
                    SimpleDialogOption(
                      child: Text('From Gallery'),
                      onPressed: () async {
                        Navigator.pop(context);
                        await chooseImage(ImageSource.gallery);
                      },
                    ),
                    SimpleDialogOption(
                      child: Text('From Camera'),
                      onPressed: () async {
                        Navigator.pop(context);
                        await chooseImage(ImageSource.camera);
                      },
                    ),
                  ],
                );
              },
            );
          },
          child: Text("Choose Image"),
        ),
        SizedBox(height: 400),
        Container(
          decoration: BoxDecoration(
            border: Border.all(color: Colors.blueAccent),
            borderRadius: BorderRadius.all(Radius.circular(5)),
          ),
          child: imageFile == null
              ? Center(child: Text("No image selected"))
              : ClipRRect(
                  borderRadius: BorderRadius.circular(8),
                  child: Image.file(
                    imageFile!,
                    frameBuilder: (BuildContext context, Widget child,
                        int? frame, bool wasSynchronouslyLoaded) {
                      if (frame != null || wasSynchronouslyLoaded) {
                        widget.scrollToBottom();
                      }
                      return child;
                    },
                  ),
                ),
        ),
      ],
    );
  }
}