cursor.dart 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import 'dart:async';
  2. import 'package:flutter/foundation.dart';
  3. import 'package:flutter/widgets.dart';
  4. import '../rendering/box.dart';
  5. const Duration _FADE_DURATION = Duration(milliseconds: 250);
  6. class CursorStyle {
  7. const CursorStyle({
  8. required this.color,
  9. required this.backgroundColor,
  10. this.width = 1.0,
  11. this.height,
  12. this.radius,
  13. this.offset,
  14. this.opacityAnimates = false,
  15. this.paintAboveText = false,
  16. });
  17. final Color color;
  18. final Color backgroundColor;
  19. final double width;
  20. final double? height;
  21. final Radius? radius;
  22. final Offset? offset;
  23. final bool opacityAnimates;
  24. final bool paintAboveText;
  25. @override
  26. bool operator ==(Object other) =>
  27. identical(this, other) ||
  28. other is CursorStyle &&
  29. runtimeType == other.runtimeType &&
  30. color == other.color &&
  31. backgroundColor == other.backgroundColor &&
  32. width == other.width &&
  33. height == other.height &&
  34. radius == other.radius &&
  35. offset == other.offset &&
  36. opacityAnimates == other.opacityAnimates &&
  37. paintAboveText == other.paintAboveText;
  38. @override
  39. int get hashCode =>
  40. color.hashCode ^
  41. backgroundColor.hashCode ^
  42. width.hashCode ^
  43. height.hashCode ^
  44. radius.hashCode ^
  45. offset.hashCode ^
  46. opacityAnimates.hashCode ^
  47. paintAboveText.hashCode;
  48. }
  49. /* ------------------------------- Controller ------------------------------- */
  50. class CursorController extends ChangeNotifier {
  51. CursorController({
  52. required this.show,
  53. required CursorStyle style,
  54. required TickerProvider tickerProvider,
  55. }) : _style = style,
  56. _blink = ValueNotifier(false),
  57. color = ValueNotifier(style.color) {
  58. _blinkOpacityController =
  59. AnimationController(vsync: tickerProvider, duration: _FADE_DURATION);
  60. _blinkOpacityController.addListener(_onColorTick);
  61. }
  62. final ValueNotifier<bool> show;
  63. final ValueNotifier<bool> _blink;
  64. final ValueNotifier<Color> color;
  65. late AnimationController _blinkOpacityController;
  66. Timer? _cursorTimer;
  67. bool _targetCursorVisibility = false;
  68. CursorStyle _style;
  69. ValueNotifier<bool> get cursorBlink => _blink;
  70. ValueNotifier<Color> get cursorColor => color;
  71. CursorStyle get style => _style;
  72. set style(CursorStyle value) {
  73. if (_style == value) return;
  74. _style = value;
  75. notifyListeners();
  76. }
  77. @override
  78. void dispose() {
  79. _blinkOpacityController.removeListener(_onColorTick);
  80. stopCursorTimer();
  81. _blinkOpacityController.dispose();
  82. assert(_cursorTimer == null);
  83. super.dispose();
  84. }
  85. void _cursorTick(Timer timer) {
  86. _targetCursorVisibility = !_targetCursorVisibility;
  87. final targetOpacity = _targetCursorVisibility ? 1.0 : 0.0;
  88. if (style.opacityAnimates) {
  89. _blinkOpacityController.animateTo(targetOpacity, curve: Curves.easeOut);
  90. } else {
  91. _blinkOpacityController.value = targetOpacity;
  92. }
  93. }
  94. void _cursorWaitForStart(Timer timer) {
  95. _cursorTimer?.cancel();
  96. _cursorTimer =
  97. Timer.periodic(const Duration(milliseconds: 500), _cursorTick);
  98. }
  99. void startCursorTimer() {
  100. _targetCursorVisibility = true;
  101. _blinkOpacityController.value = 1.0;
  102. if (style.opacityAnimates) {
  103. _cursorTimer = Timer.periodic(
  104. const Duration(milliseconds: 150), _cursorWaitForStart);
  105. } else {
  106. _cursorTimer =
  107. Timer.periodic(const Duration(milliseconds: 500), _cursorTick);
  108. }
  109. }
  110. void stopCursorTimer({bool resetCharTicks = true}) {
  111. _cursorTimer?.cancel();
  112. _cursorTimer = null;
  113. _targetCursorVisibility = false;
  114. _blinkOpacityController.value = 0.0;
  115. if (style.opacityAnimates) {
  116. _blinkOpacityController
  117. ..stop()
  118. ..value = 0.0;
  119. }
  120. }
  121. void startOrStopCursorTimerIfNeeded(
  122. bool hasFocus, TextSelection textSelection) {
  123. if (show.value &&
  124. _cursorTimer == null &&
  125. hasFocus &&
  126. textSelection.isCollapsed) {
  127. startCursorTimer();
  128. } else if (_cursorTimer != null &&
  129. (!hasFocus || !textSelection.isCollapsed)) {
  130. stopCursorTimer();
  131. }
  132. }
  133. void _onColorTick() {
  134. color.value = _style.color.withOpacity(_blinkOpacityController.value);
  135. _blink.value = show.value && _blinkOpacityController.value > 0.0;
  136. }
  137. }
  138. /* --------------------------------- Painter -------------------------------- */
  139. class CursorPainter {
  140. CursorPainter(
  141. this.editable,
  142. this.style,
  143. this.prototype,
  144. this.color,
  145. this.devicePixelRatio,
  146. );
  147. final RenderContentProxyBox? editable;
  148. final CursorStyle style;
  149. final Rect? prototype;
  150. final Color color;
  151. final double devicePixelRatio;
  152. void paint(Canvas canvas, Offset offset, TextPosition position) {
  153. assert(prototype != null);
  154. final caretOffset =
  155. editable!.getOffsetForCaret(position, prototype) + offset;
  156. var caretRect = prototype!.shift(caretOffset);
  157. if (style.offset != null) {
  158. caretRect = caretRect.shift(style.offset!);
  159. }
  160. if (caretRect.left < 0.0) {
  161. caretRect = caretRect.shift(Offset(-caretRect.left, 0));
  162. }
  163. final caretHeight = editable!.getFullHeightForCaret(position);
  164. if (caretHeight != null) {
  165. switch (defaultTargetPlatform) {
  166. case TargetPlatform.android:
  167. case TargetPlatform.fuchsia:
  168. case TargetPlatform.linux:
  169. case TargetPlatform.windows:
  170. caretRect = Rect.fromLTWH(
  171. caretRect.left,
  172. caretRect.top - 2.0,
  173. caretRect.width,
  174. caretHeight,
  175. );
  176. break;
  177. case TargetPlatform.iOS:
  178. case TargetPlatform.macOS:
  179. caretRect = Rect.fromLTWH(
  180. caretRect.left,
  181. caretRect.top + (caretHeight - caretRect.height) / 2,
  182. caretRect.width,
  183. caretRect.height,
  184. );
  185. break;
  186. default:
  187. throw UnimplementedError();
  188. }
  189. }
  190. final caretPosition = editable!.localToGlobal(caretRect.topLeft);
  191. final pixelMultiple = 1.0 / devicePixelRatio;
  192. caretRect = caretRect.shift(Offset(
  193. caretPosition.dx.isFinite
  194. ? (caretPosition.dx / pixelMultiple).round() * pixelMultiple -
  195. caretPosition.dx
  196. : caretPosition.dx,
  197. caretPosition.dy.isFinite
  198. ? (caretPosition.dy / pixelMultiple).round() * pixelMultiple -
  199. caretPosition.dy
  200. : caretPosition.dy));
  201. final paint = Paint()..color = color;
  202. if (style.radius == null) {
  203. canvas.drawRect(caretRect, paint);
  204. return;
  205. }
  206. final caretRRect = RRect.fromRectAndRadius(caretRect, style.radius!);
  207. canvas.drawRRect(caretRRect, paint);
  208. }
  209. }