mirror of
https://github.com/laosb/CropImage.git
synced 2025-04-30 15:41:08 +00:00
feat: Custom cut hole.
This commit is contained in:
parent
e703e25200
commit
e4d096dafb
3 changed files with 86 additions and 29 deletions
|
@ -11,7 +11,14 @@ import UIKit
|
|||
#endif
|
||||
|
||||
/// A view that allows the user to crop an image.
|
||||
public struct CropImageView<Controls: View>: View {
|
||||
public struct CropImageView<Controls: View, CutHole: View>: View {
|
||||
/// Defines a custom view overlaid on the image cropper.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - offset: The offset binding of the image.
|
||||
/// - scale: The scale binding of the image.
|
||||
/// - rotation: The rotation binding of the image.
|
||||
/// - crop: An async function to trigger crop action. Result will be delivered via ``onCrop``.
|
||||
public typealias ControlClosure<Controls> = (
|
||||
_ offset: Binding<CGSize>,
|
||||
_ scale: Binding<CGFloat>,
|
||||
|
@ -19,6 +26,12 @@ public struct CropImageView<Controls: View>: View {
|
|||
_ crop: @escaping () async -> ()
|
||||
) -> Controls
|
||||
|
||||
/// Defines custom view that indicates the cut hole to users.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - targetSize: The size of the cut hole.
|
||||
public typealias CutHoleClosure<CutHole> = (_ targetSize: CGSize) -> CutHole
|
||||
|
||||
/// Errors that could happen during the cropping process.
|
||||
public enum CropError: Error {
|
||||
/// SwiftUI `ImageRenderer` returned nil when calling `nsImage` or `uiImage`.
|
||||
|
@ -53,13 +66,26 @@ public struct CropImageView<Controls: View>: View {
|
|||
///
|
||||
/// The error should be a ``CropError``.
|
||||
public var onCrop: (Result<PlatformImage, Error>) -> Void
|
||||
/// A custom view overlaid on the image cropper.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - crop: An async function to trigger crop action. Result will be delivered via ``onCrop``.
|
||||
public var controls: ControlClosure<Controls>
|
||||
|
||||
/// Create a ``CropImageView`` with a custom ``controls`` view.
|
||||
var controls: ControlClosure<Controls>
|
||||
var cutHole: CutHoleClosure<CutHole>
|
||||
/// Create a ``CropImageView`` with a custom controls view and a custom cut hole.
|
||||
public init(
|
||||
image: PlatformImage,
|
||||
targetSize: CGSize,
|
||||
targetScale: CGFloat = 1,
|
||||
fulfillTargetFrame: Bool = true,
|
||||
onCrop: @escaping (Result<PlatformImage, Error>) -> Void,
|
||||
@ViewBuilder controls: @escaping ControlClosure<Controls>,
|
||||
@ViewBuilder cutHole: @escaping CutHoleClosure<CutHole>
|
||||
) {
|
||||
self.image = image
|
||||
self.targetSize = targetSize
|
||||
self.targetScale = targetScale
|
||||
self.onCrop = onCrop
|
||||
self.controls = controls
|
||||
self.cutHole = cutHole
|
||||
}
|
||||
/// Create a ``CropImageView`` with a custom controls view and default cut hole.
|
||||
public init(
|
||||
image: PlatformImage,
|
||||
targetSize: CGSize,
|
||||
|
@ -67,23 +93,24 @@ public struct CropImageView<Controls: View>: View {
|
|||
fulfillTargetFrame: Bool = true,
|
||||
onCrop: @escaping (Result<PlatformImage, Error>) -> Void,
|
||||
@ViewBuilder controls: @escaping ControlClosure<Controls>
|
||||
) {
|
||||
) where CutHole == DefaultCutHoleView {
|
||||
self.image = image
|
||||
self.targetSize = targetSize
|
||||
self.targetScale = targetScale
|
||||
self.onCrop = onCrop
|
||||
self.controls = controls
|
||||
self.cutHole = { targetSize in
|
||||
DefaultCutHoleView(targetSize: targetSize)
|
||||
}
|
||||
}
|
||||
/// Create a ``CropImageView`` with the default ``controls`` view.
|
||||
///
|
||||
/// The default ``controls`` view is a simple overlay with a checkmark icon on the bottom-trailing corner to trigger crop action.
|
||||
/// Create a ``CropImageView`` with default UI elements.
|
||||
public init(
|
||||
image: PlatformImage,
|
||||
targetSize: CGSize,
|
||||
targetScale: CGFloat = 1,
|
||||
fulfillTargetFrame: Bool = true,
|
||||
onCrop: @escaping (Result<PlatformImage, Error>) -> Void
|
||||
) where Controls == DefaultControlsView {
|
||||
) where Controls == DefaultControlsView, CutHole == DefaultCutHoleView {
|
||||
self.image = image
|
||||
self.targetSize = targetSize
|
||||
self.targetScale = targetScale
|
||||
|
@ -91,6 +118,9 @@ public struct CropImageView<Controls: View>: View {
|
|||
self.controls = { $offset, $scale, $rotation, crop in
|
||||
DefaultControlsView(offset: $offset, scale: $scale, rotation: $rotation, crop: crop)
|
||||
}
|
||||
self.cutHole = { targetSize in
|
||||
DefaultCutHoleView(targetSize: targetSize)
|
||||
}
|
||||
}
|
||||
|
||||
@State private var offset: CGSize = .zero
|
||||
|
@ -164,10 +194,6 @@ public struct CropImageView<Controls: View>: View {
|
|||
.clipped()
|
||||
}
|
||||
|
||||
var cutHole: some View {
|
||||
DefaultCutHoleView(targetSize: targetSize)
|
||||
}
|
||||
|
||||
var viewSizeReadingView: some View {
|
||||
GeometryReader { geo in
|
||||
Rectangle()
|
||||
|
@ -192,7 +218,7 @@ public struct CropImageView<Controls: View>: View {
|
|||
}
|
||||
|
||||
public var body: some View {
|
||||
cutHole
|
||||
cutHole(targetSize)
|
||||
.background(underlyingImage)
|
||||
.background(viewSizeReadingView)
|
||||
.overlay(control)
|
||||
|
|
|
@ -9,6 +9,7 @@ import SwiftUI
|
|||
|
||||
struct DefaultCutHoleShape: Shape {
|
||||
var size: CGSize
|
||||
var isCircular = false
|
||||
|
||||
var animatableData: AnimatablePair<CGFloat, CGFloat> {
|
||||
get { .init(size.width, size.height) }
|
||||
|
@ -30,10 +31,14 @@ struct DefaultCutHoleShape: Shape {
|
|||
), size: size)
|
||||
|
||||
path.move(to: newRect.origin)
|
||||
path.addLine(to: .init(x: newRect.maxX, y: newRect.minY))
|
||||
path.addLine(to: .init(x: newRect.maxX, y: newRect.maxY))
|
||||
path.addLine(to: .init(x: newRect.minX, y: newRect.maxY))
|
||||
path.addLine(to: newRect.origin)
|
||||
if isCircular {
|
||||
path.addEllipse(in: newRect)
|
||||
} else {
|
||||
path.addLine(to: .init(x: newRect.maxX, y: newRect.minY))
|
||||
path.addLine(to: .init(x: newRect.maxX, y: newRect.maxY))
|
||||
path.addLine(to: .init(x: newRect.minX, y: newRect.maxY))
|
||||
path.addLine(to: newRect.origin)
|
||||
}
|
||||
path.closeSubpath()
|
||||
return Path(path)
|
||||
}
|
||||
|
@ -46,6 +51,13 @@ struct DefaultCutHoleShape_Previews: PreviewProvider {
|
|||
.fill(style: FillStyle(eoFill: true))
|
||||
.foregroundColor(.black.opacity(0.6))
|
||||
}
|
||||
.previewDisplayName("Default")
|
||||
VStack {
|
||||
DefaultCutHoleShape(size: .init(width: 100, height: 100), isCircular: true)
|
||||
.fill(style: FillStyle(eoFill: true))
|
||||
.foregroundColor(.black.opacity(0.6))
|
||||
}
|
||||
.previewDisplayName("Circular")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,27 +7,46 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct DefaultCutHoleView: View {
|
||||
/// The default cut hole view. Stroke and mask color can be adjusted.
|
||||
public struct DefaultCutHoleView: View {
|
||||
var targetSize: CGSize
|
||||
var showStroke = true
|
||||
var strokeWidth: CGFloat
|
||||
var maskColor: Color
|
||||
var isCircular: Bool
|
||||
|
||||
/// Initialize a default rectangular or circular cut hole view with specified target size, stroke width and mask color.
|
||||
public init(
|
||||
targetSize: CGSize,
|
||||
isCircular: Bool = false,
|
||||
strokeWidth: CGFloat = 1,
|
||||
maskColor: Color = .black.opacity(0.6)
|
||||
) {
|
||||
self.targetSize = targetSize
|
||||
self.strokeWidth = strokeWidth
|
||||
self.maskColor = maskColor
|
||||
self.isCircular = isCircular
|
||||
}
|
||||
|
||||
var background: some View {
|
||||
DefaultCutHoleShape(size: targetSize)
|
||||
.fill(style: FillStyle(eoFill: true))
|
||||
.foregroundColor(.black.opacity(0.6))
|
||||
.foregroundColor(maskColor)
|
||||
}
|
||||
|
||||
var stroke: some View {
|
||||
Rectangle()
|
||||
.strokeBorder(style: .init(lineWidth: 1))
|
||||
.frame(width: targetSize.width + 2, height: targetSize.height + 2)
|
||||
.strokeBorder(style: .init(lineWidth: strokeWidth))
|
||||
.frame(
|
||||
width: targetSize.width + strokeWidth * 2,
|
||||
height: targetSize.height + strokeWidth * 2
|
||||
)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
public var body: some View {
|
||||
background
|
||||
.allowsHitTesting(false)
|
||||
.overlay(showStroke ? stroke : nil)
|
||||
.overlay(strokeWidth > 0 ? stroke : nil)
|
||||
.animation(.default, value: targetSize)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue