【问题标题】:Converting Matrix to CGAffineTransform将矩阵转换为 CGAffineTransform
【发布时间】:2019-02-11 00:08:00
【问题描述】:

我在 Android 上开发了一个应用程序,您可以在其中在屏幕上添加图像并平移/旋转/缩放它们。从技术上讲,我有一个固定方面的父布局,其中包含这些图像(ImageViews)。他们已将宽度和高度设置为 match_parent。然后,我可以将他们的位置保存在服务器上,然后我可以稍后将其加载到同一设备或其他设备上。基本上,我只是保存 Matrix 值。但是,平移 x/y 具有绝对值(以像素为单位),但屏幕尺寸不同,因此我将它们设置为百分比值(因此平移 x 除以父宽度,平移 y 除以父高度)。加载图像时,我只需将平移 x/y 乘以父布局宽度/高度,即使在不同的设备上,一切看起来都一样。

我是这样做的:

float[] values = new float[9];
parentLayout.matrix.getValues(values);

values[Matrix.MTRANS_X] /= parentLayout.getWidth();
values[Matrix.MTRANS_Y] /= parentLayout.getHeight();

然后在加载函数中,我只是将这些值相乘。

图像操作是通过使用带有锚点的 postTranslate/postScale/postRotate 完成的(对于两指手势,它位于这些手指之间的中点)。

现在,我想将此应用程序移植到 iOS 并使用类似的技术在 iOS 设备上实现正确的图像位置。 CGAffineTransform 似乎与 Matrix 类非常相似。字段的顺序不同,但它们的工作方式似乎相同。

假设这个数组是来自 Android 应用的示例值数组:

let a: [CGFloat] = [0.35355338, -0.35355338, 0.44107446, 0.35355338, 0.35355338, 0.058058262]

我不保存最后 3 个字段,因为它们始终为 0、0 和 1,并且在 iOS 上它们甚至无法设置(文档还指出它是 [0, 0, 1])。所以这就是“转换”的样子:

let tx = a[2] * width //multiply percentage translation x with parent width
let ty = a[5] * height //multiply percentage translation y with parent height
let transform = CGAffineTransform(a: a[0], b: a[3], c: a[1], d: a[4], tx: tx, ty: ty)

但是,这只适用于非常简单的情况。对于其他人来说,图像通常是错位的,甚至是完全混乱的。

我注意到,默认情况下,在 Android 上我的锚点位于 [0, 0],但在 iOS 上它是图像的中心。

例如:

float midX = parentLayout.getWidth() / 2.0f;
float midY = parentLayout.getHeight() / 2.0f;

parentLayout.matrix.postScale(0.5f, 0.5f, midX, midY);

给出以下矩阵:

0.5, 0.0, 360.0, 
0.0, 0.5, 240.0, 
0.0, 0.0, 1.0

[360.0, 240.0] 只是一个从左上角开始的向量。

但是,在 iOS 上我不必提供中间点,因为它已经围绕中心进行了转换:

let transform = CGAffineTransform(scaleX: 0.5, y: 0.5)

它给了我:

CGAffineTransform(a: 0.5, b: 0.0, c: 0.0, d: 0.5, tx: 0.0, ty: 0.0)

我尝试在 iOS 上设置不同的锚点:

parentView.layer.anchorPoint = CGPoint(x: 0, y: 0)

但是,我无法得到正确的结果。我尝试过按 -width * 0.5、-height * 0.5 进行翻译、缩放和翻译回来,但我没有得到与 Android 相同的结果。

简而言之:如何将 Matrix 从 Android 修改为 CGAffineTransform 从 iOS 以获得相同的外观?

我还附上了尽可能简单的演示项目。目标是将 Android 的输出数组复制到 iOS 项目中的“a”数组中,并修改 CGAffineTransform 计算(和/或 parentView 设置),以使图像在两个平台上看起来都相同,无论 Android 的日志输出是什么。

安卓: https://github.com/piotrros/MatrixAndroid

iOS: https://github.com/piotrros/MatrixIOS

编辑:

@Sulthan 解决方案效果很好!尽管如此,我想出了一个逆转过程的子问题,我也需要它。根据他的回答,我尝试将其整合到我的应用中:

let savedTransform = CGAffineTransform.identity
        .concatenating(CGAffineTransform(translationX: -width / 2, y: -height / 2))
        .concatenating(
            self.transform
        )
        .concatenating(CGAffineTransform(translationX: width / 2, y: height / 2))

let tx = savedTransform.tx
let ty = savedTransform.ty

let t = CGAffineTransform(a: savedTransform.a, b: savedTransform.b, c: savedTransform.c, d: savedTransform.d, tx: tx / cWidth, ty: ty / cHeight)

placement.transform = [Float(t.a), Float(t.c), Float(t.tx), Float(t.b), Float(t.d), Float(t.ty), 0, 0, 1]

placement 只是一个保存“Android”版本矩阵的类。 cWidth / cHeight 是父级的宽度/高度。我将 tx/ty 除以它们以获得百分比宽度/高度,就像我在 Android 上所做的那样。

但是,它不起作用。 tx/ty 是错误的。

输入:

[0.2743883, -0.16761175, 0.43753636, 0.16761175, 0.2743883, 0.40431038, 0.0, 0.0, 1.0]

它回馈:

[0.2743883, -0.16761175, 0.18834576, 0.16761175, 0.2743883, 0.2182884, 0.0, 0.0, 1.0]

所以除了 tx/ty 以外的一切都很好。哪里出错了?

【问题讨论】:

  • 如何将所有 layer.anchorpoint 更改为 x:0、y:0 或 x:0 y:1。我们知道这是关键问题。正确的产地可以为您节省大量工作。
  • 是的,我知道。我认为这是一个简单的错误,但我找不到正确的设置。我已经尝试过 (x: 0, y: 0) 和 (x: 0, y: 1) 枢轴,但这是错误的。只需在演示项目上亲自尝试即可。

标签: ios swift core-graphics


【解决方案1】:

第一个问题是,在 Android 中,您为父布局设置了转换,然后影响子布局。但是在 iOS 上,您必须在 imageView 本身上设置转换。这对于以下两种解决方案都很常见。

解决方案 1:更新锚点

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    imageView.translatesAutoresizingMaskIntoConstraints = true
    imageView.bounds = parentView.bounds
    imageView.layer.anchorPoint = .zero
    imageView.center = .zero

    let width = parentView.frame.width
    let height = parentView.frame.height

    let a: [CGFloat] = [0.35355338, -0.35355338, 0.44107446, 0.35355338, 0.35355338, 0.058058262]

    let tx = a[2] * width
    let ty = a[5] * height
    let transform = CGAffineTransform(a: a[0], b: a[3], c: a[1], d: a[4], tx: tx, ty: ty)

    imageView.transform = transform
}

重要提示:我已从情节提要中删除了 imageView 的所有约束

我们必须将视图 anchorPoint 移动到图像的左上角以匹配 Android。但是,不明显的问题是这也会影响视图的位置,因为视图位置是相对于锚点计算的。有了约束,它开始变得混乱,因此我删除了所有约束并切换到手动定位:

imageView.bounds = parentView.bounds // set correct size
imageView.layer.anchorPoint = .zero // anchor point to top-left corner
imageView.center = .zero // set position, "center" is now the top-left corner

解决方案 2:更新变换矩阵

如果我们不想接触锚点,我们可以保持原来的约束,我们可以更新变换:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    let width = parentView.frame.width
    let height = parentView.frame.height

    let a: [CGFloat] = [0.35355338, -0.35355338, 0.44107446, 0.35355338, 0.35355338, 0.058058262]

    let tx = a[2] * width
    let ty = a[5] * height
    let savedTransform = CGAffineTransform(a: a[0], b: a[3], c: a[1], d: a[4], tx: tx, ty: ty)

    let transform = CGAffineTransform.identity
        .translatedBy(x: width / 2, y: height / 2)
        .concatenating(savedTransform)
        .concatenating(CGAffineTransform(translationX: -width / 2, y: -height / 2))

    imageView.transform = transform
}

CGAffineTransform 不明显的问题是

.concatenating(CGAffineTransform(translationX: -width / 2, y: -height / 2))

.translatedBy(x: -width / 2, y: -height / 2)

不一样。乘法顺序不同。

逆转流程:

要生成用于 Android 的相同矩阵,我们只需移动原点:

let savedTransform = CGAffineTransform.identity
    .translatedBy(x: width / 2, y: height / 2)
    .scaledBy(x: 0.5, y: 0.5)
    .rotated(by: .pi / 4)
    .translatedBy(x: -width / 2, y: -height / 2)

或使用连接:

let savedTransform = CGAffineTransform.identity
    .concatenating(CGAffineTransform(translationX: -width / 2, y: -height / 2))
    .concatenating(
        CGAffineTransform.identity
            .scaledBy(x: 0.5, y: 0.5)
            .rotated(by: .pi / 4)
    )
    .concatenating(CGAffineTransform(translationX: width / 2, y: height / 2))

通过串联,现在很明显原点转换是如何抵消的。

总结:

func androidValuesToIOSTransform() -> CGAffineTransform{
    let width = parentView.frame.width
    let height = parentView.frame.height

    let androidValues: [CGFloat] = [0.35355338, -0.35355338, 0.44107446, 0.35355338, 0.35355338, 0.058058262]
    let androidTransform = CGAffineTransform(
        a: androidValues[0], b: androidValues[3],
        c: androidValues[1], d: androidValues[4],
        tx: androidValues[2] * width, ty: androidValues[5] * height
    )

    let iOSTransform = CGAffineTransform.identity
        .concatenating(CGAffineTransform(translationX: width / 2, y: height / 2))
        .concatenating(androidTransform)
        .concatenating(CGAffineTransform(translationX: -width / 2, y: -height / 2))

    return iOSTransform
}

func iOSTransformToAndroidValues() -> [CGFloat] {
    let width = parentView.frame.width
    let height = parentView.frame.height

    let iOSTransform = CGAffineTransform.identity
        .scaledBy(x: 0.5, y: 0.5)
        .rotated(by: .pi / 4)
    let androidTransform = CGAffineTransform.identity
        .concatenating(CGAffineTransform(translationX: -width / 2, y: -height / 2))
        .concatenating(iOSTransform)
        .concatenating(CGAffineTransform(translationX: width / 2, y: height / 2))

    let androidValues = [
        androidTransform.a, androidTransform.c, androidTransform.tx / width,
        androidTransform.b, androidTransform.d, androidTransform.ty / height
    ]
    return androidValues
}

【讨论】:

  • 我已经尝试了解决方案 #2,因为这是最初的想法,并且确实有效!我的错误是没有使用“连接”。
  • 只有一个子问题:如何将 CGAffineTransform 转换回 Android 的矩阵?
  • @Makalele 更新
  • @Makalele 添加了完整的转换代码。我希望这会让一切都清楚。
  • 是的。现在我让它工作了。看起来我通过了错误的宽度/高度。现在它是正确的。非常感谢!
【解决方案2】:

这个答案怎么样:我添加了四个滑块以使事情更容易看到。

class ViewController: UIViewController {

@IBOutlet weak var parentView: UIView!
@IBOutlet weak var imageView: UIImageView!

override func viewDidLoad() {
    super.viewDidLoad()


}




let width =  UIScreen.main.bounds.width
let height = UIScreen.main.bounds.width * 2.0 / 3.0


var zoom: CGFloat = 1.0
var rotate: CGFloat = 0.0

 var locX: CGFloat = 0.0
 var locY: CGFloat = 0.0

@IBAction func rotate(_ sender: UISlider){  //0-6.28

    rotate = CGFloat(sender.value)
    parentView.transform = CGAffineTransform(rotationAngle: rotate).concatenating(CGAffineTransform(scaleX: zoom, y: zoom))
    parentView.transform.tx = (locX-0.5) * width
    parentView.transform.ty = (locY-0.5) * height
}

@IBAction func zoom(_ sender: UISlider){ //0.1 - 10

    zoom = CGFloat(sender.value)
    parentView.transform = CGAffineTransform(rotationAngle: rotate).concatenating(CGAffineTransform(scaleX: zoom, y: zoom))
    parentView.transform.tx = (locX-0.5) * width
    parentView.transform.ty = (locY-0.5) * height
}

@IBAction func locationX(_ sender: UISlider){  //0 - 1

    locX = CGFloat(sender.value)
    parentView.transform = CGAffineTransform(rotationAngle: rotate).concatenating(CGAffineTransform(scaleX: zoom, y: zoom))
    parentView.transform.tx = (locX-0.5) * width
    parentView.transform.ty = (locY-0.5) * height
}

@IBAction func locationY(_ sender: UISlider){ //0 - 1
    locY = CGFloat(sender.value)
    parentView.transform = CGAffineTransform(rotationAngle: rotate).concatenating(CGAffineTransform(scaleX: zoom, y: zoom))
    parentView.transform.tx = (locX-0.5) * width
    parentView.transform.ty = (locY-0.5) * height
}



override func viewWillAppear(_ animated: Bool) {
  let a: [CGFloat] = [0.35355338, -0.35355338, 0.44107446, 0.35355338, 0.35355338, 0.058058262]
    let tx = (a[2] - 0.5) * width
   let ty = (a[5] - 0.5) * height
let transform =  CGAffineTransform(a: a[0], b: a[3], c: a[1], d: a[4], tx:tx, ty: ty)
parentView.layer.anchorPoint   = CGPoint.zero
parentView.layer.position = CGPoint.zero
parentView.transform = transform

}

}

【讨论】:

  • 我已经尝试过了,但仍然得到错误的结果。只需从 android / iOS 运行演示项目并查看结果。
  • 我运行 iOS 版本,看起来还可以。
  • 在 android 上看起来像这样:imgur.com/a/k9JOC1k 在 iOS 上:imgur.com/a/QkyRCjV
  • 看起来很相似,但应该稍微低一点,所以还是有问题
  • 0.058058262 非常接近顶部。您可以在上面添加一些视图以降低排名。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-04
  • 1970-01-01
  • 2011-01-01
  • 1970-01-01
  • 2017-02-21
相关资源
最近更新 更多