繁体   English   中英

"如何在保持纵横比的同时缩放 UIView 的内容以适应目标矩形?"

[英]How To Scale The Contents Of A UIView To Fit A Destination Rectangle Whilst Maintaining The Aspect Ratio?

我试图解决一个没有成功的问题,希望有人能提供帮助。

我一直在寻找类似的帖子,但找不到任何可以解决我问题的东西。

我的场景如下:我有一个UIView可以放置许多其他UIView 这些可以使用手势识别器移动、缩放和旋转(这里没有问题)。 用户能够更改主视图(画布)的纵横比,我的问题是尝试缩放画布的内容以适应新的目标大小。

有许多具有相似主题的帖子,例如:

计算 CGRect 上的新大小和位置

如何从 UIView 创建特定大小的图像

但是这些并没有解决比率的多次变化。

我的方法:

当我更改画布的纵横比时,我使用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

测试用例 红框只是为了可视化 Aspect Fitted Rectangle。 正如您所看到的,虽然方面适合的矩形是正确的,但对象的缩放不起作用。 当我将缩放和旋转应用于子视图(CanvasElement)时尤其如此。

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

@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
  }
}

任何帮助或建议,方法,以及一些实际的例子都会很棒。 我不希望任何人提供完整的源代码,而是我可以用作基础的东西。

感谢您花时间阅读我的问题。

这里有一些想法和发现,同时玩这个

1. 是否使用了正确的比例因子?

您使用的缩放比例有点自定义,不能直接与只有 1 个比例因子(如 2 或 3)的示例进行比较。但是,您的比例因子有 2 个维度,但我看到您对此进行了补偿以获得最小的宽度和高度缩放:

let scale = min(aspectFittedFrame.size.width / canvas.frame.width,
                aspectFittedFrame.size.height / canvas.frame.height)

在我看来,我认为这不是正确的比例因子。 对我来说,这将新的aspectFittedFrame与新的canvas frame进行了比较

纵横比 UIView 平移缩放旋转 Swift ios

实际上我认为正确的缩放因子是将新的aspectFittedFrame与以前的canvas frame进行比较

let scale
    = min(aspectFittedFrame.size.width / sourceRectangleSize.width,
          aspectFittedFrame.size.height / sourceRectangleSize.height)

UIView 平移缩放旋转纵横比 Swift ios

2. 量表是否适用于正确的值?

如果您注意到,从 1:1 到 16:9 的第一个顺序效果很好。 但是在那之后它似乎不起作用,我相信问题就在这里:

for case let canvasElement as CanvasElement in strongSelf.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
    )
}

第一次,缩放效果很好,因为画布和画布元素正在同步缩放或正确映射:

Swift ios UIView 缩放变换旋转平移

但是,如果您超出此范围,因为您总是根据基本值进行缩放,因此您的纵横比框架和画布元素不同步

UIView缩放变换旋转平移Swift ios

所以在 1:1 -> 16:9 -> 3:2 的例子中

  • 您的视口已缩放两次 1:1 -> 16:9 和 16:9 -> 3:2
  • 虽然您的元素每次缩放一次,但 1:1 -> 16:9 和 1:1 -> 3:2 因为您总是从基本值缩放

所以我觉得看到红色视口中的值,您需要基于前一个视图而不是基础视图应用相同的连续缩放。

只是为了立即快速修复,我通过调用canvasElement.storeState()在每次更改画布大小后更新画布元素的基本值:

for case let canvasElement as CanvasElement in strongSelf.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
    )
    
    // I added this
    canvasElement.storeState()
}

结果也许更接近你想要的?

UIView 缩放 平移 旋转 纵横比 Swift iOS

最后的想法

虽然这可能会解决您的问题,但您会注意到不可能恢复到原始状态,因为在每一步都应用了转换。

一种解决方案可能是存储映射到特定视口纵横比的当前值,并为其他值计算正确的大小,以便如果您需要返回原始值,您可以这样做。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM