#swift #transform #calayer
#быстрый #трансформировать #калайер
Вопрос:
В настоящее время я немного играю с CALayer
s. Для демонстрационных целей я создаю пользовательский UIView
интерфейс, который отображает указатель уровня топлива. Представление имеет два подслоя:
- один для фона
- один для руки
Затем слой, представляющий руку, просто поворачивается соответствующим образом, чтобы указать правильное значение. Пока все идет так хорошо. Теперь я хочу, чтобы вид изменял размер своих слоев всякий раз, когда изменяется размер вида. Чтобы добиться этого, я создал переопределение layoutSubviews
метода следующим образом:
public override func layoutSubviews()
{
super.layoutSubviews()
if previousBounds == nil || !previousBounds!.equalTo(self.bounds)
{
previousBounds = self.bounds
self.updateLayers(self.bounds)
}
}
Поскольку метод вызывается много раз, я использую previousBounds
его, чтобы убедиться, что я выполняю обновление слоев только тогда, когда размер действительно изменился.
Сначала у меня был только следующий код в updateLayers
методе для установки рамок подслоев:
backgroundLayer.frame = bounds.insetBy(dx: 5, dy: 5)
handLayer.frame = bounds.insetBy(dx: 5, dy: 5)
Это работало нормально — до тех handLayer
пор, пока не было повернуто. В этом случае с его размером происходят какие-то странные вещи. Я полагаю, это потому, что рамка применяется после поворота, и, конечно, повернутый слой на самом деле не соответствует границам и, таким образом, изменяется по размеру.
Мое текущее решение состоит в том, чтобы временно создать новое CATransaction
, которое подавляет анимацию, возвращая преобразование обратно к идентификатору, устанавливая фрейм, а затем повторно применяя преобразование следующим образом:
CATransaction.begin()
CATransaction.setDisableActions(true)
let oldTransform = scaleLayer.transform
handLayer.transform = CATransform3DIdentity
handLayer.frame = bounds.insetBy(dx: 5, dy: 5)
handLayer.transform = oldTransform
CATransaction.commit()
Я уже пытался опустить CATransaction
и вместо этого применить handLayer.affineTransform
к bounds
настройке I, но это не дало ожидаемых результатов — возможно, я сделал это неправильно (побочный вопрос: как повернуть заданное CGRect
вокруг его центра, не выполняя всю математику самостоятельно)?
Мой вопрос прост: есть ли рекомендуемый способ установки рамки преобразованного слоя или решение, которое я нашел, уже «является» способом сделать это?
Редактировать
Kevvv предоставил несколько примеров кода, которые я изменил, чтобы продемонстрировать свою проблему:
class ViewController: UIViewController {
let customView = CustomView(frame: .init(origin: .init(x: 200, y: 200), size: .init(width: 200, height: 200)))
let backgroundLayer = CALayer()
let handLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(customView)
customView.backgroundColor = UIColor.blue
backgroundLayer.backgroundColor = UIColor.yellow.cgColor
backgroundLayer.frame = customView.bounds
let handPath = UIBezierPath()
handPath.move(to: backgroundLayer.position)
handPath.addLine(to: .init(x: 0, y: backgroundLayer.position.y))
handLayer.frame = customView.bounds
handLayer.path = handPath.cgPath
handLayer.strokeColor = UIColor.black.cgColor
handLayer.backgroundColor = UIColor.red.cgColor
customView.layer.addSublayer(backgroundLayer)
customView.layer.addSublayer(handLayer)
handLayer.transform = CATransform3DMakeRotation(5, 0, 0, 1)
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
customView.addGestureRecognizer(tap)
}
@objc func tapped(_ sender: UITapGestureRecognizer) {
customView.frame = customView.frame.insetBy(dx:10, dy:10)
/*let animation = CABasicAnimation(keyPath: #keyPath(CALayer.transform))
let fromValue = self.handLayer.transform
let toValue = CGFloat.pi * 2
animation.duration = 2
animation.fromValue = fromValue
animation.toValue = toValue
animation.valueFunction = CAValueFunction(name: .rotateZ)
self.handLayer.add(animation, forKey: nil)*/
}
}
class CustomView: UIView {
var previousBounds: CGRect!
override func layoutSubviews() {
super.layoutSubviews()
if previousBounds == nil || !previousBounds!.equalTo(self.bounds) {
previousBounds = self.bounds
self.updateLayers(self.bounds)
}
}
func updateLayers(_ bounds: CGRect) {
guard let sublayers = self.layer.sublayers else { return }
for sublayer in sublayers {
sublayer.frame = bounds.insetBy(dx: 5, dy: 5)
}
}
}
Если вы добавите это на игровую площадку, затем запустите и коснитесь элемента управления, вы поймете, что я имею в виду. Наблюдайте за красным «квадратом».
Ответ №1:
Не могли бы вы объяснить, что означают «странные вещи, происходящие с размером»? Я попытался воспроизвести его, но не смог найти неожиданных эффектов:
class ViewController: UIViewController {
let customView = CustomView(frame: .init(origin: .init(x: 200, y: 200), size: .init(width: 200, height: 200)))
let backgroundLayer = CALayer()
let handLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(customView)
backgroundLayer.backgroundColor = UIColor.yellow.cgColor
backgroundLayer.frame = customView.bounds
let handPath = UIBezierPath()
handPath.move(to: backgroundLayer.position)
handPath.addLine(to: .init(x: 0, y: backgroundLayer.position.y))
handLayer.frame = customView.bounds
handLayer.path = handPath.cgPath
handLayer.strokeColor = UIColor.black.cgColor
customView.layer.addSublayer(backgroundLayer)
customView.layer.addSublayer(handLayer)
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
customView.addGestureRecognizer(tap)
}
@objc func tapped(_ sender: UITapGestureRecognizer) {
let animation = CABasicAnimation(keyPath: #keyPath(CALayer.transform))
let fromValue = self.handLayer.transform
let toValue = CGFloat.pi * 2
animation.duration = 2
animation.fromValue = fromValue
animation.toValue = toValue
animation.valueFunction = CAValueFunction(name: .rotateZ)
self.handLayer.add(animation, forKey: nil)
}
}
class CustomView: UIView {
var previousBounds: CGRect!
override func layoutSubviews() {
super.layoutSubviews()
if previousBounds == nil || !previousBounds!.equalTo(self.bounds) {
previousBounds = self.bounds
self.updateLayers(self.bounds)
}
}
func updateLayers(_ bounds: CGRect) {
guard let sublayers = self.layer.sublayers else { return }
for sublayer in sublayers {
sublayer.frame = bounds.insetBy(dx: 5, dy: 5)
}
}
}
Редактировать
Я думаю, проблема в том, что размер красного прямоугольника изменяется с помощью рамки. Поскольку рамка всегда находится в вертикальном положении, даже если она повернута, если бы вы делали вставку из рамки, это выглядело бы так:
Однако, если бы вы изменили размер красного прямоугольника с границами:
sublayer.bounds = bounds.insetBy(dx: 5, dy: 5)
sublayer.position = self.convert(self.center, from: self.superview)
вместо того , чтобы:
sublayer.frame = bounds.insetBy(dx: 5, dy: 5)
Вероятно, вам также придется соответствующим образом переориентировать handPath
и все остальное в нем.
Комментарии:
1. Привет, спасибо за ваши усилия и ваш ответ. Я отредактирую свой вопрос и предоставлю скриншоты для демонстрации, но сейчас я не могу, потому что я на работе.
2. Извините, что так долго — я попробовал ваш код на игровой площадке, и он действительно работает. Я углублюсь в свой код и посмотрю, что смогу узнать.
3. Что ж, я отредактировал свой вопрос и добавил незначительные изменения в ваш код, чтобы на самом деле продемонстрировать проблему, с которой я столкнулся.