Русский
Русский
English
Статистика
Реклама

IOS. UI. Примы. Часть 1

Привет читателям Хабра!

Я 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 UIKitfinal 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 UIKitfinal 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 UIKitfinal 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 UIKitfinal 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 UIKitfinal 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 UIKitfinal 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
К списку статей
Опубликовано: 27.03.2021 20:12:03
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Разработка под ios

Разработка мобильных приложений

Swift

Дизайн мобильных приложений

Ios

Ios development

Ui

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru