Преобразование матрицы в CGAffineTransform - PullRequest
0 голосов
/ 07 февраля 2019

Я разработал приложение для Android, в котором вы можете добавлять изображения на экран и перемещать / поворачивать / масштабировать их.Технически, у меня есть родительский макет с фиксированным аспектом, который содержит эти изображения (ImageViews).Они установили ширину и высоту для match_parent.Затем я могу сохранить свои позиции на сервере, который затем могу загрузить на то же или другое устройство.По сути, я просто сохраняю значения матрицы.Однако перевод 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)

Однако я не могу получить правильные результаты.Я пробовал переводить по ширине * 0,5, -высоте * 0,5, масштабировать и переводить обратно, но я не получаю тот же результат, что и на Android.

Короче говоря: как я могу изменить матрицу из Androidв CGAffineTransform из iOS для достижения того же внешнего вида?

Я также прилагаю как можно более простые демонстрационные проекты.Цель состоит в том, чтобы скопировать выходной массив из Android в массив «a» в проекте iOS и изменить вычисление CGAffineTransform (и / или настройку parentView) таким образом, чтобы изображение выглядело одинаково на обеих платформах, независимо от того, что выводит журнал из Android.

Android: https://github.com/piotrros/MatrixAndroid

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

edit:

@ Решение 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]

Размещение - это просто класс для хранения "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 в порядке.Где ошибка?

Ответы [ 2 ]

0 голосов
/ 10 февраля 2019

Первая проблема заключается в том, что в 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)

не совпадают.Порядок умножения другой.

iOS

В обратном порядке:

Чтобы сгенерировать ту же матрицу, которая будет использоваться для 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
}
0 голосов
/ 09 февраля 2019

Как насчет этого ответа: я добавляю четыре ползунка, чтобы все было легко увидеть.

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

}

}

...