Flutter에서 커스텀 텍스트 필드를 구현할 때, 사용자의 선택 영역 및 백스페이스 키 입력을 섬세하게 처리하는 것은 중요한 사용자 경험(UX) 요소입니다. 특히 태그 멘션 기능과 같이 특정 패턴의 텍스트를 특별하게 관리해야 하는 경우, 이 부분에 대한 정확한 이해와 구현이 필요합니다.
우선 커스텀 필드에서 TextSelection컨트롤 하는 부분에 대해 파악하고 넘어가야할것 같습니다.
CustomTagController({
required this.onSearchHospital,
required this.onExitSearch,
String? text,
}) : super(text: text);
@override
set value(TextEditingValue newValue) {
final oldText = this.text;
final newText = newValue.text;
this.text는 TextEditingController의 현재 텍스트 값을 나타냅니다.
oldText 변수는 텍스트 필드의 이전 텍스트 값을 저장합니다. 즉, 텍스트가 변경되기 전의 텍스트 내용을 담고 있습니다.
final 키워드를 사용하여 선언되었으므로, oldText 변수는 한 번 할당된 후에는 값을 변경할 수 없습니다.
newValue.text는 TextEditingValue 객체의 text 속성을 나타냅니다.
newValue는 value setter가 호출될 때 전달되는 새로운 TextEditingValue 객체이며, 여기에는 텍스트 필드의 변경된 상태가 포함됩니다.
newText 변수는 텍스트 필드의 새로운 텍스트 값을 저장합니다. 즉, 텍스트가 변경된 후의 텍스트 내용을 담고 있습니다.
final 키워드를 사용하여 선언되었으므로, newText 변수는 한 번 할당된 후에는 값을 변경할 수 없습니다.
newValue.selection은 TextEditingValue 객체의 selection 속성을 나타내며, 이는 TextSelection 객체입니다.
TextSelection은 텍스트 필드 내에서 선택된 텍스트의 범위를 나타냅니다.
baseOffset은 선택 영역의 시작 위치를 나타냅니다. 즉, 선택된 텍스트의 첫 번째 문자의 인덱스를 나타냅니다.
만약 텍스트가 선택되지 않았다면, baseOffset은 커서의 위치를 나타냅니다.
newValue.selection은 TextEditingValue 객체의 selection 속성을 나타내며, 이는 TextSelection 객체입니다.
TextSelection은 텍스트 필드 내에서 선택된 텍스트의 범위를 나타냅니다.
extentOffset은 선택 영역의 끝 위치를 나타냅니다. 즉, 선택된 텍스트의 마지막 문자의 다음 인덱스를 나타냅니다.
만약 텍스트가 선택되지 않았다면, extentOffset는 커서의 위치를 나타냅니다.
아래 코드는 사용자가 텍스트 필드에서 특정 텍스트 영역(멘션 토큰)을 부분적으로 선택했을 때, 선택 영역을 해당 토큰 전체로 확장하는 역할을 합니다.
코드 분석:
final newSelection = TextSelection(
baseOffset: match.start,
extentOffset: match.end,
);
// 선택 영역만 변경
final updatedValue = TextEditingValue(
text: oldText,
selection: newSelection,
);
super.value = updatedValue;
return;
TextSelection 생성:
TextSelection은 텍스트 필드 내에서 선택된 텍스트의 범위를 나타내는 클래스입니다.
baseOffset은 선택 영역의 시작 위치를 나타내며, extentOffset은 선택 영역의 끝 위치를 나타냅니다.
이 코드에서는 match.start와 match.end를 사용하여 선택 영역을 멘션 토큰의 시작과 끝으로 설정합니다. 즉, 사용자가 토큰의 일부분만 선택하더라도 전체 토큰이 선택되도록 범위를 확장합니다.
TextEditingValue 생성:
TextEditingValue는 텍스트 필드의 현재 상태를 나타내는 클래스입니다. 여기에는 텍스트 내용, 선택 영역, 커서 위치 등이 포함됩니다.
이 코드에서는 oldText를 사용하여 기존 텍스트 내용을 유지하고, newSelection을 사용하여 새로운 선택 영역을 설정합니다.
즉, 텍스트 내용은 변경하지 않고 선택 영역만 멘션 토큰 전체로 확장하는 새로운 TextEditingValue를 생성합니다.
super.value 업데이트:
super.value는 TextEditingController의 부모 클래스인 ValueNotifier의 value 속성을 나타냅니다.
TextEditingController의 value를 업데이트하면 텍스트 필드의 상태가 변경되고, 텍스트 필드가 다시 그려져 선택 영역이 업데이트됩니다.
return; 문을 사용하여 더 이상의 처리를 중단하고, 텍스트 컨트롤러를 업데이트합니다.
코드의 역할:
이 코드는 사용자가 멘션 토큰의 일부분만 선택했을 때, 전체 토큰을 선택하도록 선택 영역을 확장하여 일관된 사용자 경험을 제공합니다.
예를 들어, 사용자가 "[HOSPITAL:123]" 토큰에서 "[HOSPITAL"만 선택하더라도, 이 코드는 전체 "[HOSPITAL:123]" 토큰을 선택하도록 선택 영역을 확장합니다.
추가 설명:
match는 정규 표현식 tokenRegx를 사용하여 찾은 멘션 토큰의 일치 항목입니다. match.start와 match.end는 일치 항목의 시작과 끝 위치를 나타냅니다.
이 코드는 value setter 내부에 위치하므로, 텍스트 필드의 선택 영역이 변경될 때마다 실행됩니다.
이 코드는 텍스트 내용을 변경하지 않고 선택 영역만 변경하므로, 사용자가 텍스트를 입력하거나 삭제할 때에는 실행되지 않습니다.
이 코드는 커스텀 텍스트 필드에서 멘션 토큰과 같은 특정 패턴의 텍스트를 효과적으로 관리하는 데 유용합니다.
if (oldText.length > newText.length &&
newValue.selection.baseOffset == newValue.selection.extentOffset) {
// 지워진 위치 계산
final cursorPosition = newValue.selection.baseOffset;
// 멘션 토큰 패턴
final mentionPattern = RegExp(r'#\[HOSPITAL:(.*?)\]');
final matches = mentionPattern.allMatches(oldText).toList();
// 삭제된 부분이 멘션 토큰 내부인지 확인
for (final match in matches) {
// 커서가 멘션 토큰 내부 또는 첫 부분에 있는 경우
if ((cursorPosition >= match.start && cursorPosition < match.end) ||
(cursorPosition == match.start - 1)) {
// 토큰 전체를 지우기 위한 새 TextEditingValue 생성
final beforeToken = oldText.substring(0, match.start);
final afterToken = oldText.substring(match.end);
// 수정된 텍스트와 커서 위치로 새 값 설정
final updatedValue = TextEditingValue(
text: beforeToken + afterToken,
selection: TextSelection.collapsed(offset: match.start),
);
super.value = updatedValue;
return;
}
}
}
목적: 사용자가 백스페이스 키를 눌러 텍스트를 삭제할 때, 멘션 토큰의 일부분만 삭제되는 것을 방지하고 토큰 전체를 삭제합니다.
동작 방식:
이전 텍스트(oldText)의 길이보다 새로운 텍스트(newText)의 길이가 짧고, 선택 영역이 없는 경우(커서만 있는 경우)에만 백스페이스 키 입력으로 판단합니다.
삭제된 위치(cursorPosition)를 계산합니다.
이전 텍스트에서 멘션 토큰 패턴을 찾아 삭제된 위치가 토큰 내부에 있는지 확인합니다.
삭제된 위치가 토큰 내부에 있거나 토큰 바로 앞인 경우, 토큰 전체를 삭제하고 커서를 토큰 시작 위치로 이동합니다.
final selStart = newValue.selection.start;
final selEnd = newValue.selection.end;
final mentionPattern = RegExp(tokenRegx);
final matches = mentionPattern.allMatches(oldText).toList();
for (final match in matches) {
// 선택 영역이 멘션 토큰의 일부만 포함하는 경우
if ((selStart > match.start && selStart < match.end) ||
(selEnd > match.start && selEnd < match.end) ||
(selStart < match.start && selEnd > match.start && selEnd < match.end) ||
(selStart > match.start && selStart < match.end && selEnd > match.end)) {
// 선택 범위를 멘션 전체로 확장
final newSelection = TextSelection(
baseOffset: match.start,
extentOffset: match.end,
);
// 선택 영역만 변경
final updatedValue = TextEditingValue(
text: oldText,
selection: newSelection,
);
super.value = updatedValue;
return;
}
}
목적: 사용자가 멘션 토큰의 일부분만 선택했을 때, 선택 범위를 토큰 전체로 확장하여 일관된 선택 경험을 제공합니다.
동작 방식:
선택 영역의 시작 위치(selStart)와 끝 위치(selEnd)를 가져옵니다.
이전 텍스트에서 멘션 토큰 패턴을 찾아 선택 영역이 토큰의 일부분만 포함하는지 확인합니다.
선택 영역이 토큰의 일부분만 포함하는 경우, 선택 범위를 토큰 전체로 확장하고 텍스트 컨트롤러를 업데이트합니다.
정규 표현식(tokenRegx): 멘션 토큰 패턴을 정확하게 정의해야 합니다. 패턴이 잘못 정의되면 의도치 않은 동작이 발생할 수 있습니다.
성능: 텍스트가 길거나 멘션 토큰이 많은 경우, 정규 표현식 매칭 및 텍스트 처리에 시간이 오래 걸릴 수 있습니다. 성능 최적화를 위해 필요한 경우 비동기 처리나 다른 효율적인 알고리즘을 고려해야 합니다.
사용자 경험: 멘션 토큰 선택 및 삭제 시 사용자에게 명확한 피드백을 제공하는 것이 중요합니다. 예를 들어, 선택된 토큰에 하이라이트 효과를 주거나 삭제 시 확인 메시지를 표시할 수 있습니다.
커스텀 텍스트 필드에서 선택 영역 및 백스페이스 키 입력을 섬세하게 처리하는 것은 사용자 경험을 향상시키는 데 중요한 요소입니다. 위에서 설명한 내용을 참고하여 사용자의 의도를 정확하게 반영하고 일관된 동작을 제공하는 커스텀 텍스트 필드를 구현할 수 있습니다.