将矩阵转换为 CGAffineTransform
Converting Matrix to CGAffineTransform
我在 Android 上开发了一个应用程序,您可以在其中添加图像到屏幕上并 pan/rotate/scale 它们周围。从技术上讲,我有一个固定纵横比的父布局,其中包含这些图像 (ImageViews)。他们已将宽度和高度设置为 match_parent。然后,我可以将他们的位置保存在服务器上,稍后我可以将其加载到同一设备或其他设备上。基本上,我只是简单地保存 Matrix 值。然而,翻译 x/y 有绝对值(以像素为单位),但屏幕尺寸不同,所以我将它们设置为百分比值(因此翻译 x 除以父级宽度,翻译 y 除以父级高度)。加载图像时,我只是将翻译 x/y 乘以父布局 width/height,即使在不同的设备上,一切看起来都一样。
这是我的做法:
float[] values = new float[9];
parentLayout.matrix.getValues(values);
values[Matrix.MTRANS_X] /= parentLayout.getWidth();
values[Matrix.MTRANS_Y] /= parentLayout.getHeight();
然后在加载函数中,我简单地将这些值相乘。
图像处理是通过使用 postTranslate/postScale/postRotate 和锚点完成的(对于 2 指手势,它是这些手指之间的中点)。
现在,我想将此应用程序移植到 iOS 并使用类似的技术在 iOS 设备上也实现正确的图像位置。 CGAffineTransform 貌似很像Matrix class。字段的顺序不同,但它们的工作方式似乎相同。
假设这个数组是来自 Android app:
的示例值数组
let a: [CGFloat] = [0.35355338, -0.35355338, 0.44107446, 0.35355338, 0.35355338, 0.058058262]
我不保存最后 3 个字段,因为它们始终是 0、0 和 1,而在 iOS 上它们甚至无法设置(文档还声明它是 [0, 0, 1] ).所以这就是 "conversion" 的样子:
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 以获得相同的外观?
我还附加了尽可能简单的演示项目。 objective 是将输出数组从 Android 复制到 iOS 项目中的 "a" 数组并修改 CGAffineTransform 计算(and/or parentView 设置)以图像看起来的方式无论 Android.
的日志输出是什么,两个平台都一样
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 只是一个 class 来容纳 "Android" 版本的矩阵。 cWidth / cHeight 是父项的 width/height。我将 tx/ty 除以它们得到百分比 width/height,就像我在 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 以外的一切都很好。哪里出错了?
这个答案怎么样:我添加了四个滑块以使事情更容易看到。
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 上,您必须在 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
}
我在 Android 上开发了一个应用程序,您可以在其中添加图像到屏幕上并 pan/rotate/scale 它们周围。从技术上讲,我有一个固定纵横比的父布局,其中包含这些图像 (ImageViews)。他们已将宽度和高度设置为 match_parent。然后,我可以将他们的位置保存在服务器上,稍后我可以将其加载到同一设备或其他设备上。基本上,我只是简单地保存 Matrix 值。然而,翻译 x/y 有绝对值(以像素为单位),但屏幕尺寸不同,所以我将它们设置为百分比值(因此翻译 x 除以父级宽度,翻译 y 除以父级高度)。加载图像时,我只是将翻译 x/y 乘以父布局 width/height,即使在不同的设备上,一切看起来都一样。
这是我的做法:
float[] values = new float[9];
parentLayout.matrix.getValues(values);
values[Matrix.MTRANS_X] /= parentLayout.getWidth();
values[Matrix.MTRANS_Y] /= parentLayout.getHeight();
然后在加载函数中,我简单地将这些值相乘。
图像处理是通过使用 postTranslate/postScale/postRotate 和锚点完成的(对于 2 指手势,它是这些手指之间的中点)。
现在,我想将此应用程序移植到 iOS 并使用类似的技术在 iOS 设备上也实现正确的图像位置。 CGAffineTransform 貌似很像Matrix class。字段的顺序不同,但它们的工作方式似乎相同。
假设这个数组是来自 Android app:
的示例值数组let a: [CGFloat] = [0.35355338, -0.35355338, 0.44107446, 0.35355338, 0.35355338, 0.058058262]
我不保存最后 3 个字段,因为它们始终是 0、0 和 1,而在 iOS 上它们甚至无法设置(文档还声明它是 [0, 0, 1] ).所以这就是 "conversion" 的样子:
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 以获得相同的外观?
我还附加了尽可能简单的演示项目。 objective 是将输出数组从 Android 复制到 iOS 项目中的 "a" 数组并修改 CGAffineTransform 计算(and/or parentView 设置)以图像看起来的方式无论 Android.
的日志输出是什么,两个平台都一样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 只是一个 class 来容纳 "Android" 版本的矩阵。 cWidth / cHeight 是父项的 width/height。我将 tx/ty 除以它们得到百分比 width/height,就像我在 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 以外的一切都很好。哪里出错了?
这个答案怎么样:我添加了四个滑块以使事情更容易看到。
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 上,您必须在 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
}