Flutter開發(fā)之支持放大鏡的輸入框功能實(shí)現(xiàn)
功能需求
最近需求開發(fā)中遇到一個(gè)Flutter開發(fā)問題,為了優(yōu)化用戶輸入體驗(yàn)。產(chǎn)品同學(xué)希望能夠在輸入框支持在移動(dòng)光標(biāo)過程中可以出現(xiàn)放大鏡功能。原先以為是一個(gè)小需求,因?yàn)樵到y(tǒng)上iOS和安卓印象中是自帶這個(gè)功能的。在實(shí)施開發(fā)時(shí)才發(fā)現(xiàn)原來(lái)并不是這樣的,Flutter好像并沒有去支持原有的功能。

需求調(diào)研
為了確認(rèn)官方是否支持了輸入框放大鏡功能,去github項(xiàng)目上搜索issue后發(fā)現(xiàn)這個(gè)問題在18年就有人提到過,但官方卻一直沒有去支持實(shí)現(xiàn)。

既然官方?jīng)]有支持,秉承有輪子我就用的思想繼續(xù)通過github搜索是否有開發(fā)者自定義實(shí)現(xiàn)了這個(gè)功能。
搜索Magnifier找到了一篇文章是對(duì)放大鏡的實(shí)現(xiàn),但他并不是在輸入框上的實(shí)現(xiàn),只對(duì)屏幕手勢(shì)觸摸的地方進(jìn)行放大。

因?yàn)檎也坏酵耆珜?shí)現(xiàn)輸入框放大鏡功能,那么只能自行去實(shí)現(xiàn)該功能了??梢愿鶕?jù)Magnifier來(lái)為輸入框?qū)崿F(xiàn)放大鏡功能。
需求實(shí)現(xiàn)
通過對(duì)TextField的使用會(huì)發(fā)現(xiàn),當(dāng)使用光標(biāo)雙擊或是長(zhǎng)按會(huì)出現(xiàn)TextToolBar功能欄,隨著光標(biāo)的移動(dòng),上方的編輯欄也會(huì)跟著光標(biāo)進(jìn)行移動(dòng)。這個(gè)發(fā)現(xiàn)正好能夠在放大鏡功能上運(yùn)用:跟隨光標(biāo)移動(dòng)+放大就能夠?qū)崿F(xiàn)最終期望的效果了。

