如何将透视变换应用于UIView?

201

我想在UIView上执行透视变换(例如在coverflow中看到的那样)。

有人知道是否可能吗?

我已经研究过使用CALayer,并且已经通过所有务实的程序员Core Animation播客,但是我仍然不清楚如何在iPhone上创建这种类型的变换。

任何帮助、指导或示例代码片段都将非常感谢!


我不确定这是否适合您,但是当我制作动画时,我发现M14对我来说更好。仅供参考 :) - b123400
谢谢! - 你给这个哪些值? - Nick Cartwright
1
通过设置m14,您实际上是在X轴上奇怪地扭曲视锥体。数学完全有效,但在一般情况下会使事情变得有点混乱。 - fluffy
一篇非常好的关于CALayer 3D变换和透视的文章,包括对m34字段的详细解释,可以在这篇优秀的文章中找到:core-animation-3d-model - Markus
4个回答

332

就像Ben所说的那样,您需要使用CATransform3DUIView的层(layer)一起工作,来执行旋转操作(rotation)。要使透视(perspective)生效,就像这里描述的那样,需要直接访问CATransform3D矩阵中的一个元素(m34)。我不是数学专家,无法解释为什么这样做有效,但确实如此。您需要将该值设置为负分数以进行初始变换(transform),然后将图层旋转(transforms)应用于其上。您还可以做以下操作:

Objective-C

UIView *myView = [[self subviews] objectAtIndex:0];
CALayer *layer = myView.layer;
CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity;
rotationAndPerspectiveTransform.m34 = 1.0 / -500;
rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, 45.0f * M_PI / 180.0f, 0.0f, 1.0f, 0.0f);
layer.transform = rotationAndPerspectiveTransform;

Swift 5.0

if let myView = self.subviews.first {
    let layer = myView.layer
    var rotationAndPerspectiveTransform = CATransform3DIdentity
    rotationAndPerspectiveTransform.m34 = 1.0 / -500
    rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, 45.0 * .pi / 180.0, 0.0, 1.0, 0.0)
    layer.transform = rotationAndPerspectiveTransform
}

这会为每次旋转重新构建图层变换。

您可以在此处找到完整的示例(包括代码),我已经基于Bill Dudney的示例在一些CALayers上实现了基于触摸的旋转和缩放。该页面最底部提供了程序的最新版本,其中包含这种透视操作。代码应该相对容易阅读。

您在回答中提到的sublayerTransform是应用于UIViewCALayer子层的变换。如果您没有任何子层,请不必担心。我在示例中使用sublayerTransform仅是因为一个图层中包含两个CALayers


1
谢谢Brad - 你真是个明星。 PS:对于迟到的点赞,我很抱歉! Nick. - Nick Cartwright
18
很可能您正在尝试旋转一个视图或层,但是在同一平面上还有另一个不透明的视图或层。因此,从屏幕上突出的那一半视图或层将位于该其他视图下方,从而被隐藏。您可以重新排列视图层次结构以防止这种情况发生,或者使用zPosition属性将前景视图移动到足够高的位置,超过遮挡它的那个视图。 - Brad Larson
26
FYI,m34单元格影响透视变换的原因是该单元格影响世界空间Z值映射到剪裁空间W值的方式(用于透视投影)。要了解更多信息,请阅读有关齐次变换矩阵的内容。 - fluffy
2
@uerceg - 你最好单独提出这个问题,并最好附上能够说明发生情况及您期望的结果的图片。 - Brad Larson
@BradLarson。我一直在想如何影响矩阵,以便移动旋转轴。改变原点并不理想。http://stackoverflow.com/questions/37364831 也许你对此有线索,谢谢。 - Fattie
显示剩余8条评论

7
您只能直接使用应用于UIView的transform属性的Core Graphics(Quartz,仅2D)变换。要在coverflow中获得效果,您需要使用CATransform3D,它们应用于3D空间,因此可以为您提供所需的透视视图。您只能将CATransform3D应用于层,而不是视图,因此您需要切换到层来实现这一点。
请查看附带Xcode的“CovertFlow”示例。它仅适用于Mac(即不适用于iPhone),但很多概念都可以转移。

我正在寻找/Developer/Examples中的这个Coverflow示例,但没有成功,你在哪里可以找到它? - Alex
它实际上是“CovertFlow”,位于/Developer/Examples/Quartz/Core Animation/目录下。 - Ben Gottlieb

0

Swift 5.0

func makeTransform(horizontalDegree: CGFloat, verticalDegree: CGFloat, maxVertical: CGFloat,rotateDegree: CGFloat, maxHorizontal: CGFloat) -> CATransform3D {
    var transform = CATransform3DIdentity
           
    transform.m34 = 1 / -500
    
    let xAnchor = (horizontalDegree / (2 * maxHorizontal)) + 0.5
    let yAnchor = (verticalDegree / (-2 * maxVertical)) + 0.5
    let anchor = CGPoint(x: xAnchor, y: yAnchor)
    
    setAnchorPoint(anchorPoint: anchor, forView: self.imgView)
    let hDegree  = (CGFloat(horizontalDegree) * .pi)  / 180
    let vDegree  = (CGFloat(verticalDegree) * .pi)  / 180
    let rDegree  = (CGFloat(rotateDegree) * .pi)  / 180
    transform = CATransform3DRotate(transform, vDegree , 1, 0, 0)
    transform = CATransform3DRotate(transform, hDegree , 0, 1, 0)
    transform = CATransform3DRotate(transform, rDegree , 0, 0, 1)
    
    return transform
}

func setAnchorPoint(anchorPoint: CGPoint, forView view: UIView) {
    var newPoint = CGPoint(x: view.bounds.size.width * anchorPoint.x, y: view.bounds.size.height * anchorPoint.y)
    var oldPoint = CGPoint(x: view.bounds.size.width * view.layer.anchorPoint.x, y: view.bounds.size.height * view.layer.anchorPoint.y)
    
    newPoint = newPoint.applying(view.transform)
    oldPoint = oldPoint.applying(view.transform)
    
    var position = view.layer.position
    position.x -= oldPoint.x
    position.x += newPoint.x
    
    position.y -= oldPoint.y
    position.y += newPoint.y
    
    print("Anchor: \(anchorPoint)")
    
    view.layer.position = position
    view.layer.anchorPoint = anchorPoint
}

您只需要使用您的度数调用该函数。例如:

var transform = makeTransform(horizontalDegree: 20.0 , verticalDegree: 25.0, maxVertical: 25, rotateDegree: 20, maxHorizontal: 25)
imgView.layer.transform = transform

-1

使用iCarousel SDK可以获得准确的旋转木马效果。

通过使用神奇而免费的iCarousel库,您可以在iOS上立即获得Cover Flow效果。 您可以从https://github.com/nicklockwood/iCarousel下载它,并通过添加桥接头文件(它是用Objective-C编写的)相对容易地将其放入Xcode项目中。

如果您以前没有将Objective-C代码添加到Swift项目中,请按照以下步骤操作:

  • 下载iCarousel并解压缩
  • 进入您解压缩的文件夹,打开其iCarousel子文件夹,然后选择iCarousel.h和iCarousel.m并将它们拖到您的项目导航中 - 这是Xcode中的左窗格。就在Info.plist下面。
  • 勾选“如果需要复制项目”,然后点击完成。
  • Xcode会提示您是否要配置Objective-C桥接头文件?点击“创建桥接头文件”
  • 您应该在您的项目中看到一个新文件,名为YourProjectName-Bridging-Header.h。
  • 将此行添加到文件中:#import "iCarousel.h"
  • 一旦您将iCarousel添加到您的项目中,您就可以开始使用它了。
  • 确保您符合iCarouselDelegate和iCarouselDataSource协议。

Swift 3示例代码:

    override func viewDidLoad() {
      super.viewDidLoad()
      let carousel = iCarousel(frame: CGRect(x: 0, y: 0, width: 300, height: 200))
      carousel.dataSource = self
      carousel.type = .coverFlow
      view.addSubview(carousel) 
    }

   func numberOfItems(in carousel: iCarousel) -> Int {
        return 10
    }

    func carousel(_ carousel: iCarousel, viewForItemAt index: Int, reusing view: UIView?) -> UIView {
        let imageView: UIImageView

        if view != nil {
            imageView = view as! UIImageView
        } else {
            imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 128, height: 128))
        }

        imageView.image = UIImage(named: "example")

        return imageView
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接