我正在试着解决一个没有成功的问题,希望有人能帮忙.

我找过类似的帖子,但没能找到任何解决我问题的方法.

我的场景如下: 我有一张UIView,上面可以放一些其他的UIView.这些可以使用手势识别器移动、zoom 和旋转(这里没有问题). 用户可以更改主视图(画布)的纵横比,而我的问题是try zoom 画布的内容以适应新的目标大小.

有许多帖子主题类似,例如:

calculate new size and location on a CGRect

How to create an image of specific size from UIView

但这些并不能解决比率多次变化的问题.

My Approach:

当我更改画布的纵横比时,我使用AVFoundation来计算画布的子视图应该适合的纵横比适合的矩形:

let sourceRectangleSize = canvas.frame.size

canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
view.layoutIfNeeded()

let destinationRectangleSize = canvas.frame.size

let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
ratioVisualizer.frame = aspectFittedFrame

Test Cases The Red frame is simply to visualise the Aspect Fitted Rectangle. As you can see whilst the aspect fitted rectangle is correct, the scaling of objects isn't working. This is especially true when I apply scale and rotation to the subviews (CanvasElement).

我zoom 对象的逻辑显然是错误的:

@objc
private func setRatio(_ control: UISegmentedControl) {
  guard let aspect = Aspect(rawValue: control.selectedSegmentIndex) else { return }
  
  let sourceRectangleSize = canvas.frame.size
 
  canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
  view.layoutIfNeeded()
 
  let destinationRectangleSize = canvas.frame.size
  
  let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
  ratioVisualizer.frame = aspectFittedFrame
  
  let scale = min(aspectFittedFrame.size.width/canvas.frame.width, aspectFittedFrame.size.height/canvas.frame.height)
  
  for case let canvasElement as CanvasElement in canvas.subviews {
  
    canvasElement.frame.size = CGSize(
      width: canvasElement.baseFrame.width * scale,
      height: canvasElement.baseFrame.height * scale
    )
    canvasElement.frame.origin = CGPoint(
      x: aspectFittedFrame.origin.x + canvasElement.baseFrame.origin.x * scale,
      y:  aspectFittedFrame.origin.y + canvasElement.baseFrame.origin.y * scale
    )
  }
}

如果有帮助,我还附带了CanvasElement类:

final class CanvasElement: UIView {
  
  var rotation: CGFloat = 0
  var baseFrame: CGRect = .zero

  var id: String = UUID().uuidString
  
  // MARK: - Initialization
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    storeState()
    setupGesture()
  }
  
  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }
  
  // MARK: - Gesture Setup
  
  private func setupGesture() {
    let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGesture(_:)))
    let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(pinchGesture(_:)))
    let rotateGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(rotateGesture(_:)))
    addGestureRecognizer(panGestureRecognizer)
    addGestureRecognizer(pinchGestureRecognizer)
    addGestureRecognizer(rotateGestureRecognizer)
  }
  
  // MARK: - Touches
  
  override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesBegan(touches, with: event)
    moveToFront()
  }
  
  //MARK: - Gestures
  
  @objc
  private func panGesture(_ sender: UIPanGestureRecognizer) {
    let move = sender.translation(in: self)
    transform = transform.concatenating(.init(translationX: move.x, y: move.y))
    sender.setTranslation(CGPoint.zero, in: self)
    storeState()
  }
  
  @objc
  private func pinchGesture(_ sender: UIPinchGestureRecognizer) {
    transform = transform.scaledBy(x: sender.scale, y: sender.scale)
    sender.scale = 1
    storeState()
  }
  
  @objc
  private func rotateGesture(_ sender: UIRotationGestureRecognizer) {
    rotation += sender.rotation
    transform = transform.rotated(by: sender.rotation)
    sender.rotation = 0
    storeState()
  }
  
  // MARK: - Miscelaneous
  
  func moveToFront() {
    superview?.bringSubviewToFront(self)
  }
  
  public func rotated(by degrees: CGFloat) {
    transform = transform.rotated(by: degrees)
    rotation += degrees
  }
  
  func storeState() {
    print("""
    Element Frame = \(frame)
    Element Bounds = \(bounds)
    Element Center = \(center)
    """)
    baseFrame = frame
  }
}

任何帮助、建议、方法,加上一些实际的例子,都是很棒的.我不期望任何人提供完整的源代码,但我可以使用一些东西作为基础.

感谢您抽出时间阅读我的问题.

推荐答案

有几个建议...

首先,使用CanvasElement时,如果视图已旋转,则平移不能正常工作.

因此,与其使用平移变换来移动视图,不如更改.center本身.此外,在平移时,我们希望在superview中使用翻译,而不是在视图本身中:

@objc
func panGesture(_ gest: UIPanGestureRecognizer) {
    // change the view's .center instead of applying translate transform
    //  use translation in superview, not in self
    guard let superV = superview else { return }
    let translation = gest.translation(in: superV)
    center = CGPoint(x: center.x + translation.x, y: center.y + translation.y)
    gest.setTranslation(CGPoint.zero, in: superV)
}

现在,当我们想要在"画布"改变大小时zoom 子视图时,我们可以这样做…

我们将跟踪"先前"界限并使用"新界限"来计算比例:

let newBounds: CGRect = bounds
    
let scW: CGFloat = newBounds.size.width / prevBounds.size.width
let scH: CGFloat = newBounds.size.height / prevBounds.size.height
    
for case let v as CanvasElement in subviews {
    // reset transform before scaling / positioning
    let tr = v.transform
    v.transform = .identity

    let w = v.frame.width * scW
    let h = v.frame.height * scH
    let cx = v.center.x * scW
    let cy = v.center.y * scH

    v.frame.size = CGSize(width: w, height: h)
    v.center = CGPoint(x: cx, y: cy)

    // re-apply transform
    v.transform = tr
}

prevBounds = newBounds

下面是一个完整的示例实现.请注意:这是100,它的目的不是"生产就绪".

import UIKit

// MARK: enum to provide strings and aspect ratio values
enum Aspect: Int, Codable, CaseIterable {
    case a1to1
    case a16to9
    case a3to2
    case a4to3
    case a9to16
    var stringValue: String {
        switch self {
        case .a1to1:
            return "1:1"
        case .a16to9:
            return "16:9"
        case .a3to2:
            return "3:2"
        case .a4to3:
            return "4:3"
        case .a9to16:
            return "9:16"
        }
    }
    var aspect: CGFloat {
        switch self {
        case .a1to1:
            return 1
        case .a16to9:
            return 9.0 / 16.0
        case .a3to2:
            return 2.0 / 3.0
        case .a4to3:
            return 3.0 / 4.0
        case .a9to16:
            return 16.0 / 9.0
        }
    }
}

class EditorView: UIView {
    // no code -
    //  just makes it easier to identify
    //  this view when debugging
}

// CanvasElement views will be added as subviews
//  this handles the scaling / positioning when the bounds changes
//  also (optionally) draws a grid (for use during development)
class CanvasView: UIView {
    
    public var showGrid: Bool = true

    private let gridLayer: CAShapeLayer = CAShapeLayer()
    
    private var prevBounds: CGRect = .zero
    
    // MARK: init
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {

        gridLayer.fillColor = UIColor.clear.cgColor
        gridLayer.strokeColor = UIColor.red.cgColor
        gridLayer.lineWidth = 1
        
        layer.addSublayer(gridLayer)
        
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        
        // MARK: 10 x 10 grid
        if showGrid {
            // draw a grid on the inside of the bounds
            //  so the edges are not 1/2 point width
            let gridBounds: CGRect = bounds.insetBy(dx: 0.5, dy: 0.5)
            
            let path: UIBezierPath = UIBezierPath()
            
            let w: CGFloat = gridBounds.width / 10.0
            let h: CGFloat = gridBounds.height / 10.0
            
            var p: CGPoint = .zero
            
            p = CGPoint(x: gridBounds.minX, y: gridBounds.minY)
            for _ in 0...10 {
                path.move(to: p)
                path.addLine(to: CGPoint(x: p.x, y: gridBounds.maxY))
                p.x += w
            }
            
            p = CGPoint(x: gridBounds.minX, y: gridBounds.minY)
            for _ in 0...10 {
                path.move(to: p)
                path.addLine(to: CGPoint(x: gridBounds.maxX, y: p.y))
                p.y += h
            }
            
            gridLayer.path = path.cgPath
        }
        
        // MARK: update subviews
        // we only want to move/scale the subviews if
        //  the bounds has > 0 width and height and
        //  prevBounds has > 0 width and height and
        //  the bounds has changed

        guard bounds != prevBounds,
              bounds.width > 0, prevBounds.width > 0,
              bounds.height > 0, prevBounds.height > 0
        else { return }

        let newBounds: CGRect = bounds
        
        let scW: CGFloat = newBounds.size.width / prevBounds.size.width
        let scH: CGFloat = newBounds.size.height / prevBounds.size.height
        
        for case let v as CanvasElement in subviews {
            // reset transform before scaling / positioning
            let tr = v.transform
            v.transform = .identity

            let w = v.frame.width * scW
            let h = v.frame.height * scH
            let cx = v.center.x * scW
            let cy = v.center.y * scH

            v.frame.size = CGSize(width: w, height: h)
            v.center = CGPoint(x: cx, y: cy)

            // re-apply transform
            v.transform = tr
        }

        prevBounds = newBounds
    }
    
    override var bounds: CGRect {
        willSet {
            prevBounds = bounds
        }
    }
}

// self-contained Pan/Pinch/Rotate view
//  set allowSimultaneous to TRUE to enable
//  simultaneous gestures
class CanvasElement: UIView, UIGestureRecognizerDelegate {
    
    public var allowSimultaneous: Bool = false
    
    // MARK: init
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {
        
        isUserInteractionEnabled = true
        isMultipleTouchEnabled = true
        
        let panG = UIPanGestureRecognizer(target: self, action: #selector(panGesture(_:)))
        let pinchG = UIPinchGestureRecognizer(target: self, action: #selector(pinchGesture(_:)))
        let rotateG = UIRotationGestureRecognizer(target: self, action: #selector(rotateGesture(_:)))
        
        [panG, pinchG, rotateG].forEach { g in
            g.delegate = self
            addGestureRecognizer(g)
        }
        
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        // unwrap optional superview
        guard let superV = superview else { return }
        superV.bringSubviewToFront(self)
    }
    
    // MARK: UIGestureRecognizer Methods
    
    @objc
    func panGesture(_ gest: UIPanGestureRecognizer) {
        // change the view's .center instead of applying translate transform
        //  use translation in superview, not in self
        guard let superV = superview else { return }
        let translation = gest.translation(in: superV)
        center = CGPoint(x: center.x + translation.x, y: center.y + translation.y)
        gest.setTranslation(CGPoint.zero, in: superV)
    }
    
    @objc
    func pinchGesture(_ gest: UIPinchGestureRecognizer) {
        // apply scale transform
        transform = transform.scaledBy(x: gest.scale, y: gest.scale)
        gest.scale = 1
    }
    
    @objc
    func rotateGesture(_ gest : UIRotationGestureRecognizer) {
        // apply rotate transform
        transform = transform.rotated(by: gest.rotation)
        gest.rotation = 0
    }
    
    // MARK: UIGestureRecognizerDelegate Methods
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return allowSimultaneous
    }
    
}

// example view controller
//  Aspect Ratio segmented control
//      changes the Aspect Ratio of the Editor View
//  includes triple-tap gesture to cycle through
//      3 "starting subview" layouts
class ViewController: UIViewController, UIGestureRecognizerDelegate {
    
    let editorView: EditorView = {
        let v = EditorView()
        v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    
    let canvasView: CanvasView = {
        let v = CanvasView()
        v.backgroundColor = .yellow
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    
    // segmented control for selecting Aspect Ratio
    let aspectRatioSeg: UISegmentedControl = {
        let v = UISegmentedControl()
        v.setContentCompressionResistancePriority(.required, for: .vertical)
        v.setContentHuggingPriority(.required, for: .vertical)
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    
    // this will be changed by the Aspect Ratio segmented control
    var evAspectConstraint: NSLayoutConstraint!
    
    // used to cycle through intitial subviews layout
    var layoutMode: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = UIColor(white: 0.99, alpha: 1.0)
        
        // container view for laying out editor view
        let containerView: UIView = {
            let v = UIView()
            v.backgroundColor = .cyan
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()

        // setup the aspect ratio segmented control
        for (idx, m) in Aspect.allCases.enumerated() {
            aspectRatioSeg.insertSegment(withTitle: m.stringValue, at: idx, animated: false)
        }
        
        // add canvas view to editor view
        editorView.addSubview(canvasView)
        
        // add editor view to container view
        containerView.addSubview(editorView)
        
        // add container view to self's view
        view.addSubview(containerView)
        
        // add UI Aspect Ratio segmented control to self's view
        view.addSubview(aspectRatioSeg)
        
        // always respect the safe area
        let safeG = view.safeAreaLayoutGuide
        
        // editor view inset from container view sides
        let evInset: CGFloat = 0
        
        // canvas view inset from editor view sides
        let cvInset: CGFloat = 0
        
        // these sets of constraints will make the Editor View and the Canvas View
        //  as large as their superviews (with "Inset Edge Padding" if set above)
        //  while maintaining aspect ratios and centering
        let evMaxW = editorView.widthAnchor.constraint(lessThanOrEqualTo: containerView.widthAnchor, constant: -evInset)
        let evMaxH = editorView.heightAnchor.constraint(lessThanOrEqualTo: containerView.heightAnchor, constant: -evInset)
        
        let evW = editorView.widthAnchor.constraint(equalTo: containerView.widthAnchor)
        let evH = editorView.heightAnchor.constraint(equalTo: containerView.heightAnchor)
        evW.priority = .required - 1
        evH.priority = .required - 1
        
        let cvMaxW = canvasView.widthAnchor.constraint(lessThanOrEqualTo: editorView.widthAnchor, constant: -cvInset)
        let cvMaxH = canvasView.heightAnchor.constraint(lessThanOrEqualTo: editorView.heightAnchor, constant: -cvInset)
        
        let cvW = canvasView.widthAnchor.constraint(equalTo: editorView.widthAnchor)
        let cvH = canvasView.heightAnchor.constraint(equalTo: editorView.heightAnchor)
        cvW.priority = .required - 1
        cvH.priority = .required - 1
        
        // editor view starting aspect ratio
        //  this is changed by the segmented control
        let editorAspect: Aspect = .a1to1
        aspectRatioSeg.selectedSegmentIndex = editorAspect.rawValue
        evAspectConstraint = editorView.heightAnchor.constraint(equalTo: editorView.widthAnchor, multiplier: editorAspect.aspect)

        // we can set the Aspect Ratio of the CanvasView here
        //  it will maintain its Aspect Ratio independent of
        //  the Editor View's Aspect Ratio
        let canvasAspect: Aspect = .a1to1

        NSLayoutConstraint.activate([
            containerView.topAnchor.constraint(equalTo: safeG.topAnchor),
            containerView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor),
            containerView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor),
            
            editorView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
            editorView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
            evMaxW, evMaxH,
            evW, evH,
            evAspectConstraint,
            
            canvasView.centerXAnchor.constraint(equalTo: editorView.centerXAnchor),
            canvasView.centerYAnchor.constraint(equalTo: editorView.centerYAnchor),
            cvMaxW, cvMaxH,
            cvW, cvH,
            canvasView.heightAnchor.constraint(equalTo: canvasView.widthAnchor, multiplier: canvasAspect.aspect),

            aspectRatioSeg.topAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 8.0),
            aspectRatioSeg.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -8.0),
            aspectRatioSeg.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
            aspectRatioSeg.widthAnchor.constraint(greaterThanOrEqualTo: safeG.widthAnchor, multiplier: 0.5),
            aspectRatioSeg.widthAnchor.constraint(lessThanOrEqualTo: safeG.widthAnchor),
        ])
        
        aspectRatioSeg.addTarget(self, action: #selector(aspectRatioSegmentChanged(_:)), for: .valueChanged)
        
        // triple-tap anywhere to "reset" the 3 subviews
        //  cycling between starting sizes/positions
        let tt = UITapGestureRecognizer(target: self, action: #selector(resetCanvas))
        tt.numberOfTapsRequired = 3
        tt.delaysTouchesEnded = false
        view.addGestureRecognizer(tt)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        // we don't have the frames in viewDidLoad,
        //  so wait until now to add the CanvasElement views
        resetCanvas()
    }
    
    @objc func resetCanvas() {
        
        canvasView.subviews.forEach { v in
            v.removeFromSuperview()
        }

        // add 3 views to the canvas

        let v1 = CanvasElement()
        v1.backgroundColor = .systemYellow

        let v2 = CanvasElement()
        v2.backgroundColor = .systemGreen

        let v3 = CanvasElement()
        v3.backgroundColor = .systemBlue

        // default size of subviews is 2/10ths the width of the canvas
        let w: CGFloat = canvasView.bounds.width * 0.2
        
        [v1, v2, v3].forEach { v in
            v.frame = CGRect(x: 0, y: 0, width: w, height: w)
            canvasView.addSubview(v)
            // if we want to allow simultaneous gestures
            //  i.e. pan/scale/rotate all at the same time
            //v.allowSimultaneous = true
        }
        
        switch (layoutMode % 3) {
        case 1:
            //  top-left corner
            //  center at 1.5 times the size
            //  bottom-right corner
            v1.frame.origin = CGPoint(x: 0, y: 0)
            v2.frame.size = CGSize(width: w * 1.5, height: w * 1.5)
            v2.center = CGPoint(x: canvasView.bounds.midX, y: canvasView.bounds.midY)
            v3.center = CGPoint(x: canvasView.bounds.maxX - w * 0.5, y: canvasView.bounds.maxY - w * 0.5)
            ()
        case 2:
            // different sized views
            v1.frame = CGRect(x: 0, y: 0, width: w * 0.5, height: w)
            v2.frame.size = CGSize(width: w, height: w)
            v2.center = CGPoint(x: canvasView.bounds.midX, y: canvasView.bounds.midY)
            v3.frame.size = CGSize(width: w, height: w * 0.5)
            v3.center = CGPoint(x: canvasView.bounds.maxX - v3.frame.width * 0.5, y: canvasView.bounds.maxY - v3.frame.height * 0.5)
            ()
        default:
            //  on a "diagonal"
            //  starting at top-left corner
            v1.frame.origin = CGPoint(x: 0, y: 0)
            v2.frame.origin = CGPoint(x: w, y: w)
            v3.frame.origin = CGPoint(x: w * 2, y: w * 2)
            ()
        }
        
        layoutMode += 1
    }

    @objc func aspectRatioSegmentChanged(_ sender: Any?) {
        if let seg = sender as? UISegmentedControl,
           let r = Aspect.init(rawValue: seg.selectedSegmentIndex)
        {
            evAspectConstraint.isActive = false
            evAspectConstraint = editorView.heightAnchor.constraint(equalTo: editorView.widthAnchor, multiplier: r.aspect)
            evAspectConstraint.isActive = true
        }
    }

}

一些示例截图...

  • 黄色是画布视图...带有可选的红色10x10网格
  • 灰色是编辑器视图...这是改变纵横比的视图
  • 青色是"容器"视图....编辑器视图适合自己/居中

enter image description here

enter image description here

enter image description here

请注意,画布视图可以设置为正方形以外的值(比例为1:1).例如,此处将其设置为9:16比率--并保持其纵横比独立于编辑器视图的纵横比:

enter image description here

enter image description here

enter image description here

在这个示例控制器中,三次点击任意位置以循环3个"起始布局":

enter image description here

Ios相关问答推荐

当S没有身体时该如何处理

在 Android 和 iOS 应用程序上下文中阻止后台线程有哪些缺点?

AVAudioRecord 没有音频

如何创建装饰视图?

如何在 Flutter 应用程序中测试来自 App/Play Store 的移动深层链接?

在 Swift 项目中使用情节提要加载 obj-c 文件

如何在用户点击的位置使用SceneKit渲染球体?

占位符文本未显示在 TextEditor 上

如何让替换视图淡入旧视图而不是交叉淡入淡出?

如何在 SwiftUI 中只显示一次随机数组的项目?

一个 SwiftUI 视图中的多个垂直滚动视图

滑动列表项以获取更多选项(Flutter)

iOS:应用程序在安装应用程序时未征求用户许可.每次都获得 kCLAuthorizationStatusNotDetermined - Objective-c & Swift

Xcode 8:函数类型不能有参数标签 destruct 我的构建

在 iOS 8.1 模拟器上更改语言不起作用

iOS中的apk类似功能是什么?

Swift 中的日期到毫秒和回溯到日期

Swift:如何在设备旋转后刷新 UICollectionView 布局

更新字段时,UITextField值已更改未触发

使用 swift 3 在 UIView 上添加阴影