Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Лотерея в приложении: отрисовка, анимация и никакого мошенничества

Лотерея в приложении: отрисовка, анимация и никакого мошенничества

CocoaHeads

October 04, 2019
Tweet

More Decks by CocoaHeads

Other Decks in Programming

Transcript

  1. 3 Revolut Компания основана в 2015 г. 8+ млн пользователей

    20+ офисов в мире 1400+ сотрудников Оценка компании $1,7 млрд
  2. •Цель: привлекать и удерживать пользователей в приложении Revolut •Моя задача:

    разработка реферальной программы 4 Команда: 
 Growth & Engagement
  3. 5 Что из себя представляет лотерея
 в приложении, зачем она

    нужна Как реализовывали: рисовали, 
 анимировали
 и обрабатывали взаимодействие
 с колесом Где могут пригодиться описанные технологии и возможности О чём расскажу
  4. •Пользователь может выиграть какую-то сумму •Может крутить колесо пальцем, свайпать,

    запустить по нажатию на кнопку •Лотерея — часть одной из реферальных кампаний Как выглядит лотерея 7
  5. 8 Первые идеи реализации •Использовать готовую анимацию. Например, Lottie ❌

    Нет возможности задавать разное количество слотов на колесе ❌ Нельзя задать разные значения на слотах, менять скорость, пользователь не сможет взаимодействовать •Использовать готовую картинку для отрисовки
  6. 11 Что нужно было сделать •Добавить треугольник с лейблом •Добавить

    все треугольники по кругу •Вырезать круг, чтобы убрать края •Добавить дуги •Добавить точки ❗Использовала Core Graphics
  7. 12 Что нужно было сделать •Добавить треугольник с лейблом •Добавить

    все треугольники по кругу •Вырезать круг, чтобы убрать края •Добавить дуги •Добавить точки
  8. Впишем треугольник в UIView с заданным размером Знаем: количество слотов

    и размер колеса Найдём: высоту UIView tg(A) = a / b a = b * tg(A) let angle = 360.0 / slots.count / 2 let height = width * tan(angle * .pi / 180) * 2 Рисуем треугольник 13 ➡
  9. 14 Код метода drawRect: override public func draw(_ rect: CGRect)

    { guard let context = UIGraphicsGetCurrentContext() else { return } let points = [ CGPoint(x: 0, y: model.height / 2), CGPoint(x: model.width, y: 0), CGPoint(x: model.width, y: model.height) ] context.beginPath() context.addLines(between: points) context.closePath() let shapeLayer = CAShapeLayer() shapeLayer.path = context.path layer.mask = shapeLayer }
  10. 15 Код метода drawRect: override public func draw(_ rect: CGRect)

    { guard let context = UIGraphicsGetCurrentContext() else { return } let points = [ CGPoint(x: 0, y: model.height / 2), CGPoint(x: model.width, y: 0), CGPoint(x: model.width, y: model.height) ] context.beginPath() context.addLines(between: points) context.closePath() let shapeLayer = CAShapeLayer() shapeLayer.path = context.path layer.mask = shapeLayer }
  11. 16 Код метода drawRect: override public func draw(_ rect: CGRect)

    { guard let context = UIGraphicsGetCurrentContext() else { return } let points = [ CGPoint(x: 0, y: model.height / 2), CGPoint(x: model.width, y: 0), CGPoint(x: model.width, y: model.height) ] context.beginPath() context.addLines(between: points) context.closePath() let shapeLayer = CAShapeLayer() shapeLayer.path = context.path layer.mask = shapeLayer }
  12. 17 Код метода drawRect: override public func draw(_ rect: CGRect)

    { guard let context = UIGraphicsGetCurrentContext() else { return } let points = [ CGPoint(x: 0, y: model.height / 2), CGPoint(x: model.width, y: 0), CGPoint(x: model.width, y: model.height) ] context.beginPath() context.addLines(between: points) context.closePath() let shapeLayer = CAShapeLayer() shapeLayer.path = context.path layer.mask = shapeLayer }
  13. 18 Что нужно было сделать •Добавить треугольник с лейблом •Добавить

    все треугольники по кругу •Вырезать круг, чтобы убрать края •Добавить дуги •Добавить точки
  14. 19 Упрощённый код func addSlice(width: CGFloat, angle: CGFloat, index: Int,

    text: String) { let model = SliceViewModel(width: width, angle: angle, text: text) let slice = SliceView(model: model) view.addSubview(slice) < … setup constraints, width & height of a slice … > slice.layer.anchorPoint = CGPoint(x: 0, y: 0.5) slice.transform = slice.transform.rotated( by: index * angle * .pi / 180 ) return model }
  15. func addSlice(width: CGFloat, angle: CGFloat, index: Int, text: String) {

    let model = SliceViewModel(width: width, angle: angle, text: text) let slice = SliceView(model: model) view.addSubview(slice) < … setup constraints, width & height of a slice … > slice.layer.anchorPoint = CGPoint(x: 0, y: 0.5) slice.transform = slice.transform.rotated( by: index * angle * .pi / 180 ) return model } Упрощённый код 20
  16. 21 Что нужно было сделать •Добавить треугольник с лейблом •Добавить

    все треугольники по кругу •Вырезать круг, чтобы убрать края •Добавить дуги •Добавить точки
  17. 22 Код let rect = CGRect( origin: .zero, size: CGSize(width:

    wheelSize, height: wheelSize) ) let path = UIBezierPath(roundedRect: rect, cornerRadius: wheelSize / 2) let shapeLayer = CAShapeLayer() shapeLayer.path = path.cgPath view.layer.mask = shapeLayer
  18. 23 Что нужно было сделать •Добавить треугольник с лейблом •Добавить

    все треугольники по кругу •Вырезать круг, чтобы убрать края •Добавить дуги •Добавить точки
  19. 24 Код let path = UIBezierPath( arcCenter: CGPoint(x: wheelSize /

    2, y: wheelSize / 2), radius: wheelSize / 2, startAngle: (index * angle - angle / 2) * .pi / 180, endAngle: (index * angle + angle / 2) * .pi / 180, clockwise: true ) let shapeLayer = CAShapeLayer() shapeLayer.path = path.cgPath shapeLayer.strokeColor = index % 2 == 0 ? .mainPink : .mainBlue shapeLayer.fillColor = UIColor.clear.cgColor shapeLayer.lineWidth = borderWidth layer.addSublayer(shapeLayer)
  20. 25 Что нужно было сделать •Добавить треугольник с лейблом •Добавить

    все треугольники по кругу •Вырезать круг, чтобы убрать края •Добавить дуги •Добавить точки path.currentPoint
  21. •Позволяет реагировать на непрерывное движение по экрану •Можно определить текущую

    позицию и скорость жеста: location(in:) + velocity(in:) •Нужно следить за изменением угла, который проходит через текущую точку под пальцем пользователя 27 UIPanGestureRecognizer
  22. •tg(A) = x / y •arctg(tg(x / y)) = A

    28 Как посчитать текущий угол let currentPoint = pan.location(in: view) let x = currentPoint.x - wheelCenter.x let y = currentPoint.y - wheelCenter.y let angle = 180 - atan2(x, y) * 180 / .pi let diff = angle - previousAngle view.transform = view.transform.rotated(by: diff)
  23. 30 Механика •Заранее знаем, какую сумму выиграет пользователь •Быстрый жест

    — колесо крутится до выигрышного слота •Медленный жест — крутится до момента естественной остановки, затем возвращается назад •Если пользователь крутил колесо и потом отпустил — возвращается назад
  24. 31 Какие анимации надо было сделать •Прокрутка до выигрышного слота:

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

    •за какое время с учетом начальной скорости должно остановиться колесо •на сколько градусов должно прокрутиться колесо за это время ❌ Сложновато 32
  26. 33 Упрощённый код let angle = winSliceAngle(index: index) let rotationKeyPath

    = "transform.rotation.z" let fromValue = view.layer.value(forKeyPath: rotationKeyPath) let toValue = isRotatingClockwise ? angle + .pi * 8 : angle - .pi * 8 view.layer.setValue(toValue, forKeyPath: view.rotationKeyPath) let animation = CABasicAnimation() animation.keyPath = rotationKeyPath animation.duration = 3 animation.fromValue = fromValue animation.toValue = toValue animation.timingFunction = CAMediaTimingFunction(controlPoints: 0.16, 0.46, 0.33, 1) view.layer.add(animation, forKey: nil)
  27. 34 Упрощённый код let angle = winSliceAngle(index: index) let rotationKeyPath

    = "transform.rotation.z" let fromValue = view.layer.value(forKeyPath: rotationKeyPath) let toValue = isRotatingClockwise ? angle + .pi * 8 : angle - .pi * 8 view.layer.setValue(toValue, forKeyPath: view.rotationKeyPath) let animation = CABasicAnimation() animation.keyPath = rotationKeyPath animation.duration = 3 animation.fromValue = fromValue animation.toValue = toValue animation.timingFunction = CAMediaTimingFunction(controlPoints: 0.16, 0.46, 0.33, 1) view.layer.add(animation, forKey: nil)
  28. 35 Находим угол выигрышного слота let sliceAngle = wheelModel.sliceAngle *

    CGFloat(index) let angle = (isRotatingClockwise ? 360 - rotationAngle : -rotationAngle) * .pi / 180
  29. 36 Упрощённый код let angle = winSliceAngle(index: index) let rotationKeyPath

    = "transform.rotation.z" let fromValue = view.layer.value(forKeyPath: rotationKeyPath) let toValue = isRotatingClockwise ? angle + .pi * 8 : angle - .pi * 8 view.layer.setValue(toValue, forKeyPath: view.rotationKeyPath) let animation = CABasicAnimation() animation.keyPath = rotationKeyPath animation.duration = 3 animation.fromValue = fromValue animation.toValue = toValue animation.timingFunction = CAMediaTimingFunction(controlPoints: 0.16, 0.46, 0.33, 1) view.layer.add(animation, forKey: nil)
  30. 37 CABasicAnimation & CAMediaTimingFunction •animate(withDuration:delay:options:animations:completion:) •CABasicAnimation - an object that

    provides basic, single-keyframe animation capabilities for a layer property •Timing functions: .easeIn, .easeOut и т.д. + кастомные
  31. 38 •init(name:) — стандартные timing-функции •init(controlPoints:_:_:_:) — кастомные с помощью

    кубической кривой Безье График отображает, насколько быстро в зависимости
 от времени (ось X) меняется значение, которое вы анимируете (ось Y) CAMediaTimingFunction
  32. 40

  33. 41 Упрощённый код let angle = winSliceAngle(index: index) let rotationKeyPath

    = "transform.rotation.z" let fromValue = view.layer.value(forKeyPath: rotationKeyPath) let toValue = isRotatingClockwise ? angle + .pi * 8 : angle - .pi * 8 view.layer.setValue(toValue, forKeyPath: view.rotationKeyPath) let animation = CABasicAnimation() animation.keyPath = rotationKeyPath animation.duration = 3 animation.fromValue = fromValue animation.toValue = toValue animation.timingFunction = CAMediaTimingFunction(controlPoints: 0.16, 0.46, 0.33, 1) view.layer.add(animation, forKey: nil)
  34. Прокрутка до момента естественной остановки •те же CABasicAnimation и timing-функция

    •продолжительность — 1 секунда •угол поворота — velocity / 5 42
  35. Прокрутка назад до исходного положения UIView.animate( withDuration: 1, delay: 0,

    usingSpringWithDamping: 0.7, initialSpringVelocity: 1, options: [.curveEaseOut], animations: { view.transform = CGAffineTransform( rotationAngle: angle ) }, completion: completion ) damping ratio — от 1 (без колебаний) до 0 (сильные колебания) 43
  36. 45 Выводы •Рисовать кастомные элементы в iOS можно с помощью

    геометрических фигур, кривых, масок (Core Graphics, CAShapeLayer, UIBezierPath). •Для изменения угла поворота вьюхи, положения, скейлинга можем использовать свойство transform (CGAffineTransform). •Анимации можно реализовать разными способами, и всё зависит от потребностей (UIView.animate, CABasicAnimation, timing functions и используя другие возможности классов CAAnimation).
  37. 46 Выводы •Кастомный UI-элемент не возьмешь из коробки, но создать

    его самому не так сложно. •Большую непонятную задачу можно разделить на маленькие. Тогда реализовать её будет проще. •Лучшее решение может оказаться простым, если взглянуть на задачу шире. •Кастомные элементы и анимации — это весело. И ещё — полезно для бизнеса. :)