Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SuperEditor][SuperTextField][iOS] Allow animating caret blink (Resolves #2099) #2238

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

angelosilvestre
Copy link
Collaborator

[SuperEditor][SuperTextField][iOS] Allow animating caret blink. Resolves #2099

Currently, our blinking caret has an on/off visibility, it doesn't animates the visibility changes.

This PR adds a fade in/out transition when the caret switches from visible to invisible.

Some problems that I found:

  • Although we have an option to use a Ticker to drive the visibility change, we are setting the _ticker to null when stopBlinking is called. It means that once the user types or places the caret somewhere, which causes stopBlinking to be called to momentary stop blinking, we are switching to the timer mode:

void stopBlinking() {
_isVisible = true; // If we're not blinking then we need to be visible
if (_ticker != null) {
// We're using a Ticker to blink. Stop it.
_ticker?.stop();
_ticker = null;

void startBlinking() {
if (!indeterminateAnimationsEnabled) {
// Never animate a blink when the app/test wants to avoid
// indeterminate animations.
return;
}
if (_ticker != null) {
// We're using a Ticker to blink. Restart it.
_ticker!
..stop()
..start();
} else {
// We're using a Timer to blink. Restart it.
_timer?.cancel();
_timer = Timer(_flashPeriod, _blink);
}
_lastBlinkTime = Duration.zero;
notifyListeners();
}

  • Fixing the previous issue creates an issue in some tests that check if the caret is blinking. Such tests were working before because we were always using the timer mode. When switching to ticker mode, there are some tests failing due to pumpAndSettleTimeout. The timeout happens because two methods used in those tests (tester.ime.backspace and tester.doubleTapInParagraph) internally call pumpAndSettle.

Open questions:

  • How should we expose the configuration to animate the caret blink? Via SuperEditorIosControlsController ?
  • Should we add parameters to tester.ime.backspace and tester.doubleTapInParagraph to avoid calling pumpAndSettle?

@angelosilvestre
Copy link
Collaborator Author

@matthew-carroll Can you take a look at the questions mentioned in the PR description?

///
/// If [animate] is `true`, the caret animates its [opacity] when switching
/// between visible and invisible. If [animate] is `false`, the caret
/// switches between fully opaque and fully transparent.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add a point here about performance? The reason we originally got rid of animated blinking was to reduce Flutter re-renders.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was the original performance issue really about re-renders? Or was it related to repaints? I'm wondering if using e RepaintBoundary in our overlayers would make a difference.

bool animate = false,
Duration fadeDuration = const Duration(milliseconds: 200),
}) : _flashPeriod = flashPeriod,
_fadeDuration = fadeDuration,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can the fade duration be independent of the flash period?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The caret isn't constantly fading in and out. It stays visible or invisible during the flash period, then quickly fades in/out.

Should we just use half of the flash period?

@@ -62,8 +76,24 @@ class BlinkController with ChangeNotifier {
notifyListeners();
}

/// Whether or not the caret is currently visible.
///
/// It's possible that [isVisible] is `false` while [opacity] is greater than `0.0`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than sayign something is possible, please describe under which circumstances something happens or doesn't happen.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

double get opacity => _opacity;
double _opacity = 1.0;

/// Whether or not the caret should animate its opacity.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also specify the alternative. It's possible a developer reads this and then asks "what does it mean to not animate the opacity?" - let's make the distinction between fading blinking vs on/off blinking.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updates.

/// Whether or not the caret should animate its opacity.
bool _animate = false;

/// Whether or not the caret should maintain its opacity until the next blink.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Dart Doc comment makes it sound like this property is a permanent configuration on this controller, but in fact it looks like a momentary flag that's switched on and off to capture the fact that we're at a specific moment in time. Please try to replace this description with one that describes what this property is tracking, including when we expect it to flip from true to false, or false to true. Also, the name of this should probably start with _isSomething..., if it represents a temporary condition.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to improve it a little bit.

@@ -118,6 +156,12 @@ class BlinkController with ChangeNotifier {
}

void _onTick(Duration elapsedTime) {
if (isBlinking && _animate && !_remainOpaqueUntilNextBlink) {
final percentage = ((elapsedTime - _lastBlinkTime).inMilliseconds / _fadeDuration.inMilliseconds).clamp(0.0, 1.0);
_opacity = _isVisible ? percentage : 1 - percentage;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment to clarify the meaning of _isVisible in this calculation, because you're actually using it as a proxy to decide whether we're "fading in" or "fading out", and that's not immediately obvious.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

@matthew-carroll
Copy link
Contributor

Should we add parameters to tester.ime.backspace and tester.doubleTapInParagraph to avoid calling pumpAndSettle?

My initial thought is, no. I think we should adjust the Super Editor side to play well with common test behaviors, rather than change the test behaviors to fit Super Editor.

How should we expose the configuration to animate the caret blink? Via SuperEditorIosControlsController ?

I wanna make sure I understand this question - you exposed the property from the controller already, right?

@angelosilvestre
Copy link
Collaborator Author

My initial thought is, no. I think we should adjust the Super Editor side to play well with common test behaviors, rather than change the test behaviors to fit Super Editor.

But how should we do that, if the ticker is constantly scheduling frames?

I wanna make sure I understand this question - you exposed the property from the controller already, right?

I exposed from the controller, but it still isn't configurable in a way that apps can enable or disable it. The BlinkController is instantiated by us.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[SuperEditor][SuperTextField][iOS] - Make it possible to animate the iOS caret blink
2 participants