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