中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

怎么利用SwiftUI實現可縮放的圖片預覽器

發布時間:2021-09-13 15:16:25 來源:億速云 閱讀:265 作者:小新 欄目:開發技術

這篇文章給大家分享的是有關怎么利用SwiftUI實現可縮放的圖片預覽器的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。

    實現過程

    程序的初步構想

    要做一個程序,首先肯定是給它起個名字。既然是圖片預覽器(Image Previewer),再加上我自己習慣用的前綴 LBJ,就把它命名為 LBJImagePreviewer 吧。

    既然是圖片預覽器,所以需要外部提供圖片給我們;然后是可縮放,所以需要一個最大的縮放倍數。有了這些思考,可以把 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 設置了一個默認值。

    另外還可以看到 maxScale 的默認值是通過 LBJImagePreviewerConstants.defaultMaxScale 來設置的,而不是直接寫 16,這樣做的目的是把代碼中用到的數值和經驗值等整理到一個地方,方便后續的修改。這是一個好的編程習慣。

    細心的讀者可能還會注意到 LBJImagePreviewerConstants 是一個 enum 類型。為什么不用 struct 或者 class 呢? 點擊這里可以找到答案 >>

    顯示 UIImage

    當用戶點開圖片預覽器,當然是希望圖片等比例占據整個圖片預覽器,所以需要知道圖片預覽器當前的尺寸和圖片尺寸,從而通過計算讓圖片等比例占據整個圖片預覽器。

    圖片預覽器當前的尺寸可以通過 GeometryReader 得到;圖片大小可以直接從 UIImage 得到。所以我們可以把

    LBJImagePreviewer 的 body 定義如下:

    public struct LBJImagePreviewer: View {
      public var body: some View {
        GeometryReader { geometry in                  // 用于獲取圖片預覽器所占據的尺寸
          let imageSize = imageSize(fits: geometry)   // 計算圖片等比例鋪滿整個預覽器時的尺寸
          ScrollView([.vertical, .horizontal]) {
            imageContent
              .frame(
                width: imageSize.width,
                height: imageSize.height
              )
              .padding(.vertical, (max(0, geometry.size.height - imageSize.height) / 2))  // 讓圖片在預覽器垂直方向上居中
          }
          .background(Color.black)
        }
        .ignoresSafeArea()
      }
    }
    
    private extension LBJImagePreviewer {
      var imageContent: some View {
        Image(uiImage: uiImage)
          .resizable()
          .aspectRatio(contentMode: .fit)
      }
    
      /// 計算圖片等比例鋪滿整個預覽器時的尺寸
      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 的內容可以滾動起來,必須要讓它的尺寸大于 ScrollView 的尺寸。沿著這個思路可以想到,我們可修改 imageContent 的大小來實現放大縮小,也就是修改下面這個 frame:

    imageContent
      .frame(
        width: imageSize.width,
        height: imageSize.height
      )

    我們通過用 imageSize(fits: geometry) 的返回值乘以一個倍數,就可以改變 frame 的大小。這個倍數就是放大的倍數。因此我們定義一個變量記錄倍數,然后通過雙擊手勢改變它,就能把圖片放大縮小,有變動的代碼如下:

    // 當前的放大倍數
    @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
            }
          }
        }
    }
    
    // 縮放時圖片的大小
    func zoomedImageSize(fits geometry: GeometryProxy) -> CGSize {
      imageSize(fits: geometry) * zoomScale
    }

    放大手勢縮放

    放大手勢縮放的原理與雙擊一樣,都是想辦法通過修改 zoomScale 來達到縮放圖片的目的。SwiftUI 中的放大手勢是 MagnificationGesture。代碼變動如下:

    // 穩定的放大倍數,放大手勢以此為基準來改變 zoomScale 的值
    @State
    private var steadyStateZoomScale: CGFloat = 1
    
    // 放大手勢縮放過程中產生的倍數變化
    @GestureState
    private var gestureZoomScale: CGFloat = 1
    
    // 變成了只讀屬性,當前圖片被放大的倍數
    var zoomScale: CGFloat {
      steadyStateZoomScale * gestureZoomScale
    }
    
    func zoomGesture() -> some Gesture {
      MagnificationGesture()
        .updating($gestureZoomScale) { latestGestureScale, gestureZoomScale, _ in
          // 縮放過程中,不斷地更新 `gestureZoomScale` 的值
          gestureZoomScale = latestGestureScale
        }
        .onEnded { gestureScaleAtEnd in
          // 手勢結束,更新 steadyStateZoomScale 的值;
          // 此時 gestureZoomScale 的值會被重置為初始值 1
          steadyStateZoomScale *= gestureScaleAtEnd
          makeSureZoomScaleInBounds()
        }
    }
    
    // 確保放大倍數在我們設置的范圍內;Haptics 是加上震動效果
    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()
      }
    }

    到目前為止,我們的圖片預覽器就實現了。是不是很簡單????

    但是仔細回顧一下代碼,目前這個圖片預覽器只支持 UIImage 的預覽。如果預覽器的用戶查看的圖片是 Image 呢?又或者是其他任何通過 View 來顯示的圖片呢?所以我們還得進一步增強預覽器的可用性。

    預覽任意 View

    既然是任意 View,很容易想到泛型。我們可以將 LBJImagePreviewer 定義為泛型。代碼變動如下:

    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 來初始化預覽器,還需要傳入 aspectRatio (寬高比),因為不能從傳入的 content 得到它的比例,所以需要外部告訴我們。

    通過修改,目前的圖片預覽器就可以支持任意 View 的縮放了。但如果我們就是要預覽 UIImage,在初始化預覽器的時候,它還要求指定泛型的具體類型。例如:

    // EmptyView 可以換成其他任意遵循 `View` 協議的類型
    LBJImagePreviewer<EmptyView>(uiImage: UIImage(named: "IMG_0001")!)

    如果不加上 <EmptyView> 就會報錯,這顯然是不合理的設計。我們還得進一步優化。

    將 UIImage 從 LBJImagePreviewer 剝離

    在預覽 UIImage 時,不需要用到任何與泛型有關的代碼,所以只能將 UIImage 從 LBJImagePreviewer 剝離出來。

    從復用代碼的角度出發,我們可以想到新定義一個 LBJUIImagePreviewer 專門用于預覽 UIImage,內部實現直接調用 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 的職責只負責縮放 View,所以應該給它重命名,我將它改為 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
      }
    }

    另外,為了方便預覽 Image 類型的圖片,我們可以定義一個類型:

    public typealias LBJImagePreviewer = LBJViewZoomer<Image>

    至此,我們的圖片預覽器就真正完成了。我們一共給外部暴露了三個類型:

    LBJUIImagePreviewer
    LBJImagePreviewer
    LBJViewZoomer

    感謝各位的閱讀!關于“怎么利用SwiftUI實現可縮放的圖片預覽器”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    绥江县| 三台县| 西乌| 穆棱市| 深水埗区| 西昌市| 长垣县| 乡城县| 吉水县| 历史| 鄯善县| 四会市| 印江| 石楼县| 芜湖市| 织金县| 合水县| 灵石县| 庆云县| 惠水县| 永昌县| 西乌珠穆沁旗| 克拉玛依市| 平湖市| 永春县| 焦作市| 达州市| 大冶市| 将乐县| 萝北县| 宁河县| 泽库县| 河池市| 汾西县| 比如县| 汤阴县| 墨江| 浦江县| 大理市| 法库县| 宜君县|