[Разработка под iOS, Разработка мобильных приложений, Swift, Дизайн мобильных приложений] iOS. UI. Приëмы. Часть 1
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Привет читателям Хабра!Я iOS-разработчик, и так случилось, что мне приходилось много делать в ui: кастомные view, тени, layout-ы, кнопки и вот это всё. В этой и паре следующих статей хочу поделиться некоторыми приёмами, которые помогали мне добиваться весьма красивых и интересных эффектов в плане рисования компонентов ui. Надеюсь, кому-нибудь это будет полезно. Ну или просто интересно.Небольшое введениеНе берусь говорить за всех, но, исходя из личного опыта, сложилось впечатление, что для достаточно большого количества разработчиков рисование каких-то "плашек" с нестандартными формой и поведением – крайне нежелательная задача. Кто-то больше в архитектуре, кто-то больше про "сделать бизнесу хорошо" с минимальными усилиями (соответственно, просят поумерить пыл дизайнеров) и т.п. И если уж приходится делать что-то из ряда вон, то начинается google, stackoverflow, эксперименты и т.д., что занимает немало времени, и появляется ощущение, что оно того вообще не стоит. Собственно, эту небольшую серию статей я и задумал как некоторую справку, прочтение которой снимет ряд вопросов и позволит быстрее оценивать/реализовывать нетипичные ui-компоненты. На конкретных примерах постараюсь продемонстрировать, как, что и почему можно делать.Пример 1: view с нестандартными границей и тенью
В данном случае идея простая: добавить ещё один слой в иерархию слоёв нашей view, порезать границы у этого слоя, а форму тени (уже у самой view) сделать ровно такой же, как и форма границы слоя.
Теперь чуть подробнее. У CALayer есть свойство mask. В документации можно прочитать, что это тот же самый опциональный CALayer, и если он не nil, то его альфа-канал используется как маска для контента исходного layer. То есть если взять png-картинку с котом и прозрачностью и каким-то образом засунуть ее в CALayer (назовем его catLayer), то при присваивании layer.mask = catLayer контент нашего исходного layer будет в виде кота, что бы ни находилось у него внутри. Может, текстовый кот получится, если внутри layer много текста. В нашем же случае нужен layer-маска в виде произвольной фигуры. Тут может помочь CAShapeLayer - наследник CALayer, который, грубо говоря, умеет внутри себя рисовать произвольную форму посредством задания ему проперти path. При использовании shapeLayer в качестве маски, всё, что находится вне формы, описываемой shapeLayer.path, работает как фильтр с alpha = 0.
Саму форму можно задать, используя UIBezierPath: для этого у последнего есть функции
addLine(to:), move(to:), addArc(withCenter:radius:startAngle:endAngle:clockwise) и т.д.
Здесь хотелось бы отметить пару моментов. Итоговый path должен выглядеть так, будто его "нарисовали, не отрывая карандаш от бумаги": стартуем из произвольной точки на границе и постепенно добавляем линии к общему пути так, чтобы конец предыдущей линии был началом следующей линии, и так далее. В конце возвращаемся в исходную точку. Некоторых сбивает с толку функция addArc, потому что в ней есть вроде и startAngle и endAngle, и clockwise. Вот clockwise как раз и нужен для того, чтобы управлять тем, вдоль какой из частей окружности, заданной двумя углами, мы двигаемся. В нашем примере в правом верхнем углу добавляется кусок окружности от -π/2 до 0 с clockwise равным именно true, иначе мы бы просто вырезали целую окружность из нашей view:
А зачем здесь вообще дополнительный слой? Почему бы не задать маску у исходного?
Проблема в том, что маска работает так, что отрезает просто всё, что ей попадётся, в том числе и тень слоя. Так что если задавать mask у слоя исходной view, то тени просто не будет видно.
Наконец, чтобы придать нужную форму тени, у CALayer есть свойство shadowPath.Полный код примера 1
import UIKit
final class SimpleCustomBorderAndShadowView: UIView {
private let frontLayer = CALayer()
private let inset: CGFloat = 40
// MARK: Override
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
frontLayer.frame = bounds
let maskAndShadowPath = UIBezierPath()
maskAndShadowPath.move(to: CGPoint(x: 0, y: inset))
maskAndShadowPath.addLine(to: CGPoint(x: inset, y: 0))
maskAndShadowPath.addLine(to: CGPoint(x: bounds.width - inset, y: 0))
maskAndShadowPath.addArc(withCenter: CGPoint(x: bounds.width - inset, y: inset),
radius: inset,
startAngle: -CGFloat.pi / 2,
endAngle: 0,
clockwise: true)
maskAndShadowPath.addLine(to: CGPoint(x: bounds.width, y: bounds.height - inset))
maskAndShadowPath.addLine(to: CGPoint(x: bounds.width - inset, y: bounds.height))
maskAndShadowPath.addLine(to: CGPoint(x: inset, y: bounds.height))
maskAndShadowPath.addArc(withCenter: CGPoint(x: inset, y: bounds.height - inset),
radius: inset,
startAngle: CGFloat.pi / 2,
endAngle: CGFloat.pi,
clockwise: true)
maskAndShadowPath.close()
(frontLayer.mask as? CAShapeLayer)?.frame = bounds
(frontLayer.mask as? CAShapeLayer)?.path = maskAndShadowPath.cgPath
layer.shadowPath = maskAndShadowPath.cgPath
}
// MARK: Setup
private func setup() {
backgroundColor = .clear
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = .zero
layer.shadowRadius = 20
layer.shadowOpacity = 1
frontLayer.mask = CAShapeLayer()
frontLayer.backgroundColor = UIColor.white.cgColor
layer.addSublayer(frontLayer)
}
}
Пример 2: view с вырезанной кривой произвольного вида
Данный пример выбран, чтобы продемонстрировать два момента: как вырезать что-то внутри слоя и как создать путь, как бы обводящий кривую линию на некотором расстоянии от неё.
Для того, чтобы вырезать что-то внутри слоя, нужно понимать, по какому правилу происходит раскрашивание форм, созданных с помощью UIBezierPath. В принципе, про это довольно внятно написано здесь. Получается, чтобы добиться эффекта как на картинке выше, нужно в итоговый path для маски добавить путь, обходящий внешнюю границу view, что делается с помощью UIBezierPath(roundedRect:cornerRadius:), и после добавить путь, отвечающей вырезу в форме кривой.
Для формы кривой используется функция addQuadCurve(to:controlPoint:). И если взять UIBezierPath, вызывать addQuadCurve, проставить ему ширину с помощью lineWidth, и добавить это в итоговый path для маски то... Ничего не выйдет. Если чуть-чуть задуматься и ещё вспомнить про это, то всё начинает казаться логичным: CoreGraphics нужно как-то сказать о границах, при переходе через которые происходит подсчёт каких-то counter-ов для дальнейшего решения о том, красить данную область или нет. Чтобы построить путь именно вокруг кривой, у CGPath есть функция copy(strokingWithWidth:lineCap:lineJoin:miterLimit:). Сам CGPath, в свою очередь, можно получить из UIBezierPath, обращаясь к свойству cgPath.
Конечно, насчёт кривой именно произвольной формы, описывающей вырезаемую область, я немного слукавил, потому что при возникновении самопересечений с учётом ширины будут возникать проблемы.Полный код примера 2
import UIKit
final class ErasedPathView: UIView {
private let frontLayer = CAShapeLayer()
// MARK: Override
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
frontLayer.frame = bounds
let maskAndShadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: 20)
let curvePath = UIBezierPath()
curvePath.move(to: CGPoint(x: bounds.width / 4, y: bounds.height / 4))
curvePath.addQuadCurve(to: CGPoint(x: bounds.width * 3 / 4, y: bounds.height * 3 / 4),
controlPoint: CGPoint(x: bounds.width, y: 0))
let innerPath = UIBezierPath(cgPath: curvePath.cgPath.copy(strokingWithWidth: 70, lineCap: .round, lineJoin: .round, miterLimit: 0))
maskAndShadowPath.append(innerPath)
(frontLayer.mask as? CAShapeLayer)?.frame = bounds
(frontLayer.mask as? CAShapeLayer)?.path = maskAndShadowPath.cgPath
layer.shadowPath = maskAndShadowPath.cgPath
}
// MARK: Setup
private func setup() {
backgroundColor = .clear
frontLayer.backgroundColor = UIColor.white.cgColor
layer.addSublayer(frontLayer)
let mask = CAShapeLayer()
mask.fillRule = .evenOdd
frontLayer.mask = mask
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = .zero
layer.shadowRadius = 20
layer.shadowOpacity = 1
}
}
Пример 3: рисование форм внутри view
Для того, чтобы просто рисовать внутри вашей view всё, что нравится, без создания дополнительных слоёв, можно опять же использовать CAShapeLayer. Нужно сделать override статического свойства layerClass у исходной view, возвращая ShapeLayer.self, и так же как и в Примере 1 задать этому слою path.
Есть один нюанс, не упомянутый ранее. При построении непрерывного пути при рисовании произвольной формы можно случайно перепрыгнуть из конца очередной линии в совершенно другое место. Типичный пример – добавление нового куска окружности при непустом path. В таких случаях CoreGraphics просто напросто дорисует за вас недостающую линию, соединяющую последнюю точку пути и новую точку очередной добавляемой линии. В совокупности с fillRule у CAShapeLayer этим можно аккуратно пользоваться. Например, на третьей справа картинке (карта треф) этот подход существенно упрощает рисование: не нужно думать о том, в каких именно местах пересекаются окружности. Пики
import UIKit
final class SpadeCardView: UIView {
var selfLayer: CAShapeLayer { layer as! CAShapeLayer }
private let inset: CGFloat = 20
// MARK: Override
static override var layerClass: AnyClass { CAShapeLayer.self }
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath()
let size = bounds.width - 2 * inset
let radius = size / 4
let alpha = atan(2 * radius / size)
path.move(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2))
path.addArc(withCenter: CGPoint(x: inset + radius, y: bounds.height / 2),
radius: radius, startAngle: 0,
endAngle: CGFloat.pi + 2 * alpha,
clockwise: true)
path.addLine(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 - size / 2))
path.addArc(withCenter: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2),
radius: radius,
startAngle: -2 * alpha,
endAngle: CGFloat.pi,
clockwise: true)
path.addQuadCurve(to: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2 + size / 2),
controlPoint: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))
path.addLine(to: CGPoint(x: bounds.width / 2 - radius, y: bounds.height / 2 + size / 2))
path.addQuadCurve(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2),
controlPoint: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))
selfLayer.path = path.cgPath
}
// MARK: Setup
private func setup() {
selfLayer.fillColor = UIColor.black.cgColor
selfLayer.strokeColor = UIColor.black.cgColor
selfLayer.lineWidth = 2
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = .zero
layer.shadowRadius = 10
layer.shadowOpacity = 1
}
}
Бубны
import UIKit
final class DiamondCardView: UIView {
var selfLayer: CAShapeLayer { layer as! CAShapeLayer }
private let inset: CGFloat = 20
private let adjustment: CGFloat = 10
// MARK: Override
static override var layerClass: AnyClass { CAShapeLayer.self }
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath()
let size = bounds.width - 2 * inset
path.move(to: CGPoint(x: inset, y: bounds.height / 2))
path.addQuadCurve(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 - size / 2),
controlPoint: CGPoint(x: bounds.width / 2 - adjustment, y: bounds.height / 2 - adjustment))
path.addQuadCurve(to: CGPoint(x: bounds.width - inset, y: bounds.height / 2),
controlPoint: CGPoint(x: bounds.width / 2 + adjustment, y: bounds.height / 2 - adjustment))
path.addQuadCurve(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2),
controlPoint: CGPoint(x: bounds.width / 2 + adjustment, y: bounds.height / 2 + adjustment))
path.addQuadCurve(to: CGPoint(x: inset, y: bounds.height / 2),
controlPoint: CGPoint(x: bounds.width / 2 - adjustment, y: bounds.height / 2 + adjustment))
selfLayer.path = path.cgPath
}
// MARK: Setup
private func setup() {
selfLayer.fillColor = UIColor.red.cgColor
selfLayer.strokeColor = UIColor.red.cgColor
selfLayer.lineWidth = 2
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = .zero
layer.shadowRadius = 20
layer.shadowOpacity = 1
}
}
Трефы
import UIKit
final class ClubCardView: UIView {
var selfLayer: CAShapeLayer { layer as! CAShapeLayer }
private let inset: CGFloat = 20
private let adjustment: CGFloat = 10
// MARK: Override
static override var layerClass: AnyClass { CAShapeLayer.self }
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath()
let size = bounds.width - 2 * inset
let radius = size / 4
path.move(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2))
path.addArc(withCenter: CGPoint(x: bounds.width / 2 - radius, y: bounds.height / 2 + adjustment),
radius: radius,
startAngle: 0,
endAngle: 2 * CGFloat.pi,
clockwise: true)
path.addArc(withCenter: CGPoint(x: bounds.width / 2, y: bounds.height / 2 - radius),
radius: radius,
startAngle: CGFloat.pi / 2,
endAngle: 5 * CGFloat.pi / 2,
clockwise: true)
path.addArc(withCenter: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2 + adjustment),
radius: radius,
startAngle: CGFloat.pi,
endAngle: 3 * CGFloat.pi,
clockwise: true)
path.addQuadCurve(to: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2 + size / 2),
controlPoint: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))
path.addLine(to: CGPoint(x: bounds.width / 2 - radius, y: bounds.height / 2 + size / 2))
path.addQuadCurve(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2),
controlPoint: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))
selfLayer.path = path.cgPath
}
// MARK: Setup
private func setup() {
selfLayer.fillColor = UIColor.black.cgColor
selfLayer.strokeColor = UIColor.black.cgColor
selfLayer.fillRule = .nonZero
selfLayer.lineWidth = 2
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = .zero
layer.shadowRadius = 20
layer.shadowOpacity = 1
}
}
Черви
import UIKit
final class HeartCardView: UIView {
var selfLayer: CAShapeLayer { layer as! CAShapeLayer }
private let inset: CGFloat = 20
// MARK: Override
static override var layerClass: AnyClass { CAShapeLayer.self }
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath()
let size = bounds.width - 2 * inset
let radius = size / 4
let alpha = atan(4 * radius / (3 * size))
path.move(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))
path.addArc(withCenter: CGPoint(x: inset + radius, y: bounds.height / 2 - radius),
radius: radius,
startAngle: CGFloat.pi - 2 * alpha,
endAngle: 0,
clockwise: true)
path.addArc(withCenter: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2 - radius),
radius: radius,
startAngle: -CGFloat.pi,
endAngle: 2 * alpha,
clockwise: true)
path.addLine(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))
selfLayer.path = path.cgPath
}
// MARK: Setup
private func setup() {
selfLayer.fillColor = UIColor.red.cgColor
selfLayer.strokeColor = UIColor.red.cgColor
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = .zero
layer.shadowRadius = 20
layer.shadowOpacity = 1
}
}
ЗаключениеНиже, так сказать, things to remember:
- +1 CALayer, mask, CAShapeLayer, shadowPath – для кастомной границы и тени
- copy(strokingWithWidth:lineCap:lineJoin:miterLimit:) – для объемной обводки path
- CAShapeLayer, path + fillRule – даёт интересные возможности
В следующей статье на эту тему постараюсь рассказать про layout-ы в коллекциях. Всем добра, пишите классные приложения и делайте красоту в интерфейсах!
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка мобильных приложений, Разработка под Android, Kotlin] Android и привязка к жизненному циклу компонентов
- [Python, GitHub, Flask] Делаем телеграм бота за 5 минут: быстрый старт с продвинутым шаблоном
- [Разработка мобильных приложений, Машинное обучение, Искусственный интеллект, Natural Language Processing] OpenAI: более 300 сторонних приложений работают на GPT-3
- [Программирование, C++, C, Разработка под Linux] Приёмы неблокирующего программирования: полные барьеры памяти (перевод)
- [.NET, C#, Программирование микроконтроллеров, Интернет вещей, DIY или Сделай сам] .NET nanoFramework — платформа для разработки приложений на C# для микроконтроллеров
- [Тестирование IT-систем, API] Создание в SoapUI асинхронного REST MockService с запуском в Portainer
- [Разработка мобильных приложений, Git, Big Data, Машинное обучение] DVC — Git для данных на примере ML-проекта
- [Разработка мобильных приложений, Разработка под Android, Kotlin] Android + Redux = <3
- [Разработка мобильных приложений, Разработка под Android, DevOps, Gradle] Советы по работе с Gradle для Android-разработчиков
- [Java, Администрирование баз данных, DevOps] Версионирование структуры БД при помощи Liquibase
Теги для поиска: #_razrabotka_pod_ios (Разработка под iOS), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_swift, #_dizajn_mobilnyh_prilozhenij (Дизайн мобильных приложений), #_swift, #_ios, #_ios_development, #_ui, #_razrabotka_pod_ios (
Разработка под iOS
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_swift, #_dizajn_mobilnyh_prilozhenij (
Дизайн мобильных приложений
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:26
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Привет читателям Хабра!Я iOS-разработчик, и так случилось, что мне приходилось много делать в ui: кастомные view, тени, layout-ы, кнопки и вот это всё. В этой и паре следующих статей хочу поделиться некоторыми приёмами, которые помогали мне добиваться весьма красивых и интересных эффектов в плане рисования компонентов ui. Надеюсь, кому-нибудь это будет полезно. Ну или просто интересно.Небольшое введениеНе берусь говорить за всех, но, исходя из личного опыта, сложилось впечатление, что для достаточно большого количества разработчиков рисование каких-то "плашек" с нестандартными формой и поведением – крайне нежелательная задача. Кто-то больше в архитектуре, кто-то больше про "сделать бизнесу хорошо" с минимальными усилиями (соответственно, просят поумерить пыл дизайнеров) и т.п. И если уж приходится делать что-то из ряда вон, то начинается google, stackoverflow, эксперименты и т.д., что занимает немало времени, и появляется ощущение, что оно того вообще не стоит. Собственно, эту небольшую серию статей я и задумал как некоторую справку, прочтение которой снимет ряд вопросов и позволит быстрее оценивать/реализовывать нетипичные ui-компоненты. На конкретных примерах постараюсь продемонстрировать, как, что и почему можно делать.Пример 1: view с нестандартными границей и тенью В данном случае идея простая: добавить ещё один слой в иерархию слоёв нашей view, порезать границы у этого слоя, а форму тени (уже у самой view) сделать ровно такой же, как и форма границы слоя. Теперь чуть подробнее. У CALayer есть свойство mask. В документации можно прочитать, что это тот же самый опциональный CALayer, и если он не nil, то его альфа-канал используется как маска для контента исходного layer. То есть если взять png-картинку с котом и прозрачностью и каким-то образом засунуть ее в CALayer (назовем его catLayer), то при присваивании layer.mask = catLayer контент нашего исходного layer будет в виде кота, что бы ни находилось у него внутри. Может, текстовый кот получится, если внутри layer много текста. В нашем же случае нужен layer-маска в виде произвольной фигуры. Тут может помочь CAShapeLayer - наследник CALayer, который, грубо говоря, умеет внутри себя рисовать произвольную форму посредством задания ему проперти path. При использовании shapeLayer в качестве маски, всё, что находится вне формы, описываемой shapeLayer.path, работает как фильтр с alpha = 0. Саму форму можно задать, используя UIBezierPath: для этого у последнего есть функции addLine(to:), move(to:), addArc(withCenter:radius:startAngle:endAngle:clockwise) и т.д. Здесь хотелось бы отметить пару моментов. Итоговый path должен выглядеть так, будто его "нарисовали, не отрывая карандаш от бумаги": стартуем из произвольной точки на границе и постепенно добавляем линии к общему пути так, чтобы конец предыдущей линии был началом следующей линии, и так далее. В конце возвращаемся в исходную точку. Некоторых сбивает с толку функция addArc, потому что в ней есть вроде и startAngle и endAngle, и clockwise. Вот clockwise как раз и нужен для того, чтобы управлять тем, вдоль какой из частей окружности, заданной двумя углами, мы двигаемся. В нашем примере в правом верхнем углу добавляется кусок окружности от -π/2 до 0 с clockwise равным именно true, иначе мы бы просто вырезали целую окружность из нашей view: А зачем здесь вообще дополнительный слой? Почему бы не задать маску у исходного? Проблема в том, что маска работает так, что отрезает просто всё, что ей попадётся, в том числе и тень слоя. Так что если задавать mask у слоя исходной view, то тени просто не будет видно. Наконец, чтобы придать нужную форму тени, у CALayer есть свойство shadowPath.Полный код примера 1 import UIKit
final class SimpleCustomBorderAndShadowView: UIView { private let frontLayer = CALayer() private let inset: CGFloat = 40 // MARK: Override override init(frame: CGRect) { super.init(frame: frame) setup() } required init?(coder: NSCoder) { super.init(coder: coder) setup() } override func layoutSubviews() { super.layoutSubviews() frontLayer.frame = bounds let maskAndShadowPath = UIBezierPath() maskAndShadowPath.move(to: CGPoint(x: 0, y: inset)) maskAndShadowPath.addLine(to: CGPoint(x: inset, y: 0)) maskAndShadowPath.addLine(to: CGPoint(x: bounds.width - inset, y: 0)) maskAndShadowPath.addArc(withCenter: CGPoint(x: bounds.width - inset, y: inset), radius: inset, startAngle: -CGFloat.pi / 2, endAngle: 0, clockwise: true) maskAndShadowPath.addLine(to: CGPoint(x: bounds.width, y: bounds.height - inset)) maskAndShadowPath.addLine(to: CGPoint(x: bounds.width - inset, y: bounds.height)) maskAndShadowPath.addLine(to: CGPoint(x: inset, y: bounds.height)) maskAndShadowPath.addArc(withCenter: CGPoint(x: inset, y: bounds.height - inset), radius: inset, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: true) maskAndShadowPath.close() (frontLayer.mask as? CAShapeLayer)?.frame = bounds (frontLayer.mask as? CAShapeLayer)?.path = maskAndShadowPath.cgPath layer.shadowPath = maskAndShadowPath.cgPath } // MARK: Setup private func setup() { backgroundColor = .clear layer.shadowColor = UIColor.black.cgColor layer.shadowOffset = .zero layer.shadowRadius = 20 layer.shadowOpacity = 1 frontLayer.mask = CAShapeLayer() frontLayer.backgroundColor = UIColor.white.cgColor layer.addSublayer(frontLayer) } } Данный пример выбран, чтобы продемонстрировать два момента: как вырезать что-то внутри слоя и как создать путь, как бы обводящий кривую линию на некотором расстоянии от неё. Для того, чтобы вырезать что-то внутри слоя, нужно понимать, по какому правилу происходит раскрашивание форм, созданных с помощью UIBezierPath. В принципе, про это довольно внятно написано здесь. Получается, чтобы добиться эффекта как на картинке выше, нужно в итоговый path для маски добавить путь, обходящий внешнюю границу view, что делается с помощью UIBezierPath(roundedRect:cornerRadius:), и после добавить путь, отвечающей вырезу в форме кривой. Для формы кривой используется функция addQuadCurve(to:controlPoint:). И если взять UIBezierPath, вызывать addQuadCurve, проставить ему ширину с помощью lineWidth, и добавить это в итоговый path для маски то... Ничего не выйдет. Если чуть-чуть задуматься и ещё вспомнить про это, то всё начинает казаться логичным: CoreGraphics нужно как-то сказать о границах, при переходе через которые происходит подсчёт каких-то counter-ов для дальнейшего решения о том, красить данную область или нет. Чтобы построить путь именно вокруг кривой, у CGPath есть функция copy(strokingWithWidth:lineCap:lineJoin:miterLimit:). Сам CGPath, в свою очередь, можно получить из UIBezierPath, обращаясь к свойству cgPath. Конечно, насчёт кривой именно произвольной формы, описывающей вырезаемую область, я немного слукавил, потому что при возникновении самопересечений с учётом ширины будут возникать проблемы.Полный код примера 2 import UIKit
final class ErasedPathView: UIView { private let frontLayer = CAShapeLayer() // MARK: Override override init(frame: CGRect) { super.init(frame: frame) setup() } required init?(coder: NSCoder) { super.init(coder: coder) setup() } override func layoutSubviews() { super.layoutSubviews() frontLayer.frame = bounds let maskAndShadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: 20) let curvePath = UIBezierPath() curvePath.move(to: CGPoint(x: bounds.width / 4, y: bounds.height / 4)) curvePath.addQuadCurve(to: CGPoint(x: bounds.width * 3 / 4, y: bounds.height * 3 / 4), controlPoint: CGPoint(x: bounds.width, y: 0)) let innerPath = UIBezierPath(cgPath: curvePath.cgPath.copy(strokingWithWidth: 70, lineCap: .round, lineJoin: .round, miterLimit: 0)) maskAndShadowPath.append(innerPath) (frontLayer.mask as? CAShapeLayer)?.frame = bounds (frontLayer.mask as? CAShapeLayer)?.path = maskAndShadowPath.cgPath layer.shadowPath = maskAndShadowPath.cgPath } // MARK: Setup private func setup() { backgroundColor = .clear frontLayer.backgroundColor = UIColor.white.cgColor layer.addSublayer(frontLayer) let mask = CAShapeLayer() mask.fillRule = .evenOdd frontLayer.mask = mask layer.shadowColor = UIColor.black.cgColor layer.shadowOffset = .zero layer.shadowRadius = 20 layer.shadowOpacity = 1 } } Для того, чтобы просто рисовать внутри вашей view всё, что нравится, без создания дополнительных слоёв, можно опять же использовать CAShapeLayer. Нужно сделать override статического свойства layerClass у исходной view, возвращая ShapeLayer.self, и так же как и в Примере 1 задать этому слою path. Есть один нюанс, не упомянутый ранее. При построении непрерывного пути при рисовании произвольной формы можно случайно перепрыгнуть из конца очередной линии в совершенно другое место. Типичный пример – добавление нового куска окружности при непустом path. В таких случаях CoreGraphics просто напросто дорисует за вас недостающую линию, соединяющую последнюю точку пути и новую точку очередной добавляемой линии. В совокупности с fillRule у CAShapeLayer этим можно аккуратно пользоваться. Например, на третьей справа картинке (карта треф) этот подход существенно упрощает рисование: не нужно думать о том, в каких именно местах пересекаются окружности. Пики import UIKit
final class SpadeCardView: UIView { var selfLayer: CAShapeLayer { layer as! CAShapeLayer } private let inset: CGFloat = 20 // MARK: Override static override var layerClass: AnyClass { CAShapeLayer.self } override init(frame: CGRect) { super.init(frame: frame) setup() } required init?(coder: NSCoder) { super.init(coder: coder) setup() } override func layoutSubviews() { super.layoutSubviews() let path = UIBezierPath() let size = bounds.width - 2 * inset let radius = size / 4 let alpha = atan(2 * radius / size) path.move(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2)) path.addArc(withCenter: CGPoint(x: inset + radius, y: bounds.height / 2), radius: radius, startAngle: 0, endAngle: CGFloat.pi + 2 * alpha, clockwise: true) path.addLine(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 - size / 2)) path.addArc(withCenter: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2), radius: radius, startAngle: -2 * alpha, endAngle: CGFloat.pi, clockwise: true) path.addQuadCurve(to: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2 + size / 2), controlPoint: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2)) path.addLine(to: CGPoint(x: bounds.width / 2 - radius, y: bounds.height / 2 + size / 2)) path.addQuadCurve(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2), controlPoint: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2)) selfLayer.path = path.cgPath } // MARK: Setup private func setup() { selfLayer.fillColor = UIColor.black.cgColor selfLayer.strokeColor = UIColor.black.cgColor selfLayer.lineWidth = 2 layer.shadowColor = UIColor.black.cgColor layer.shadowOffset = .zero layer.shadowRadius = 10 layer.shadowOpacity = 1 } } import UIKit
final class DiamondCardView: UIView { var selfLayer: CAShapeLayer { layer as! CAShapeLayer } private let inset: CGFloat = 20 private let adjustment: CGFloat = 10 // MARK: Override static override var layerClass: AnyClass { CAShapeLayer.self } override init(frame: CGRect) { super.init(frame: frame) setup() } required init?(coder: NSCoder) { super.init(coder: coder) setup() } override func layoutSubviews() { super.layoutSubviews() let path = UIBezierPath() let size = bounds.width - 2 * inset path.move(to: CGPoint(x: inset, y: bounds.height / 2)) path.addQuadCurve(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 - size / 2), controlPoint: CGPoint(x: bounds.width / 2 - adjustment, y: bounds.height / 2 - adjustment)) path.addQuadCurve(to: CGPoint(x: bounds.width - inset, y: bounds.height / 2), controlPoint: CGPoint(x: bounds.width / 2 + adjustment, y: bounds.height / 2 - adjustment)) path.addQuadCurve(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2), controlPoint: CGPoint(x: bounds.width / 2 + adjustment, y: bounds.height / 2 + adjustment)) path.addQuadCurve(to: CGPoint(x: inset, y: bounds.height / 2), controlPoint: CGPoint(x: bounds.width / 2 - adjustment, y: bounds.height / 2 + adjustment)) selfLayer.path = path.cgPath } // MARK: Setup private func setup() { selfLayer.fillColor = UIColor.red.cgColor selfLayer.strokeColor = UIColor.red.cgColor selfLayer.lineWidth = 2 layer.shadowColor = UIColor.black.cgColor layer.shadowOffset = .zero layer.shadowRadius = 20 layer.shadowOpacity = 1 } } import UIKit
final class ClubCardView: UIView { var selfLayer: CAShapeLayer { layer as! CAShapeLayer } private let inset: CGFloat = 20 private let adjustment: CGFloat = 10 // MARK: Override static override var layerClass: AnyClass { CAShapeLayer.self } override init(frame: CGRect) { super.init(frame: frame) setup() } required init?(coder: NSCoder) { super.init(coder: coder) setup() } override func layoutSubviews() { super.layoutSubviews() let path = UIBezierPath() let size = bounds.width - 2 * inset let radius = size / 4 path.move(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2)) path.addArc(withCenter: CGPoint(x: bounds.width / 2 - radius, y: bounds.height / 2 + adjustment), radius: radius, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true) path.addArc(withCenter: CGPoint(x: bounds.width / 2, y: bounds.height / 2 - radius), radius: radius, startAngle: CGFloat.pi / 2, endAngle: 5 * CGFloat.pi / 2, clockwise: true) path.addArc(withCenter: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2 + adjustment), radius: radius, startAngle: CGFloat.pi, endAngle: 3 * CGFloat.pi, clockwise: true) path.addQuadCurve(to: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2 + size / 2), controlPoint: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2)) path.addLine(to: CGPoint(x: bounds.width / 2 - radius, y: bounds.height / 2 + size / 2)) path.addQuadCurve(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2), controlPoint: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2)) selfLayer.path = path.cgPath } // MARK: Setup private func setup() { selfLayer.fillColor = UIColor.black.cgColor selfLayer.strokeColor = UIColor.black.cgColor selfLayer.fillRule = .nonZero selfLayer.lineWidth = 2 layer.shadowColor = UIColor.black.cgColor layer.shadowOffset = .zero layer.shadowRadius = 20 layer.shadowOpacity = 1 } } import UIKit
final class HeartCardView: UIView { var selfLayer: CAShapeLayer { layer as! CAShapeLayer } private let inset: CGFloat = 20 // MARK: Override static override var layerClass: AnyClass { CAShapeLayer.self } override init(frame: CGRect) { super.init(frame: frame) setup() } required init?(coder: NSCoder) { super.init(coder: coder) setup() } override func layoutSubviews() { super.layoutSubviews() let path = UIBezierPath() let size = bounds.width - 2 * inset let radius = size / 4 let alpha = atan(4 * radius / (3 * size)) path.move(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2)) path.addArc(withCenter: CGPoint(x: inset + radius, y: bounds.height / 2 - radius), radius: radius, startAngle: CGFloat.pi - 2 * alpha, endAngle: 0, clockwise: true) path.addArc(withCenter: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2 - radius), radius: radius, startAngle: -CGFloat.pi, endAngle: 2 * alpha, clockwise: true) path.addLine(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2)) selfLayer.path = path.cgPath } // MARK: Setup private func setup() { selfLayer.fillColor = UIColor.red.cgColor selfLayer.strokeColor = UIColor.red.cgColor layer.shadowColor = UIColor.black.cgColor layer.shadowOffset = .zero layer.shadowRadius = 20 layer.shadowOpacity = 1 } }
=========== Источник: habr.com =========== Похожие новости:
Разработка под iOS ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_swift, #_dizajn_mobilnyh_prilozhenij ( Дизайн мобильных приложений ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:26
Часовой пояс: UTC + 5