您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關(guān)怎么利用SwiftUI實(shí)現(xiàn)可縮放的圖片預(yù)覽器的內(nèi)容。小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,一起跟隨小編過來看看吧。
要做一個(gè)程序,首先肯定是給它起個(gè)名字。既然是圖片預(yù)覽器(Image Previewer),再加上我自己習(xí)慣用的前綴 LBJ,就把它命名為 LBJImagePreviewer 吧。
既然是圖片預(yù)覽器,所以需要外部提供圖片給我們;然后是可縮放,所以需要一個(gè)最大的縮放倍數(shù)。有了這些思考,可以把 LBJImagePreviewer 簡單定義為:
import SwiftUI public struct LBJImagePreviewer: View { private let uiImage: UIImage private let maxScale: CGFloat public init(uiImage: UIImage, maxScale: CGFloat = LBJImagePreviewerConstants.defaultMaxScale) { self.uiImage = uiImage self.maxScale = maxScale } public var body: some View { EmptyView() } } public enum LBJImagePreviewerConstants { public static let defaultMaxScale: CGFloat = 16 }
在上面代碼中,給 maxScale 設(shè)置了一個(gè)默認(rèn)值。
另外還可以看到 maxScale 的默認(rèn)值是通過 LBJImagePreviewerConstants.defaultMaxScale 來設(shè)置的,而不是直接寫 16,這樣做的目的是把代碼中用到的數(shù)值和經(jīng)驗(yàn)值等整理到一個(gè)地方,方便后續(xù)的修改。這是一個(gè)好的編程習(xí)慣。
細(xì)心的讀者可能還會(huì)注意到 LBJImagePreviewerConstants 是一個(gè) enum 類型。為什么不用 struct 或者 class 呢? 點(diǎn)擊這里可以找到答案 >>
當(dāng)用戶點(diǎn)開圖片預(yù)覽器,當(dāng)然是希望圖片等比例占據(jù)整個(gè)圖片預(yù)覽器,所以需要知道圖片預(yù)覽器當(dāng)前的尺寸和圖片尺寸,從而通過計(jì)算讓圖片等比例占據(jù)整個(gè)圖片預(yù)覽器。
圖片預(yù)覽器當(dāng)前的尺寸可以通過 GeometryReader 得到;圖片大小可以直接從 UIImage 得到。所以我們可以把
LBJImagePreviewer 的 body 定義如下:
public struct LBJImagePreviewer: View { public var body: some View { GeometryReader { geometry in // 用于獲取圖片預(yù)覽器所占據(jù)的尺寸 let imageSize = imageSize(fits: geometry) // 計(jì)算圖片等比例鋪滿整個(gè)預(yù)覽器時(shí)的尺寸 ScrollView([.vertical, .horizontal]) { imageContent .frame( width: imageSize.width, height: imageSize.height ) .padding(.vertical, (max(0, geometry.size.height - imageSize.height) / 2)) // 讓圖片在預(yù)覽器垂直方向上居中 } .background(Color.black) } .ignoresSafeArea() } } private extension LBJImagePreviewer { var imageContent: some View { Image(uiImage: uiImage) .resizable() .aspectRatio(contentMode: .fit) } /// 計(jì)算圖片等比例鋪滿整個(gè)預(yù)覽器時(shí)的尺寸 func imageSize(fits geometry: GeometryProxy) -> CGSize { let hZoom = geometry.size.width / uiImage.size.width let vZoom = geometry.size.height / uiImage.size.height return uiImage.size * min(hZoom, vZoom) } } extension CGSize { /// CGSize 乘以 CGFloat static func * (lhs: Self, rhs: CGFloat) -> CGSize { CGSize(width: lhs.width * rhs, height: lhs.height * rhs) } }
這樣我們就把圖片用 ScrollView 顯示出來了。
想要 ScrollView 的內(nèi)容可以滾動(dòng)起來,必須要讓它的尺寸大于 ScrollView 的尺寸。沿著這個(gè)思路可以想到,我們可修改 imageContent 的大小來實(shí)現(xiàn)放大縮小,也就是修改下面這個(gè) frame:
imageContent .frame( width: imageSize.width, height: imageSize.height )
我們通過用 imageSize(fits: geometry) 的返回值乘以一個(gè)倍數(shù),就可以改變 frame 的大小。這個(gè)倍數(shù)就是放大的倍數(shù)。因此我們定義一個(gè)變量記錄倍數(shù),然后通過雙擊手勢改變它,就能把圖片放大縮小,有變動(dòng)的代碼如下:
// 當(dāng)前的放大倍數(shù) @State private var zoomScale: CGFloat = 1 public var body: some View { GeometryReader { geometry in let zoomedImageSize = zoomedImageSize(fits: geometry) ScrollView([.vertical, .horizontal]) { imageContent .gesture(doubleTapGesture()) .frame( width: zoomedImageSize.width, height: zoomedImageSize.height ) .padding(.vertical, (max(0, geometry.size.height - zoomedImageSize.height) / 2)) } .background(Color.black) } .ignoresSafeArea() } // 雙擊手勢 func doubleTapGesture() -> some Gesture { TapGesture(count: 2) .onEnded { withAnimation { if zoomScale > 1 { zoomScale = 1 } else { zoomScale = maxScale } } } } // 縮放時(shí)圖片的大小 func zoomedImageSize(fits geometry: GeometryProxy) -> CGSize { imageSize(fits: geometry) * zoomScale }
放大手勢縮放的原理與雙擊一樣,都是想辦法通過修改 zoomScale 來達(dá)到縮放圖片的目的。SwiftUI 中的放大手勢是 MagnificationGesture。代碼變動(dòng)如下:
// 穩(wěn)定的放大倍數(shù),放大手勢以此為基準(zhǔn)來改變 zoomScale 的值 @State private var steadyStateZoomScale: CGFloat = 1 // 放大手勢縮放過程中產(chǎn)生的倍數(shù)變化 @GestureState private var gestureZoomScale: CGFloat = 1 // 變成了只讀屬性,當(dāng)前圖片被放大的倍數(shù) var zoomScale: CGFloat { steadyStateZoomScale * gestureZoomScale } func zoomGesture() -> some Gesture { MagnificationGesture() .updating($gestureZoomScale) { latestGestureScale, gestureZoomScale, _ in // 縮放過程中,不斷地更新 `gestureZoomScale` 的值 gestureZoomScale = latestGestureScale } .onEnded { gestureScaleAtEnd in // 手勢結(jié)束,更新 steadyStateZoomScale 的值; // 此時(shí) gestureZoomScale 的值會(huì)被重置為初始值 1 steadyStateZoomScale *= gestureScaleAtEnd makeSureZoomScaleInBounds() } } // 確保放大倍數(shù)在我們設(shè)置的范圍內(nèi);Haptics 是加上震動(dòng)效果 func makeSureZoomScaleInBounds() { withAnimation { if steadyStateZoomScale < 1 { steadyStateZoomScale = 1 Haptics.impact(.light) } else if steadyStateZoomScale > maxScale { steadyStateZoomScale = maxScale Haptics.impact(.light) } } } // Haptics.swift enum Haptics { static func impact(_ style: UIImpactFeedbackGenerator.FeedbackStyle) { let generator = UIImpactFeedbackGenerator(style: style) generator.impactOccurred() } }
到目前為止,我們的圖片預(yù)覽器就實(shí)現(xiàn)了。是不是很簡單????
但是仔細(xì)回顧一下代碼,目前這個(gè)圖片預(yù)覽器只支持 UIImage 的預(yù)覽。如果預(yù)覽器的用戶查看的圖片是 Image 呢?又或者是其他任何通過 View 來顯示的圖片呢?所以我們還得進(jìn)一步增強(qiáng)預(yù)覽器的可用性。
既然是任意 View,很容易想到泛型。我們可以將 LBJImagePreviewer 定義為泛型。代碼變動(dòng)如下:
public struct LBJImagePreviewer<Content: View>: View { private let uiImage: UIImage? private let contentInfo: (content: Content, aspectRatio: CGFloat)? private let maxScale: CGFloat public init( uiImage: UIImage, maxScale: CGFloat = LBJImagePreviewerConstants.defaultMaxScale ) { self.uiImage = uiImage self.contentInfo = nil self.maxScale = maxScale } public init( content: Content, aspectRatio: CGFloat, maxScale: CGFloat = LBJImagePreviewerConstants.defaultMaxScale ) { self.uiImage = nil self.contentInfo = (content, aspectRatio) self.maxScale = maxScale } @ViewBuilder var imageContent: some View { if let uiImage = uiImage { Image(uiImage: uiImage) .resizable() .aspectRatio(contentMode: .fit) } else if let content = contentInfo?.content { if let image = content as? Image { image.resizable() } else { content } } } func imageSize(fits geometry: GeometryProxy) -> CGSize { if let uiImage = uiImage { let hZoom = geometry.size.width / uiImage.size.width let vZoom = geometry.size.height / uiImage.size.height return uiImage.size * min(hZoom, vZoom) } else if let contentInfo = contentInfo { let geoRatio = geometry.size.width / geometry.size.height let imageRatio = contentInfo.aspectRatio let width: CGFloat let height: CGFloat if imageRatio < geoRatio { height = geometry.size.height width = height * imageRatio } else { width = geometry.size.width height = width / imageRatio } return .init(width: width, height: height) } return .zero } }
從代碼中可以看到,如果是用 content 來初始化預(yù)覽器,還需要傳入 aspectRatio (寬高比),因?yàn)椴荒軓膫魅氲?content 得到它的比例,所以需要外部告訴我們。
通過修改,目前的圖片預(yù)覽器就可以支持任意 View 的縮放了。但如果我們就是要預(yù)覽 UIImage,在初始化預(yù)覽器的時(shí)候,它還要求指定泛型的具體類型。例如:
// EmptyView 可以換成其他任意遵循 `View` 協(xié)議的類型 LBJImagePreviewer<EmptyView>(uiImage: UIImage(named: "IMG_0001")!)
如果不加上 <EmptyView> 就會(huì)報(bào)錯(cuò),這顯然是不合理的設(shè)計(jì)。我們還得進(jìn)一步優(yōu)化。
在預(yù)覽 UIImage 時(shí),不需要用到任何與泛型有關(guān)的代碼,所以只能將 UIImage 從 LBJImagePreviewer 剝離出來。
從復(fù)用代碼的角度出發(fā),我們可以想到新定義一個(gè) LBJUIImagePreviewer 專門用于預(yù)覽 UIImage,內(nèi)部實(shí)現(xiàn)直接調(diào)用 LBJImagePreviewer 即可。
LBJUIImagePreviewer 的代碼如下:
public struct LBJUIImagePreviewer: View { private let uiImage: UIImage private let maxScale: CGFloat public init( uiImage: UIImage, maxScale: CGFloat = LBJImagePreviewerConstants.defaultMaxScale ) { self.uiImage = uiImage self.maxScale = maxScale } public var body: some View { // LBJImagePreviewer 重命名為 LBJViewZoomer LBJViewZoomer( content: Image(uiImage: uiImage), aspectRatio: uiImage.size.width / uiImage.size.height, maxScale: maxScale ) } }
將 UIImage 從 LBJImagePreviewer 剝離后,LBJImagePreviewer 的職責(zé)只負(fù)責(zé)縮放 View,所以應(yīng)該給它重命名,我將它改為 LBJViewZoomer。完整代碼如下:
public struct LBJViewZoomer<Content: View>: View { private let contentInfo: (content: Content, aspectRatio: CGFloat) private let maxScale: CGFloat public init( content: Content, aspectRatio: CGFloat, maxScale: CGFloat = LBJImagePreviewerConstants.defaultMaxScale ) { self.contentInfo = (content, aspectRatio) self.maxScale = maxScale } @State private var steadyStateZoomScale: CGFloat = 1 @GestureState private var gestureZoomScale: CGFloat = 1 public var body: some View { GeometryReader { geometry in let zoomedImageSize = zoomedImageSize(in: geometry) ScrollView([.vertical, .horizontal]) { imageContent .gesture(doubleTapGesture()) .gesture(zoomGesture()) .frame( width: zoomedImageSize.width, height: zoomedImageSize.height ) .padding(.vertical, (max(0, geometry.size.height - zoomedImageSize.height) / 2)) } .background(Color.black) } .ignoresSafeArea() } } // MARK: - Subviews private extension LBJViewZoomer { @ViewBuilder var imageContent: some View { if let image = contentInfo.content as? Image { image .resizable() .aspectRatio(contentMode: .fit) } else { contentInfo.content } } } // MARK: - Gestures private extension LBJViewZoomer { // MARK: Tap func doubleTapGesture() -> some Gesture { TapGesture(count: 2) .onEnded { withAnimation { if zoomScale > 1 { steadyStateZoomScale = 1 } else { steadyStateZoomScale = maxScale } } } } // MARK: Zoom var zoomScale: CGFloat { steadyStateZoomScale * gestureZoomScale } func zoomGesture() -> some Gesture { MagnificationGesture() .updating($gestureZoomScale) { latestGestureScale, gestureZoomScale, _ in gestureZoomScale = latestGestureScale } .onEnded { gestureScaleAtEnd in steadyStateZoomScale *= gestureScaleAtEnd makeSureZoomScaleInBounds() } } func makeSureZoomScaleInBounds() { withAnimation { if steadyStateZoomScale < 1 { steadyStateZoomScale = 1 Haptics.impact(.light) } else if steadyStateZoomScale > maxScale { steadyStateZoomScale = maxScale Haptics.impact(.light) } } } } // MARK: - Helper Methods private extension LBJViewZoomer { func imageSize(fits geometry: GeometryProxy) -> CGSize { let geoRatio = geometry.size.width / geometry.size.height let imageRatio = contentInfo.aspectRatio let width: CGFloat let height: CGFloat if imageRatio < geoRatio { height = geometry.size.height width = height * imageRatio } else { width = geometry.size.width height = width / imageRatio } return .init(width: width, height: height) } func zoomedImageSize(in geometry: GeometryProxy) -> CGSize { imageSize(fits: geometry) * zoomScale } }
另外,為了方便預(yù)覽 Image 類型的圖片,我們可以定義一個(gè)類型:
public typealias LBJImagePreviewer = LBJViewZoomer<Image>
至此,我們的圖片預(yù)覽器就真正完成了。我們一共給外部暴露了三個(gè)類型:
LBJUIImagePreviewer LBJImagePreviewer LBJViewZoomer
感謝各位的閱讀!關(guān)于“怎么利用SwiftUI實(shí)現(xiàn)可縮放的圖片預(yù)覽器”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,讓大家可以學(xué)到更多知識(shí),如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到吧!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。