Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions mobile/ios/Runner/InlinePhotoPicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import PhotosUI
import UIKit
import UniformTypeIdentifiers

enum EmbeddedPhotoPickerLayout {
static func applyPreferredScale(_ zoomIn: () -> Void) {
UIView.performWithoutAnimation {
zoomIn()
}
}
}

final class InlinePhotoPickerFactory: NSObject, FlutterPlatformViewFactory {
private let messenger: FlutterBinaryMessenger
private weak var parentViewController: UIViewController?
Expand Down Expand Up @@ -68,6 +76,7 @@ final class InlinePhotoPickerPlatformView: NSObject, FlutterPlatformView {
}

containerView.backgroundColor = .clear
containerView.clipsToBounds = true
if #available(iOS 17.0, *) {
installPicker()
}
Expand Down Expand Up @@ -120,6 +129,10 @@ final class InlinePhotoPickerPlatformView: NSObject, FlutterPlatformView {
picker.didMove(toParent: parentViewController)
}
pickerViewController = picker
containerView.layoutIfNeeded()
EmbeddedPhotoPickerLayout.applyPreferredScale {
picker.zoomIn()
}
}

private func exportPickerResult(_ result: PHPickerResult) async throws -> String {
Expand Down
210 changes: 165 additions & 45 deletions mobile/ios/Runner/NativeAttachmentPopover.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ final class NativeAttachmentPopoverViewController:
case camera
}

private typealias ContentPreparation = (@escaping () -> Void) -> Void

private let channel: FlutterMethodChannel
private let expandedWidth: CGFloat
private let menuSize = CGSize(width: 176, height: 208)
private let expandedHeight: CGFloat = 372
private let expandedHorizontalOffset: CGFloat = 10
private let expandedVerticalOffset: CGFloat = 40
private let maximumMenuHeight: CGFloat
private let expandedHeight = NativeAttachmentMenuLayout.maximumHeight
private let contentHost = UIView()
private let cameraSession = AVCaptureSession()
private let cameraOutput = AVCapturePhotoOutput()
Expand Down Expand Up @@ -50,14 +50,31 @@ final class NativeAttachmentPopoverViewController:
private var activeCameraCaptureID: Int64?
private var isFinishing = false
private var didNotifyDismissal = false
private var keyboardDismissalOffset: CGFloat = 0
private var menuStackHeightConstraint: NSLayoutConstraint?

var onDismiss: (() -> Void)?

init(channel: FlutterMethodChannel, expandedWidth: CGFloat) {
private var menuSize: CGSize {
NativeAttachmentMenuLayout.size(
compatibleWith: traitCollection,
maximumHeight: maximumMenuHeight
)
}

init(
channel: FlutterMethodChannel,
expandedWidth: CGFloat,
maximumMenuHeight: CGFloat = NativeAttachmentMenuLayout.maximumHeight
) {
self.channel = channel
self.expandedWidth = expandedWidth
self.maximumMenuHeight = maximumMenuHeight
super.init(nibName: nil, bundle: nil)
preferredContentSize = menuSize
preferredContentSize = NativeAttachmentMenuLayout.size(
compatibleWith: .current,
maximumHeight: maximumMenuHeight
)
}

@available(*, unavailable)
Expand Down Expand Up @@ -106,6 +123,20 @@ final class NativeAttachmentPopoverViewController:
stopCamera()
}

override func traitCollectionDidChange(
_ previousTraitCollection: UITraitCollection?
) {
super.traitCollectionDidChange(previousTraitCollection)
guard
previousTraitCollection?.preferredContentSizeCategory
!= traitCollection.preferredContentSizeCategory
else {
return
}

updateMenuLayout()
}

func adaptivePresentationStyle(
for controller: UIPresentationController
) -> UIModalPresentationStyle {
Expand All @@ -122,16 +153,45 @@ final class NativeAttachmentPopoverViewController:
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false

let scrollView = UIScrollView()
scrollView.alwaysBounceVertical = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(scrollView)

let stack = UIStackView()
stack.axis = .vertical
stack.distribution = .fillEqually
stack.spacing = NativeAttachmentMenuLayout.itemSpacing
stack.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(stack)
scrollView.addSubview(stack)
let stackHeightConstraint = stack.heightAnchor.constraint(
equalToConstant: NativeAttachmentMenuLayout.itemsHeight(
compatibleWith: traitCollection
)
)
menuStackHeightConstraint = stackHeightConstraint
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: container.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: container.trailingAnchor),
stack.topAnchor.constraint(equalTo: container.topAnchor, constant: 8),
stack.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -8),
scrollView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: container.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
stack.leadingAnchor.constraint(
equalTo: scrollView.frameLayoutGuide.leadingAnchor,
constant: NativeAttachmentMenuLayout.contentPadding
),
stack.trailingAnchor.constraint(
equalTo: scrollView.frameLayoutGuide.trailingAnchor,
constant: -NativeAttachmentMenuLayout.contentPadding
),
stack.topAnchor.constraint(
equalTo: scrollView.contentLayoutGuide.topAnchor,
constant: NativeAttachmentMenuLayout.contentPadding
),
stack.bottomAnchor.constraint(
equalTo: scrollView.contentLayoutGuide.bottomAnchor,
constant: -NativeAttachmentMenuLayout.contentPadding
),
stackHeightConstraint,
])

stack.addArrangedSubview(
Expand Down Expand Up @@ -169,8 +229,19 @@ final class NativeAttachmentPopoverViewController:
return container
}

private func updateMenuLayout() {
menuStackHeightConstraint?.constant =
NativeAttachmentMenuLayout.itemsHeight(
compatibleWith: traitCollection
)
if surface == .menu {
preferredContentSize = menuSize
}
}

private func showPhotos() {
guard surface != .photos else { return }
prepareForExpandedSurface()
stopCamera()

var configuration = PHPickerConfiguration(photoLibrary: .shared())
Expand All @@ -192,16 +263,14 @@ final class NativeAttachmentPopoverViewController:

let container = UIView()
container.backgroundColor = .clear
container.clipsToBounds = true
addChild(picker)
picker.view.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(picker.view)
NSLayoutConstraint.activate([
picker.view.leadingAnchor.constraint(equalTo: container.leadingAnchor),
picker.view.trailingAnchor.constraint(equalTo: container.trailingAnchor),
picker.view.topAnchor.constraint(
equalTo: container.topAnchor,
constant: -8
),
picker.view.topAnchor.constraint(equalTo: container.topAnchor),
picker.view.bottomAnchor.constraint(equalTo: container.bottomAnchor),
])
picker.didMove(toParent: self)
Expand All @@ -227,11 +296,32 @@ final class NativeAttachmentPopoverViewController:
trailing: actionButton
)

transition(to: .photos, content: container)
transition(
to: .photos,
content: container,
preparation: { [weak picker] reveal in
guard let picker else {
reveal()
return
}
// PHPicker ignores scale changes while its remote grid is still
// adapting to the compact menu bounds. Give it one main-loop turn at
// the final popover size, apply the scale offscreen, then reveal it.
DispatchQueue.main.async {
picker.view.layoutIfNeeded()
EmbeddedPhotoPickerLayout.applyPreferredScale {
picker.zoomIn()
picker.view.layoutIfNeeded()
}
DispatchQueue.main.async(execute: reveal)
}
}
)
}

private func showCamera() {
guard surface != .camera else { return }
prepareForExpandedSurface()
removePhotoPicker()

let container = UIView()
Expand Down Expand Up @@ -288,9 +378,26 @@ final class NativeAttachmentPopoverViewController:
stopCamera()

let menu = makeMenuView()
transition(to: .menu, content: menu) { [weak self] in
self?.removePhotoPicker()
transition(
to: .menu,
content: menu,
completion: { [weak self] in
self?.removePhotoPicker()
}
)
}

private func prepareForExpandedSurface() {
if let sourceHost = popoverPresentationController?.sourceView?.superview {
keyboardDismissalOffset = max(
keyboardDismissalOffset,
NativeAttachmentExpandedSurfaceBehavior.keyboardOverlap(
containerBounds: sourceHost.bounds,
keyboardLayoutFrame: sourceHost.keyboardLayoutGuide.layoutFrame
)
)
}
NativeAttachmentExpandedSurfaceBehavior.dismissKeyboard(in: view.window)
Comment thread
klopez4212 marked this conversation as resolved.
}

private func installContent(_ content: UIView) {
Expand All @@ -308,6 +415,7 @@ final class NativeAttachmentPopoverViewController:
private func transition(
to nextSurface: Surface,
content nextView: UIView,
preparation: ContentPreparation? = nil,
completion: (() -> Void)? = nil
) {
let previousView = visibleContentView
Expand All @@ -327,50 +435,61 @@ final class NativeAttachmentPopoverViewController:
])
contentHost.layoutIfNeeded()

let direction: CGFloat = isExpanding ? 34 : -34
nextView.alpha = UIAccessibility.isReduceMotionEnabled ? 0 : 0.01
nextView.transform =
UIAccessibility.isReduceMotionEnabled
? .identity
: CGAffineTransform(translationX: direction, y: 0).scaledBy(
x: 0.97,
y: 0.97
)
let shouldAnimate = !UIAccessibility.isReduceMotionEnabled
// Do not expose embedded surfaces while the popover changes size. In
// particular, PHPicker visibly reflows its grid from the compact menu
// width to the expanded width if it is allowed to paint during this step.
nextView.alpha = 0
visibleContentView = nextView
surface = nextSurface

let duration = UIAccessibility.isReduceMotionEnabled ? 0.16 : 0.36
let duration = shouldAnimate ? (isExpanding ? 0.24 : 0.2) : 0
UIView.animate(
withDuration: duration,
delay: 0,
usingSpringWithDamping: 0.86,
initialSpringVelocity: 0.18,
options: [.beginFromCurrentState, .allowUserInteraction]
options: [
.beginFromCurrentState,
.allowUserInteraction,
.curveEaseInOut,
]
) {
self.preferredContentSize = targetSize
if let popover = self.popoverPresentationController,
let sourceView = popover.sourceView
{
popover.sourceRect = sourceView.bounds.offsetBy(
dx: isExpanding ? self.expandedHorizontalOffset : 0,
dy: isExpanding ? self.expandedVerticalOffset : 0
popover.sourceRect = NativeAttachmentPopoverAnchorLayout.sourceRect(
anchorBounds: sourceView.bounds,
keyboardDismissalOffset: self.keyboardDismissalOffset,
isExpanded: isExpanding
)
}
previousView?.alpha = 0
previousView?.transform =
UIAccessibility.isReduceMotionEnabled
? .identity
: CGAffineTransform(translationX: -direction, y: 0).scaledBy(
x: 0.97,
y: 0.97
)
nextView.alpha = 1
nextView.transform = .identity
self.view.layoutIfNeeded()
} completion: { _ in
previousView?.removeFromSuperview()
previousView?.transform = .identity
completion?()
self.view.layoutIfNeeded()

let reveal = {
UIView.animate(
withDuration: shouldAnimate ? 0.14 : 0,
delay: 0,
options: [
.beginFromCurrentState,
.allowUserInteraction,
.curveEaseOut,
]
) {
nextView.alpha = 1
} completion: { _ in
completion?()
}
}

if let preparation {
preparation(reveal)
} else {
reveal()
}
}
}

Expand Down Expand Up @@ -917,6 +1036,7 @@ final class NativeAttachmentPopoverViewController:
Self.removeTemporaryFiles(temporaryPaths)
return
}
NativeAttachmentExpandedSurfaceBehavior.dismissKeyboard(in: view.window)
isFinishing = true
view.isUserInteractionEnabled = false
selectionGeneration += 1
Expand Down
Loading
Loading