源碼解讀
那么在功能實(shí)現(xiàn)之前就需要閱讀TextField源碼了解光標(biāo)上方的編輯欄是如何實(shí)現(xiàn)并且能夠跟隨光標(biāo)的。
PS:源碼解析使用的是extended_text_field,主因是項(xiàng)目中使用了富文本輸入和顯示。
ExtendedTextField輸入框組件源碼找到ExtendedEditableText中視圖build方法可以看到CompositedTransformTarget和_toolbarLayerLink。而這兩個(gè)已經(jīng)是實(shí)現(xiàn)放大鏡功能的關(guān)鍵信息了。
關(guān)于CompositedTransformTarget的使用可以在網(wǎng)上搜到很多,作用是來(lái)綁定兩個(gè)View視圖。除了CompositedTransformTarget之外還有CompositedTransformFollower。簡(jiǎn)單理解就是CompositedTransformFollower是綁定者,CompositedTransformTarget是被綁定者,前者跟隨后者。_toolbarLayerLink就是跟隨光標(biāo)操作欄的綁定媒介。
return CompositedTransformTarget(
link: _toolbarLayerLink, // 操作工具
child: Semantics(
...
child: _Editable(
key: _editableKey,
startHandleLayerLink: _startHandleLayerLink, //左邊光標(biāo)位置
endHandleLayerLink: _endHandleLayerLink, //右邊光標(biāo)位置
textSpan: _buildTextSpan(context),
value: _value,
cursorColor: _cursorColor,
......
),
),
);通過源碼查詢找到_toolbarLayerLink另一個(gè)使用者ExtendedTextSelectionOverlay。
void createSelectionOverlay({ //創(chuàng)建操作欄
ExtendedRenderEditable? renderObject,
bool showHandles = true,
}) {
_selectionOverlay = ExtendedTextSelectionOverlay(
clipboardStatus: _clipboardStatus,
context: context,
value: _value,
debugRequiredFor: widget,
toolbarLayerLink: _toolbarLayerLink,
startHandleLayerLink: _startHandleLayerLink,
endHandleLayerLink: _endHandleLayerLink,
renderObject: renderObject ?? renderEditable,
selectionControls: widget.selectionControls,
.....
);
...通過源碼查詢可以找到CompositedTransformFollower組件使用,可以通過代碼看到selectionControls!.buildToolbar就是編輯欄的實(shí)現(xiàn)。
return Directionality(
textDirection: Directionality.of(this.context),
child: FadeTransition(
opacity: _toolbarOpacity,
child: CompositedTransformFollower( // 操作欄的跟蹤組件
link: toolbarLayerLink,
showWhenUnlinked: false,
offset: -editingRegion.topLeft,
child: Builder(
builder: (BuildContext context) {
return selectionControls!.buildToolbar(
context,
editingRegion,
renderObject.preferredLineHeight,
midpoint,
endpoints,
selectionDelegate!,
clipboardStatus!,
renderObject.lastSecondaryTapDownPosition,
);
},
),
),
),
);
然后返回去找selectionControls是如何實(shí)現(xiàn)的。在_ExtendedTextFieldState中build方法中可以找到textSelectionControls默認(rèn)創(chuàng)建。由于安卓和iOS平臺(tái)存在差異性,因此有cupertinoTextSelectionControls和materialTextSelectionControls兩個(gè)selectionControls。
switch (theme.platform) {
case TargetPlatform.iOS:
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
forcePressEnabled = true;
textSelectionControls ??= cupertinoTextSelectionControls;
......
break;
......
case TargetPlatform.android:
case TargetPlatform.fuchsia:
forcePressEnabled = false;
textSelectionControls ??= materialTextSelectionControls;
.....
break;
....
}這里就只看MaterialTextSelectionControls源碼實(shí)現(xiàn)。布局實(shí)現(xiàn)在_TextSelectionControlsToolbar中。_TextSelectionHandlePainter是繪制光標(biāo)樣式的方法。
@override
Widget build(BuildContext context) {
// 左右光標(biāo)的定位位置
final TextSelectionPoint startTextSelectionPoint = widget.endpoints[0];
// 這里做了判斷是否是兩個(gè)光標(biāo)
final TextSelectionPoint endTextSelectionPoint = widget.endpoints.length > 1
? widget.endpoints[1]
: widget.endpoints[0];
final Offset anchorAbove = Offset(
widget.globalEditableRegion.left + widget.selectionMidpoint.dx,
widget.globalEditableRegion.top + startTextSelectionPoint.point.dy - widget.textLineHeight - _kToolbarContentDistance,
);
final Offset anchorBelow = Offset(
widget.globalEditableRegion.left + widget.selectionMidpoint.dx,
widget.globalEditableRegion.top + endTextSelectionPoint.point.dy + _kToolbarContentDistanceBelow,
);
....
return TextSelectionToolbar(
anchorAbove: anchorAbove, // 左邊光標(biāo)
anchorBelow: anchorBelow,// 右邊光標(biāo)
children: itemDatas.asMap().entries.map((MapEntry<int, _TextSelectionToolbarItemData> entry) {
return TextSelectionToolbarTextButton(
padding: TextSelectionToolbarTextButton.getPadding(entry.key, itemDatas.length),
onPressed: entry.value.onPressed,
child: Text(entry.value.label),
);
}).toList(), // 每個(gè)編輯操作的按鈕功能
);
}
}
/// 安卓選中樣式繪制(默認(rèn)是圓點(diǎn)加上一個(gè)箭頭)
class _TextSelectionHandlePainter extends CustomPainter {
_TextSelectionHandlePainter({ required this.color });
final Color color;
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()..color = color;
final double radius = size.width/2.0;
final Rect circle = Rect.fromCircle(center: Offset(radius, radius), radius: radius);
final Rect point = Rect.fromLTWH(0.0, 0.0, radius, radius);
final Path path = Path()..addOval(circle)..addRect(point);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(_TextSelectionHandlePainter oldPainter) {
return color != oldPainter.color;
}
}功能復(fù)刻
了解源碼功能之后就能拷貝MaterialTextSelectionControls實(shí)現(xiàn)來(lái)完成放大鏡功能了。同樣是繼承TextSelectionControls,實(shí)現(xiàn)MaterialMagnifierControls功能。
主要修改點(diǎn)在_MagnifierControlsToolbar的實(shí)現(xiàn)以及MaterialMagnifier功能
MagnifierControlsToolbar
其中的build方法返回了widget.endpoints光標(biāo)的定位信息,定位信息去計(jì)算出偏移量。最后將兩個(gè)光標(biāo)信息入?yún)⒌?code>MaterialMagnifier組件。
const double _kHandleSize = 22.0;
const double _kToolbarContentDistanceBelow = _kHandleSize - 2.0;
const double _kToolbarContentDistance = 8.0;
class MaterialMagnifierControls extends TextSelectionControls {
@override
Size getHandleSize(double textLineHeight) =>
const Size(_kHandleSize, _kHandleSize);
@override
Widget buildToolbar(
BuildContext context,
Rect globalEditableRegion,
double textLineHeight,
Offset selectionMidpoint,
List<TextSelectionPoint> endpoints,
TextSelectionDelegate delegate,
ClipboardStatusNotifier clipboardStatus,
Offset? lastSecondaryTapDownPosition,
) {
return _MagnifierControlsToolbar(
globalEditableRegion: globalEditableRegion,
textLineHeight: textLineHeight,
selectionMidpoint: selectionMidpoint,
endpoints: endpoints,
delegate: delegate,
clipboardStatus: clipboardStatus,
);
}
@override
Widget buildHandle(
BuildContext context, TextSelectionHandleType type, double textHeight,
[VoidCallback? onTap, double? startGlyphHeight, double? endGlyphHeight]) {
return const SizedBox();
}
@override
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight,
[double? startGlyphHeight, double? endGlyphHeight]) {
switch (type) {
case TextSelectionHandleType.left:
return const Offset(_kHandleSize, 0);
case TextSelectionHandleType.right:
return Offset.zero;
default:
return const Offset(_kHandleSize / 2, -4);
}
}
}
class _MagnifierControlsToolbar extends StatefulWidget {
const _MagnifierControlsToolbar({
Key? key,
required this.clipboardStatus,
required this.delegate,
required this.endpoints,
required this.globalEditableRegion,
required this.selectionMidpoint,
required this.textLineHeight,
}) : super(key: key);
final ClipboardStatusNotifier clipboardStatus;
final TextSelectionDelegate delegate;
final List<TextSelectionPoint> endpoints;
final Rect globalEditableRegion;
final Offset selectionMidpoint;
final double textLineHeight;
@override
_MagnifierControlsToolbarState createState() =>
_MagnifierControlsToolbarState();
}
class _MagnifierControlsToolbarState extends State<_MagnifierControlsToolbar>
with TickerProviderStateMixin {
Offset offset1 = Offset.zero;
Offset offset2 = Offset.zero;
void _onChangedClipboardStatus() {
setState(() {
});
}
@override
void initState() {
super.initState();
widget.clipboardStatus.addListener(_onChangedClipboardStatus);
widget.clipboardStatus.update();
}
@override
void didUpdateWidget(_MagnifierControlsToolbar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.clipboardStatus != oldWidget.clipboardStatus) {
widget.clipboardStatus.addListener(_onChangedClipboardStatus);
oldWidget.clipboardStatus.removeListener(_onChangedClipboardStatus);
}
widget.clipboardStatus.update();
}
@override
void dispose() {
super.dispose();
if (!widget.clipboardStatus.disposed) {
widget.clipboardStatus.removeListener(_onChangedClipboardStatus);
}
}
@override
Widget build(BuildContext context) {
TextSelectionPoint point = widget.endpoints[0];
if(widget.endpoints.length > 1){
if(offset1 != widget.endpoints[0].point){
point = widget.endpoints[0];
offset1 = point.point;
}
if(offset2 != widget.endpoints[1].point){
point = widget.endpoints[1];
offset2 = point.point;
}
}
final TextSelectionPoint startTextSelectionPoint = point;
final Offset anchorAbove = Offset(
widget.globalEditableRegion.left + startTextSelectionPoint.point.dx,
widget.globalEditableRegion.top +
startTextSelectionPoint.point.dy -
widget.textLineHeight -
_kToolbarContentDistance,
);
final Offset anchorBelow = Offset(
widget.globalEditableRegion.left + startTextSelectionPoint.point.dx,
widget.globalEditableRegion.top +
startTextSelectionPoint.point.dy +
_kToolbarContentDistanceBelow,
);
return MaterialMagnifier(
anchorAbove: anchorAbove,
anchorBelow: anchorBelow,
textLineHeight: widget.textLineHeight,
);
}
}
final TextSelectionControls materialMagnifierControls =
MaterialMagnifierControls();MaterialMagnifier
MaterialMagnifier是參考Widget Magnifier放大鏡的實(shí)現(xiàn)。這里是引入了安卓的一些布局參數(shù)來(lái)實(shí)現(xiàn),iOS是另外定制了布局參數(shù)可以參考Flutter官方源碼定制iOS布局。
放大鏡實(shí)現(xiàn)方法主要是BackdropFilter和ImageFilter來(lái)實(shí)現(xiàn)的,根據(jù)Matrix4做scale和translate操作完成放大功能。
const double _kToolbarScreenPadding = 8.0;
const double _kToolbarHeight = 44.0;
class MaterialMagnifier extends StatelessWidget {
const MaterialMagnifier({
Key? key,
required this.anchorAbove,
required this.anchorBelow,
required this.textLineHeight,
this.size = const Size(90, 50),
this.scale = 1.7,
}) : super(key: key);
final Offset anchorAbove;
final Offset anchorBelow;
final Size size;
final double scale;
final double textLineHeight;
@override
Widget build(BuildContext context) {
final double paddingAbove =
MediaQuery.of(context).padding.top + _kToolbarScreenPadding;
final double availableHeight = anchorAbove.dy - paddingAbove;
final bool fitsAbove = _kToolbarHeight <= availableHeight;
final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove);
final Matrix4 updatedMatrix = Matrix4.identity()
..scale(1.1,1.1)
..translate(0.0,-50.0);
Matrix4 _matrix = updatedMatrix;
return Container(
child: Padding(
padding: EdgeInsets.fromLTRB(
_kToolbarScreenPadding,
paddingAbove,
_kToolbarScreenPadding,
_kToolbarScreenPadding,
),
child: Stack(
children: <Widget>[
CustomSingleChildLayout(
delegate: TextSelectionToolbarLayoutDelegate(
anchorAbove: anchorAbove - localAdjustment,
anchorBelow: anchorBelow - localAdjustment,
fitsAbove: fitsAbove,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: BackdropFilter(
filter: ImageFilter.matrix(_matrix.storage),
child: CustomPaint(
painter: const MagnifierPainter(color: Color(0xFFdfdfdf)),
size: size,
),
),
),
),
],
),
),
);
}
}交互優(yōu)化
實(shí)現(xiàn)放大鏡功能之外還需要控制顯示,由于在拖動(dòng)狀態(tài)下才顯示放大鏡,隱藏操作欄功能,因此需要去監(jiān)聽手勢(shì)狀態(tài)信息。
手勢(shì)監(jiān)聽是在_TextSelectionHandleOverlayState中,需要去監(jiān)聽onPanStart、onPanUpdate、onPanEnd、onPanCancel這幾個(gè)狀態(tài)。
| 狀態(tài) | 行動(dòng) |
|---|---|
| onPanStart | 隱藏操作欄、顯示放大鏡 |
| onPanUpdate | 顯示放大鏡,獲取到偏移信息 |
| onPanEnd | 顯示操作欄、隱藏放大鏡 |
| onPanCancel | 顯示操作欄、隱藏放大鏡 |
final Widget child = GestureDetector(
behavior: HitTestBehavior.translucent,
dragStartBehavior: widget.dragStartBehavior,
onPanStart: _handleDragStart,
onPanUpdate: _handleDragUpdate,
onPanEnd: _handleDragEnd,
onPanCancel: _handleDragCancel,
onTap: _handleTap,
child: Padding(
padding: EdgeInsets.only(
left: padding.left,
top: padding.top,
right: padding.right,
bottom: padding.bottom,
),
child: widget.selectionControls!.buildHandle(
context,
type,
widget.renderObject.preferredLineHeight,
() {},
),
),
);在開始拓展手勢(shì)時(shí)展示放大鏡,隱藏操作。_builderMagnifier嵌套在OverlayEntry組件在Overlay上插入,實(shí)現(xiàn)方式是和操作欄完全一樣的。
void _handleDragStart(DragStartDetails details) {
final Size handleSize = widget.selectionControls!.getHandleSize(
widget.renderObject.preferredLineHeight,
);
_dragPosition = details.globalPosition + Offset(0.0, -handleSize.height);
widget.showMagnifierBarFunc(); // 回調(diào)展示放大鏡功能
toolBarRecover = widget.hideToolbarFunc();
}
void showMagnifierBar() {
assert(_magnifier == null);
_magnifier = OverlayEntry(builder: _builderMagnifier);
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!
.insert(_magnifier!);
}
同理在拖拽結(jié)束時(shí)去隱藏放大鏡,重新創(chuàng)建操作欄恢復(fù)顯示。
void _handleDragEnd(DragEndDetails details) {
widget.hideMagnifierBarFunc();
if (toolBarRecover) {
widget.showToolbarFunc();
toolBarRecover = false;
}
}
void hideMagnifierBar() {
if (_magnifier != null) {
_magnifier!.remove();
_magnifier = null;
}
}最終效果
最后實(shí)現(xiàn)效果如下,通過移動(dòng)光標(biāo)可顯示放大鏡功能,松開手勢(shì)就是操作欄顯示恢復(fù)。

以上就是Flutter開發(fā)之支持放大鏡的輸入框功能實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于Flutter的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android 顯示刷新頻率的實(shí)現(xiàn)代碼
這篇文章主要介紹了Android 顯示刷新頻率的實(shí)現(xiàn)代碼,代碼簡(jiǎn)單易懂,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08
Android Volley擴(kuò)展實(shí)現(xiàn)支持進(jìn)度條的文件上傳功能
這篇文章主要為大家詳細(xì)介紹了Android Volley擴(kuò)展實(shí)現(xiàn)文件上傳與下載功能,支持進(jìn)度條,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12
Android 桌面Widget開發(fā)要點(diǎn)解析(時(shí)間日期Widget)
總的來(lái)說,widget主要功能就是顯示一些信息。我們今天編寫一個(gè)很簡(jiǎn)單的作為widget,顯示時(shí)間、日期、星期幾等信息。需要顯示時(shí)間信息,那就需要實(shí)時(shí)更新,一秒或者一分鐘更新一次2013-07-07
安卓(Android)實(shí)現(xiàn)選擇時(shí)間功能
安卓開發(fā)過程中難免會(huì)碰到需要選擇日期時(shí)間的情況,當(dāng)然不可能讓用戶自己輸入日期時(shí)間,小編收集整理了一些資料,總結(jié)了一下如何實(shí)現(xiàn)android選擇時(shí)間的功能,方便后來(lái)者參考2016-08-08
Android中CheckBox復(fù)選框控件使用方法詳解
這篇文章主要為大家詳細(xì)介紹了Android中CheckBox復(fù)選框控件的使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
Android對(duì)so進(jìn)行簡(jiǎn)單hook思路解析
這篇文章主要為大家介紹了Android對(duì)so進(jìn)行簡(jiǎn)單hook思路解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04

