Туториалы
01 апреля 2021
Туториалы
01 апреля 2021
Туториал по Core Graphics: градиенты и контексты

Привет!

Из этого туториала по Core Graphics вы узнаете, как разработать современное приложение для iOS с продвинутыми элементами Core Graphics, такими как градиенты и трансформации.

В этой статье мы научимся рисовать градиенты и манипулировать CGContexts с преобразованиями.

Core Graphics

Теперь вам придется покинуть уютный мир UIKit и окунуться в подземелья Core Graphics.

Это изображение от Apple концептуально описывает соответствующие фреймворки:

UIKit - это верхний слой, самый простой для понимания. Вы использовали UIBezierPath, который являлся UIKit-оболочкой Core Graphics CGPath.

Фреймворк Core Graphics основан на продвинутом движке Quartz. Это обеспечивает низкоуровневый, легкий 2D рендеринг. Вы можете использовать этот фреймворк для обработки изображений на основе контуров, их преобразований, управления цветами и многого другого.

Первое, что вам нужно запомнить о низкоуровневых объектах и ​​функциях Core Graphics, это то, что они всегда имеют префикс CG, поэтому их легко распознать.

Давайте начнем

К концу данного туториала вы создадите некий график, который будет выглядеть примерно так:

Прежде выводить информацию на графике, вы настроите его в Storyboard и создадите код, который создает анимированный переход и показывает график.

Полная иерархия представлений будет выглядеть следующим образом:

Сначала загрузите материалы проекта. Когда вы откроете проект, вы заметите, что он очень похож на то, что у вас получилось после прохождения предыдущего туториала. Единственная отличие от проекта из предыдущего туториала состоит в том, что в Main.storyboard CounterView находится внутри другого представления с желтым фоном. Запустите проект, вот что вы увидите:

Создание графа

Перейдите в File ▸ New ▸ File…, выберите шаблон iOS ▸ Source ▸ Cocoa Touch Class и нажмите Next . Введите имя GraphView в качестве имени класса, выберите подкласс UIView и установите язык Swift . Нажмите Next, затем Create.
Теперь, в Main.storyboard щелкните название желтого представления в Document Outline и нажмите Enter, чтобы переименовать его. Назовите его Container View. Перетащите новый UIView-объект из библиотеки объектов в Container View, под Counter View.
Измените класс нового представления на GraphView в Identity inspector. Осталось только добавить ограничения для нового GraphView, аналогично тому, как вы добавляли ограничения в предыдущем туториале:

  • Выделите GraphView, удерживая control, перетащите курсор из центра немного влево, не заходя за рамки представления, и выберите Width в всплывающем меню.
  • По-прежнему выберите GraphView и, удерживая control, перетащите курсор из центра немного вверх, не выходя за рамки представления. В всплывающем меню выберите Height.
  • Удерживая control, перетащите курсор влево от центра представления за его пределы и выберите Center Horizontally in Container.
  • Удерживая control, перетащите курсор вверх от центра представления за его пределы и выберите Center Vertically in Container.

Измените константы ограничений в Size Inspector, чтобы они соответствовали константам, приведенным на изображении:

Ваш Document Outline должен выглядеть так:

Причина, по которой вам нужен Container View, заключается в том, что без него не получиться сделать анимированный переход между Counter View и Graph View.
Перейдите в раздел ViewController.swift и добавьте аутлеты свойств для Container view и Graph view.


@IBOutlet weak var containerView: UIView!
@IBOutlet weak var graphView: GraphView!

Это создает аутлет для Container view и Graph view. Теперь подключите их к представлениям, которые вы создали в Storyboard.
Вернитесь к Main.storyboard и перетащите Container view и Graph view к соответствующим аутлетам:

Настройка анимированного перехода

Все еще находясь в разделе Main.storyboard, перетащите Tap Gesture Recognizer из Object Library в Container View в Document Outline.

Затем, перейдите в ViewController.swift и добавьте это свойство в начало класса:


var isGraphViewShowing = false

Это свойство указывает, отображается ли в данный момент представление графика.

Теперь добавьте этот метод касания, чтобы сделать переход:


@IBAction func counterViewTap(_ gesture: UITapGestureRecognizer?) {
  // Hide Graph
  if isGraphViewShowing {
    UIView.transition(
      from: graphView,
      to: counterView,
      duration: 1.0,
      options: [.transitionFlipFromLeft, .showHideTransitionViews],
      completion: nil
    )
  } else {
    // Show Graph
    UIView.transition(
      from: counterView,
      to: graphView,
      duration: 1.0,
      options: [.transitionFlipFromRight, .showHideTransitionViews],
      completion: nil
    )
  }
  isGraphViewShowing.toggle()
}

UIView.transition(from:to:duration:options:completion:) выполняет горизонтальный флип-переход. Другими доступными переходами являются перекрестное растворение, вертикальное переворачивание и скручивание вверх или вниз. Переход использует .showHideTransitionViews таким образом, вам не нужно будет удалять представление, чтобы оно не отображалось, когда станет «скрыто» в переходе.
Добавьте этот код в конце pushButtonPressed(_:):


if isGraphViewShowing {
  counterViewTap(nil)
}

Если пользователь нажимает кнопку “+” во время отображения графика, дисплей поворачивается назад, чтобы показать счетчик.
Теперь, чтобы этот переход заработал, вернитесь на Main.storyboard и перетащите ваш жест касания к недавно добавленному counterViewTap(gesture:):

Запустите. Вы увидите Graph View при запуске приложения. Позже вы сделаете Graph View скрытым для того, чтобы сначала на экране появилось представление счетчика. Нажмите на него, и вы увидите флип-переход.

Анализ Graph View

В Core Graphics изображение будет отрисовываться от заднего плана к переднему. Поэтому сначала вам нужно сформировать в голове определенный порядок элементов, а уже потом переводить его в код. Это последовательность для графика Flo:

  1. Градиентный фон
  2. Отсеченный градиент под графиком
  3. Сам график
  4. Окружности на точках графика
  5. Горизонтальные линии графика
  6. Графические метки

Рисование градиента

Нарисуем градиент в представлении графика.

Откройте GraphView.swift и замените код на:


import UIKit

@IBDesignable
class GraphView: UIView {
  // 1
  @IBInspectable var startColor: UIColor = .red
  @IBInspectable var endColor: UIColor = .green

  override func draw(_ rect: CGRect) {
    // 2
    guard let context = UIGraphicsGetCurrentContext() else {
      return
    }
    let colors = [startColor.cgColor, endColor.cgColor]
    
    // 3
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    
    // 4
    let colorLocations: [CGFloat] = [0.0, 1.0]
    
    // 5
    guard let gradient = CGGradient(
      colorsSpace: colorSpace,
      colors: colors as CFArray,
      locations: colorLocations
    ) else {
      return
    }
    
    // 6
    let startPoint = CGPoint.zero
    let endPoint = CGPoint(x: 0, y: bounds.height)
    context.drawLinearGradient(
      gradient,
      start: startPoint,
      end: endPoint,
      options: []
    )
  }
}

Вот что вам нужно знать из приведенного выше кода:

  1. Вам необходимо установить начальный и конечный цвета для градиента как свойство @IBInspectable, чтобы потом можно было изменить их в Storyboard.
  2. Функции рисования CG должны знать контекст, в котором они будут рисоваться, поэтому Вы используете метод UIKit UIGraphicsGetCurrentContext() для получения текущего контекста. В него будет рисоваться draw(_:).
  3. Все контексты имеют цветовое пространство. Это может быть CMYK или оттенки серого, но здесь вы используете цветовое пространство RGB.
  4. Остановки цвета (color stops) описывают, где изменяются цвета в градиенте. В этом примере у вас есть только два цвета: красный и зеленый, однако вы можете иметь массив из трех цветов, например красный, синий и зеленый. Остановки находятся между 0 и 1, где 0,33 - ⅓ часть всего градиента.
  5. Затем вам нужно создать градиент, определяя цветовое пространство, цвета и цветовые остановки.
  6. Наконец, вам нужно нарисовать градиент. drawLinearGradient(_:start:end:options:) принимает следующие параметры:
  • CGGradient с цветовым пространством, цветами и остановками
  • Начальная точка
  • Конечная точка
  • Опциональные флаги для расширения градиента

