Add comments to cursor class

pull/236/head
Till Friebe 4 years ago
parent 3b429840b6
commit 1fedaf0d13
  1. 135
      lib/src/widgets/cursor.dart
  2. 2
      lib/src/widgets/text_line.dart

@ -5,8 +5,7 @@ import 'package:flutter/widgets.dart';
import 'box.dart'; import 'box.dart';
const Duration _FADE_DURATION = Duration(milliseconds: 250); /// Style properties of editing cursor.
class CursorStyle { class CursorStyle {
const CursorStyle({ const CursorStyle({
required this.color, required this.color,
@ -19,13 +18,52 @@ class CursorStyle {
this.paintAboveText = false, this.paintAboveText = false,
}); });
/// The color to use when painting the cursor.
final Color color; final Color color;
/// The color to use when painting the background cursor aligned with the text
/// while rendering the floating cursor.
final Color backgroundColor; final Color backgroundColor;
/// How thick the cursor will be.
///
/// The cursor will draw under the text. The cursor width will extend
/// to the right of the boundary between characters for left-to-right text
/// and to the left for right-to-left text. This corresponds to extending
/// downstream relative to the selected position. Negative values may be used
/// to reverse this behavior.
final double width; final double width;
/// How tall the cursor will be.
///
/// By default, the cursor height is set to the preferred line height of the
/// text.
final double? height; final double? height;
/// How rounded the corners of the cursor should be.
///
/// By default, the cursor has no radius.
final Radius? radius; final Radius? radius;
/// The offset that is used, in pixels, when painting the cursor on screen.
///
/// By default, the cursor position should be set to an offset of
/// (-[cursorWidth] * 0.5, 0.0) on iOS platforms and (0, 0) on Android
/// platforms. The origin from where the offset is applied to is the arbitrary
/// location where the cursor ends up being rendered from by default.
final Offset? offset; final Offset? offset;
/// Whether the cursor will animate from fully transparent to fully opaque
/// during each cursor blink.
///
/// By default, the cursor opacity will animate on iOS platforms and will not
/// animate on Android platforms.
final bool opacityAnimates; final bool opacityAnimates;
/// If the cursor should be painted on top of the text or underneath it.
///
/// By default, the cursor should be painted on top for iOS platforms and
/// underneath for Android platforms.
final bool paintAboveText; final bool paintAboveText;
@override @override
@ -54,6 +92,10 @@ class CursorStyle {
paintAboveText.hashCode; paintAboveText.hashCode;
} }
/// Controls the cursor of an editable widget.
///
/// This class is a [ChangeNotifier] and allows to listen for updates on the
/// cursor [style].
class CursorCont extends ChangeNotifier { class CursorCont extends ChangeNotifier {
CursorCont({ CursorCont({
required this.show, required this.show,
@ -62,21 +104,35 @@ class CursorCont extends ChangeNotifier {
}) : _style = style, }) : _style = style,
blink = ValueNotifier(false), blink = ValueNotifier(false),
color = ValueNotifier(style.color) { color = ValueNotifier(style.color) {
_blinkOpacityCont = _blinkOpacityController =
AnimationController(vsync: tickerProvider, duration: _FADE_DURATION); AnimationController(vsync: tickerProvider, duration: _fadeDuration);
_blinkOpacityCont.addListener(_onColorTick); _blinkOpacityController.addListener(_onColorTick);
} }
// The time it takes for the cursor to fade from fully opaque to fully
// transparent and vice versa. A full cursor blink, from transparent to opaque
// to transparent, is twice this duration.
static const Duration _blinkHalfPeriod = Duration(milliseconds: 500);
// The time the cursor is static in opacity before animating to become
// transparent.
static const Duration _blinkWaitForStart = Duration(milliseconds: 150);
// This value is an eyeball estimation of the time it takes for the iOS cursor
// to ease in and out.
static const Duration _fadeDuration = Duration(milliseconds: 250);
final ValueNotifier<bool> show; final ValueNotifier<bool> show;
final ValueNotifier<bool> blink;
final ValueNotifier<Color> color; final ValueNotifier<Color> color;
late AnimationController _blinkOpacityCont; final ValueNotifier<bool> blink;
late final AnimationController _blinkOpacityController;
Timer? _cursorTimer; Timer? _cursorTimer;
bool _targetCursorVisibility = false; bool _targetCursorVisibility = false;
CursorStyle _style;
CursorStyle _style;
CursorStyle get style => _style; CursorStyle get style => _style;
set style(CursorStyle value) { set style(CursorStyle value) {
if (_style == value) return; if (_style == value) return;
_style = value; _style = value;
@ -85,9 +141,9 @@ class CursorCont extends ChangeNotifier {
@override @override
void dispose() { void dispose() {
_blinkOpacityCont.removeListener(_onColorTick); _blinkOpacityController.removeListener(_onColorTick);
stopCursorTimer(); stopCursorTimer();
_blinkOpacityCont.dispose(); _blinkOpacityController.dispose();
show.dispose(); show.dispose();
blink.dispose(); blink.dispose();
color.dispose(); color.dispose();
@ -99,28 +155,32 @@ class CursorCont extends ChangeNotifier {
_targetCursorVisibility = !_targetCursorVisibility; _targetCursorVisibility = !_targetCursorVisibility;
final targetOpacity = _targetCursorVisibility ? 1.0 : 0.0; final targetOpacity = _targetCursorVisibility ? 1.0 : 0.0;
if (style.opacityAnimates) { if (style.opacityAnimates) {
_blinkOpacityCont.animateTo(targetOpacity, curve: Curves.easeOut); // If we want to show the cursor, we will animate the opacity to the value
// of 1.0, and likewise if we want to make it disappear, to 0.0. An easing
// curve is used for the animation to mimic the aesthetics of the native
// iOS cursor.
//
// These values and curves have been obtained through eyeballing, so are
// likely not exactly the same as the values for native iOS.
_blinkOpacityController.animateTo(targetOpacity, curve: Curves.easeOut);
} else { } else {
_blinkOpacityCont.value = targetOpacity; _blinkOpacityController.value = targetOpacity;
} }
} }
void _cursorWaitForStart(Timer timer) { void _waitForStart(Timer timer) {
_cursorTimer?.cancel(); _cursorTimer?.cancel();
_cursorTimer = _cursorTimer = Timer.periodic(_blinkHalfPeriod, _cursorTick);
Timer.periodic(const Duration(milliseconds: 500), _cursorTick);
} }
void startCursorTimer() { void startCursorTimer() {
_targetCursorVisibility = true; _targetCursorVisibility = true;
_blinkOpacityCont.value = 1.0; _blinkOpacityController.value = 1.0;
if (style.opacityAnimates) { if (style.opacityAnimates) {
_cursorTimer = Timer.periodic( _cursorTimer = Timer.periodic(_blinkWaitForStart, _waitForStart);
const Duration(milliseconds: 150), _cursorWaitForStart);
} else { } else {
_cursorTimer = _cursorTimer = Timer.periodic(_blinkHalfPeriod, _cursorTick);
Timer.periodic(const Duration(milliseconds: 500), _cursorTick);
} }
} }
@ -128,10 +188,10 @@ class CursorCont extends ChangeNotifier {
_cursorTimer?.cancel(); _cursorTimer?.cancel();
_cursorTimer = null; _cursorTimer = null;
_targetCursorVisibility = false; _targetCursorVisibility = false;
_blinkOpacityCont.value = 0.0; _blinkOpacityController.value = 0.0;
if (style.opacityAnimates) { if (style.opacityAnimates) {
_blinkOpacityCont _blinkOpacityController
..stop() ..stop()
..value = 0.0; ..value = 0.0;
} }
@ -149,32 +209,42 @@ class CursorCont extends ChangeNotifier {
} }
void _onColorTick() { void _onColorTick() {
color.value = _style.color.withOpacity(_blinkOpacityCont.value); color.value = _style.color.withOpacity(_blinkOpacityController.value);
blink.value = show.value && _blinkOpacityCont.value > 0; blink.value = show.value && _blinkOpacityController.value > 0;
} }
} }
/// Paints the editing cursor.
class CursorPainter { class CursorPainter {
CursorPainter(this.editable, this.style, this.prototype, this.color, CursorPainter(
this.devicePixelRatio); this.editable,
this.style,
this.prototype,
this.color,
this.devicePixelRatio,
);
final RenderContentProxyBox? editable; final RenderContentProxyBox? editable;
final CursorStyle style; final CursorStyle style;
final Rect? prototype; final Rect prototype;
final Color color; final Color color;
final double devicePixelRatio; final double devicePixelRatio;
/// Paints cursor on [canvas] at specified [position].
void paint(Canvas canvas, Offset offset, TextPosition position) { void paint(Canvas canvas, Offset offset, TextPosition position) {
assert(prototype != null);
final caretOffset = final caretOffset =
editable!.getOffsetForCaret(position, prototype) + offset; editable!.getOffsetForCaret(position, prototype) + offset;
var caretRect = prototype!.shift(caretOffset); var caretRect = prototype.shift(caretOffset);
if (style.offset != null) { if (style.offset != null) {
caretRect = caretRect.shift(style.offset!); caretRect = caretRect.shift(style.offset!);
} }
if (caretRect.left < 0.0) { if (caretRect.left < 0.0) {
// For iOS the cursor may get clipped by the scroll view when
// it's located at a beginning of a line. We ensure that this
// does not happen here. This may result in the cursor being painted
// closer to the character on the right, but it's arguably better
// then painting clipped cursor (or even cursor completely hidden).
caretRect = caretRect.shift(Offset(-caretRect.left, 0)); caretRect = caretRect.shift(Offset(-caretRect.left, 0));
} }
@ -185,6 +255,8 @@ class CursorPainter {
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
case TargetPlatform.linux: case TargetPlatform.linux:
case TargetPlatform.windows: case TargetPlatform.windows:
// Override the height to take the full height of the glyph at the TextPosition
// when not on iOS. iOS has special handling that creates a taller caret.
caretRect = Rect.fromLTWH( caretRect = Rect.fromLTWH(
caretRect.left, caretRect.left,
caretRect.top - 2.0, caretRect.top - 2.0,
@ -194,6 +266,7 @@ class CursorPainter {
break; break;
case TargetPlatform.iOS: case TargetPlatform.iOS:
case TargetPlatform.macOS: case TargetPlatform.macOS:
// Center the caret vertically along the text.
caretRect = Rect.fromLTWH( caretRect = Rect.fromLTWH(
caretRect.left, caretRect.left,
caretRect.top + (caretHeight - caretRect.height) / 2, caretRect.top + (caretHeight - caretRect.height) / 2,

@ -727,7 +727,7 @@ class RenderEditableTextLine extends RenderEditableBox {
CursorPainter get _cursorPainter => CursorPainter( CursorPainter get _cursorPainter => CursorPainter(
_body, _body,
cursorCont.style, cursorCont.style,
_caretPrototype, _caretPrototype!,
cursorCont.color.value, cursorCont.color.value,
devicePixelRatio, devicePixelRatio,
); );

Loading…
Cancel
Save