Flutter iOS Embedder
FlutterViewController.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #define FML_USED_ON_EMBEDDER
6 
8 
9 #import <os/log.h>
10 #include <memory>
11 
12 #include "flutter/common/constants.h"
13 #include "flutter/fml/memory/weak_ptr.h"
14 #include "flutter/fml/message_loop.h"
15 #include "flutter/fml/platform/darwin/platform_version.h"
16 #include "flutter/runtime/ptrace_check.h"
17 #include "flutter/shell/common/thread_host.h"
18 #import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h"
38 #import "flutter/shell/platform/embedder/embedder.h"
39 #import "flutter/third_party/spring_animation/spring_animation.h"
40 
42 
43 static constexpr int kMicrosecondsPerSecond = 1000 * 1000;
44 static constexpr CGFloat kScrollViewContentSize = 2.0;
45 
46 static NSString* const kFlutterRestorationStateAppData = @"FlutterRestorationStateAppData";
47 
48 NSNotificationName const FlutterSemanticsUpdateNotification = @"FlutterSemanticsUpdate";
49 NSNotificationName const FlutterViewControllerWillDealloc = @"FlutterViewControllerWillDealloc";
50 NSNotificationName const FlutterViewControllerHideHomeIndicator =
51  @"FlutterViewControllerHideHomeIndicator";
52 NSNotificationName const FlutterViewControllerShowHomeIndicator =
53  @"FlutterViewControllerShowHomeIndicator";
54 
55 // Struct holding data to help adapt system mouse/trackpad events to embedder events.
56 typedef struct MouseState {
57  // Current coordinate of the mouse cursor in physical device pixels.
58  CGPoint location = CGPointZero;
59 
60  // Last reported translation for an in-flight pan gesture in physical device pixels.
61  CGPoint last_translation = CGPointZero;
63 
64 // This is left a FlutterBinaryMessenger privately for now to give people a chance to notice the
65 // change. Unfortunately unless you have Werror turned on, incompatible pointers as arguments are
66 // just a warning.
67 @interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegate>
68 // TODO(dkwingsmt): Make the view ID property public once the iOS shell
69 // supports multiple views.
70 // https://github.com/flutter/flutter/issues/138168
71 @property(nonatomic, readonly) int64_t viewIdentifier;
72 
73 // We keep a separate reference to this and create it ahead of time because we want to be able to
74 // set up a shell along with its platform view before the view has to appear.
75 @property(nonatomic, strong) FlutterView* flutterView;
76 @property(nonatomic, strong) void (^flutterViewRenderedCallback)(void);
77 
78 @property(nonatomic, assign) UIInterfaceOrientationMask orientationPreferences;
79 @property(nonatomic, assign) UIStatusBarStyle statusBarStyle;
80 @property(nonatomic, assign) BOOL initialized;
81 @property(nonatomic, assign) BOOL engineNeedsLaunch;
82 @property(nonatomic, assign) BOOL awokenFromNib;
83 
84 @property(nonatomic, readwrite, getter=isDisplayingFlutterUI) BOOL displayingFlutterUI;
85 @property(nonatomic, assign) BOOL isHomeIndicatorHidden;
86 @property(nonatomic, assign) BOOL isPresentingViewControllerAnimating;
87 
88 // Internal state backing override of UIView.prefersStatusBarHidden.
89 @property(nonatomic, assign) BOOL flutterPrefersStatusBarHidden;
90 
91 @property(nonatomic, strong) NSMutableSet<NSNumber*>* ongoingTouches;
92 // This scroll view is a workaround to accommodate iOS 13 and higher. There isn't a way to get
93 // touches on the status bar to trigger scrolling to the top of a scroll view. We place a
94 // UIScrollView with height zero and a content offset so we can get those events. See also:
95 // https://github.com/flutter/flutter/issues/35050
96 @property(nonatomic, strong) UIScrollView* scrollView;
97 @property(nonatomic, strong) UIView* keyboardAnimationView;
98 @property(nonatomic, strong) SpringAnimation* keyboardSpringAnimation;
99 
100 /**
101  * Whether we should ignore viewport metrics updates during rotation transition.
102  */
103 @property(nonatomic, assign) BOOL shouldIgnoreViewportMetricsUpdatesDuringRotation;
104 
105 /**
106  * Keyboard animation properties
107  */
108 @property(nonatomic, assign) CGFloat targetViewInsetBottom;
109 @property(nonatomic, assign) CGFloat originalViewInsetBottom;
110 @property(nonatomic, strong) VSyncClient* keyboardAnimationVSyncClient;
111 @property(nonatomic, assign) BOOL keyboardAnimationIsShowing;
112 @property(nonatomic, assign) fml::TimePoint keyboardAnimationStartTime;
113 @property(nonatomic, assign) BOOL isKeyboardInOrTransitioningFromBackground;
114 
115 /// Timestamp after which a scroll inertia cancel event should be inferred.
116 @property(nonatomic, assign) NSTimeInterval scrollInertiaEventStartline;
117 
118 /// When an iOS app is running in emulation on an Apple Silicon Mac, trackpad input goes through
119 /// a translation layer, and events are not received with precise deltas. Due to this, we can't
120 /// rely on checking for a stationary trackpad event. Fortunately, AppKit will send an event of
121 /// type UIEventTypeScroll following a scroll when inertia should stop. This field is needed to
122 /// estimate if such an event represents the natural end of scrolling inertia or a user-initiated
123 /// cancellation.
124 @property(nonatomic, assign) NSTimeInterval scrollInertiaEventAppKitDeadline;
125 
126 /// VSyncClient for touch events delivery frame rate correction.
127 ///
128 /// On promotion devices(eg: iPhone13 Pro), the delivery frame rate of touch events is 60HZ
129 /// but the frame rate of rendering is 120HZ, which is different and will leads jitter and laggy.
130 /// With this VSyncClient, it can correct the delivery frame rate of touch events to let it keep
131 /// the same with frame rate of rendering.
132 @property(nonatomic, strong) VSyncClient* touchRateCorrectionVSyncClient;
133 
134 /*
135  * Mouse and trackpad gesture recognizers
136  */
137 // Mouse and trackpad hover
138 @property(nonatomic, strong)
139  UIHoverGestureRecognizer* hoverGestureRecognizer API_AVAILABLE(ios(13.4));
140 // Mouse wheel scrolling
141 @property(nonatomic, strong)
142  UIPanGestureRecognizer* discreteScrollingPanGestureRecognizer API_AVAILABLE(ios(13.4));
143 // Trackpad and Magic Mouse scrolling
144 @property(nonatomic, strong)
145  UIPanGestureRecognizer* continuousScrollingPanGestureRecognizer API_AVAILABLE(ios(13.4));
146 // Trackpad pinching
147 @property(nonatomic, strong)
148  UIPinchGestureRecognizer* pinchGestureRecognizer API_AVAILABLE(ios(13.4));
149 // Trackpad rotating
150 @property(nonatomic, strong)
151  UIRotationGestureRecognizer* rotationGestureRecognizer API_AVAILABLE(ios(13.4));
152 
153 /// Creates and registers plugins used by this view controller.
154 - (void)addInternalPlugins;
155 - (void)deregisterNotifications;
156 
157 /// Called when the first frame has been rendered. Invokes any registered first-frame callback.
158 - (void)onFirstFrameRendered;
159 
160 /// Handles updating viewport metrics on keyboard animation.
161 - (void)handleKeyboardAnimationCallbackWithTargetTime:(fml::TimePoint)targetTime;
162 @end
163 
164 @implementation FlutterViewController {
165  flutter::ViewportMetrics _viewportMetrics;
167 }
168 
169 // Synthesize properties with an overridden getter/setter.
170 @synthesize viewOpaque = _viewOpaque;
171 @synthesize displayingFlutterUI = _displayingFlutterUI;
172 
173 // TODO(dkwingsmt): https://github.com/flutter/flutter/issues/138168
174 // No backing ivar is currently required; when multiple views are supported, we'll need to
175 // synthesize the ivar and store the view identifier.
176 @dynamic viewIdentifier;
177 
178 #pragma mark - Manage and override all designated initializers
179 
180 - (instancetype)initWithEngine:(FlutterEngine*)engine
181  nibName:(nullable NSString*)nibName
182  bundle:(nullable NSBundle*)nibBundle {
183  FML_CHECK(engine) << "initWithEngine:nibName:bundle: must be called with non-nil engine";
184  self = [super initWithNibName:nibName bundle:nibBundle];
185  if (self) {
186  _viewOpaque = YES;
187  if (engine.viewController) {
188  NSString* errorMessage =
189  [NSString stringWithFormat:
190  @"The supplied FlutterEngine %@ is already used with FlutterViewController "
191  "instance %@. One instance of the FlutterEngine can only be attached to "
192  "one FlutterViewController at a time. Set FlutterEngine.viewController to "
193  "nil before attaching it to another FlutterViewController.",
194  engine.description, engine.viewController.description];
195  [FlutterLogger logError:errorMessage];
196  }
197  _engine = engine;
198  _engineNeedsLaunch = NO;
199  _flutterView = [[FlutterView alloc] initWithDelegate:_engine
200  opaque:self.isViewOpaque
201  enableWideGamut:engine.project.isWideGamutEnabled];
202  _ongoingTouches = [[NSMutableSet alloc] init];
203 
204  // TODO(cbracken): https://github.com/flutter/flutter/issues/157140
205  // Eliminate method calls in initializers and dealloc.
206  [self performCommonViewControllerInitialization];
207  [engine setViewController:self];
208  }
209 
210  return self;
211 }
212 
213 - (instancetype)initWithProject:(FlutterDartProject*)project
214  nibName:(NSString*)nibName
215  bundle:(NSBundle*)nibBundle {
216  self = [super initWithNibName:nibName bundle:nibBundle];
217  if (self) {
218  // TODO(cbracken): https://github.com/flutter/flutter/issues/157140
219  // Eliminate method calls in initializers and dealloc.
220  [self sharedSetupWithProject:project initialRoute:nil];
221  }
222 
223  return self;
224 }
225 
226 - (instancetype)initWithProject:(FlutterDartProject*)project
227  initialRoute:(NSString*)initialRoute
228  nibName:(NSString*)nibName
229  bundle:(NSBundle*)nibBundle {
230  self = [super initWithNibName:nibName bundle:nibBundle];
231  if (self) {
232  // TODO(cbracken): https://github.com/flutter/flutter/issues/157140
233  // Eliminate method calls in initializers and dealloc.
234  [self sharedSetupWithProject:project initialRoute:initialRoute];
235  }
236 
237  return self;
238 }
239 
240 - (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil {
241  return [self initWithProject:nil nibName:nil bundle:nil];
242 }
243 
244 - (instancetype)initWithCoder:(NSCoder*)aDecoder {
245  self = [super initWithCoder:aDecoder];
246  return self;
247 }
248 
249 - (void)awakeFromNib {
250  [super awakeFromNib];
251  self.awokenFromNib = YES;
252  if (!self.engine) {
253  [self sharedSetupWithProject:nil initialRoute:nil];
254  }
255 }
256 
257 - (instancetype)init {
258  return [self initWithProject:nil nibName:nil bundle:nil];
259 }
260 
261 - (void)sharedSetupWithProject:(nullable FlutterDartProject*)project
262  initialRoute:(nullable NSString*)initialRoute {
263  id appDelegate = FlutterSharedApplication.application.delegate;
265  if ([appDelegate respondsToSelector:@selector(takeLaunchEngine)]) {
266  if (self.nibName) {
267  // Only grab the launch engine if it was created with a nib.
268  // FlutterViewControllers created from nibs can't specify their initial
269  // routes so it's safe to take it.
270  engine = [appDelegate takeLaunchEngine];
271  } else {
272  // If we registered plugins with a FlutterAppDelegate without a xib, throw
273  // away the engine that was registered through the FlutterAppDelegate.
274  // That's not a valid usage of the API.
275  [appDelegate takeLaunchEngine];
276  }
277  }
278  if (!engine) {
279  // Need the project to get settings for the view. Initializing it here means
280  // the Engine class won't initialize it later.
281  if (!project) {
282  project = [[FlutterDartProject alloc] init];
283  }
284 
285  engine = [[FlutterEngine alloc] initWithName:@"io.flutter"
286  project:project
287  allowHeadlessExecution:self.engineAllowHeadlessExecution
288  restorationEnabled:self.restorationIdentifier != nil];
289  }
290  if (!engine) {
291  return;
292  }
293 
294  _viewOpaque = YES;
295  _engine = engine;
296  _flutterView = [[FlutterView alloc] initWithDelegate:_engine
297  opaque:_viewOpaque
298  enableWideGamut:engine.project.isWideGamutEnabled];
299  [_engine createShell:nil libraryURI:nil initialRoute:initialRoute];
300 
301  // We call this from the FlutterViewController instead of the FlutterEngine directly because this
302  // is only needed when the FlutterEngine is implicit. If it's not implicit there's no need for
303  // them to have a callback to expose the engine since they created the FlutterEngine directly.
304  // This is the earliest this can be called because it depends on the shell being created.
305  BOOL performedCallback = [_engine performImplicitEngineCallback];
306 
307  // TODO(vashworth): Deprecate, see https://github.com/flutter/flutter/issues/176424
309  respondsToSelector:@selector(pluginRegistrant)]) {
310  NSObject<FlutterPluginRegistrant>* pluginRegistrant =
311  [FlutterSharedApplication.application.delegate performSelector:@selector(pluginRegistrant)];
312  [pluginRegistrant registerWithRegistry:self];
313  performedCallback = YES;
314  }
315  // When migrated to scenes, the FlutterViewController from the storyboard is initialized after the
316  // application launch events. Therefore, plugins may not be registered yet since they're expected
317  // to be registered during the implicit engine callbacks. As a workaround, send the app launch
318  // events after the application callbacks.
319  if (self.awokenFromNib && performedCallback && FlutterSharedApplication.hasSceneDelegate &&
320  [appDelegate isKindOfClass:[FlutterAppDelegate class]]) {
321  id applicationLifeCycleDelegate = ((FlutterAppDelegate*)appDelegate).lifeCycleDelegate;
322  [applicationLifeCycleDelegate
323  sceneFallbackWillFinishLaunchingApplication:FlutterSharedApplication.application];
324  [applicationLifeCycleDelegate
325  sceneFallbackDidFinishLaunchingApplication:FlutterSharedApplication.application];
326  }
327 
328  _engineNeedsLaunch = YES;
329  _ongoingTouches = [[NSMutableSet alloc] init];
330 
331  // TODO(cbracken): https://github.com/flutter/flutter/issues/157140
332  // Eliminate method calls in initializers and dealloc.
333  [self loadDefaultSplashScreenView];
334  [self performCommonViewControllerInitialization];
335 }
336 
337 - (BOOL)isViewOpaque {
338  return _viewOpaque;
339 }
340 
341 - (void)setViewOpaque:(BOOL)value {
342  _viewOpaque = value;
343  if (self.flutterView.layer.opaque != value) {
344  self.flutterView.layer.opaque = value;
345  [self.flutterView.layer setNeedsLayout];
346  }
347 }
348 
349 #pragma mark - Common view controller initialization tasks
350 
351 - (void)performCommonViewControllerInitialization {
352  if (_initialized) {
353  return;
354  }
355 
356  _initialized = YES;
357  _orientationPreferences = UIInterfaceOrientationMaskAll;
358  _statusBarStyle = UIStatusBarStyleDefault;
359 
360  // TODO(cbracken): https://github.com/flutter/flutter/issues/157140
361  // Eliminate method calls in initializers and dealloc.
362  [self setUpNotificationCenterObservers];
363 }
364 
365 - (void)setUpNotificationCenterObservers {
366  NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
367  [center addObserver:self
368  selector:@selector(onOrientationPreferencesUpdated:)
369  name:@(flutter::kOrientationUpdateNotificationName)
370  object:nil];
371 
372  [center addObserver:self
373  selector:@selector(onPreferredStatusBarStyleUpdated:)
374  name:@(flutter::kOverlayStyleUpdateNotificationName)
375  object:nil];
376 
378  [self setUpApplicationLifecycleNotifications:center];
379  } else {
380  [self setUpSceneLifecycleNotifications:center];
381  }
382 
383  [center addObserver:self
384  selector:@selector(keyboardWillChangeFrame:)
385  name:UIKeyboardWillChangeFrameNotification
386  object:nil];
387 
388  [center addObserver:self
389  selector:@selector(keyboardWillShowNotification:)
390  name:UIKeyboardWillShowNotification
391  object:nil];
392 
393  [center addObserver:self
394  selector:@selector(keyboardWillBeHidden:)
395  name:UIKeyboardWillHideNotification
396  object:nil];
397 
398  [center addObserver:self
399  selector:@selector(onAccessibilityStatusChanged:)
400  name:UIAccessibilityVoiceOverStatusDidChangeNotification
401  object:nil];
402 
403  [center addObserver:self
404  selector:@selector(onAccessibilityStatusChanged:)
405  name:UIAccessibilitySwitchControlStatusDidChangeNotification
406  object:nil];
407 
408  [center addObserver:self
409  selector:@selector(onAccessibilityStatusChanged:)
410  name:UIAccessibilitySpeakScreenStatusDidChangeNotification
411  object:nil];
412 
413  [center addObserver:self
414  selector:@selector(onAccessibilityStatusChanged:)
415  name:UIAccessibilityInvertColorsStatusDidChangeNotification
416  object:nil];
417 
418  [center addObserver:self
419  selector:@selector(onAccessibilityStatusChanged:)
420  name:UIAccessibilityReduceMotionStatusDidChangeNotification
421  object:nil];
422 
423  [center addObserver:self
424  selector:@selector(onAccessibilityStatusChanged:)
425  name:UIAccessibilityBoldTextStatusDidChangeNotification
426  object:nil];
427 
428  [center addObserver:self
429  selector:@selector(onAccessibilityStatusChanged:)
430  name:UIAccessibilityDarkerSystemColorsStatusDidChangeNotification
431  object:nil];
432 
433  [center addObserver:self
434  selector:@selector(onAccessibilityStatusChanged:)
435  name:UIAccessibilityOnOffSwitchLabelsDidChangeNotification
436  object:nil];
437 
438  [center addObserver:self
439  selector:@selector(onUserSettingsChanged:)
440  name:UIContentSizeCategoryDidChangeNotification
441  object:nil];
442 
443  [center addObserver:self
444  selector:@selector(onHideHomeIndicatorNotification:)
445  name:FlutterViewControllerHideHomeIndicator
446  object:nil];
447 
448  [center addObserver:self
449  selector:@selector(onShowHomeIndicatorNotification:)
450  name:FlutterViewControllerShowHomeIndicator
451  object:nil];
452 }
453 
454 - (void)setUpSceneLifecycleNotifications:(NSNotificationCenter*)center API_AVAILABLE(ios(13.0)) {
455  [center addObserver:self
456  selector:@selector(sceneBecameActive:)
457  name:UISceneDidActivateNotification
458  object:nil];
459 
460  [center addObserver:self
461  selector:@selector(sceneWillResignActive:)
462  name:UISceneWillDeactivateNotification
463  object:nil];
464 
465  [center addObserver:self
466  selector:@selector(sceneWillDisconnect:)
467  name:UISceneDidDisconnectNotification
468  object:nil];
469 
470  [center addObserver:self
471  selector:@selector(sceneDidEnterBackground:)
472  name:UISceneDidEnterBackgroundNotification
473  object:nil];
474 
475  [center addObserver:self
476  selector:@selector(sceneWillEnterForeground:)
477  name:UISceneWillEnterForegroundNotification
478  object:nil];
479 }
480 
481 - (void)setUpApplicationLifecycleNotifications:(NSNotificationCenter*)center {
482  [center addObserver:self
483  selector:@selector(applicationBecameActive:)
484  name:UIApplicationDidBecomeActiveNotification
485  object:nil];
486 
487  [center addObserver:self
488  selector:@selector(applicationWillResignActive:)
489  name:UIApplicationWillResignActiveNotification
490  object:nil];
491 
492  [center addObserver:self
493  selector:@selector(applicationWillTerminate:)
494  name:UIApplicationWillTerminateNotification
495  object:nil];
496 
497  [center addObserver:self
498  selector:@selector(applicationDidEnterBackground:)
499  name:UIApplicationDidEnterBackgroundNotification
500  object:nil];
501 
502  [center addObserver:self
503  selector:@selector(applicationWillEnterForeground:)
504  name:UIApplicationWillEnterForegroundNotification
505  object:nil];
506 }
507 
508 - (void)setInitialRoute:(NSString*)route {
509  [self.engine.navigationChannel invokeMethod:@"setInitialRoute" arguments:route];
510 }
511 
512 - (void)popRoute {
513  [self.engine.navigationChannel invokeMethod:@"popRoute" arguments:nil];
514 }
515 
516 - (void)pushRoute:(NSString*)route {
517  [self.engine.navigationChannel invokeMethod:@"pushRoute" arguments:route];
518 }
519 
520 #pragma mark - Loading the view
521 
522 static UIView* GetViewOrPlaceholder(UIView* existing_view) {
523  if (existing_view) {
524  return existing_view;
525  }
526 
527  auto placeholder = [[UIView alloc] init];
528 
529  placeholder.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
530  placeholder.backgroundColor = UIColor.systemBackgroundColor;
531  placeholder.autoresizesSubviews = YES;
532 
533  // Only add the label when we know we have failed to enable tracing (and it was necessary).
534  // Otherwise, a spurious warning will be shown in cases where an engine cannot be initialized for
535  // other reasons.
536  if (flutter::GetTracingResult() == flutter::TracingResult::kDisabled) {
537  auto messageLabel = [[UILabel alloc] init];
538  messageLabel.numberOfLines = 0u;
539  messageLabel.textAlignment = NSTextAlignmentCenter;
540  messageLabel.autoresizingMask =
541  UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
542  messageLabel.text =
543  @"In iOS 14+, debug mode Flutter apps can only be launched from Flutter tooling, "
544  @"IDEs with Flutter plugins or from Xcode.\n\nAlternatively, build in profile or release "
545  @"modes to enable launching from the home screen.";
546  [placeholder addSubview:messageLabel];
547  }
548 
549  return placeholder;
550 }
551 
552 - (void)loadView {
553  self.view = GetViewOrPlaceholder(self.flutterView);
554  self.view.multipleTouchEnabled = YES;
555  self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
556 
557  [self installSplashScreenViewIfNecessary];
558 
559  // Create and set up the scroll view.
560  UIScrollView* scrollView = [[UIScrollView alloc] init];
561  scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
562  // The color shouldn't matter since it is offscreen.
563  scrollView.backgroundColor = UIColor.whiteColor;
564  scrollView.delegate = self;
565  // This is an arbitrary small size.
566  scrollView.contentSize = CGSizeMake(kScrollViewContentSize, kScrollViewContentSize);
567  // This is an arbitrary offset that is not CGPointZero.
568  scrollView.contentOffset = CGPointMake(kScrollViewContentSize, kScrollViewContentSize);
569 
570  [self.view addSubview:scrollView];
571  self.scrollView = scrollView;
572 }
573 
574 - (flutter::PointerData)generatePointerDataForFake {
575  flutter::PointerData pointer_data;
576  pointer_data.Clear();
577  pointer_data.kind = flutter::PointerData::DeviceKind::kTouch;
578  // `UITouch.timestamp` is defined as seconds since system startup. Synthesized events can get this
579  // time with `NSProcessInfo.systemUptime`. See
580  // https://developer.apple.com/documentation/uikit/uitouch/1618144-timestamp?language=objc
581  pointer_data.time_stamp = [[NSProcessInfo processInfo] systemUptime] * kMicrosecondsPerSecond;
582  return pointer_data;
583 }
584 
585 static void SendFakeTouchEvent(UIScreen* screen,
587  CGPoint location,
588  flutter::PointerData::Change change) {
589  const CGFloat scale = screen.scale;
590  flutter::PointerData pointer_data = [[engine viewController] generatePointerDataForFake];
591  pointer_data.physical_x = location.x * scale;
592  pointer_data.physical_y = location.y * scale;
593  auto packet = std::make_unique<flutter::PointerDataPacket>(/*count=*/1);
594  pointer_data.change = change;
595  packet->SetPointerData(0, pointer_data);
596  [engine dispatchPointerDataPacket:std::move(packet)];
597 }
598 
599 - (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView {
600  if (!self.engine) {
601  return NO;
602  }
603  CGPoint statusBarPoint = CGPointZero;
604  UIScreen* screen = self.flutterScreenIfViewLoaded;
605  if (screen) {
606  SendFakeTouchEvent(screen, self.engine, statusBarPoint, flutter::PointerData::Change::kDown);
607  SendFakeTouchEvent(screen, self.engine, statusBarPoint, flutter::PointerData::Change::kUp);
608  }
609  return NO;
610 }
611 
612 #pragma mark - Managing launch views
613 
614 - (void)installSplashScreenViewIfNecessary {
615  // Show the launch screen view again on top of the FlutterView if available.
616  // This launch screen view will be removed once the first Flutter frame is rendered.
617  if (self.splashScreenView && (self.isBeingPresented || self.isMovingToParentViewController)) {
618  [self.splashScreenView removeFromSuperview];
619  self.splashScreenView = nil;
620  return;
621  }
622 
623  // Use the property getter to initialize the default value.
624  UIView* splashScreenView = self.splashScreenView;
625  if (splashScreenView == nil) {
626  return;
627  }
628  splashScreenView.frame = self.view.bounds;
629  [self.view addSubview:splashScreenView];
630 }
631 
632 + (BOOL)automaticallyNotifiesObserversOfDisplayingFlutterUI {
633  return NO;
634 }
635 
636 - (void)setDisplayingFlutterUI:(BOOL)displayingFlutterUI {
637  if (_displayingFlutterUI != displayingFlutterUI) {
638  if (displayingFlutterUI == YES) {
639  if (!self.viewIfLoaded.window) {
640  return;
641  }
642  }
643  [self willChangeValueForKey:@"displayingFlutterUI"];
644  _displayingFlutterUI = displayingFlutterUI;
645  [self didChangeValueForKey:@"displayingFlutterUI"];
646  }
647 }
648 
649 - (void)callViewRenderedCallback {
650  self.displayingFlutterUI = YES;
651  if (self.flutterViewRenderedCallback) {
652  self.flutterViewRenderedCallback();
653  self.flutterViewRenderedCallback = nil;
654  }
655 }
656 
657 - (void)removeSplashScreenWithCompletion:(dispatch_block_t _Nullable)onComplete {
658  NSAssert(self.splashScreenView, @"The splash screen view must not be nil");
659  UIView* splashScreen = self.splashScreenView;
660  // setSplashScreenView calls this method. Assign directly to ivar to avoid an infinite loop.
661  _splashScreenView = nil;
662  [UIView animateWithDuration:0.2
663  animations:^{
664  splashScreen.alpha = 0;
665  }
666  completion:^(BOOL finished) {
667  [splashScreen removeFromSuperview];
668  if (onComplete) {
669  onComplete();
670  }
671  }];
672 }
673 
674 - (void)onFirstFrameRendered {
675  if (self.splashScreenView) {
676  __weak FlutterViewController* weakSelf = self;
677  [self removeSplashScreenWithCompletion:^{
678  [weakSelf callViewRenderedCallback];
679  }];
680  } else {
681  [self callViewRenderedCallback];
682  }
683 }
684 
685 - (void)installFirstFrameCallback {
686  if (!self.engine) {
687  return;
688  }
689  __weak FlutterViewController* weakSelf = self;
690  [self.engine installFirstFrameCallback:^{
691  [weakSelf onFirstFrameRendered];
692  }];
693 }
694 
695 #pragma mark - Properties
696 
697 - (int64_t)viewIdentifier {
698  // TODO(dkwingsmt): Fill the view ID property with the correct value once the
699  // iOS shell supports multiple views.
700  return flutter::kFlutterImplicitViewId;
701 }
702 
703 - (BOOL)loadDefaultSplashScreenView {
704  NSString* launchscreenName =
705  [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UILaunchStoryboardName"];
706  if (launchscreenName == nil) {
707  return NO;
708  }
709  UIView* splashView = [self splashScreenFromStoryboard:launchscreenName];
710  if (!splashView) {
711  splashView = [self splashScreenFromXib:launchscreenName];
712  }
713  if (!splashView) {
714  return NO;
715  }
716  self.splashScreenView = splashView;
717  return YES;
718 }
719 
720 - (UIView*)splashScreenFromStoryboard:(NSString*)name {
721  UIStoryboard* storyboard = nil;
722  @try {
723  storyboard = [UIStoryboard storyboardWithName:name bundle:nil];
724  } @catch (NSException* exception) {
725  return nil;
726  }
727  if (storyboard) {
728  UIViewController* splashScreenViewController = [storyboard instantiateInitialViewController];
729  return splashScreenViewController.view;
730  }
731  return nil;
732 }
733 
734 - (UIView*)splashScreenFromXib:(NSString*)name {
735  NSArray* objects = nil;
736  @try {
737  objects = [[NSBundle mainBundle] loadNibNamed:name owner:self options:nil];
738  } @catch (NSException* exception) {
739  return nil;
740  }
741  if ([objects count] != 0) {
742  UIView* view = [objects objectAtIndex:0];
743  return view;
744  }
745  return nil;
746 }
747 
748 - (void)setSplashScreenView:(UIView*)view {
749  if (view == _splashScreenView) {
750  return;
751  }
752 
753  // Special case: user wants to remove the splash screen view.
754  if (!view) {
755  if (_splashScreenView) {
756  [self removeSplashScreenWithCompletion:nil];
757  }
758  return;
759  }
760 
761  _splashScreenView = view;
762  _splashScreenView.autoresizingMask =
763  UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
764 }
765 
766 - (void)setFlutterViewDidRenderCallback:(void (^)(void))callback {
767  _flutterViewRenderedCallback = callback;
768 }
769 
770 - (UISceneActivationState)activationState {
771  return self.flutterWindowSceneIfViewLoaded.activationState;
772 }
773 
774 - (BOOL)stateIsActive {
775  // [UIApplication sharedApplication API is not available for app extension.
776  UIApplication* flutterApplication = FlutterSharedApplication.application;
777  BOOL isActive = flutterApplication
778  ? [self isApplicationStateMatching:UIApplicationStateActive
779  withApplication:flutterApplication]
780  : [self isSceneStateMatching:UISceneActivationStateForegroundActive];
781  return isActive;
782 }
783 
784 - (BOOL)stateIsBackground {
785  // [UIApplication sharedApplication API is not available for app extension.
786  UIApplication* flutterApplication = FlutterSharedApplication.application;
787  return flutterApplication ? [self isApplicationStateMatching:UIApplicationStateBackground
788  withApplication:flutterApplication]
789  : [self isSceneStateMatching:UISceneActivationStateBackground];
790 }
791 
792 - (BOOL)isApplicationStateMatching:(UIApplicationState)match
793  withApplication:(UIApplication*)application {
794  switch (application.applicationState) {
795  case UIApplicationStateActive:
796  case UIApplicationStateInactive:
797  case UIApplicationStateBackground:
798  return application.applicationState == match;
799  }
800 }
801 
802 - (BOOL)isSceneStateMatching:(UISceneActivationState)match API_AVAILABLE(ios(13.0)) {
803  switch (self.activationState) {
804  case UISceneActivationStateForegroundActive:
805  case UISceneActivationStateUnattached:
806  case UISceneActivationStateForegroundInactive:
807  case UISceneActivationStateBackground:
808  return self.activationState == match;
809  }
810 }
811 
812 #pragma mark - Surface creation and teardown updates
813 
814 - (void)surfaceUpdated:(BOOL)appeared {
815  if (!self.engine) {
816  return;
817  }
818 
819  // NotifyCreated/NotifyDestroyed are synchronous and require hops between the UI and raster
820  // thread.
821  if (appeared) {
822  [self installFirstFrameCallback];
823  self.platformViewsController.flutterView = self.flutterView;
824  self.platformViewsController.flutterViewController = self;
825  [self.engine notifyViewCreated];
826  } else {
827  self.displayingFlutterUI = NO;
828  [self.engine notifyViewDestroyed];
829  self.platformViewsController.flutterView = nil;
830  self.platformViewsController.flutterViewController = nil;
831  }
832 }
833 
834 #pragma mark - UIViewController lifecycle notifications
835 
836 - (void)viewDidLoad {
837  TRACE_EVENT0("flutter", "viewDidLoad");
838 
839  if (self.engine && self.engineNeedsLaunch) {
840  [self.engine launchEngine:nil libraryURI:nil entrypointArgs:nil];
841  [self.engine setViewController:self];
842  self.engineNeedsLaunch = NO;
843  } else if (self.engine.viewController == self) {
844  [self.engine attachView];
845  }
846 
847  // Register internal plugins.
848  [self addInternalPlugins];
849 
850  // Create a vsync client to correct delivery frame rate of touch events if needed.
851  [self createTouchRateCorrectionVSyncClientIfNeeded];
852 
853  if (@available(iOS 13.4, *)) {
854  _hoverGestureRecognizer =
855  [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(hoverEvent:)];
856  _hoverGestureRecognizer.delegate = self;
857  [self.flutterView addGestureRecognizer:_hoverGestureRecognizer];
858 
859  _discreteScrollingPanGestureRecognizer =
860  [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(discreteScrollEvent:)];
861  _discreteScrollingPanGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskDiscrete;
862  // Disallowing all touch types. If touch events are allowed here, touches to the screen will be
863  // consumed by the UIGestureRecognizer instead of being passed through to flutter via
864  // touchesBegan. Trackpad and mouse scrolls are sent by the platform as scroll events rather
865  // than touch events, so they will still be received.
866  _discreteScrollingPanGestureRecognizer.allowedTouchTypes = @[];
867  _discreteScrollingPanGestureRecognizer.delegate = self;
868  [self.flutterView addGestureRecognizer:_discreteScrollingPanGestureRecognizer];
869  _continuousScrollingPanGestureRecognizer =
870  [[UIPanGestureRecognizer alloc] initWithTarget:self
871  action:@selector(continuousScrollEvent:)];
872  _continuousScrollingPanGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskContinuous;
873  _continuousScrollingPanGestureRecognizer.allowedTouchTypes = @[];
874  _continuousScrollingPanGestureRecognizer.delegate = self;
875  [self.flutterView addGestureRecognizer:_continuousScrollingPanGestureRecognizer];
876  _pinchGestureRecognizer =
877  [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchEvent:)];
878  _pinchGestureRecognizer.allowedTouchTypes = @[];
879  _pinchGestureRecognizer.delegate = self;
880  [self.flutterView addGestureRecognizer:_pinchGestureRecognizer];
881  _rotationGestureRecognizer = [[UIRotationGestureRecognizer alloc] init];
882  _rotationGestureRecognizer.allowedTouchTypes = @[];
883  _rotationGestureRecognizer.delegate = self;
884  [self.flutterView addGestureRecognizer:_rotationGestureRecognizer];
885  }
886 
887  [super viewDidLoad];
888 }
889 
890 - (void)addInternalPlugins {
891  self.keyboardManager = [[FlutterKeyboardManager alloc] init];
892  __weak FlutterViewController* weakSelf = self;
893  FlutterSendKeyEvent sendEvent =
894  ^(const FlutterKeyEvent& event, FlutterKeyEventCallback callback, void* userData) {
895  [weakSelf.engine sendKeyEvent:event callback:callback userData:userData];
896  };
897  [self.keyboardManager
898  addPrimaryResponder:[[FlutterEmbedderKeyResponder alloc] initWithSendEvent:sendEvent]];
899  FlutterChannelKeyResponder* responder =
900  [[FlutterChannelKeyResponder alloc] initWithChannel:self.engine.keyEventChannel];
901  [self.keyboardManager addPrimaryResponder:responder];
902  FlutterTextInputPlugin* textInputPlugin = self.engine.textInputPlugin;
903  if (textInputPlugin != nil) {
904  [self.keyboardManager addSecondaryResponder:textInputPlugin];
905  }
906  if (self.engine.viewController == self) {
907  [textInputPlugin setUpIndirectScribbleInteraction:self];
908  }
909 }
910 
911 - (void)removeInternalPlugins {
912  self.keyboardManager = nil;
913 }
914 
915 - (void)viewWillAppear:(BOOL)animated {
916  TRACE_EVENT0("flutter", "viewWillAppear");
917  if (self.engine.viewController == self) {
918  // Send platform settings to Flutter, e.g., platform brightness.
919  [self onUserSettingsChanged:nil];
920 
921  // Only recreate surface on subsequent appearances when viewport metrics are known.
922  // First time surface creation is done on viewDidLayoutSubviews.
923  if (_viewportMetrics.physical_width) {
924  [self surfaceUpdated:YES];
925  }
926  [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.inactive"];
927  [self.engine.restorationPlugin markRestorationComplete];
928  }
929 
930  [super viewWillAppear:animated];
931 }
932 
933 - (void)viewDidAppear:(BOOL)animated {
934  TRACE_EVENT0("flutter", "viewDidAppear");
935  if (self.engine.viewController == self) {
936  [self onUserSettingsChanged:nil];
937  [self onAccessibilityStatusChanged:nil];
938 
939  if (self.stateIsActive) {
940  [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.resumed"];
941  }
942  }
943  [super viewDidAppear:animated];
944 }
945 
946 - (void)viewWillDisappear:(BOOL)animated {
947  TRACE_EVENT0("flutter", "viewWillDisappear");
948  if (self.engine.viewController == self) {
949  [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.inactive"];
950  }
951  [super viewWillDisappear:animated];
952 }
953 
954 - (void)viewDidDisappear:(BOOL)animated {
955  TRACE_EVENT0("flutter", "viewDidDisappear");
956  if (self.engine.viewController == self) {
957  [self invalidateKeyboardAnimationVSyncClient];
958  [self ensureViewportMetricsIsCorrect];
959  [self surfaceUpdated:NO];
960  [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.paused"];
961  [self flushOngoingTouches];
962  [self.engine notifyLowMemory];
963  }
964 
965  [super viewDidDisappear:animated];
966 }
967 
968 - (void)viewWillTransitionToSize:(CGSize)size
969  withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
970  [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
971 
972  // We delay the viewport metrics update for half of rotation transition duration, to address
973  // a bug with distorted aspect ratio.
974  // See: https://github.com/flutter/flutter/issues/16322
975  //
976  // This approach does not fully resolve all distortion problem. But instead, it reduces the
977  // rotation distortion roughly from 4x to 2x. The most distorted frames occur in the middle
978  // of the transition when it is rotating the fastest, making it hard to notice.
979 
980  NSTimeInterval transitionDuration = coordinator.transitionDuration;
981  // Do not delay viewport metrics update if zero transition duration.
982  if (transitionDuration == 0) {
983  return;
984  }
985 
986  __weak FlutterViewController* weakSelf = self;
987  _shouldIgnoreViewportMetricsUpdatesDuringRotation = YES;
988  dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
989  static_cast<int64_t>(transitionDuration / 2.0 * NSEC_PER_SEC)),
990  dispatch_get_main_queue(), ^{
991  FlutterViewController* strongSelf = weakSelf;
992  if (!strongSelf) {
993  return;
994  }
995 
996  // `viewWillTransitionToSize` is only called after the previous rotation is
997  // complete. So there won't be race condition for this flag.
998  strongSelf.shouldIgnoreViewportMetricsUpdatesDuringRotation = NO;
999  [strongSelf updateViewportMetricsIfNeeded];
1000  });
1001 }
1002 
1003 - (void)flushOngoingTouches {
1004  if (self.engine && self.ongoingTouches.count > 0) {
1005  auto packet = std::make_unique<flutter::PointerDataPacket>(self.ongoingTouches.count);
1006  size_t pointer_index = 0;
1007  // If the view controller is going away, we want to flush cancel all the ongoing
1008  // touches to the framework so nothing gets orphaned.
1009  for (NSNumber* device in self.ongoingTouches) {
1010  // Create fake PointerData to balance out each previously started one for the framework.
1011  flutter::PointerData pointer_data = [self generatePointerDataForFake];
1012 
1013  pointer_data.change = flutter::PointerData::Change::kCancel;
1014  pointer_data.device = device.longLongValue;
1015  pointer_data.pointer_identifier = 0;
1016  pointer_data.view_id = self.viewIdentifier;
1017 
1018  // Anything we put here will be arbitrary since there are no touches.
1019  pointer_data.physical_x = 0;
1020  pointer_data.physical_y = 0;
1021  pointer_data.physical_delta_x = 0.0;
1022  pointer_data.physical_delta_y = 0.0;
1023  pointer_data.pressure = 1.0;
1024  pointer_data.pressure_max = 1.0;
1025 
1026  packet->SetPointerData(pointer_index++, pointer_data);
1027  }
1028 
1029  [self.ongoingTouches removeAllObjects];
1030  [self.engine dispatchPointerDataPacket:std::move(packet)];
1031  }
1032 }
1033 
1034 - (void)deregisterNotifications {
1035  [[NSNotificationCenter defaultCenter] postNotificationName:FlutterViewControllerWillDealloc
1036  object:self
1037  userInfo:nil];
1038  [[NSNotificationCenter defaultCenter] removeObserver:self];
1039 }
1040 
1041 - (void)dealloc {
1042  // TODO(cbracken): https://github.com/flutter/flutter/issues/157140
1043  // Eliminate method calls in initializers and dealloc.
1044  [self removeInternalPlugins];
1045  [self deregisterNotifications];
1046 
1047  [self invalidateKeyboardAnimationVSyncClient];
1048  [self invalidateTouchRateCorrectionVSyncClient];
1049 
1050  // TODO(cbracken): https://github.com/flutter/flutter/issues/156222
1051  // Ensure all delegates are weak and remove this.
1052  _scrollView.delegate = nil;
1053  _hoverGestureRecognizer.delegate = nil;
1054  _discreteScrollingPanGestureRecognizer.delegate = nil;
1055  _continuousScrollingPanGestureRecognizer.delegate = nil;
1056  _pinchGestureRecognizer.delegate = nil;
1057  _rotationGestureRecognizer.delegate = nil;
1058 }
1059 
1060 #pragma mark - Application lifecycle notifications
1061 
1062 - (void)applicationBecameActive:(NSNotification*)notification {
1063  TRACE_EVENT0("flutter", "applicationBecameActive");
1064  [self appOrSceneBecameActive];
1065 }
1066 
1067 - (void)applicationWillResignActive:(NSNotification*)notification {
1068  TRACE_EVENT0("flutter", "applicationWillResignActive");
1069  [self appOrSceneWillResignActive];
1070 }
1071 
1072 - (void)applicationWillTerminate:(NSNotification*)notification {
1073  [self appOrSceneWillTerminate];
1074 }
1075 
1076 - (void)applicationDidEnterBackground:(NSNotification*)notification {
1077  TRACE_EVENT0("flutter", "applicationDidEnterBackground");
1078  [self appOrSceneDidEnterBackground];
1079 }
1080 
1081 - (void)applicationWillEnterForeground:(NSNotification*)notification {
1082  TRACE_EVENT0("flutter", "applicationWillEnterForeground");
1083  [self appOrSceneWillEnterForeground];
1084 }
1085 
1086 #pragma mark - Scene lifecycle notifications
1087 
1088 - (void)sceneBecameActive:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
1089  TRACE_EVENT0("flutter", "sceneBecameActive");
1090  [self appOrSceneBecameActive];
1091 }
1092 
1093 - (void)sceneWillResignActive:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
1094  TRACE_EVENT0("flutter", "sceneWillResignActive");
1095  [self appOrSceneWillResignActive];
1096 }
1097 
1098 - (void)sceneWillDisconnect:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
1099  [self appOrSceneWillTerminate];
1100 }
1101 
1102 - (void)sceneDidEnterBackground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
1103  TRACE_EVENT0("flutter", "sceneDidEnterBackground");
1104  [self appOrSceneDidEnterBackground];
1105 }
1106 
1107 - (void)sceneWillEnterForeground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
1108  TRACE_EVENT0("flutter", "sceneWillEnterForeground");
1109  [self appOrSceneWillEnterForeground];
1110 }
1111 
1112 #pragma mark - Lifecycle shared
1113 
1114 - (void)appOrSceneBecameActive {
1115  self.isKeyboardInOrTransitioningFromBackground = NO;
1116  if (_viewportMetrics.physical_width) {
1117  [self surfaceUpdated:YES];
1118  }
1119  [self performSelector:@selector(goToApplicationLifecycle:)
1120  withObject:@"AppLifecycleState.resumed"
1121  afterDelay:0.0f];
1122 }
1123 
1124 - (void)appOrSceneWillResignActive {
1125  [NSObject cancelPreviousPerformRequestsWithTarget:self
1126  selector:@selector(goToApplicationLifecycle:)
1127  object:@"AppLifecycleState.resumed"];
1128  [self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
1129 }
1130 
1131 - (void)appOrSceneWillTerminate {
1132  [self goToApplicationLifecycle:@"AppLifecycleState.detached"];
1133  [self.engine destroyContext];
1134 }
1135 
1136 - (void)appOrSceneDidEnterBackground {
1137  self.isKeyboardInOrTransitioningFromBackground = YES;
1138  [self surfaceUpdated:NO];
1139  [self goToApplicationLifecycle:@"AppLifecycleState.paused"];
1140 }
1141 
1142 - (void)appOrSceneWillEnterForeground {
1143  [self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
1144 }
1145 
1146 // Make this transition only while this current view controller is visible.
1147 - (void)goToApplicationLifecycle:(nonnull NSString*)state {
1148  // Accessing self.view will create the view. Instead use viewIfLoaded
1149  // to check whether the view is attached to window.
1150  if (self.viewIfLoaded.window) {
1151  [self.engine.lifecycleChannel sendMessage:state];
1152  }
1153 }
1154 
1155 #pragma mark - Touch event handling
1156 
1157 static flutter::PointerData::Change PointerDataChangeFromUITouchPhase(UITouchPhase phase) {
1158  switch (phase) {
1159  case UITouchPhaseBegan:
1160  return flutter::PointerData::Change::kDown;
1161  case UITouchPhaseMoved:
1162  case UITouchPhaseStationary:
1163  // There is no EVENT_TYPE_POINTER_STATIONARY. So we just pass a move type
1164  // with the same coordinates
1165  return flutter::PointerData::Change::kMove;
1166  case UITouchPhaseEnded:
1167  return flutter::PointerData::Change::kUp;
1168  case UITouchPhaseCancelled:
1169  return flutter::PointerData::Change::kCancel;
1170  default:
1171  // TODO(53695): Handle the `UITouchPhaseRegion`... enum values.
1172  FML_DLOG(INFO) << "Unhandled touch phase: " << phase;
1173  break;
1174  }
1175 
1176  return flutter::PointerData::Change::kCancel;
1177 }
1178 
1179 static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) {
1180  switch (touch.type) {
1181  case UITouchTypeDirect:
1182  case UITouchTypeIndirect:
1183  return flutter::PointerData::DeviceKind::kTouch;
1184  case UITouchTypeStylus:
1185  return flutter::PointerData::DeviceKind::kStylus;
1186  case UITouchTypeIndirectPointer:
1187  return flutter::PointerData::DeviceKind::kMouse;
1188  default:
1189  FML_DLOG(INFO) << "Unhandled touch type: " << touch.type;
1190  break;
1191  }
1192 
1193  return flutter::PointerData::DeviceKind::kTouch;
1194 }
1195 
1196 // Dispatches the UITouches to the engine. Usually, the type of change of the touch is determined
1197 // from the UITouch's phase. However, FlutterAppDelegate fakes touches to ensure that touch events
1198 // in the status bar area are available to framework code. The change type (optional) of the faked
1199 // touch is specified in the second argument.
1200 - (void)dispatchTouches:(NSSet*)touches
1201  pointerDataChangeOverride:(flutter::PointerData::Change*)overridden_change
1202  event:(UIEvent*)event {
1203  if (!self.engine) {
1204  return;
1205  }
1206 
1207  // If the UIApplicationSupportsIndirectInputEvents in Info.plist returns YES, then the platform
1208  // dispatches indirect pointer touches (trackpad clicks) as UITouch with a type of
1209  // UITouchTypeIndirectPointer and different identifiers for each click. They are translated into
1210  // Flutter pointer events with type of kMouse and different device IDs. These devices must be
1211  // terminated with kRemove events when the touches end, otherwise they will keep triggering hover
1212  // events.
1213  //
1214  // If the UIApplicationSupportsIndirectInputEvents in Info.plist returns NO, then the platform
1215  // dispatches indirect pointer touches (trackpad clicks) as UITouch with a type of
1216  // UITouchTypeIndirectPointer and different identifiers for each click. They are translated into
1217  // Flutter pointer events with type of kTouch and different device IDs. Removing these devices is
1218  // neither necessary nor harmful.
1219  //
1220  // Therefore Flutter always removes these devices. The touches_to_remove_count tracks how many
1221  // remove events are needed in this group of touches to properly allocate space for the packet.
1222  // The remove event of a touch is synthesized immediately after its normal event.
1223  //
1224  // See also:
1225  // https://developer.apple.com/documentation/uikit/pointer_interactions?language=objc
1226  // https://developer.apple.com/documentation/bundleresources/information_property_list/uiapplicationsupportsindirectinputevents?language=objc
1227  NSUInteger touches_to_remove_count = 0;
1228  for (UITouch* touch in touches) {
1229  if (touch.phase == UITouchPhaseEnded || touch.phase == UITouchPhaseCancelled) {
1230  touches_to_remove_count++;
1231  }
1232  }
1233 
1234  // Activate or pause the correction of delivery frame rate of touch events.
1235  [self triggerTouchRateCorrectionIfNeeded:touches];
1236 
1237  const CGFloat scale = self.flutterScreenIfViewLoaded.scale;
1238  auto packet =
1239  std::make_unique<flutter::PointerDataPacket>(touches.count + touches_to_remove_count);
1240 
1241  size_t pointer_index = 0;
1242 
1243  for (UITouch* touch in touches) {
1244  CGPoint windowCoordinates = [touch locationInView:self.view];
1245 
1246  flutter::PointerData pointer_data;
1247  pointer_data.Clear();
1248 
1249  constexpr int kMicrosecondsPerSecond = 1000 * 1000;
1250  pointer_data.time_stamp = touch.timestamp * kMicrosecondsPerSecond;
1251 
1252  pointer_data.change = overridden_change != nullptr
1253  ? *overridden_change
1254  : PointerDataChangeFromUITouchPhase(touch.phase);
1255 
1256  pointer_data.kind = DeviceKindFromTouchType(touch);
1257 
1258  pointer_data.device = reinterpret_cast<int64_t>(touch);
1259 
1260  pointer_data.view_id = self.viewIdentifier;
1261 
1262  // Pointer will be generated in pointer_data_packet_converter.cc.
1263  pointer_data.pointer_identifier = 0;
1264 
1265  pointer_data.physical_x = windowCoordinates.x * scale;
1266  pointer_data.physical_y = windowCoordinates.y * scale;
1267 
1268  // Delta will be generated in pointer_data_packet_converter.cc.
1269  pointer_data.physical_delta_x = 0.0;
1270  pointer_data.physical_delta_y = 0.0;
1271 
1272  NSNumber* deviceKey = [NSNumber numberWithLongLong:pointer_data.device];
1273  // Track touches that began and not yet stopped so we can flush them
1274  // if the view controller goes away.
1275  switch (pointer_data.change) {
1276  case flutter::PointerData::Change::kDown:
1277  [self.ongoingTouches addObject:deviceKey];
1278  break;
1279  case flutter::PointerData::Change::kCancel:
1280  case flutter::PointerData::Change::kUp:
1281  [self.ongoingTouches removeObject:deviceKey];
1282  break;
1283  case flutter::PointerData::Change::kHover:
1284  case flutter::PointerData::Change::kMove:
1285  // We're only tracking starts and stops.
1286  break;
1287  case flutter::PointerData::Change::kAdd:
1288  case flutter::PointerData::Change::kRemove:
1289  // We don't use kAdd/kRemove.
1290  break;
1291  case flutter::PointerData::Change::kPanZoomStart:
1292  case flutter::PointerData::Change::kPanZoomUpdate:
1293  case flutter::PointerData::Change::kPanZoomEnd:
1294  // We don't send pan/zoom events here
1295  break;
1296  }
1297 
1298  // pressure_min is always 0.0
1299  pointer_data.pressure = touch.force;
1300  pointer_data.pressure_max = touch.maximumPossibleForce;
1301  pointer_data.radius_major = touch.majorRadius;
1302  pointer_data.radius_min = touch.majorRadius - touch.majorRadiusTolerance;
1303  pointer_data.radius_max = touch.majorRadius + touch.majorRadiusTolerance;
1304 
1305  // iOS Documentation: altitudeAngle
1306  // A value of 0 radians indicates that the stylus is parallel to the surface. The value of
1307  // this property is Pi/2 when the stylus is perpendicular to the surface.
1308  //
1309  // PointerData Documentation: tilt
1310  // The angle of the stylus, in radians in the range:
1311  // 0 <= tilt <= pi/2
1312  // giving the angle of the axis of the stylus, relative to the axis perpendicular to the input
1313  // surface (thus 0.0 indicates the stylus is orthogonal to the plane of the input surface,
1314  // while pi/2 indicates that the stylus is flat on that surface).
1315  //
1316  // Discussion:
1317  // The ranges are the same. Origins are swapped.
1318  pointer_data.tilt = M_PI_2 - touch.altitudeAngle;
1319 
1320  // iOS Documentation: azimuthAngleInView:
1321  // With the tip of the stylus touching the screen, the value of this property is 0 radians
1322  // when the cap end of the stylus (that is, the end opposite of the tip) points along the
1323  // positive x axis of the device's screen. The azimuth angle increases as the user swings the
1324  // cap end of the stylus in a clockwise direction around the tip.
1325  //
1326  // PointerData Documentation: orientation
1327  // The angle of the stylus, in radians in the range:
1328  // -pi < orientation <= pi
1329  // giving the angle of the axis of the stylus projected onto the input surface, relative to
1330  // the positive y-axis of that surface (thus 0.0 indicates the stylus, if projected onto that
1331  // surface, would go from the contact point vertically up in the positive y-axis direction, pi
1332  // would indicate that the stylus would go down in the negative y-axis direction; pi/4 would
1333  // indicate that the stylus goes up and to the right, -pi/2 would indicate that the stylus
1334  // goes to the left, etc).
1335  //
1336  // Discussion:
1337  // Sweep direction is the same. Phase of M_PI_2.
1338  pointer_data.orientation = [touch azimuthAngleInView:nil] - M_PI_2;
1339 
1340  if (@available(iOS 13.4, *)) {
1341  if (event != nullptr) {
1342  pointer_data.buttons = (((event.buttonMask & UIEventButtonMaskPrimary) > 0)
1343  ? flutter::PointerButtonMouse::kPointerButtonMousePrimary
1344  : 0) |
1345  (((event.buttonMask & UIEventButtonMaskSecondary) > 0)
1346  ? flutter::PointerButtonMouse::kPointerButtonMouseSecondary
1347  : 0);
1348  }
1349  }
1350 
1351  packet->SetPointerData(pointer_index++, pointer_data);
1352 
1353  if (touch.phase == UITouchPhaseEnded || touch.phase == UITouchPhaseCancelled) {
1354  flutter::PointerData remove_pointer_data = pointer_data;
1355  remove_pointer_data.change = flutter::PointerData::Change::kRemove;
1356  packet->SetPointerData(pointer_index++, remove_pointer_data);
1357  }
1358  }
1359 
1360  [self.engine dispatchPointerDataPacket:std::move(packet)];
1361 }
1362 
1363 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
1364  [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1365 }
1366 
1367 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
1368  [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1369 }
1370 
1371 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
1372  [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1373 }
1374 
1375 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
1376  [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1377 }
1378 
1379 - (void)forceTouchesCancelled:(NSSet*)touches {
1380  flutter::PointerData::Change cancel = flutter::PointerData::Change::kCancel;
1381  [self dispatchTouches:touches pointerDataChangeOverride:&cancel event:nullptr];
1382 }
1383 
1384 #pragma mark - Touch events rate correction
1385 
1386 - (void)createTouchRateCorrectionVSyncClientIfNeeded {
1387  if (_touchRateCorrectionVSyncClient != nil) {
1388  return;
1389  }
1390 
1391  double displayRefreshRate = DisplayLinkManager.displayRefreshRate;
1392  const double epsilon = 0.1;
1393  if (displayRefreshRate < 60.0 + epsilon) { // displayRefreshRate <= 60.0
1394 
1395  // If current device's max frame rate is not larger than 60HZ, the delivery rate of touch events
1396  // is the same with render vsync rate. So it is unnecessary to create
1397  // _touchRateCorrectionVSyncClient to correct touch callback's rate.
1398  return;
1399  }
1400 
1401  auto callback = [](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {
1402  // Do nothing in this block. Just trigger system to callback touch events with correct rate.
1403  };
1404  _touchRateCorrectionVSyncClient =
1405  [[VSyncClient alloc] initWithTaskRunner:self.engine.platformTaskRunner callback:callback];
1406  _touchRateCorrectionVSyncClient.allowPauseAfterVsync = NO;
1407 }
1408 
1409 - (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches {
1410  if (_touchRateCorrectionVSyncClient == nil) {
1411  // If the _touchRateCorrectionVSyncClient is not created, means current devices doesn't
1412  // need to correct the touch rate. So just return.
1413  return;
1414  }
1415 
1416  // As long as there is a touch's phase is UITouchPhaseBegan or UITouchPhaseMoved,
1417  // activate the correction. Otherwise pause the correction.
1418  BOOL isUserInteracting = NO;
1419  for (UITouch* touch in touches) {
1420  if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved) {
1421  isUserInteracting = YES;
1422  break;
1423  }
1424  }
1425 
1426  if (isUserInteracting && self.engine.viewController == self) {
1427  [_touchRateCorrectionVSyncClient await];
1428  } else {
1429  [_touchRateCorrectionVSyncClient pause];
1430  }
1431 }
1432 
1433 - (void)invalidateTouchRateCorrectionVSyncClient {
1434  [_touchRateCorrectionVSyncClient invalidate];
1435  _touchRateCorrectionVSyncClient = nil;
1436 }
1437 
1438 #pragma mark - Handle view resizing
1439 
1440 - (void)updateViewportMetricsIfNeeded {
1441  if (_shouldIgnoreViewportMetricsUpdatesDuringRotation) {
1442  return;
1443  }
1444  if (self.engine.viewController == self) {
1445  [self.engine updateViewportMetrics:_viewportMetrics];
1446  }
1447 }
1448 
1449 - (void)viewDidLayoutSubviews {
1450  CGRect viewBounds = self.view.bounds;
1451  CGFloat scale = self.flutterScreenIfViewLoaded.scale;
1452 
1453  // Purposefully place this not visible.
1454  self.scrollView.frame = CGRectMake(0.0, 0.0, viewBounds.size.width, 0.0);
1455  self.scrollView.contentOffset = CGPointMake(kScrollViewContentSize, kScrollViewContentSize);
1456 
1457  // First time since creation that the dimensions of its view is known.
1458  bool firstViewBoundsUpdate = !_viewportMetrics.physical_width;
1459  _viewportMetrics.device_pixel_ratio = scale;
1460  [self setViewportMetricsSize];
1461  [self setViewportMetricsPaddings];
1462  [self updateViewportMetricsIfNeeded];
1463 
1464  // There is no guarantee that UIKit will layout subviews when the application/scene is active.
1465  // Creating the surface when inactive will cause GPU accesses from the background. Only wait for
1466  // the first frame to render when the application/scene is actually active.
1467  // This must run after updateViewportMetrics so that the surface creation tasks are queued after
1468  // the viewport metrics update tasks.
1469  if (firstViewBoundsUpdate && self.stateIsActive && self.engine) {
1470  [self surfaceUpdated:YES];
1471 #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG
1472  NSTimeInterval timeout = 0.2;
1473 #else
1474  NSTimeInterval timeout = 0.1;
1475 #endif
1476  [self.engine
1477  waitForFirstFrameSync:timeout
1478  callback:^(BOOL didTimeout) {
1479  if (didTimeout) {
1480  [FlutterLogger logInfo:@"Timeout waiting for the first frame to render. "
1481  "This may happen in unoptimized builds. If this is"
1482  "a release build, you should load a less complex "
1483  "frame to avoid the timeout."];
1484  }
1485  }];
1486  }
1487 }
1488 
1489 - (void)viewSafeAreaInsetsDidChange {
1490  [self setViewportMetricsPaddings];
1491  [self updateViewportMetricsIfNeeded];
1492  [super viewSafeAreaInsetsDidChange];
1493 }
1494 
1495 // Set _viewportMetrics physical size.
1496 - (void)setViewportMetricsSize {
1497  UIScreen* screen = self.flutterScreenIfViewLoaded;
1498  if (!screen) {
1499  return;
1500  }
1501 
1502  CGFloat scale = screen.scale;
1503  _viewportMetrics.physical_width = self.view.bounds.size.width * scale;
1504  _viewportMetrics.physical_height = self.view.bounds.size.height * scale;
1505  // TODO(louisehsu): update for https://github.com/flutter/flutter/issues/169147
1506  _viewportMetrics.physical_min_width_constraint = _viewportMetrics.physical_width;
1507  _viewportMetrics.physical_max_width_constraint = _viewportMetrics.physical_width;
1508  _viewportMetrics.physical_min_height_constraint = _viewportMetrics.physical_height;
1509  _viewportMetrics.physical_max_height_constraint = _viewportMetrics.physical_height;
1510 }
1511 
1512 // Set _viewportMetrics physical paddings.
1513 //
1514 // Viewport paddings represent the iOS safe area insets.
1515 - (void)setViewportMetricsPaddings {
1516  UIScreen* screen = self.flutterScreenIfViewLoaded;
1517  if (!screen) {
1518  return;
1519  }
1520 
1521  CGFloat scale = screen.scale;
1522  _viewportMetrics.physical_padding_top = self.view.safeAreaInsets.top * scale;
1523  _viewportMetrics.physical_padding_left = self.view.safeAreaInsets.left * scale;
1524  _viewportMetrics.physical_padding_right = self.view.safeAreaInsets.right * scale;
1525  _viewportMetrics.physical_padding_bottom = self.view.safeAreaInsets.bottom * scale;
1526 }
1527 
1528 #pragma mark - Keyboard events
1529 
1530 - (void)keyboardWillShowNotification:(NSNotification*)notification {
1531  // Immediately prior to a docked keyboard being shown or when a keyboard goes from
1532  // undocked/floating to docked, this notification is triggered. This notification also happens
1533  // when Minimized/Expanded Shortcuts bar is dropped after dragging (the keyboard's end frame will
1534  // be CGRectZero).
1535  [self handleKeyboardNotification:notification];
1536 }
1537 
1538 - (void)keyboardWillChangeFrame:(NSNotification*)notification {
1539  // Immediately prior to a change in keyboard frame, this notification is triggered.
1540  // Sometimes when the keyboard is being hidden or undocked, this notification's keyboard's end
1541  // frame is not yet entirely out of screen, which is why we also use
1542  // UIKeyboardWillHideNotification.
1543  [self handleKeyboardNotification:notification];
1544 }
1545 
1546 - (void)keyboardWillBeHidden:(NSNotification*)notification {
1547  // When keyboard is hidden or undocked, this notification will be triggered.
1548  // This notification might not occur when the keyboard is changed from docked to floating, which
1549  // is why we also use UIKeyboardWillChangeFrameNotification.
1550  [self handleKeyboardNotification:notification];
1551 }
1552 
1553 - (void)handleKeyboardNotification:(NSNotification*)notification {
1554  // See https://flutter.cn/go/ios-keyboard-calculating-inset for more details
1555  // on why notifications are used and how things are calculated.
1556  if ([self shouldIgnoreKeyboardNotification:notification]) {
1557  return;
1558  }
1559 
1560  NSDictionary* info = notification.userInfo;
1561  CGRect beginKeyboardFrame = [info[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
1562  CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
1563  FlutterKeyboardMode keyboardMode = [self calculateKeyboardAttachMode:notification];
1564  CGFloat calculatedInset = [self calculateKeyboardInset:keyboardFrame keyboardMode:keyboardMode];
1565  NSTimeInterval duration = [info[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
1566 
1567  // If the software keyboard is displayed before displaying the PasswordManager prompt,
1568  // UIKeyboardWillHideNotification will occur immediately after UIKeyboardWillShowNotification.
1569  // The duration of the animation will be 0.0, and the calculated inset will be 0.0.
1570  // In this case, it is necessary to cancel the animation and hide the keyboard immediately.
1571  // https://github.com/flutter/flutter/pull/164884
1572  if (keyboardMode == FlutterKeyboardModeHidden && calculatedInset == 0.0 && duration == 0.0) {
1573  [self hideKeyboardImmediately];
1574  return;
1575  }
1576 
1577  // Avoid double triggering startKeyBoardAnimation.
1578  if (self.targetViewInsetBottom == calculatedInset) {
1579  return;
1580  }
1581 
1582  self.targetViewInsetBottom = calculatedInset;
1583 
1584  // Flag for simultaneous compounding animation calls.
1585  // This captures animation calls made while the keyboard animation is currently animating. If the
1586  // new animation is in the same direction as the current animation, this flag lets the current
1587  // animation continue with an updated targetViewInsetBottom instead of starting a new keyboard
1588  // animation. This allows for smoother keyboard animation interpolation.
1589  BOOL keyboardWillShow = beginKeyboardFrame.origin.y > keyboardFrame.origin.y;
1590  BOOL keyboardAnimationIsCompounding =
1591  self.keyboardAnimationIsShowing == keyboardWillShow && _keyboardAnimationVSyncClient != nil;
1592 
1593  // Mark keyboard as showing or hiding.
1594  self.keyboardAnimationIsShowing = keyboardWillShow;
1595 
1596  if (!keyboardAnimationIsCompounding) {
1597  [self startKeyBoardAnimation:duration];
1598  } else if (self.keyboardSpringAnimation) {
1599  self.keyboardSpringAnimation.toValue = self.targetViewInsetBottom;
1600  }
1601 }
1602 
1603 - (BOOL)shouldIgnoreKeyboardNotification:(NSNotification*)notification {
1604  // Don't ignore UIKeyboardWillHideNotification notifications.
1605  // Even if the notification is triggered in the background or by a different app/view controller,
1606  // we want to always handle this notification to avoid inaccurate inset when in a mulitasking mode
1607  // or when switching between apps.
1608  if (notification.name == UIKeyboardWillHideNotification) {
1609  return NO;
1610  }
1611 
1612  // Ignore notification when keyboard's dimensions and position are all zeroes for
1613  // UIKeyboardWillChangeFrameNotification. This happens when keyboard is dragged. Do not ignore if
1614  // the notification is UIKeyboardWillShowNotification, as CGRectZero for that notfication only
1615  // occurs when Minimized/Expanded Shortcuts Bar is dropped after dragging, which we later use to
1616  // categorize it as floating.
1617  NSDictionary* info = notification.userInfo;
1618  CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
1619  if (notification.name == UIKeyboardWillChangeFrameNotification &&
1620  CGRectEqualToRect(keyboardFrame, CGRectZero)) {
1621  return YES;
1622  }
1623 
1624  // When keyboard's height or width is set to 0, don't ignore. This does not happen
1625  // often but can happen sometimes when switching between multitasking modes.
1626  if (CGRectIsEmpty(keyboardFrame)) {
1627  return NO;
1628  }
1629 
1630  // Ignore keyboard notifications related to other apps or view controllers.
1631  if ([self isKeyboardNotificationForDifferentView:notification]) {
1632  return YES;
1633  }
1634  return NO;
1635 }
1636 
1637 - (BOOL)isKeyboardNotificationForDifferentView:(NSNotification*)notification {
1638  NSDictionary* info = notification.userInfo;
1639  // Keyboard notifications related to other apps.
1640  // If the UIKeyboardIsLocalUserInfoKey key doesn't exist (this should not happen after iOS 8),
1641  // proceed as if it was local so that the notification is not ignored.
1642  id isLocal = info[UIKeyboardIsLocalUserInfoKey];
1643  if (isLocal && ![isLocal boolValue]) {
1644  return YES;
1645  }
1646  return self.engine.viewController != self;
1647 }
1648 
1649 - (FlutterKeyboardMode)calculateKeyboardAttachMode:(NSNotification*)notification {
1650  // There are multiple types of keyboard: docked, undocked, split, split docked,
1651  // floating, expanded shortcuts bar, minimized shortcuts bar. This function will categorize
1652  // the keyboard as one of the following modes: docked, floating, or hidden.
1653  // Docked mode includes docked, split docked, expanded shortcuts bar (when opening via click),
1654  // and minimized shortcuts bar (when opened via click).
1655  // Floating includes undocked, split, floating, expanded shortcuts bar (when dragged and dropped),
1656  // and minimized shortcuts bar (when dragged and dropped).
1657  NSDictionary* info = notification.userInfo;
1658  CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
1659 
1660  if (notification.name == UIKeyboardWillHideNotification) {
1661  return FlutterKeyboardModeHidden;
1662  }
1663 
1664  // If keyboard's dimensions and position are all zeroes, that means it's a Minimized/Expanded
1665  // Shortcuts Bar that has been dropped after dragging, which we categorize as floating.
1666  if (CGRectEqualToRect(keyboardFrame, CGRectZero)) {
1667  return FlutterKeyboardModeFloating;
1668  }
1669  // If keyboard's width or height are 0, it's hidden.
1670  if (CGRectIsEmpty(keyboardFrame)) {
1671  return FlutterKeyboardModeHidden;
1672  }
1673 
1674  CGRect screenRect = self.flutterScreenIfViewLoaded.bounds;
1675  CGRect adjustedKeyboardFrame = keyboardFrame;
1676  adjustedKeyboardFrame.origin.y += [self calculateMultitaskingAdjustment:screenRect
1677  keyboardFrame:keyboardFrame];
1678 
1679  // If the keyboard is partially or fully showing within the screen, it's either docked or
1680  // floating. Sometimes with custom keyboard extensions, the keyboard's position may be off by a
1681  // small decimal amount (which is why CGRectIntersectRect can't be used). Round to compare.
1682  CGRect intersection = CGRectIntersection(adjustedKeyboardFrame, screenRect);
1683  CGFloat intersectionHeight = CGRectGetHeight(intersection);
1684  CGFloat intersectionWidth = CGRectGetWidth(intersection);
1685  if (round(intersectionHeight) > 0 && intersectionWidth > 0) {
1686  // If the keyboard is above the bottom of the screen, it's floating.
1687  CGFloat screenHeight = CGRectGetHeight(screenRect);
1688  CGFloat adjustedKeyboardBottom = CGRectGetMaxY(adjustedKeyboardFrame);
1689  if (round(adjustedKeyboardBottom) < screenHeight) {
1690  return FlutterKeyboardModeFloating;
1691  }
1692  return FlutterKeyboardModeDocked;
1693  }
1694  return FlutterKeyboardModeHidden;
1695 }
1696 
1697 - (CGFloat)calculateMultitaskingAdjustment:(CGRect)screenRect keyboardFrame:(CGRect)keyboardFrame {
1698  // In Slide Over mode, the keyboard's frame does not include the space
1699  // below the app, even though the keyboard may be at the bottom of the screen.
1700  // To handle, shift the Y origin by the amount of space below the app.
1701  if (self.viewIfLoaded.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiomPad &&
1702  self.viewIfLoaded.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact &&
1703  self.viewIfLoaded.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassRegular) {
1704  CGFloat screenHeight = CGRectGetHeight(screenRect);
1705  CGFloat keyboardBottom = CGRectGetMaxY(keyboardFrame);
1706 
1707  // Stage Manager mode will also meet the above parameters, but it does not handle
1708  // the keyboard positioning the same way, so skip if keyboard is at bottom of page.
1709  if (screenHeight == keyboardBottom) {
1710  return 0;
1711  }
1712  CGRect viewRectRelativeToScreen =
1713  [self.viewIfLoaded convertRect:self.viewIfLoaded.frame
1714  toCoordinateSpace:self.flutterScreenIfViewLoaded.coordinateSpace];
1715  CGFloat viewBottom = CGRectGetMaxY(viewRectRelativeToScreen);
1716  CGFloat offset = screenHeight - viewBottom;
1717  if (offset > 0) {
1718  return offset;
1719  }
1720  }
1721  return 0;
1722 }
1723 
1724 - (CGFloat)calculateKeyboardInset:(CGRect)keyboardFrame keyboardMode:(NSInteger)keyboardMode {
1725  // Only docked keyboards will have an inset.
1726  if (keyboardMode == FlutterKeyboardModeDocked) {
1727  // Calculate how much of the keyboard intersects with the view.
1728  CGRect viewRectRelativeToScreen =
1729  [self.viewIfLoaded convertRect:self.viewIfLoaded.frame
1730  toCoordinateSpace:self.flutterScreenIfViewLoaded.coordinateSpace];
1731  CGRect intersection = CGRectIntersection(keyboardFrame, viewRectRelativeToScreen);
1732  CGFloat portionOfKeyboardInView = CGRectGetHeight(intersection);
1733 
1734  // The keyboard is treated as an inset since we want to effectively reduce the window size by
1735  // the keyboard height. The Dart side will compute a value accounting for the keyboard-consuming
1736  // bottom padding.
1737  CGFloat scale = self.flutterScreenIfViewLoaded.scale;
1738  return portionOfKeyboardInView * scale;
1739  }
1740  return 0;
1741 }
1742 
1743 - (void)startKeyBoardAnimation:(NSTimeInterval)duration {
1744  // If current physical_view_inset_bottom == targetViewInsetBottom, do nothing.
1745  if (_viewportMetrics.physical_view_inset_bottom == self.targetViewInsetBottom) {
1746  return;
1747  }
1748 
1749  // When this method is called for the first time,
1750  // initialize the keyboardAnimationView to get animation interpolation during animation.
1751  if (!self.keyboardAnimationView) {
1752  UIView* keyboardAnimationView = [[UIView alloc] init];
1753  keyboardAnimationView.hidden = YES;
1754  self.keyboardAnimationView = keyboardAnimationView;
1755  }
1756 
1757  if (!self.keyboardAnimationView.superview) {
1758  [self.view addSubview:self.keyboardAnimationView];
1759  }
1760 
1761  // Remove running animation when start another animation.
1762  [self.keyboardAnimationView.layer removeAllAnimations];
1763 
1764  // Set animation begin value and DisplayLink tracking values.
1765  self.keyboardAnimationView.frame =
1766  CGRectMake(0, _viewportMetrics.physical_view_inset_bottom, 0, 0);
1767  self.keyboardAnimationStartTime = fml::TimePoint().Now();
1768  self.originalViewInsetBottom = _viewportMetrics.physical_view_inset_bottom;
1769 
1770  // Invalidate old vsync client if old animation is not completed.
1771  [self invalidateKeyboardAnimationVSyncClient];
1772 
1773  __weak FlutterViewController* weakSelf = self;
1774  [self setUpKeyboardAnimationVsyncClient:^(fml::TimePoint targetTime) {
1775  [weakSelf handleKeyboardAnimationCallbackWithTargetTime:targetTime];
1776  }];
1777  VSyncClient* currentVsyncClient = _keyboardAnimationVSyncClient;
1778 
1779  [UIView animateWithDuration:duration
1780  animations:^{
1781  FlutterViewController* strongSelf = weakSelf;
1782  if (!strongSelf) {
1783  return;
1784  }
1785 
1786  // Set end value.
1787  strongSelf.keyboardAnimationView.frame = CGRectMake(0, self.targetViewInsetBottom, 0, 0);
1788 
1789  // Setup keyboard animation interpolation.
1790  CAAnimation* keyboardAnimation =
1791  [strongSelf.keyboardAnimationView.layer animationForKey:@"position"];
1792  [strongSelf setUpKeyboardSpringAnimationIfNeeded:keyboardAnimation];
1793  }
1794  completion:^(BOOL finished) {
1795  if (_keyboardAnimationVSyncClient == currentVsyncClient) {
1796  FlutterViewController* strongSelf = weakSelf;
1797  if (!strongSelf) {
1798  return;
1799  }
1800 
1801  // Indicates the vsync client captured by this block is the original one, which also
1802  // indicates the animation has not been interrupted from its beginning. Moreover,
1803  // indicates the animation is over and there is no more to execute.
1804  [strongSelf invalidateKeyboardAnimationVSyncClient];
1805  [strongSelf removeKeyboardAnimationView];
1806  [strongSelf ensureViewportMetricsIsCorrect];
1807  }
1808  }];
1809 }
1810 
1811 - (void)hideKeyboardImmediately {
1812  [self invalidateKeyboardAnimationVSyncClient];
1813  if (self.keyboardAnimationView) {
1814  [self.keyboardAnimationView.layer removeAllAnimations];
1815  [self removeKeyboardAnimationView];
1816  self.keyboardAnimationView = nil;
1817  }
1818  if (self.keyboardSpringAnimation) {
1819  self.keyboardSpringAnimation = nil;
1820  }
1821  // Reset targetViewInsetBottom to 0.0.
1822  self.targetViewInsetBottom = 0.0;
1823  [self ensureViewportMetricsIsCorrect];
1824 }
1825 
1826 - (void)setUpKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation {
1827  // If keyboard animation is null or not a spring animation, fallback to DisplayLink tracking.
1828  if (keyboardAnimation == nil || ![keyboardAnimation isKindOfClass:[CASpringAnimation class]]) {
1829  _keyboardSpringAnimation = nil;
1830  return;
1831  }
1832 
1833  // Setup keyboard spring animation details for spring curve animation calculation.
1834  CASpringAnimation* keyboardCASpringAnimation = (CASpringAnimation*)keyboardAnimation;
1835  _keyboardSpringAnimation =
1836  [[SpringAnimation alloc] initWithStiffness:keyboardCASpringAnimation.stiffness
1837  damping:keyboardCASpringAnimation.damping
1838  mass:keyboardCASpringAnimation.mass
1839  initialVelocity:keyboardCASpringAnimation.initialVelocity
1840  fromValue:self.originalViewInsetBottom
1841  toValue:self.targetViewInsetBottom];
1842 }
1843 
1844 - (void)handleKeyboardAnimationCallbackWithTargetTime:(fml::TimePoint)targetTime {
1845  // If the view controller's view is not loaded, bail out.
1846  if (!self.isViewLoaded) {
1847  return;
1848  }
1849  // If the view for tracking keyboard animation is nil, means it is not
1850  // created, bail out.
1851  if (!self.keyboardAnimationView) {
1852  return;
1853  }
1854  // If keyboardAnimationVSyncClient is nil, means the animation ends.
1855  // And should bail out.
1856  if (!self.keyboardAnimationVSyncClient) {
1857  return;
1858  }
1859 
1860  if (!self.keyboardAnimationView.superview) {
1861  // Ensure the keyboardAnimationView is in view hierarchy when animation running.
1862  [self.view addSubview:self.keyboardAnimationView];
1863  }
1864 
1865  if (!self.keyboardSpringAnimation) {
1866  if (self.keyboardAnimationView.layer.presentationLayer) {
1867  self->_viewportMetrics.physical_view_inset_bottom =
1868  self.keyboardAnimationView.layer.presentationLayer.frame.origin.y;
1869  [self updateViewportMetricsIfNeeded];
1870  }
1871  } else {
1872  fml::TimeDelta timeElapsed = targetTime - self.keyboardAnimationStartTime;
1873  self->_viewportMetrics.physical_view_inset_bottom =
1874  [self.keyboardSpringAnimation curveFunction:timeElapsed.ToSecondsF()];
1875  [self updateViewportMetricsIfNeeded];
1876  }
1877 }
1878 
1879 - (void)setUpKeyboardAnimationVsyncClient:
1880  (FlutterKeyboardAnimationCallback)keyboardAnimationCallback {
1881  if (!keyboardAnimationCallback) {
1882  return;
1883  }
1884  NSAssert(_keyboardAnimationVSyncClient == nil,
1885  @"_keyboardAnimationVSyncClient must be nil when setting up.");
1886 
1887  // Make sure the new viewport metrics get sent after the begin frame event has processed.
1888  FlutterKeyboardAnimationCallback animationCallback = [keyboardAnimationCallback copy];
1889  auto uiCallback = [animationCallback](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {
1890  fml::TimeDelta frameInterval = recorder->GetVsyncTargetTime() - recorder->GetVsyncStartTime();
1891  fml::TimePoint targetTime = recorder->GetVsyncTargetTime() + frameInterval;
1892  dispatch_async(dispatch_get_main_queue(), ^(void) {
1893  animationCallback(targetTime);
1894  });
1895  };
1896 
1897  _keyboardAnimationVSyncClient = [[VSyncClient alloc] initWithTaskRunner:self.engine.uiTaskRunner
1898  callback:uiCallback];
1899  _keyboardAnimationVSyncClient.allowPauseAfterVsync = NO;
1900  [_keyboardAnimationVSyncClient await];
1901 }
1902 
1903 - (void)invalidateKeyboardAnimationVSyncClient {
1904  [_keyboardAnimationVSyncClient invalidate];
1905  _keyboardAnimationVSyncClient = nil;
1906 }
1907 
1908 - (void)removeKeyboardAnimationView {
1909  if (self.keyboardAnimationView.superview != nil) {
1910  [self.keyboardAnimationView removeFromSuperview];
1911  }
1912 }
1913 
1914 - (void)ensureViewportMetricsIsCorrect {
1915  if (_viewportMetrics.physical_view_inset_bottom != self.targetViewInsetBottom) {
1916  // Make sure the `physical_view_inset_bottom` is the target value.
1917  _viewportMetrics.physical_view_inset_bottom = self.targetViewInsetBottom;
1918  [self updateViewportMetricsIfNeeded];
1919  }
1920 }
1921 
1922 - (void)handlePressEvent:(FlutterUIPressProxy*)press
1923  nextAction:(void (^)())next API_AVAILABLE(ios(13.4)) {
1924  if (@available(iOS 13.4, *)) {
1925  } else {
1926  next();
1927  return;
1928  }
1929  [self.keyboardManager handlePress:press nextAction:next];
1930 }
1931 
1932 // The documentation for presses* handlers (implemented below) is entirely
1933 // unclear about how to handle the case where some, but not all, of the presses
1934 // are handled here. I've elected to call super separately for each of the
1935 // presses that aren't handled, but it's not clear if this is correct. It may be
1936 // that iOS intends for us to either handle all or none of the presses, and pass
1937 // the original set to super. I have not yet seen multiple presses in the set in
1938 // the wild, however, so I suspect that the API is built for a tvOS remote or
1939 // something, and perhaps only one ever appears in the set on iOS from a
1940 // keyboard.
1941 //
1942 // We define separate superPresses* overrides to avoid implicitly capturing self in the blocks
1943 // passed to the presses* methods below.
1944 
1945 - (void)superPressesBegan:(NSSet<UIPress*>*)presses withEvent:(UIPressesEvent*)event {
1946  [super pressesBegan:presses withEvent:event];
1947 }
1948 
1949 - (void)superPressesChanged:(NSSet<UIPress*>*)presses withEvent:(UIPressesEvent*)event {
1950  [super pressesChanged:presses withEvent:event];
1951 }
1952 
1953 - (void)superPressesEnded:(NSSet<UIPress*>*)presses withEvent:(UIPressesEvent*)event {
1954  [super pressesEnded:presses withEvent:event];
1955 }
1956 
1957 - (void)superPressesCancelled:(NSSet<UIPress*>*)presses withEvent:(UIPressesEvent*)event {
1958  [super pressesCancelled:presses withEvent:event];
1959 }
1960 
1961 // If you substantially change these presses overrides, consider also changing
1962 // the similar ones in FlutterTextInputPlugin. They need to be overridden in
1963 // both places to capture keys both inside and outside of a text field, but have
1964 // slightly different implementations.
1965 
1966 - (void)pressesBegan:(NSSet<UIPress*>*)presses
1967  withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
1968  if (@available(iOS 13.4, *)) {
1969  __weak FlutterViewController* weakSelf = self;
1970  for (UIPress* press in presses) {
1971  [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press event:event]
1972  nextAction:^() {
1973  [weakSelf superPressesBegan:[NSSet setWithObject:press] withEvent:event];
1974  }];
1975  }
1976  } else {
1977  [super pressesBegan:presses withEvent:event];
1978  }
1979 }
1980 
1981 - (void)pressesChanged:(NSSet<UIPress*>*)presses
1982  withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
1983  if (@available(iOS 13.4, *)) {
1984  __weak FlutterViewController* weakSelf = self;
1985  for (UIPress* press in presses) {
1986  [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press event:event]
1987  nextAction:^() {
1988  [weakSelf superPressesChanged:[NSSet setWithObject:press] withEvent:event];
1989  }];
1990  }
1991  } else {
1992  [super pressesChanged:presses withEvent:event];
1993  }
1994 }
1995 
1996 - (void)pressesEnded:(NSSet<UIPress*>*)presses
1997  withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
1998  if (@available(iOS 13.4, *)) {
1999  __weak FlutterViewController* weakSelf = self;
2000  for (UIPress* press in presses) {
2001  [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press event:event]
2002  nextAction:^() {
2003  [weakSelf superPressesEnded:[NSSet setWithObject:press] withEvent:event];
2004  }];
2005  }
2006  } else {
2007  [super pressesEnded:presses withEvent:event];
2008  }
2009 }
2010 
2011 - (void)pressesCancelled:(NSSet<UIPress*>*)presses
2012  withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2013  if (@available(iOS 13.4, *)) {
2014  __weak FlutterViewController* weakSelf = self;
2015  for (UIPress* press in presses) {
2016  [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press event:event]
2017  nextAction:^() {
2018  [weakSelf superPressesCancelled:[NSSet setWithObject:press] withEvent:event];
2019  }];
2020  }
2021  } else {
2022  [super pressesCancelled:presses withEvent:event];
2023  }
2024 }
2025 
2026 #pragma mark - Orientation updates
2027 
2028 - (void)onOrientationPreferencesUpdated:(NSNotification*)notification {
2029  // Notifications may not be on the iOS UI thread
2030  __weak FlutterViewController* weakSelf = self;
2031  dispatch_async(dispatch_get_main_queue(), ^{
2032  NSDictionary* info = notification.userInfo;
2033  NSNumber* update = info[@(flutter::kOrientationUpdateNotificationKey)];
2034  if (update == nil) {
2035  return;
2036  }
2037  [weakSelf performOrientationUpdate:update.unsignedIntegerValue];
2038  });
2039 }
2040 
2041 - (void)requestGeometryUpdateForWindowScenes:(NSSet<UIScene*>*)windowScenes
2042  API_AVAILABLE(ios(16.0)) {
2043  for (UIScene* windowScene in windowScenes) {
2044  FML_DCHECK([windowScene isKindOfClass:[UIWindowScene class]]);
2045  UIWindowSceneGeometryPreferencesIOS* preference = [[UIWindowSceneGeometryPreferencesIOS alloc]
2046  initWithInterfaceOrientations:self.orientationPreferences];
2047  [(UIWindowScene*)windowScene
2048  requestGeometryUpdateWithPreferences:preference
2049  errorHandler:^(NSError* error) {
2050  os_log_error(OS_LOG_DEFAULT,
2051  "Failed to change device orientation: %@", error);
2052  }];
2053  [self setNeedsUpdateOfSupportedInterfaceOrientations];
2054  }
2055 }
2056 
2057 - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences {
2058  if (new_preferences != self.orientationPreferences) {
2059  self.orientationPreferences = new_preferences;
2060 
2061  if (@available(iOS 16.0, *)) {
2062  UIApplication* flutterApplication = FlutterSharedApplication.application;
2063  NSSet<UIScene*>* scenes = [NSSet set];
2064  if (flutterApplication) {
2065  scenes = [flutterApplication.connectedScenes
2066  filteredSetUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
2067  id scene, NSDictionary* bindings) {
2068  return [scene isKindOfClass:[UIWindowScene class]];
2069  }]];
2070  } else if (self.flutterWindowSceneIfViewLoaded) {
2071  scenes = [NSSet setWithObject:self.flutterWindowSceneIfViewLoaded];
2072  }
2073  [self requestGeometryUpdateForWindowScenes:scenes];
2074  } else {
2075  UIInterfaceOrientationMask currentInterfaceOrientation = 0;
2076  UIWindowScene* windowScene = self.flutterWindowSceneIfViewLoaded;
2077  if (!windowScene) {
2078  [FlutterLogger
2079  logWarning:
2080  @"Accessing the interface orientation when the window scene is unavailable."];
2081  return;
2082  }
2083  currentInterfaceOrientation = 1 << windowScene.interfaceOrientation;
2084  if (!(self.orientationPreferences & currentInterfaceOrientation)) {
2085  [UIViewController attemptRotationToDeviceOrientation];
2086  // Force orientation switch if the current orientation is not allowed
2087  if (self.orientationPreferences & UIInterfaceOrientationMaskPortrait) {
2088  // This is no official API but more like a workaround / hack (using
2089  // key-value coding on a read-only property). This might break in
2090  // the future, but currently it´s the only way to force an orientation change
2091  [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortrait)
2092  forKey:@"orientation"];
2093  } else if (self.orientationPreferences & UIInterfaceOrientationMaskPortraitUpsideDown) {
2094  [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortraitUpsideDown)
2095  forKey:@"orientation"];
2096  } else if (self.orientationPreferences & UIInterfaceOrientationMaskLandscapeLeft) {
2097  [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationLandscapeLeft)
2098  forKey:@"orientation"];
2099  } else if (self.orientationPreferences & UIInterfaceOrientationMaskLandscapeRight) {
2100  [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationLandscapeRight)
2101  forKey:@"orientation"];
2102  }
2103  }
2104  }
2105  }
2106 }
2107 
2108 - (void)onHideHomeIndicatorNotification:(NSNotification*)notification {
2109  self.isHomeIndicatorHidden = YES;
2110 }
2111 
2112 - (void)onShowHomeIndicatorNotification:(NSNotification*)notification {
2113  self.isHomeIndicatorHidden = NO;
2114 }
2115 
2116 - (void)setIsHomeIndicatorHidden:(BOOL)hideHomeIndicator {
2117  if (hideHomeIndicator != _isHomeIndicatorHidden) {
2118  _isHomeIndicatorHidden = hideHomeIndicator;
2119  [self setNeedsUpdateOfHomeIndicatorAutoHidden];
2120  }
2121 }
2122 
2123 - (BOOL)prefersHomeIndicatorAutoHidden {
2124  return self.isHomeIndicatorHidden;
2125 }
2126 
2127 - (BOOL)shouldAutorotate {
2128  return YES;
2129 }
2130 
2131 - (NSUInteger)supportedInterfaceOrientations {
2132  return self.orientationPreferences;
2133 }
2134 
2135 #pragma mark - Accessibility
2136 
2137 - (void)onAccessibilityStatusChanged:(NSNotification*)notification {
2138  if (!self.engine) {
2139  return;
2140  }
2141  BOOL enabled = NO;
2142  int32_t flags = self.accessibilityFlags;
2143 #if TARGET_OS_SIMULATOR
2144  // There doesn't appear to be any way to determine whether the accessibility
2145  // inspector is enabled on the simulator. We conservatively always turn on the
2146  // accessibility bridge in the simulator, but never assistive technology.
2147  enabled = YES;
2148 #else
2149  _isVoiceOverRunning = UIAccessibilityIsVoiceOverRunning();
2150  enabled = _isVoiceOverRunning || UIAccessibilityIsSwitchControlRunning();
2151  if (enabled) {
2152  flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kAccessibleNavigation);
2153  }
2154  enabled |= UIAccessibilityIsSpeakScreenEnabled();
2155 #endif
2156  [self.engine enableSemantics:enabled withFlags:flags];
2157 }
2158 
2159 - (int32_t)accessibilityFlags {
2160  int32_t flags = 0;
2161  if (UIAccessibilityIsInvertColorsEnabled()) {
2162  flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kInvertColors);
2163  }
2164  if (UIAccessibilityIsReduceMotionEnabled()) {
2165  flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kReduceMotion);
2166  }
2167  if (UIAccessibilityIsBoldTextEnabled()) {
2168  flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kBoldText);
2169  }
2170  if (UIAccessibilityDarkerSystemColorsEnabled()) {
2171  flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kHighContrast);
2172  }
2173  if ([FlutterViewController accessibilityIsOnOffSwitchLabelsEnabled]) {
2174  flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kOnOffSwitchLabels);
2175  }
2176 
2177  return flags;
2178 }
2179 
2180 - (BOOL)accessibilityPerformEscape {
2181  FlutterMethodChannel* navigationChannel = self.engine.navigationChannel;
2182  if (navigationChannel) {
2183  [self popRoute];
2184  return YES;
2185  }
2186  return NO;
2187 }
2188 
2189 + (BOOL)accessibilityIsOnOffSwitchLabelsEnabled {
2190  return UIAccessibilityIsOnOffSwitchLabelsEnabled();
2191 }
2192 
2193 #pragma mark - Set user settings
2194 
2195 - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
2196  [super traitCollectionDidChange:previousTraitCollection];
2197  [self onUserSettingsChanged:nil];
2198 }
2199 
2200 - (void)onUserSettingsChanged:(NSNotification*)notification {
2201  [self.engine.settingsChannel sendMessage:@{
2202  @"textScaleFactor" : @(self.textScaleFactor),
2203  @"alwaysUse24HourFormat" : @(FlutterHourFormat.isAlwaysUse24HourFormat),
2204  @"platformBrightness" : self.brightnessMode,
2205  @"platformContrast" : self.contrastMode,
2206  @"nativeSpellCheckServiceDefined" : @YES,
2207  @"supportsShowingSystemContextMenu" : @(self.supportsShowingSystemContextMenu)
2208  }];
2209 }
2210 
2211 - (CGFloat)textScaleFactor {
2212  UIApplication* flutterApplication = FlutterSharedApplication.application;
2213  if (flutterApplication == nil) {
2214  [FlutterLogger logWarning:@"Dynamic content size update is not supported in app extension."];
2215  return 1.0;
2216  }
2217 
2218  UIContentSizeCategory category = flutterApplication.preferredContentSizeCategory;
2219  // The delta is computed by approximating Apple's typography guidelines:
2220  // https://developer.apple.com/ios/human-interface-guidelines/visual-design/typography/
2221  //
2222  // Specifically:
2223  // Non-accessibility sizes for "body" text are:
2224  const CGFloat xs = 14;
2225  const CGFloat s = 15;
2226  const CGFloat m = 16;
2227  const CGFloat l = 17;
2228  const CGFloat xl = 19;
2229  const CGFloat xxl = 21;
2230  const CGFloat xxxl = 23;
2231 
2232  // Accessibility sizes for "body" text are:
2233  const CGFloat ax1 = 28;
2234  const CGFloat ax2 = 33;
2235  const CGFloat ax3 = 40;
2236  const CGFloat ax4 = 47;
2237  const CGFloat ax5 = 53;
2238 
2239  // We compute the scale as relative difference from size L (large, the default size), where
2240  // L is assumed to have scale 1.0.
2241  if ([category isEqualToString:UIContentSizeCategoryExtraSmall]) {
2242  return xs / l;
2243  } else if ([category isEqualToString:UIContentSizeCategorySmall]) {
2244  return s / l;
2245  } else if ([category isEqualToString:UIContentSizeCategoryMedium]) {
2246  return m / l;
2247  } else if ([category isEqualToString:UIContentSizeCategoryLarge]) {
2248  return 1.0;
2249  } else if ([category isEqualToString:UIContentSizeCategoryExtraLarge]) {
2250  return xl / l;
2251  } else if ([category isEqualToString:UIContentSizeCategoryExtraExtraLarge]) {
2252  return xxl / l;
2253  } else if ([category isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) {
2254  return xxxl / l;
2255  } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityMedium]) {
2256  return ax1 / l;
2257  } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityLarge]) {
2258  return ax2 / l;
2259  } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) {
2260  return ax3 / l;
2261  } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) {
2262  return ax4 / l;
2263  } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) {
2264  return ax5 / l;
2265  } else {
2266  return 1.0;
2267  }
2268 }
2269 
2270 - (BOOL)supportsShowingSystemContextMenu {
2271  if (@available(iOS 16.0, *)) {
2272  return YES;
2273  } else {
2274  return NO;
2275  }
2276 }
2277 
2278 // The brightness mode of the platform, e.g., light or dark, expressed as a string that
2279 // is understood by the Flutter framework. See the settings
2280 // system channel for more information.
2281 - (NSString*)brightnessMode {
2282  UIUserInterfaceStyle style = self.traitCollection.userInterfaceStyle;
2283 
2284  if (style == UIUserInterfaceStyleDark) {
2285  return @"dark";
2286  } else {
2287  return @"light";
2288  }
2289 }
2290 
2291 // The contrast mode of the platform, e.g., normal or high, expressed as a string that is
2292 // understood by the Flutter framework. See the settings system channel for more
2293 // information.
2294 - (NSString*)contrastMode {
2295  UIAccessibilityContrast contrast = self.traitCollection.accessibilityContrast;
2296 
2297  if (contrast == UIAccessibilityContrastHigh) {
2298  return @"high";
2299  } else {
2300  return @"normal";
2301  }
2302 }
2303 
2304 #pragma mark - Status bar style
2305 
2306 - (UIStatusBarStyle)preferredStatusBarStyle {
2307  return self.statusBarStyle;
2308 }
2309 
2310 - (void)onPreferredStatusBarStyleUpdated:(NSNotification*)notification {
2311  // Notifications may not be on the iOS UI thread
2312  __weak FlutterViewController* weakSelf = self;
2313  dispatch_async(dispatch_get_main_queue(), ^{
2314  FlutterViewController* strongSelf = weakSelf;
2315  if (!strongSelf) {
2316  return;
2317  }
2318 
2319  NSDictionary* info = notification.userInfo;
2320  NSNumber* update = info[@(flutter::kOverlayStyleUpdateNotificationKey)];
2321  if (update == nil) {
2322  return;
2323  }
2324 
2325  UIStatusBarStyle style = static_cast<UIStatusBarStyle>(update.integerValue);
2326  if (style != strongSelf.statusBarStyle) {
2327  strongSelf.statusBarStyle = style;
2328  [strongSelf setNeedsStatusBarAppearanceUpdate];
2329  }
2330  });
2331 }
2332 
2333 - (void)setPrefersStatusBarHidden:(BOOL)hidden {
2334  if (hidden != self.flutterPrefersStatusBarHidden) {
2335  self.flutterPrefersStatusBarHidden = hidden;
2336  [self setNeedsStatusBarAppearanceUpdate];
2337  }
2338 }
2339 
2340 - (BOOL)prefersStatusBarHidden {
2341  return self.flutterPrefersStatusBarHidden;
2342 }
2343 
2344 #pragma mark - Platform views
2345 
2346 - (FlutterPlatformViewsController*)platformViewsController {
2347  return self.engine.platformViewsController;
2348 }
2349 
2350 - (NSObject<FlutterBinaryMessenger>*)binaryMessenger {
2351  return self.engine.binaryMessenger;
2352 }
2353 
2354 #pragma mark - FlutterBinaryMessenger
2355 
2356 - (void)sendOnChannel:(NSString*)channel message:(NSData*)message {
2357  [self.engine.binaryMessenger sendOnChannel:channel message:message];
2358 }
2359 
2360 - (void)sendOnChannel:(NSString*)channel
2361  message:(NSData*)message
2362  binaryReply:(FlutterBinaryReply)callback {
2363  NSAssert(channel, @"The channel must not be null");
2364  [self.engine.binaryMessenger sendOnChannel:channel message:message binaryReply:callback];
2365 }
2366 
2367 - (NSObject<FlutterTaskQueue>*)makeBackgroundTaskQueue {
2368  return [self.engine.binaryMessenger makeBackgroundTaskQueue];
2369 }
2370 
2371 - (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(NSString*)channel
2372  binaryMessageHandler:
2373  (FlutterBinaryMessageHandler)handler {
2374  return [self setMessageHandlerOnChannel:channel binaryMessageHandler:handler taskQueue:nil];
2375 }
2376 
2378  setMessageHandlerOnChannel:(NSString*)channel
2379  binaryMessageHandler:(FlutterBinaryMessageHandler _Nullable)handler
2380  taskQueue:(NSObject<FlutterTaskQueue>* _Nullable)taskQueue {
2381  NSAssert(channel, @"The channel must not be null");
2382  return [self.engine.binaryMessenger setMessageHandlerOnChannel:channel
2383  binaryMessageHandler:handler
2384  taskQueue:taskQueue];
2385 }
2386 
2387 - (void)cleanUpConnection:(FlutterBinaryMessengerConnection)connection {
2388  [self.engine.binaryMessenger cleanUpConnection:connection];
2389 }
2390 
2391 #pragma mark - FlutterTextureRegistry
2392 
2393 - (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture {
2394  return [self.engine.textureRegistry registerTexture:texture];
2395 }
2396 
2397 - (void)unregisterTexture:(int64_t)textureId {
2398  [self.engine.textureRegistry unregisterTexture:textureId];
2399 }
2400 
2401 - (void)textureFrameAvailable:(int64_t)textureId {
2402  [self.engine.textureRegistry textureFrameAvailable:textureId];
2403 }
2404 
2405 - (NSString*)lookupKeyForAsset:(NSString*)asset {
2406  return [FlutterDartProject lookupKeyForAsset:asset];
2407 }
2408 
2409 - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package {
2410  return [FlutterDartProject lookupKeyForAsset:asset fromPackage:package];
2411 }
2412 
2413 - (id<FlutterPluginRegistry>)pluginRegistry {
2414  return self.engine;
2415 }
2416 
2417 + (BOOL)isUIAccessibilityIsVoiceOverRunning {
2418  return UIAccessibilityIsVoiceOverRunning();
2419 }
2420 
2421 #pragma mark - FlutterPluginRegistry
2422 
2423 - (NSObject<FlutterPluginRegistrar>*)registrarForPlugin:(NSString*)pluginKey {
2424  return [self.engine registrarForPlugin:pluginKey];
2425 }
2426 
2427 - (BOOL)hasPlugin:(NSString*)pluginKey {
2428  return [self.engine hasPlugin:pluginKey];
2429 }
2430 
2431 - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
2432  return [self.engine valuePublishedByPlugin:pluginKey];
2433 }
2434 
2435 - (void)presentViewController:(UIViewController*)viewControllerToPresent
2436  animated:(BOOL)flag
2437  completion:(void (^)(void))completion {
2438  self.isPresentingViewControllerAnimating = YES;
2439  __weak FlutterViewController* weakSelf = self;
2440  [super presentViewController:viewControllerToPresent
2441  animated:flag
2442  completion:^{
2443  weakSelf.isPresentingViewControllerAnimating = NO;
2444  if (completion) {
2445  completion();
2446  }
2447  }];
2448 }
2449 
2450 - (BOOL)isPresentingViewController {
2451  return self.presentedViewController != nil || self.isPresentingViewControllerAnimating;
2452 }
2453 
2454 - (flutter::PointerData)updateMousePointerDataFrom:(UIGestureRecognizer*)gestureRecognizer
2455  API_AVAILABLE(ios(13.4)) {
2456  CGPoint location = [gestureRecognizer locationInView:self.view];
2457  CGFloat scale = self.flutterScreenIfViewLoaded.scale;
2458  _mouseState.location = {location.x * scale, location.y * scale};
2459  flutter::PointerData pointer_data;
2460  pointer_data.Clear();
2461  pointer_data.time_stamp = [[NSProcessInfo processInfo] systemUptime] * kMicrosecondsPerSecond;
2462  pointer_data.physical_x = _mouseState.location.x;
2463  pointer_data.physical_y = _mouseState.location.y;
2464  return pointer_data;
2465 }
2466 
2467 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
2468  shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer
2469  API_AVAILABLE(ios(13.4)) {
2470  return YES;
2471 }
2472 
2473 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
2474  shouldReceiveEvent:(UIEvent*)event API_AVAILABLE(ios(13.4)) {
2475  if (gestureRecognizer == _continuousScrollingPanGestureRecognizer &&
2476  event.type == UIEventTypeScroll) {
2477  // Events with type UIEventTypeScroll are only received when running on macOS under emulation.
2478  flutter::PointerData pointer_data = [self updateMousePointerDataFrom:gestureRecognizer];
2479  pointer_data.device = reinterpret_cast<int64_t>(_continuousScrollingPanGestureRecognizer);
2480  pointer_data.kind = flutter::PointerData::DeviceKind::kTrackpad;
2481  pointer_data.signal_kind = flutter::PointerData::SignalKind::kScrollInertiaCancel;
2482  pointer_data.view_id = self.viewIdentifier;
2483 
2484  if (event.timestamp < self.scrollInertiaEventAppKitDeadline) {
2485  // Only send the event if it occured before the expected natural end of gesture momentum.
2486  // If received after the deadline, it's not likely the event is from a user-initiated cancel.
2487  auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2488  packet->SetPointerData(/*i=*/0, pointer_data);
2489  [self.engine dispatchPointerDataPacket:std::move(packet)];
2490  self.scrollInertiaEventAppKitDeadline = 0;
2491  }
2492  }
2493  // This method is also called for UITouches, should return YES to process all touches.
2494  return YES;
2495 }
2496 
2497 - (void)hoverEvent:(UIHoverGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
2498  CGPoint oldLocation = _mouseState.location;
2499 
2500  flutter::PointerData pointer_data = [self updateMousePointerDataFrom:recognizer];
2501  pointer_data.device = reinterpret_cast<int64_t>(recognizer);
2502  pointer_data.kind = flutter::PointerData::DeviceKind::kMouse;
2503  pointer_data.view_id = self.viewIdentifier;
2504 
2505  switch (_hoverGestureRecognizer.state) {
2506  case UIGestureRecognizerStateBegan:
2507  pointer_data.change = flutter::PointerData::Change::kAdd;
2508  break;
2509  case UIGestureRecognizerStateChanged:
2510  pointer_data.change = flutter::PointerData::Change::kHover;
2511  break;
2512  case UIGestureRecognizerStateEnded:
2513  case UIGestureRecognizerStateCancelled:
2514  pointer_data.change = flutter::PointerData::Change::kRemove;
2515  break;
2516  default:
2517  // Sending kHover is the least harmful thing to do here
2518  // But this state is not expected to ever be reached.
2519  pointer_data.change = flutter::PointerData::Change::kHover;
2520  break;
2521  }
2522 
2523  NSTimeInterval time = [NSProcessInfo processInfo].systemUptime;
2524  BOOL isRunningOnMac = NO;
2525  if (@available(iOS 14.0, *)) {
2526  // This "stationary pointer" heuristic is not reliable when running within macOS.
2527  // We instead receive a scroll cancel event directly from AppKit.
2528  // See gestureRecognizer:shouldReceiveEvent:
2529  isRunningOnMac = [NSProcessInfo processInfo].iOSAppOnMac;
2530  }
2531  if (!isRunningOnMac && CGPointEqualToPoint(oldLocation, _mouseState.location) &&
2532  time > self.scrollInertiaEventStartline) {
2533  // iPadOS reports trackpad movements events with high (sub-pixel) precision. When an event
2534  // is received with the same position as the previous one, it can only be from a finger
2535  // making or breaking contact with the trackpad surface.
2536  auto packet = std::make_unique<flutter::PointerDataPacket>(2);
2537  packet->SetPointerData(/*i=*/0, pointer_data);
2538  flutter::PointerData inertia_cancel = pointer_data;
2539  inertia_cancel.device = reinterpret_cast<int64_t>(_continuousScrollingPanGestureRecognizer);
2540  inertia_cancel.kind = flutter::PointerData::DeviceKind::kTrackpad;
2541  inertia_cancel.signal_kind = flutter::PointerData::SignalKind::kScrollInertiaCancel;
2542  inertia_cancel.view_id = self.viewIdentifier;
2543  packet->SetPointerData(/*i=*/1, inertia_cancel);
2544  [self.engine dispatchPointerDataPacket:std::move(packet)];
2545  self.scrollInertiaEventStartline = DBL_MAX;
2546  } else {
2547  auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2548  packet->SetPointerData(/*i=*/0, pointer_data);
2549  [self.engine dispatchPointerDataPacket:std::move(packet)];
2550  }
2551 }
2552 
2553 - (void)discreteScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
2554  CGPoint translation = [recognizer translationInView:self.view];
2555  const CGFloat scale = self.flutterScreenIfViewLoaded.scale;
2556 
2557  translation.x *= scale;
2558  translation.y *= scale;
2559 
2560  flutter::PointerData pointer_data = [self updateMousePointerDataFrom:recognizer];
2561  pointer_data.device = reinterpret_cast<int64_t>(recognizer);
2562  pointer_data.kind = flutter::PointerData::DeviceKind::kMouse;
2563  pointer_data.signal_kind = flutter::PointerData::SignalKind::kScroll;
2564  pointer_data.scroll_delta_x = (translation.x - _mouseState.last_translation.x);
2565  pointer_data.scroll_delta_y = -(translation.y - _mouseState.last_translation.y);
2566  pointer_data.view_id = self.viewIdentifier;
2567 
2568  // The translation reported by UIPanGestureRecognizer is the total translation
2569  // generated by the pan gesture since the gesture began. We need to be able
2570  // to keep track of the last translation value in order to generate the deltaX
2571  // and deltaY coordinates for each subsequent scroll event.
2572  if (recognizer.state != UIGestureRecognizerStateEnded) {
2573  _mouseState.last_translation = translation;
2574  } else {
2575  _mouseState.last_translation = CGPointZero;
2576  }
2577 
2578  auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2579  packet->SetPointerData(/*i=*/0, pointer_data);
2580  [self.engine dispatchPointerDataPacket:std::move(packet)];
2581 }
2582 
2583 - (void)continuousScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
2584  CGPoint translation = [recognizer translationInView:self.view];
2585  const CGFloat scale = self.flutterScreenIfViewLoaded.scale;
2586 
2587  flutter::PointerData pointer_data = [self updateMousePointerDataFrom:recognizer];
2588  pointer_data.device = reinterpret_cast<int64_t>(recognizer);
2589  pointer_data.kind = flutter::PointerData::DeviceKind::kTrackpad;
2590  pointer_data.view_id = self.viewIdentifier;
2591  switch (recognizer.state) {
2592  case UIGestureRecognizerStateBegan:
2593  pointer_data.change = flutter::PointerData::Change::kPanZoomStart;
2594  break;
2595  case UIGestureRecognizerStateChanged:
2596  pointer_data.change = flutter::PointerData::Change::kPanZoomUpdate;
2597  pointer_data.pan_x = translation.x * scale;
2598  pointer_data.pan_y = translation.y * scale;
2599  pointer_data.pan_delta_x = 0; // Delta will be generated in pointer_data_packet_converter.cc.
2600  pointer_data.pan_delta_y = 0; // Delta will be generated in pointer_data_packet_converter.cc.
2601  pointer_data.scale = 1;
2602  break;
2603  case UIGestureRecognizerStateEnded:
2604  case UIGestureRecognizerStateCancelled:
2605  self.scrollInertiaEventStartline =
2606  [[NSProcessInfo processInfo] systemUptime] +
2607  0.1; // Time to lift fingers off trackpad (experimentally determined)
2608  // When running an iOS app on an Apple Silicon Mac, AppKit will send an event
2609  // of type UIEventTypeScroll when trackpad scroll momentum has ended. This event
2610  // is sent whether the momentum ended normally or was cancelled by a trackpad touch.
2611  // Since Flutter scrolling inertia will likely not match the system inertia, we should
2612  // only send a PointerScrollInertiaCancel event for user-initiated cancellations.
2613  // The following (curve-fitted) calculation provides a cutoff point after which any
2614  // UIEventTypeScroll event will likely be from the system instead of the user.
2615  // See https://github.com/flutter/engine/pull/34929.
2616  self.scrollInertiaEventAppKitDeadline =
2617  [[NSProcessInfo processInfo] systemUptime] +
2618  (0.1821 * log(fmax([recognizer velocityInView:self.view].x,
2619  [recognizer velocityInView:self.view].y))) -
2620  0.4825;
2621  pointer_data.change = flutter::PointerData::Change::kPanZoomEnd;
2622  break;
2623  default:
2624  // continuousScrollEvent: should only ever be triggered with the above phases
2625  NSAssert(NO, @"Trackpad pan event occured with unexpected phase 0x%lx",
2626  (long)recognizer.state);
2627  break;
2628  }
2629 
2630  auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2631  packet->SetPointerData(/*i=*/0, pointer_data);
2632  [self.engine dispatchPointerDataPacket:std::move(packet)];
2633 }
2634 
2635 - (void)pinchEvent:(UIPinchGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
2636  flutter::PointerData pointer_data = [self updateMousePointerDataFrom:recognizer];
2637  pointer_data.device = reinterpret_cast<int64_t>(recognizer);
2638  pointer_data.kind = flutter::PointerData::DeviceKind::kTrackpad;
2639  pointer_data.view_id = self.viewIdentifier;
2640  switch (recognizer.state) {
2641  case UIGestureRecognizerStateBegan:
2642  pointer_data.change = flutter::PointerData::Change::kPanZoomStart;
2643  break;
2644  case UIGestureRecognizerStateChanged:
2645  pointer_data.change = flutter::PointerData::Change::kPanZoomUpdate;
2646  pointer_data.scale = recognizer.scale;
2647  pointer_data.rotation = _rotationGestureRecognizer.rotation;
2648  break;
2649  case UIGestureRecognizerStateEnded:
2650  case UIGestureRecognizerStateCancelled:
2651  pointer_data.change = flutter::PointerData::Change::kPanZoomEnd;
2652  break;
2653  default:
2654  // pinchEvent: should only ever be triggered with the above phases
2655  NSAssert(NO, @"Trackpad pinch event occured with unexpected phase 0x%lx",
2656  (long)recognizer.state);
2657  break;
2658  }
2659 
2660  auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2661  packet->SetPointerData(/*i=*/0, pointer_data);
2662  [self.engine dispatchPointerDataPacket:std::move(packet)];
2663 }
2664 
2665 #pragma mark - State Restoration
2666 
2667 - (void)encodeRestorableStateWithCoder:(NSCoder*)coder {
2668  NSData* restorationData = [self.engine.restorationPlugin restorationData];
2669  [coder encodeBytes:(const unsigned char*)restorationData.bytes
2670  length:restorationData.length
2671  forKey:kFlutterRestorationStateAppData];
2672  [super encodeRestorableStateWithCoder:coder];
2673 }
2674 
2675 - (void)decodeRestorableStateWithCoder:(NSCoder*)coder {
2676  NSUInteger restorationDataLength;
2677  const unsigned char* restorationBytes = [coder decodeBytesForKey:kFlutterRestorationStateAppData
2678  returnedLength:&restorationDataLength];
2679  NSData* restorationData = [NSData dataWithBytes:restorationBytes length:restorationDataLength];
2680  [self.engine.restorationPlugin setRestorationData:restorationData];
2681 }
2682 
2683 - (FlutterRestorationPlugin*)restorationPlugin {
2684  return self.engine.restorationPlugin;
2685 }
2686 
2688  return self.engine.textInputPlugin;
2689 }
2690 
2691 @end
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)
void(^ FlutterBinaryMessageHandler)(NSData *_Nullable message, FlutterBinaryReply reply)
int64_t FlutterBinaryMessengerConnection
void(^ FlutterSendKeyEvent)(const FlutterKeyEvent &, _Nullable FlutterKeyEventCallback, void *_Nullable)
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
instancetype initWithCoder
FlutterTextInputPlugin * textInputPlugin
NSNotificationName const FlutterViewControllerHideHomeIndicator
static NSString *const kFlutterRestorationStateAppData
NSNotificationName const FlutterViewControllerShowHomeIndicator
NSNotificationName const FlutterSemanticsUpdateNotification
struct MouseState MouseState
static constexpr CGFloat kScrollViewContentSize
NSNotificationName const FlutterViewControllerWillDealloc
static constexpr FLUTTER_ASSERT_ARC int kMicrosecondsPerSecond
MouseState _mouseState
void(^ FlutterKeyboardAnimationCallback)(fml::TimePoint)
UIPanGestureRecognizer *continuousScrollingPanGestureRecognizer API_AVAILABLE(ios(13.4))
UIPanGestureRecognizer *discreteScrollingPanGestureRecognizer API_AVAILABLE(ios(13.4))
UIPinchGestureRecognizer *pinchGestureRecognizer API_AVAILABLE(ios(13.4))
UIHoverGestureRecognizer *hoverGestureRecognizer API_AVAILABLE(ios(13.4))
NSString * lookupKeyForAsset:fromPackage:(NSString *asset,[fromPackage] NSString *package)
NSString * lookupKeyForAsset:(NSString *asset)
FlutterViewController * viewController
void setUpIndirectScribbleInteraction:(id< FlutterViewResponder > viewResponder)