Flutter - 포커스 제어하기 (텍스트필드 키보드 올리기/내리기, 포커스 감지, 포커스 상태 확인하기) (FocusManager/FocusNode)

반응형

개요

이번 포스팅에서는 포커스를 다룬다.

포스팅의 주요 목적은

1) 애플리케이션 내에서 입력 상자에 대한 포커스의 상태를 확인한다.

2) 텍스트필드 입력 상태를 다른 영역을 터치하여 입력 상태를 끝내 올라오는 키보드를 없앤다.

3) 조건에 의해 텍스트필드에 입력 상태로 이동한다.

* 텍스트필드로 이동된 상태에는 포커스가 활성화되고 키보드가 노출된다.


[ 소스 코드 다운로드 ]

포스팅에서 설명하는 프로젝트는 깃허브에서 다운로드할 수 있다.


간단 요구사항

1. 텍스트 입력 기능

  • 사용자는 애플리케이션 내에서 텍스트의 내용을 입력할 수 있어야 한다.

2. 입력 키보드 제거

  • 텍스트 입력을 희망하지 않을 경우, 다른 영역을 터치하여 키보드를 없앨 수 있어야 한다.

3. 텍스트 입력 유도

  • 사용자가 입력한 내용이 존재하지 않을 경우, 알림 메시지와 함께 텍스트필드에 포커스를 이동한다.

UI 구성

1. 텍스트필드 (TextField)

  • 내용 작성을 위함

2. 버튼 (ElevatedButton)

  • 작성 내용을 확인하고, 작성된 내용이 없으면 텍스트필드로 커서를 이동시키기 위함

3. 텍스트 (Text)

  • 작성 내용 없이 버튼을 누를 경우, 내용을 입력해 달라는 알림 메시지를 사용자에게 표시하기 위함

기능 설계

1. 텍스트 입력 및 내용 확인

텍스트필드 위젯으로 구성하여 텍스트를 입력할 수 있도록 한다.

컨트롤러는 TextEditingController를 사용하여 텍스트필드 위젯의 작성 내용을 확인할 수 있도록 한다.

 

2. 텍스트 내용이 없을 경우, 입력 유도 [ 포스팅에서의 중요 요소 ]

버튼 위젯으로 구성하여 터치 시 텍스트필드의 입력 내용을 확인한다.

입력 내용이 없을 경우, 텍스트필드로 포커스를 이동시켜 입력을 유도한다.

  • 텍스트필드로 포커스를 이동시키기 위해서는 FocusNode를 사용한다.

 

3. 텍스트 내용이 없을 경우, 알림 메시지

텍스트 위젯으로 구성하여 텍스트필드의 입력 내용이 없으면 내용을 입력해 달라는 문구를 노출시킨다.

 

4. 내용 입력 상태에서 다른 영역을 터치하여 글자 입력 키보드 제거 [ 포스팅에서의 중요 요소 ]

텍스트필드를 눌러 입력 상태가 될 경우, 글자를 입력할 수 있는 키보드가 노출된다.

노출된 키보드를 사용자가 다른 영역을 터치하여 키보드를 내릴 수 있어야 한다.

(다른 영역 터치 시 키보드 내리기)

  • 다른 영역 터치 시 포커스를 제거하여 키보드를 없애기 위해서는 FocusManager를 사용한다.

포커스 제어 관련 클래스 설명

포스팅에서는 포커스를 제어하기 위한 FocusManagerFocusNode에 대한 설명을 간략히 하였습니다.

 

FocusManager

  • 포커스 전체 트리를 관리하는 클래스
  • 앱 전체의 포커스를 관리하고 모든 포커스 가능한 위젯의 부모 역할을 함
    • 포커스의 이동, 포커스의 이동 순서, 포커스의 상태 등
    • 특정 BuildContext에 대한 FocusNode를 찾으려면 Focus.of를 사용
    • 특정 BuildContext에 대한 FocusScopeNode를 찾으려면 FocusScope.of를 사용
    • 어디서든 현재 포커스 관리자 상태에 편리하게 액세스 하려면 PrimaryFocus 전역 접근자를 사용
    • 싱글톤 FocusManager Instance는 FocusManager.instance 정적 접근자를 사용
  • PrimaryFocus가 변경될 때마다 알림을 받으려면 addListner를 사용하여 리스너를 등록
  • 메모리 누수 방지를 위해 일반적으로 State.dispose에서 RemoveListner를 사용하여 등록 취소

FocusNode

  • 키보드 포커스를 얻고 키보드 이벤트 처리하는 클래스
  • 개별 위젯의 포커스 동작 제어
  • 포커스 가능한 위젯은 모두 FocusNode 속성을 가질 수 있음
  • 위젯이나 UI 요소의 포커스를 설정하고 포커스가 변경될 때 알림을 받을 수 있음
  • 부모 역할을 하는 FocusManager에 의해 관리

코딩

설명은 포커스에 관련된 주요 내용만을 포함하며, 그 외 내용은 생략하였다.

 

메인 실행 함수

  • 포커스 샘플 페이지인 FocusPage 로 이동
  • FocusPage 클래스는 포커스의 상태를 제어하고 이동해야하기 때문에 StatefulWidget으로 생성한다.
void main() {
  runApp(const FocusPage());
}

// class FocusPage extends StatefulWidget

메인 페이지 - 입력 상태 시 키보드 내리기 기능

  • 입력 상태에서 다른 영역의 터치를 감지해야 하므로 루트 최상위에 GestureDetector 위젯을 위치 시킨다.
  • GestureDetectoronTap 속성을 이용하여 포커스 제어 코드를 작성한다.
    • FocusManagerPrimaryFocus를 이용하여 현재 포커스에 접근한다.
    • 포커스 상태가 유효할 경우, unfocus() 메서드를 이용하여 포커스를 제거한다.
  @override
  Widget build(BuildContext context) {

    return GestureDetector(
      // GestureDetector 위젯을 최상위로 감싸 터치 감지 후, 입력 관련 외 위젯을 터치하면 포커스 제거
      onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
      child: MaterialApp(
        home: Scaffold(
          body: SafeArea(
            child: // UI 구성
          ),
        ),
      ),
    );
  }

기본 위젯 구조 정의

각 위젯들이 위치할 구조를 정의한다.

Column 위젯 하위의 각각 차례로 TextField, Text, ElevatedButton을 배치한다.

class _FocusPageState extends State<FocusPage> {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
      child: MaterialApp(
        home: Scaffold(
          body: SafeArea(
            child: Column(
              children: [
                // 내용을 입력할 텍스트필드
                TextField(),
                // 입력 내용 관련 알림 메시지
                Text(),
                // 확인 버튼
                ElevatedButton()
              ],
            ),
          ),
        ),
      ),
    );
  }
}

텍스트필드 위젯 포커스 상태 확인하기

변수 및 함수 정의

개별 위젯인 텍스트필드의 위젯의 포커스를 확인하기 위해 FocusNode 인스턴스를 생성한다.

final FocusNode _focusNode = FocusNode(); // 텍스트필드의 포커스노드

 

포커스의 상태 확인을 위한 함수를 정의한다.

이는 포커스 관련 리스너를 설명하고 정의하기 위해 편의상 함수로 정의하였다.

  // 포커스 상태 확인 함수
  void _handleFocusChange() {
    if (_focusNode.hasFocus) {
      debugPrint('##### focus on #####');
    } else {
      debugPrint('##### focus off #####');
    }
  }

 

포커스 노드 연결

정의한 포커스 노드와 연결할 위젯을 지정한다.

포스팅에서는 텍스트필드 위젯을 연결한다.

포커스 노드를 통해 텍스트필드 위젯의 포커스를 관리하고 제어한다.

TextField(focusNode: _focusNodd)

 

포커스 리스너 등록 / 해제

포커스의 상태를 알리기 위한 리스너를 정의한다.

  @override
  Widget build(BuildContext context) {
    // 포커스 상태 확인을 위한 리스너
    _focusNode.addListener(() => _handleFocusChange);
    
    // 생략
  }

 

메모리의 누수를 막기 위해서는 생명 주기의 dispose 단계에서 생성한 FocusNode와 등록한 리스너를 제거해 준다.

  @override
  void dispose() {
    // 포커스 노드 및 리스너 제거
    _focusNode.removeListener(_handleFocusChange);
    _focusNode.dispose();
    
    super.dispose();
  }

텍스트 내용 확인 및 포커스 제어하기

텍스트 내용 확인을 위한 컨트롤러 정의

텍스트의 내용을 확인하기 위해 TextEditingController 인스턴스를 생성하고,

TextField 위젯의 Controller 속성에 지정해 준다.

final TextEditingController _textController =
      TextEditingController(); // 텍스트필드의 컨트롤러
      
// 코드 생략

TextField(controller: _textController)

 

입력 요청 알림 메시지 정의 및 표시 위젯 생성

입력 요청 알림 메시지를 저장할 문자열 변수를 정의하고,

Text 위젯을 생성하여 변수의 값을 출력하도록 한다.

String notifyText = ""; // 텍스트필드 입력 관련 알림 메시지

// 입력 내용 관련 알림 메시지
Text(notifyText, style: const TextStyle(color: Colors.red)),

 

텍스트 내용 확인 및 포커스 이동

ElevatedButton 위젯을 이용하여 버튼을 터치하면 TextField의 내용을 확인하고,

빈 내용이면 TextField의 포커스로 이동하도록 한다.

포커스 노드를 이동시킨다는 말을 바꿔 말하면, 부모 포커스 매니저로부터 포커스를 얻어야 하는 것과 같다.

부모로부터 포커스를 요청을 해야 하기 때문에 requestFocus 메서드를 사용한다.

// 확인 버튼
ElevatedButton(
  onPressed: () {
    // 텍스트필드에 입력 내용 확인
    if (_textController.text == "") {
      // 내용이 없으면 포커스를 텍스트필드로 이동하고 알림 메시지 출력
      _focusNode.requestFocus();
      setState(() => notifyText = "내용을 입력해주세요.");
    } else {
      setState(() => notifyText = "");
    }
  },
  child: const Text("확인"),
)

전체 소스 코드

위의 조건들로 페이지와 기능을 구성해 보자.

import 'package:flutter/material.dart';

void main() {
  runApp(const FocusPage());
}

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

  @override
  State<FocusPage> createState() => _FocusPageState();
}

class _FocusPageState extends State<FocusPage> {
  final FocusNode _focusNode = FocusNode(); // 텍스트필드의 포커스노드
  final TextEditingController _textController =
      TextEditingController(); // 텍스트필드의 컨트롤러
  String notifyText = ""; // 텍스트필드 입력 관련 알림 메시지

  // 포커스 상태 확인 함수
  void _handleFocusChange() {
    if (_focusNode.hasFocus) {
      debugPrint('##### focus on #####');
    } else {
      debugPrint('##### focus off #####');
    }
  }

  @override
  Widget build(BuildContext context) {
    // 포커스 상태 확인을 위한 리스너
    _focusNode.addListener(() => _handleFocusChange);

    return GestureDetector(
      // GestureDetector 위젯을 최상위로 감싸 터치 감지 후, 입력 관련 외 위젯을 터치하면 포커스 제거
      onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
      child: MaterialApp(
        home: Scaffold(
          body: SafeArea(
            child: Column(
              children: [
                // 내용을 입력할 텍스트필드
                TextField(
                  focusNode: _focusNode,
                  controller: _textController,
                  decoration: const InputDecoration(labelText: '내용 입력'),
                ),
                // 입력 내용 관련 알림 메시지
                Text(notifyText, style: const TextStyle(color: Colors.red)),
                // 확인 버튼
                ElevatedButton(
                    onPressed: () {
                      // 텍스트필드에 입력 내용 확인
                      if (_textController.text == "") {
                        // 내용이 없으면 포커스를 텍스트필드로 이동하고 알림 메시지 출력
                        _focusNode.requestFocus();
                        setState(() => notifyText = "내용을 입력해주세요.");
                      } else {
                        setState(() => notifyText = "");
                      }
                    },
                    child: const Text("확인"))
              ],
            ),
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    // 포커스 노드 및 리스너 제거
    _focusNode.removeListener(_handleFocusChange);
    _focusNode.dispose();
    
    super.dispose();
  }
}

참고

 

반응형