您好,登錄后才能下訂單哦!
這篇文章將為大家詳細(xì)講解有關(guān)Flutter如何支持放大鏡的輸入框功能,小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。
最近需求開發(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)原來并不是這樣的,Flutter
好像并沒有去支持原有的功能。
為了確認(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
來為輸入框?qū)崿F(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)上搜到很多,作用是來綁定兩個(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; } }
了解源碼功能之后就能拷貝MaterialTextSelectionControls
實(shí)現(xiàn)來完成放大鏡功能了。同樣是繼承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ù)來實(shí)現(xiàn),iOS是另外定制了布局參數(shù)可以參考Flutter官方源碼定制iOS布局。
放大鏡實(shí)現(xiàn)方法主要是BackdropFilter
和ImageFilter
來實(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ù)。
關(guān)于“Flutter如何支持放大鏡的輸入框功能”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,使各位可以學(xué)到更多知識(shí),如果覺得文章不錯(cuò),請(qǐng)把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。