Градиент заполнит весь rect, переданный для draw(_:).

Откройте Main.storyboard, и вы увидите, что в представлении графика появился градиент.

В Storyboard выберите Graph View. Затем в Attributes inspector измените Start Color (цвет начала градиента) на RGB(250, 233, 222), а End Color (цвет конца градиента) на RGB (252, 79, 8). Для этого нажмите на Color, а затем на Custom:

Теперь давайте приведем все в порядок. В Main.storyboard выберите каждое представление по очереди, кроме основного, и установите значение «Clear Color» в параметр «Background Color». Вам больше не нужен желтый цвет, а все кнопки все равно будут иметь прозрачный фон.
Теперь запустите проект. Вы заметите, что график начал выглядеть намного лучше, ну или, по крайней мере обзавелся адекватным фоном.

Отсечение областей

Используя градиент, вы заполнили всю область контекста представления. Однако, если вы не хотите заполнять всю область, вы можете создать контуры для обрезки области градиента.

Чтобы сделать это, перейдите к GraphView.swift .

Добавьте эти константы в начало GraphView. Их, позже, вы будете использовать для отрисовки:


private enum Constants {
  static let cornerRadiusSize = CGSize(width: 8.0, height: 8.0)
  static let margin: CGFloat = 20.0
  static let topBorder: CGFloat = 60
  static let bottomBorder: CGFloat = 50
  static let colorAlpha: CGFloat = 0.3
  static let circleDiameter: CGFloat = 5.0
}

Теперь, в начало draw(_:) добавьте этот код:


let path = UIBezierPath(
  roundedRect: rect,
  byRoundingCorners: .allCorners,
  cornerRadii: Constants.cornerRadiusSize
)
path.addClip()

Это создаст область отсечения, которая будет ограничивать градиент. Позже, вы будете использовать этот же прием, чтобы нарисовать второй градиент под линией графика.

Запустите и убедитесь, что у представления графика есть красивые закругленные углы:

Заметка

Рисование статических представлений с помощью Core Graphics обычно выполняется достаточно быстро, но если ваши представления перемещаются или требуют частого перерисовывания, вам следует использовать слои Core Animation. Базовая анимация оптимизирована таким образом, что большую часть обработки выполняет графическое ядро, а не процессор. В ином случае, процессор обрабатывает чертеж представления, выполненный Core Graphics в draw(_:).

Если Вы используете Core Animation, вы будете использовать свойство CALayer cornerRadius вместо отсечения.

Расчет точек графика

Сделаем небольшой перерыв и займемся самим графиком. Вам нужны 7 точек. Ось X будет днями недели, а ось Y показателем выпитых стаканов воды.

Во-первых, создайте примерные данные.

Находясь в разделе GraphView.swift, в начале этого класса добавьте эти свойства:


// Weekly sample data
var graphPoints = [4, 2, 6, 4, 5, 8, 3]

Здесь содержатся примерные данные, которые представляют собой показания за семь дней.

Добавьте этот код в начало draw(_:):


let width = rect.width
let height = rect.height

И вот этот код в конец draw(_:):


// Calculate the x point
    
let margin = Constants.margin
let graphWidth = width - margin * 2 - 4
let columnXPoint = { (column: Int) -> CGFloat in
  // Calculate the gap between points
  let spacing = graphWidth / CGFloat(self.graphPoints.count - 1)
  return CGFloat(column) * spacing + margin + 2
}

Ось X состоит из 7 равноудаленных друг от друга точек. Приведенный выше код, по сути, является безымянной функцией. Его можно было бы добавить в наш проект как функцию, но для небольших вычислений, подобных этому, вы можете оставить все и в таком состоянии.

columnXPoint требует в качестве аргумента столбец и возвращает значение, в котором содержится положение точки на оси X.

Добавьте этот код для вычисления точек оси Y в конце draw(_:):


// Calculate the y point
    
let topBorder = Constants.topBorder
let bottomBorder = Constants.bottomBorder
let graphHeight = height - topBorder - bottomBorder
guard let maxValue = graphPoints.max() else {
  return
}
let columnYPoint = { (graphPoint: Int) -> CGFloat in
  let yPoint = CGFloat(graphPoint) / CGFloat(maxValue) * graphHeight
  return graphHeight + topBorder - yPoint // Переворот графика
}

columnYPoint также является безымянной функцией, которая берет значение из массива для дня недели в качестве параметра. Она возвращает позицию Y между 0 и наибольшим количеством выпитых стаканов.
Поскольку точка отсчета в Core Graphics находится в левом верхнем углу, а вы рисуете график из исходной точки в нижнем левом углу, функция columnYPoint корректирует возвращаемое значение, чтобы график был ориентирован в правильном направлении.
Продолжим, добавив код для рисования линии в конец draw(_:):


// Отрисовка линии графика

UIColor.white.setFill()
UIColor.white.setStroke()
    
// Задание точек графика
let graphPath = UIBezierPath()

// Идем на начало линии графика
graphPath.move(to: CGPoint(x: columnXPoint(0), y: columnYPoint(graphPoints[0])))
    
// Добавляем точки для каждого элемента в массиве graphPointsAdd
// в соответсвующие точки (x, y)
for i in 1 ..< graphPoints.count {
  let nextPoint = CGPoint(x: columnXPoint(i), y: columnYPoint(graphPoints[i]))
  graphPath.addLine(to: nextPoint)
}

graphPath.stroke()

В этом блоке кода вы задаете направление для графика. UIBezierPath построен из точек по X и Y для каждого элемента graphPoints.
Представление графика в Storyboard теперь должно выглядеть следующим образом:

Теперь, когда вы убедились, что линия рисуется правильно, удалите это из конца draw(_:):


graphPath.stroke()

Это кусочек кода нужен был того, чтобы вы могли проверить строку в Storyboard и убедиться, что все расчеты верны.

Создание градиента для графика

Теперь создадим градиент под графиком, используя сам график в качестве контура для отсечения.
Сначала обозначьте контур отсечения в конце draw(_:):


// Создаем траекторию обрезания для градиента графика

// 1 - Сохраняем состояние контекста (commented out for now)
//context.saveGState()
    
// 2 - Создаем копию кривой
guard let clippingPath = graphPath.copy() as? UIBezierPath else {
  return
}
    
// 3 - Добавляем линии в скопированную кривую для завершения обрезаемой области
clippingPath.addLine(to: CGPoint(
  x: columnXPoint(graphPoints.count - 1), 
  y: height))
clippingPath.addLine(to: CGPoint(x: columnXPoint(0), y: height))
clippingPath.close()
    
// 4 - Добавляем обрезающую кривую в контекст
clippingPath.addClip()
    
// 5 - Проверяем обрезающую кривую - Временный код
UIColor.green.setFill()
let rectPath = UIBezierPath(rect: rect)
rectPath.fill()
// Завершение временного кода

В приведенном выше коде вы:

  1. Временно раскомментировали context.saveGState(). Мы вернемся к этому моменту, как только поймем, что делает данная строка кода.
  2. Скопировали уже построенный контур в новый контур, который будет определять область для заливки градиентом.
  3. Ограничили область угловыми точками точками и завершили контур. Это действие добавит нижнюю правую и нижнюю левую точки графика.
  4. Добавили контур отсечения к контексту. Когда контекст будет заполнен, заполнится только контур, отсеченный графиком.
  5. Заполнили контекст. Помните, что rect - это область контекста, которая передается в draw(_:).

Сейчас ваше представление графика в Storyboard`е должно выглядеть следующим образом:

Сейчас мы заменим зеленый цвет градиентом, который создадите из цветов, используемых для градиента фона.
Замените временный код в комментарии №5 этим кодом:


let highestYPoint = columnYPoint(maxValue)
let graphStartPoint = CGPoint(x: margin, y: highestYPoint)
let graphEndPoint = CGPoint(x: margin, y: bounds.height)
        
context.drawLinearGradient(
  gradient, 
  start: graphStartPoint, 
  end: graphEndPoint, 
  options: [])
//context.restoreGState()

В этом блоке кода мы находим наибольшее количество выпитых стаканов и используем его в качестве отправной точки градиента.
Вы не можете заполнить rect тем же способом, как вы делали это с зеленым цветом. Градиент будет заполнять пространство над контекстом, а не над графиком, и желаемый градиент не будет отображаться.
Обратите внимание на комментарии к context.restoreGState(): Вы удалите их после того, как нарисуете круги вокруг точек графика.
В конце draw(_:) добавьте следующее:


// Draw the line on top of the clipped gradient
graphPath.lineWidth = 2.0
graphPath.stroke()

Этот код рисует исходный контур.
Теперь ваш график действительно начинает обретать внешний вид:

Рисуем точки графика

В конце draw(_:) добавьте это:


// Draw the circles on top of the graph stroke
for i in 0 ..< graphPoints.count {
  var point = CGPoint(x: columnXPoint(i), y: columnYPoint(graphPoints[i]))
  point.x -= Constants.circleDiameter / 2
  point.y -= Constants.circleDiameter / 2
      
  let circle = UIBezierPath(
    ovalIn: CGRect(
      origin: point,
      size: CGSize(
        width: Constants.circleDiameter, 
        height: Constants.circleDiameter)
    )
  )
  circle.fill()
}

В приведенном выше коде вы отрисовываете точки графика в виде заполненных круговых контуров для каждого элемента массива в вычисленных точках X и Y.

Однако, эти окружности выглядят неровными.

Рассмотрим состояние контекстов

Причина странного вида окружностей связана с контекстами. Графические контексты могут сохранять состояния. Таким образом, когда вы задаете множество свойств контекста, таких как цвет заливки, матрица преобразования, цветовое пространство или область клипа, вы фактически устанавливаете их только для текущего состояния графика.

Вы можете сохранить состояние с помощью команды context.saveGState(), которая помещает копию текущего графического состояния в стек состояний. Вы также можете вносить изменения в свойства контекста, но при вызове context.restoreGState() исходное состояние удаляется из стека и свойства контекста возвращаются. Вот почему вы видите странную проблему с кругами.

Находитесь в GraphView.swift, в draw(_:) раскомментируйте context.saveGState() прежде чем создавать контур отсечения. Кроме того, раскомментируйте context.restoreGState() до того, как начнете его использовать.

Делая это, Вы:

  1. Вставляете исходное графическое состояние в стек с помощью context.saveGState().
  2. Добавляете контур отсечения для нового графического состояния.
  3. Рисуете градиент в пределах контура отсечения.
  4. Восстанавливаете исходное графическое состояние с помощью context.restoreGState(). Это то состояние, которое было до того, как вы добавили контур отсечения.

Теперь Ваша линия графика и окружности должны выглядеть намного лучше:

В конце draw(_:) добавьте этот код, чтобы отрисовать три горизонтальные линии:


// Рисуем горизонтальные линии графика поверх всего остального
let linePath = UIBezierPath()

// Верхняя линия
linePath.move(to: CGPoint(x: margin, y: topBorder))
linePath.addLine(to: CGPoint(x: width - margin, y: topBorder))

// Центральная линия
linePath.move(to: CGPoint(x: margin, y: graphHeight / 2 + topBorder))
linePath.addLine(to: CGPoint(x: width - margin, y: graphHeight / 2 + topBorder))

// Нижняя линия
linePath.move(to: CGPoint(x: margin, y: height - bottomBorder))
linePath.addLine(to: CGPoint(x: width - margin, y: height - bottomBorder))
let color = UIColor(white: 1.0, alpha: Constants.colorAlpha)
color.setStroke()
    
linePath.lineWidth = 1.0
linePath.stroke()

Относительно не трудно, не так ли? Вы просто двигаетесь к точке и рисуете горизонтальную линию.

Добавление меток для графика

Теперь добавим метки, чтобы сделать график более понятным.

Перейдите к ViewController.swift и добавьте следующие свойства для аутлетов:


// Label outlets
@IBOutlet weak var averageWaterDrunk: UILabel!
@IBOutlet weak var maxLabel: UILabel!
@IBOutlet weak var stackView: UIStackView!

Это добавляет аутлеты для динамического изменения текста метки average water drunk, метки max water drunk, а также меток day name стекового представления.

Теперь перейдите к Main.storyboard и добавьте следующие представления как подпредставления Graph View:

Первые пять подпредставлений - UILabel. Четвертое подпредставление выровнено по правому краю рядом с верхней частью графика, а пятое выровнено по правому краю к нижней части графика. Шестое подпредставление - горизонтальный StackView с ярлыками для каждого дня недели, которые вы измените в коде.

Удерживая клавишу shift, щелкните на все метки, а затем измените шрифты на пользовательский стиль Avenir Next Condensed, Medium.

Если у вас возникли проблемы с настройкой этих меток, ознакомьтесь с кодом в конечном проекте, используя кнопку «Загрузить материалы» в верхней или нижней части этого туториала.

Соедините averageWaterDrunk, maxLabel и stackView с соответствующими представлениями в Main.storyboard.
Удерживая клавишу Control, перетащите курсор из View Controller на правильную метку и выберите аутлет из всплывающего окна:

 

Теперь, когда Вы закончили настройку Graph View, в Main.storyboard выберите Graph View и установите флажок Hidden, чтобы график не появлялся при первом запуске приложения.

Откройте ViewController.swift и добавьте этот метод для настройки меток:


func setupGraphDisplay() {
  let maxDayIndex = stackView.arrangedSubviews.count - 1
  
  // 1 - Замените информацию о последнем дне информацией сегодняшнего дня
  graphView.graphPoints[graphView.graphPoints.count - 1] = counterView.counter
  // 2 - Проинформируйте, что график нужно обновить
  graphView.setNeedsDisplay()
  maxLabel.text = "\(graphView.graphPoints.max() ?? 0)"
    
  // 3 - Высчитайте среднее значение по точкам графика
  let average = graphView.graphPoints.reduce(0, +) / graphView.graphPoints.count
  averageWaterDrunk.text = "\(average)"
    
  // 4 - Установите форматирование даты и календарь
  let today = Date()
  let calendar = Calendar.current
    
  let formatter = DateFormatter()
  formatter.setLocalizedDateFormatFromTemplate("EEEEE")
  
  // 5 - Установите корректное название дней в ярлыках
  for i in 0...maxDayIndex {
    if let date = calendar.date(byAdding: .day, value: -i, to: today),
      let label = stackView.arrangedSubviews[maxDayIndex - i] as? UILabel {
      label.text = formatter.string(from: date)
    }
  }
}

Это выглядит немного громоздко, но Вам нужно это, чтобы настроить календарь и получить текущий день недели. Чтобы сделать это, Вы:

  1. Устанóвите сегодняшнюю дату в качестве последнего элемента в массиве данных графиков
  2. Перерисуете график, чтобы учесть любые изменения в сегодняшних данных
  3. Используете Swift reduce для расчета среднего количества выпитых за неделю стаканов. К слову, это очень полезный метод для суммирования всех элементов в массиве
  4. Настроете DateFormatter для возврата первой буквы каждого дня
  5. С помощью этого цикла пройдете через все метки внутри stackView. Исходя из этого, Вы устанавливаете текст для каждой метки из DateFormatter.

Находясь в разделе ViewController.swift , используйте этот метод из counterViewTap(_:). В той части условия else, где в комментарии написано «Show graph», добавьте этот код:


setupGraphDisplay()

Запустите. Нажимаем на счетчик и видим, как график поворачивается к нам. Следовательно, все работает исправно.

Mastering the Matrix

Приложение выглядит действительно хорошо, однако, можно улучшить представление счетчика, добавив маркировку, чтобы указать каждый выпитый стакан:

Теперь, когда Вы немного попрактиковались с функциями CG, вы будете использовать их для поворота и перевода контекста чертежа.

Заметьте, что эти маркеры исходят из центра:

Помимо рисования в контексте, у Вас есть возможность манипулировать контекстом, поворачивая, масштабируя и переводя матрицу преобразования контекста.

Сначала всё это может показаться запутанным, но после того, как Вы разберете эти упражнения, Вам станет понятнее. Порядок преобразований очень важен, поэтому, вот несколько диаграмм, чтобы объяснить последовательность действий.

Следующая диаграмма является результатом поворота контекста, а затем рисования прямоугольника в его центре.

Черный прямоугольник рисуется перед поворотом контекста, за которым следуют зеленый и красный. Нужно заметить две вещи:

  1. Контекст поворачивается в левом верхнем углу (0,0)
  2. Прямоугольник по-прежнему отображается в центре контекста после его поворота.

Когда Вы рисуете маркеры Counter View, Вы сначала переводите контекст, прежде чем его поворачивать.

На этой диаграмме маркер прямоугольника находится в самом верхнем левом углу контекста. Синие линии очерчивают переведенный контекст. Красные пунктирные линии указывают на вращение. После всего этого, Вы переводите контекст снова.

Когда Вы нарисуете красный прямоугольник в контексте, Вы заставите его появиться под углом.

После того как Вы повернете и переведете контекст, чтобы нарисовать красный маркер, Вам нужно сбросить центр, чтобы Вы могли снова повернуть и перевести контекст, и затем нарисовать зеленый маркер.

Точно так же, как Вы сохранили состояние контекста с контуром отсечения в Graph View, Вы будете сохранять и восстанавливать состояние с помощью матрицы преобразования каждый раз, когда Вы рисуете маркер.

Рисование маркеров

Перейдите в CounterView.swift и добавьте этот код в конец draw(_:), чтобы добавить маркеры в счетчик:


// Counter View markers
guard let context = UIGraphicsGetCurrentContext() else {
  return
}
  
// 1 - Save original state
context.saveGState()
outlineColor.setFill()
    
let markerWidth: CGFloat = 5.0
let markerSize: CGFloat = 10.0

// 2 - The marker rectangle positioned at the top left
let markerPath = UIBezierPath(rect: CGRect(
  x: -markerWidth / 2, 
  y: 0, 
  width: markerWidth, 
  height: markerSize))

// 3 - Move top left of context to the previous center position  
context.translateBy(x: rect.width / 2, y: rect.height / 2)
    
for i in 1...Constants.numberOfGlasses {
  // 4 - Save the centered context
  context.saveGState()
  // 5 - Calculate the rotation angle
  let angle = arcLengthPerGlass * CGFloat(i) + startAngle - .pi / 2
  // Rotate and translate
  context.rotate(by: angle)
  context.translateBy(x: 0, y: rect.height / 2 - markerSize)
   
  // 6 - Fill the marker rectangle
  markerPath.fill()
  // 7 - Restore the centered context for the next rotate
  context.restoreGState()
}

// 8 - Restore the original state in case of more painting
context.restoreGState()

Используя этот код, Вы:

  1. Сохраните исходное состояние матрицы, прежде чем манипулировать матрицей контекста.
  2. Определите положение и форму пути, хоть Вы еще и не рисуете их.
  3. Переместите контекст так, чтобы вращение происходило вокруг исходного центра контекста, обозначенного синими линиями на предыдущей диаграмме.
  4. Сохраните центрированное состояние контекста для каждой метки.
  5. Определите угол для каждого маркера, используя рассчитанный ранее индивидуальный угол. Затем вы вращаете и переводите контекст.
  6. Нарисуете прямоугольник маркера в левом верхнем углу повернутого и переведенного контекста.
  7. Восстановите состояние центрированного контекста.
  8. Восстановите исходное состояние контекста перед любыми поворотами или переводами.

Хорошая работа! Теперь Вы можете восхищаться эстетичным и информативным видом интерфейса Flo:

Материалы к статье.

Оригинал статьи


Оцените статью
0
0
0
0
0

Чтобы добавить комментарий, авторизуйтесь
Войти
Swiftbook
Пишет и переводит статьи для SwiftBook