Flutter iOS Embedder
FlutterView.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 
6 
7 #include "flutter/fml/platform/darwin/cf_utils.h"
11 
13 
14 @interface FlutterView ()
15 @property(nonatomic, weak) id<FlutterViewEngineDelegate> delegate;
16 @property(nonatomic, weak) UIWindowScene* previousScene;
17 @end
18 
20 @end
21 
22 @implementation FlutterView {
23  BOOL _isWideGamutEnabled;
25 }
26 
27 - (instancetype)init {
28  NSAssert(NO, @"FlutterView must initWithDelegate");
29  return nil;
30 }
31 
32 - (instancetype)initWithFrame:(CGRect)frame {
33  NSAssert(NO, @"FlutterView must initWithDelegate");
34  return nil;
35 }
36 
37 - (instancetype)initWithCoder:(NSCoder*)aDecoder {
38  NSAssert(NO, @"FlutterView must initWithDelegate");
39  return nil;
40 }
41 
42 - (UIScreen*)screen {
43  return self.window.windowScene.screen;
44 }
45 
46 // iOS has a concept of "intrinsicContentSize", which indicates the size a view would like to be
47 // based on its content. When an intrinsicContentSize is set, iOS will automatically add Auto Layout
48 // constraints for the width and/or height. However, the constraints use a private API. There are
49 // situations where we may want to filter these constraints. To avoid using a private API, Flutter
50 // creates a custom constraint called FlutterAutoResizeLayoutConstraint to add a width/height
51 // constraint that reflects the intrinsicContentSize.
52 - (void)setIntrinsicContentSize:(CGSize)size {
53  if (!self.autoResizable) {
54  return;
55  }
56 
57  UIWindow* window = self.window;
58  CGFloat scale = window ? self.window.windowScene.screen.scale : self.traitCollection.displayScale;
59  CGSize scaledSize = CGSizeMake(size.width / scale, size.height / scale);
60 
61  CGSize roundedScaleSize = CGSizeMake(roundf(scaledSize.width), roundf(scaledSize.height));
62  CGSize roundedIntrinsicSize =
63  CGSizeMake(roundf(_intrinsicSize.width), roundf(_intrinsicSize.height));
64 
65  // If the size has not changed, don't update constraints.
66  if (CGSizeEqualToSize(roundedIntrinsicSize, roundedScaleSize)) {
67  return;
68  }
69  _intrinsicSize = scaledSize;
70 
71  self.translatesAutoresizingMaskIntoConstraints = false;
72 
73  // Remove any existing FlutterAutoResizeLayoutConstraint
74  [self removeAutoResizeLayoutConstraints];
75 
76  FlutterAutoResizeLayoutConstraint* widthConstraint =
77  [FlutterAutoResizeLayoutConstraint constraintWithItem:self
78  attribute:NSLayoutAttributeWidth
79  relatedBy:NSLayoutRelationEqual
80  toItem:nil
81  attribute:NSLayoutAttributeNotAnAttribute
82  multiplier:1.0
83  constant:scaledSize.width];
84 
85  FlutterAutoResizeLayoutConstraint* heightConstraint =
86  [FlutterAutoResizeLayoutConstraint constraintWithItem:self
87  attribute:NSLayoutAttributeHeight
88  relatedBy:NSLayoutRelationEqual
89  toItem:nil
90  attribute:NSLayoutAttributeNotAnAttribute
91  multiplier:1.0
92  constant:scaledSize.height];
93 
94  [NSLayoutConstraint activateConstraints:@[ widthConstraint, heightConstraint ]];
95  [self setNeedsLayout];
96 }
97 
98 - (void)resetIntrinsicContentSize {
99  _intrinsicSize = CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric);
100  [self removeAutoResizeLayoutConstraints];
101 }
102 
103 - (void)removeAutoResizeLayoutConstraints {
104  for (NSLayoutConstraint* constraint in self.constraints) {
105  if ([constraint isKindOfClass:[FlutterAutoResizeLayoutConstraint class]]) {
106  constraint.active = NO;
107  }
108  }
109 }
110 
111 - (MTLPixelFormat)pixelFormat {
112  if ([self.layer isKindOfClass:[CAMetalLayer class]]) {
113 // It is a known Apple bug that CAMetalLayer incorrectly reports its supported
114 // SDKs. It is, in fact, available since iOS 8.
115 #pragma clang diagnostic push
116 #pragma clang diagnostic ignored "-Wunguarded-availability-new"
117  CAMetalLayer* layer = (CAMetalLayer*)self.layer;
118  return layer.pixelFormat;
119  }
120  return MTLPixelFormatBGRA8Unorm;
121 }
122 - (BOOL)isWideGamutSupported {
123  FML_DCHECK(self.screen);
124 
125  // Wide Gamut is not supported for iOS Extensions due to memory limitations
126  // (see https://github.com/flutter/flutter/issues/165086).
128  return NO;
129  }
130 
131  // This predicates the decision on the capabilities of the iOS device's
132  // display. This means external displays will not support wide gamut if the
133  // device's display doesn't support it. It practice that should be never.
134  return self.screen.traitCollection.displayGamut != UIDisplayGamutSRGB;
135 }
136 
137 - (instancetype)initWithDelegate:(id<FlutterViewEngineDelegate>)delegate
138  opaque:(BOOL)opaque
139  enableWideGamut:(BOOL)isWideGamutEnabled {
140  if (delegate == nil) {
141  NSLog(@"FlutterView delegate was nil.");
142  return nil;
143  }
144 
145  self = [super initWithFrame:CGRectNull];
146 
147  if (self) {
148  _delegate = delegate;
149  _isWideGamutEnabled = isWideGamutEnabled;
150  self.layer.opaque = opaque;
151  _autoResizable = NO;
152  _intrinsicSize = CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric);
153  }
154 
155  return self;
156 }
157 
158 static void PrintWideGamutWarningOnce() {
159  static BOOL did_print = NO;
160  if (did_print) {
161  return;
162  }
163  FML_DLOG(WARNING) << "Rendering wide gamut colors is turned on but isn't "
164  "supported, downgrading the color gamut to sRGB.";
165  did_print = YES;
166 }
167 
168 - (void)layoutSubviews {
169  if ([self.layer isKindOfClass:[CAMetalLayer class]]) {
170 // It is a known Apple bug that CAMetalLayer incorrectly reports its supported
171 // SDKs. It is, in fact, available since iOS 8.
172 #pragma clang diagnostic push
173 #pragma clang diagnostic ignored "-Wunguarded-availability-new"
174  CAMetalLayer* layer = (CAMetalLayer*)self.layer;
175 #pragma clang diagnostic pop
176  CGFloat screenScale = self.screen.scale;
177  layer.allowsGroupOpacity = YES;
178  layer.contentsScale = screenScale;
179  layer.rasterizationScale = screenScale;
180  layer.framebufferOnly = flutter::Settings::kSurfaceDataAccessible ? NO : YES;
181  if (_isWideGamutEnabled && self.isWideGamutSupported) {
182  fml::CFRef<CGColorSpaceRef> srgb(CGColorSpaceCreateWithName(kCGColorSpaceExtendedSRGB));
183  layer.colorspace = srgb;
184  layer.pixelFormat = MTLPixelFormatBGRA10_XR;
185  } else if (_isWideGamutEnabled && !self.isWideGamutSupported) {
186  PrintWideGamutWarningOnce();
187  }
188  }
189 
190  [super layoutSubviews];
191 }
192 
193 + (Class)layerClass {
195  flutter::GetRenderingAPIForProcess(/*force_software=*/false));
196 }
197 
198 - (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context {
199  TRACE_EVENT0("flutter", "SnapshotFlutterView");
200 
201  if (layer != self.layer || context == nullptr) {
202  return;
203  }
204 
205  auto screenshot = [_delegate takeScreenshot:flutter::Rasterizer::ScreenshotType::UncompressedImage
206  asBase64Encoded:NO];
207 
208  if (!screenshot.data || screenshot.data->isEmpty() || screenshot.frame_size.IsEmpty()) {
209  return;
210  }
211 
212  NSData* data = [NSData dataWithBytes:const_cast<void*>(screenshot.data->data())
213  length:screenshot.data->size()];
214 
215  fml::CFRef<CGDataProviderRef> image_data_provider(
216  CGDataProviderCreateWithCFData(reinterpret_cast<CFDataRef>(data)));
217 
218  fml::CFRef<CGColorSpaceRef> colorspace(CGColorSpaceCreateDeviceRGB());
219 
220  // Defaults for RGBA8888.
221  size_t bits_per_component = 8u;
222  size_t bits_per_pixel = 32u;
223  size_t bytes_per_row_multiplier = 4u;
224  CGBitmapInfo bitmap_info =
225  static_cast<CGBitmapInfo>(static_cast<uint32_t>(kCGImageAlphaPremultipliedLast) |
226  static_cast<uint32_t>(kCGBitmapByteOrder32Big));
227 
228  switch (screenshot.pixel_format) {
229  case flutter::Rasterizer::ScreenshotFormat::kUnknown:
230  case flutter::Rasterizer::ScreenshotFormat::kR8G8B8A8UNormInt:
231  // Assume unknown is Skia and is RGBA8888. Keep defaults.
232  break;
233  case flutter::Rasterizer::ScreenshotFormat::kB8G8R8A8UNormInt:
234  // Treat this as little endian with the alpha first so that it's read backwards.
235  bitmap_info =
236  static_cast<CGBitmapInfo>(static_cast<uint32_t>(kCGImageAlphaPremultipliedFirst) |
237  static_cast<uint32_t>(kCGBitmapByteOrder32Little));
238  break;
239  case flutter::Rasterizer::ScreenshotFormat::kR16G16B16A16Float:
240  bits_per_component = 16u;
241  bits_per_pixel = 64u;
242  bytes_per_row_multiplier = 8u;
243  bitmap_info =
244  static_cast<CGBitmapInfo>(static_cast<uint32_t>(kCGImageAlphaPremultipliedLast) |
245  static_cast<uint32_t>(kCGBitmapFloatComponents) |
246  static_cast<uint32_t>(kCGBitmapByteOrder16Little));
247  break;
248  }
249 
250  fml::CFRef<CGImageRef> image(CGImageCreate(
251  screenshot.frame_size.width, // size_t width
252  screenshot.frame_size.height, // size_t height
253  bits_per_component, // size_t bitsPerComponent
254  bits_per_pixel, // size_t bitsPerPixel,
255  bytes_per_row_multiplier * screenshot.frame_size.width, // size_t bytesPerRow
256  colorspace, // CGColorSpaceRef space
257  bitmap_info, // CGBitmapInfo bitmapInfo
258  image_data_provider, // CGDataProviderRef provider
259  nullptr, // const CGFloat* decode
260  false, // bool shouldInterpolate
261  kCGRenderingIntentDefault // CGColorRenderingIntent intent
262  ));
263 
264  const CGRect frame_rect =
265  CGRectMake(0.0, 0.0, screenshot.frame_size.width, screenshot.frame_size.height);
266  CGContextSaveGState(context);
267  // If the CGContext is not a bitmap based context, this returns zero.
268  CGFloat height = CGBitmapContextGetHeight(context);
269  if (height == 0) {
270  height = CGFloat(screenshot.frame_size.height);
271  }
272  CGContextTranslateCTM(context, 0.0, height);
273  CGContextScaleCTM(context, 1.0, -1.0);
274  CGContextDrawImage(context, frame_rect, image);
275  CGContextRestoreGState(context);
276 }
277 
278 - (BOOL)isAccessibilityElement {
279  // iOS does not provide an API to query whether the voice control
280  // is turned on or off. It is likely at least one of the assitive
281  // technologies is turned on if this method is called. If we do
282  // not catch it in notification center, we will catch it here.
283  //
284  // TODO(chunhtai): Remove this workaround once iOS provides an
285  // API to query whether voice control is enabled.
286  // https://github.com/flutter/flutter/issues/76808.
287  [self.delegate flutterViewAccessibilityDidCall];
288  return NO;
289 }
290 
291 // Enables keyboard-based navigation when the user turns on
292 // full keyboard access (FKA), using existing accessibility information.
293 //
294 // iOS does not provide any API for monitoring or querying whether FKA is on,
295 // but it does call isAccessibilityElement if FKA is on,
296 // so the isAccessibilityElement implementation above will be called
297 // when the view appears and the accessibility information will most likely
298 // be available by the time the user starts to interact with the app using FKA.
299 //
300 // See SemanticsObject+UIFocusSystem.mm for more details.
301 - (NSArray<id<UIFocusItem>>*)focusItemsInRect:(CGRect)rect {
302  NSObject* rootAccessibilityElement =
303  [self.accessibilityElements count] > 0 ? self.accessibilityElements[0] : nil;
304  return [rootAccessibilityElement isKindOfClass:[SemanticsObjectContainer class]]
305  ? @[ [rootAccessibilityElement accessibilityElementAtIndex:0] ]
306  : nil;
307 }
308 
309 - (NSArray<id<UIFocusEnvironment>>*)preferredFocusEnvironments {
310  // Occasionally we add subviews to FlutterView (text fields for example).
311  // These views shouldn't be directly visible to the iOS focus engine, instead
312  // the focus engine should only interact with the designated focus items
313  // (SemanticsObjects).
314  return nil;
315 }
316 
317 - (void)willMoveToWindow:(UIWindow*)newWindow {
318  // When a FlutterView moves windows, it may also be moving scenes. Add/remove the FlutterEngine
319  // from the FlutterSceneLifeCycleProvider.sceneLifeCycleDelegate if it changes scenes.
320  UIWindowScene* newScene = newWindow.windowScene;
321  UIWindowScene* currentScene = self.window.windowScene;
322 
323  if (newScene == currentScene) {
324  return;
325  }
326 
327  // Remove the engine from the previous scene if it's no longer in that window and scene.
328  FlutterPluginSceneLifeCycleDelegate* previousSceneLifeCycleDelegate =
329  [FlutterPluginSceneLifeCycleDelegate fromScene:self.previousScene];
330  if (previousSceneLifeCycleDelegate) {
331  [previousSceneLifeCycleDelegate removeFlutterManagedEngine:(FlutterEngine*)self.delegate];
332  self.previousScene = nil;
333  }
334 
335  if (newScene) {
336  // Add the engine to the new scene's lifecycle delegate.
337  FlutterPluginSceneLifeCycleDelegate* newSceneLifeCycleDelegate =
338  [FlutterPluginSceneLifeCycleDelegate fromScene:newScene];
339  if (newSceneLifeCycleDelegate) {
340  [newSceneLifeCycleDelegate addFlutterManagedEngine:(FlutterEngine*)self.delegate];
341  }
342  } else {
343  // If the view is being removed from a window, store the current scene to remove the engine
344  // from it later when the view is added to a new window.
345  self.previousScene = currentScene;
346  }
347 }
348 @end
instancetype initWithFrame
instancetype initWithCoder
CGSize _intrinsicSize
Definition: FlutterView.mm:22
IOSRenderingAPI GetRenderingAPIForProcess(bool force_software)
Class GetCoreAnimationLayerClassForRenderingAPI(IOSRenderingAPI rendering_api)