Изграждане на течни интерфейси

Как да създадете естествени жестове и анимации на iOS

На WWDC 2018 дизайнерите на Apple представиха беседа, озаглавена „Проектиране на флуидни интерфейси“, обяснявайки дизайнерските разсъждения зад жестовия интерфейс на iPhone X.

Презентация на Apple WWDC18 „Проектиране на флуидни интерфейси“

Това е любимият ми WWDC разговор някога - горещо го препоръчвам.

Беседата даде някои технически насоки, което е изключително за дизайнерска презентация, но беше псевдокод, оставяйки много неизвестни.

Някакъв подобен на Swift код от презентацията.

Ако се опитате да приложите тези идеи, може да забележите разлика между вдъхновението и изпълнението.

Моята цел е да преодолея тази пропаст, като предоставя примери за работен код на всяка основна тема в презентацията.

Осемте (8) интерфейса, които ще създадем. Бутони, пружини, персонализирани взаимодействия и много други!

Ето описание на това, което ще покрием:

  1. Кратко резюме на беседата „Проектиране на течни интерфейси“.
  2. Осем течни интерфейса, теорията на дизайна зад тях и кодът за изграждането им.
  3. Приложения за дизайнери и разработчици.

Какво представляват течните интерфейси?

Флуиден интерфейс може също да се нарече „бърз“, „гладък“, „естествен“ или „магически“. Това е преживяване без триене, което просто се чувства „правилно“.

Презентацията на WWDC говори за течни интерфейси като „разширение на ума ви“ и „разширение на естествения свят“. Интерфейсът е флуиден, когато се държи според начина, по който хората мислят, а не по начина, по който мислят машините.

Какво ги прави течни?

Течните интерфейси са отзивчиви, прекъсваеми и пренасочващи се. Ето пример за жеста за придвижване с пръст към iPhone X:

Приложенията могат да бъдат затворени по време на стартирането им анимация.

Интерфейсът веднага реагира на входа на потребителя, може да бъде спрян във всеки момент от процеса и дори може да промени курса по средата.

Защо се интересуваме от течни интерфейси?

  1. Флуидните интерфейси подобряват работата на потребителя, като правят всяко взаимодействие бързо, леко и смислено.
  2. Те дават на потребителя усещане за контрол, което изгражда доверие към вашето приложение и вашата марка.
  3. Те са трудни за изграждане. Течен интерфейс е труден за копиране и може да бъде конкурентно предимство.

Интерфейсите

В останалата част от тази публикация ще ви покажа как да изградите осем (8) интерфейса, които обхващат всички основни теми в презентацията.

Икони, представляващи осемте (8) интерфейса, които ще изградим.

Интерфейс №1: Бутон за калкулатор

Това е бутон, който имитира поведението на бутоните в приложението за калкулатор на iOS.

Основни функции

  1. Акценти моментално при допир.
  2. Може да се подслушва бързо дори при средна анимация.
  3. Потребителят може да докосне и издърпа извън бутона, за да отмени докосването.
  4. Потребителят може да докосне надолу, да се плъзне навън, да плъзне обратно и да потвърди докосването.

Теория на дизайна

Искаме бутони, които се чувстват отзивчиви, признавайки на потребителя, че са функционални. Освен това искаме действието да бъде отменено, ако потребителят реши срещу действието си, след като докосне. Това позволява на потребителите да вземат по-бързи решения, тъй като могат да извършват действия паралелно с мисълта.

Слайдове от презентацията на WWDC, показващи как жестовете паралелно с мисълта правят действия по-бързи.

Критичен кодекс

Първата стъпка за създаване на този бутон е използването на подклас UIControl, а не подклас UIButton. Един UIButton би работил добре, но тъй като ние персонализираме взаимодействието, няма да се нуждаем от нито една от неговите функции.

Бутон за калкулатор: UIControl {
    стойност на обществена вар: Int = 0 {
        didSet {label.text = “\ (стойност)”}
    }
    частен ленив var етикет: UILabel = {...} ()
}

По-нататък ще използваме UIControlEvents за възлагане на функции на различните допирни взаимодействия.

addTarget (самостоятелно, действие: #selector (touchDown), за: [.touchDown, .touchDragEnter])
addTarget (самостоятелно, действие: #selector (touchUp), за: [.touchUpInside, .touchDragExit, .touchCancel])

Ние групираме събитията на touchDown и touchDragEnter в едно „събитие“, наречено touchDown, и можем да групираме събитията на touchUpInside, touchDragExit и touchCancel в едно събитие, наречено touchUp.

(За описание на всички наличниUIControlEvents, вижте документацията.)

Това ни дава две функции за работа с анимациите.

частен аниматор var = UIViewPropertyAnimator ()
@objc private func touchDown () {
    animator.stopAnimation (истина)
    backgroundColor = подчертанColor
}
@objc private func touchUp () {
    animator = UIViewPropertyAnimator (продължителност: 0.5, крива: .easeOut, анимации: {
        self.backgroundColor = self.normalColor
    })
    animator.startAnimation ()
}

При touchDown, анулираме съществуващата анимация, ако е необходимо, и незабавно задаваме цвета на подчертания цвят (в случая светлосив).

При touchUp създаваме нов аниматор и стартираме анимацията. Използването на UIViewPropertyAnimator улеснява анулирането на анимацията за подчертаване.

(Странична забележка: Това не е точното поведение на бутоните в приложението за калкулатор на iOS, които позволяват едно докосване, започнало с различен бутон, да го активира, ако докосването се влачи вътре в бутона. В повечето случаи бутон като този Създадох тук е предвиденото поведение за iOS бутони.)

Интерфейс №2: Пролетни анимации

Този интерфейс показва как може да се създаде пролетна анимация чрез посочване на "затихване" (bounciness) и "response" (скорост).

Основни функции

  1. Използва параметрите, подходящи за дизайн.
  2. Няма концепция за продължителност на анимацията.
  3. Лесно прекъсваем.

Теория на дизайна

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

Няколко допълнителни напомняния при проектирането на пролетни анимации:

  1. Изворите не трябва да са пролетни. Използването на амортизираща стойност 1 ще създаде анимация, която бавно почива без никакво оживяване. Повечето анимации трябва да използват стойност на затихване 1.
  2. Опитайте се да не мислите за продължителността. На теория пружина никога не почива напълно и принуждаването на продължителност на пружината може да доведе до това да се чувства неестествено. Вместо това играйте със стойностите на затихване и реакция, докато се почувствате правилни.
  3. Прекъсването е критично. Тъй като пружините прекарват толкова много от времето си близо до крайната си стойност, потребителите може да смятат, че анимацията е завършена и ще се опитат да взаимодействат отново с нея.

Критичен кодекс

В UIKit можем да създадем пролетна анимация с UIViewPropertyAnimator и обект UISpringTimingParameters. За съжаление няма инициализатор, който просто да заглуши и реагира. Най-близкото, което можем да достигнем е инициализаторът UISpringTimingParameters, който отнема маса, твърдост, демпфиране и начална скорост.

UISpringTimingParameters (маса: CGFloat, коравина: CGFloat, демпфиране: CGFloat, начална скорост: CGVector)

Бихме искали да създадем удобен инициализатор, който поема амортисьор и реакция и го пренасочва към необходимата маса, твърдост и демпфиране.

С малко физика можем да извлечем необходимите ни уравнения:

Решаване за постоянната пружина и коефициента на затихване.

С този резултат можем да създадем собствени UISpringTimingParameters с точно параметрите, които желаем.

разширение UISpringTimingParameters {
    удобство init (затихване: CGFloat, отговор: CGFloat, начална скорост: CGVector = .zero) {
        нека скованост = pow (2 * .pi / отговор, 2)
        оставете влажна = 4 * .pi * затихване / отговор
        self.init (маса: 1, скованост: скованост, затихване: влажна, начална скорост: начална скорост)
    }
}

Така ще уточним пролетните анимации за всички останали интерфейси.

Физиката зад пролетните анимации

Искате ли да се задълбочите в пролетните анимации? Вижте тази невероятна публикация на Christian Schnorr: Demystifying UIKit Spring Animations.

След като прочетох неговия пост, пролетните анимации най-накрая щракнаха за мен. Огромен вик на Кристиан за това, че ми помага да разбера математиката зад тези анимации и че ме научи как да решавам диференциални уравнения от втори ред.

Интерфейс №3: Бутон за фенерче

Още един бутон, но с много по-различно поведение. Това имитира поведението на бутона на фенерчето на заключения екран на iPhone X.

Основни функции

  1. Изисква умишлен жест с 3D докосване.
  2. Надутостта намеква за необходимия жест.
  3. Хаптичната обратна връзка потвърждава активирането.

Теория на дизайна

Apple искаше да създаде бутон, който е лесно и бързо достъпен, но не може да бъде задействан случайно. Изискването на натиск за сила за активиране на фенерчето е чудесен избор, но му липсват привилегии и обратна връзка.

За да реши тези проблеми, бутонът е пружиниращ и нараства, когато потребителят прилага сила, намеквайки за необходимия жест. В допълнение, има две отделни вибрации на бързата обратна връзка: една, когато се прилага необходимото количество сила, и друга, когато бутонът се активира при намаляване на силата. Тези хаптици имитират поведението на физически бутон.

Критичен кодекс

За да измерим размера на силата, която се прилага към бутона, можем да използваме обекта UITouch, предоставен при допирни събития.

отмени функционалните докосвания Преместени (_ докосва: Задайте , със събитие: UIEvent?) {
    super.touchesMoved (докосва, с: събитие)
    охрана нека докосне = докосва.Първо друго {връщане}
    нека сила = touch.force / touch.maximumPossibleForce
    нека мащаб = 1 + (maxWidth / minWidth - 1) * сила
    трансформация = CGAffineTransform (scaleX: мащаб, y: мащаб)
}

Изчисляваме мащабно преобразуване въз основа на текущата сила, така че бутонът да расте с увеличаване на налягането.

Тъй като бутонът може да бъде натиснат, но все още не е активиран, трябва да следим текущото състояние на бутона.

enum ForceState {
    нулиране на случая, активирано, потвърдено
}
частен нека resetForce: CGFloat = 0.4
частен нека активиранеForce: CGFloat = 0.5
частно нека потвърждение Force: CGFloat = 0,49

Наличието на усилието за потвърждение е малко по-ниско от силата на активиране пречи на потребителя бързо да се активира и деактивира бутона чрез бързо преминаване на прага на сила.

За бърза обратна връзка можем да използваме генераторите за обратна връзка на UIKit.

частен нека activationFeedbackGenerator = UIImpactFeedbackGenerator (стил: .light)
частни нека потвърждениеFeedbackGenerator = UIImpactFeedbackGenerator (стил: .medium)

И накрая, за оживените анимации, можем да използваме UIViewPropertyAnimator с персонализирани инициализатори UISpringTimingParameters, които създадохме преди.

нека params = UISpringTimingParameters (затихване: 0,4, отговор: 0,2)
нека animator = UIViewPropertyAnimator (продължителност: 0, времеви параметри: парами)
animator.addAnimations {
    self.transform = CGAffineTransform (мащабX: 1, y: 1)
    self.backgroundColor = self.isOn? self.onColor: self.offColor
}
animator.startAnimation ()

Интерфейс № 4: Гумиране

Гумирането се случва, когато изгледът устои на движението. Пример е, когато превъртащият се изглед достига края на съдържанието си.

Основни функции

  1. Интерфейсът винаги е отзивчив, дори когато дадено действие е невалидно.
  2. Десинхронизираното проследяване на докосване показва граница.
  3. Количеството движение намалява по-далеч от границата.

Теория на дизайна

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

Критичен кодекс

За щастие, гумената лента е лесна за изпълнение.

офсет = поу (офсет, 0,7)

Чрез използване на експонент между 0 и 1, изместването на изгледа се премества по-малко, тъй като е далеч от позицията на покой. Използвайте по-голям експонент за по-малко движение и по-малък експонент за повече движение.

За малко повече контекст, този код обикновено се реализира в обратен UIPanGestureRecognizer при обратно извикване, когато докосването се движи. Отместването може да бъде изчислено с делта между текущото и първоначалното докосване, а изместването може да се приложи с преобразуване на превода.

var offset = touchPoint.y - originalTouchPoint.y
офсет = отместване> 0? Pow (офсет, 0,7): -ww (-ofset, 0,7)
view.transform = CGAffineTransform (преводX: 0, y: офсет)

Забележка: Това не е начинът, по който Apple извършва гумиране с елементи като изгледи за превъртане. Харесва ми този метод поради неговата простота, но има по-сложни функции за различно поведение.

Интерфейс №5: Пауза на ускорението

За да видите превключвателя на приложения на iPhone X, потребителят прекарва пръст нагоре от долната част на екрана и прави пауза по средата. Този интерфейс създава отново това поведение.

Основни функции

  1. Паузата се изчислява въз основа на ускорението на жеста.
  2. По-бързото спиране води до по-бърза реакция.
  3. Без таймери.

Теория на дизайна

Течните интерфейси трябва да бъдат бързи. Забавянето от таймер, дори да е кратко, може да накара интерфейса да се чувства бавно.

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

Критичен кодекс

За да измерим ускорението, можем да проследим най-новите стойности на скоростта на жеста на панорамата.

частни вар скорости = [CGFloat] ()
частен песен (скорост: CGFloat) {
    ако velocities.count <числоOfVelocities {
        velocities.append (скорост)
    } else {
        скорости = масив (velocities.dropFirst ())
        velocities.append (скорост)
    }
}

Този код актуализира масива от скорости, за да има винаги последните седем скорости, които се използват за изчисляване на ускорението.

За да определим дали ускорението е достатъчно голямо, можем да измерим разликата между първата скорост в нашия масив спрямо текущата скорост.

ако абс (скорост)> 100 || абс (компенсиране) <50 {връщане}
нека съотношение = abs (firstRecordedVelocity - скорост) / abs (firstRecordedVelocity)
ако съотношението> 0,9 {
    pauseLabel.alpha = 1
    feedbackGenerator.impactOccurred ()
    hasPaused = true
}

Проверяваме също така, за да се уверим, че движението има минимално изместване и скорост. Ако жестът е загубил повече от 90% от скоростта си, считаме, че е спрян.

Моето изпълнение не е перфектно. По мое тестване изглежда работи доста добре, но има възможност за по-добра евристика за измерване на ускорението.

Интерфейс № 6: Награждаващ момент

Чекмедже с отворени и затворени състояния, което има щедрост въз основа на скоростта на жеста.

Основни функции

  1. Докосването на чекмеджето го отваря, без да оживява.
  2. Щракването с чекмеджето го отваря с обилно усещане.
  3. Интерактивен, прекъсваем и обратим.

Теория на дизайна

Това чекмедже показва концепцията за възнаграждение. Когато потребителят премества изглед със скорост, е много по-удовлетворяващо да оживява изгледа с добродушие. Това прави интерфейсът да се чувства жив и забавен.

Когато чекмеджето е подслушвано, то се анимира без подскачане, което се чувства подходящо, тъй като кранът няма скорост в определена посока.

Когато проектирате персонализирани взаимодействия, важно е да запомните, че интерфейсите могат да имат различни анимации за различни взаимодействия.

Критичен кодекс

За да опростим логиката на докосване и панорамиране, можем да използваме персонализиран подклас на разпознаване на жестове, който веднага влиза в началното състояние при натискане надолу.

клас InstantPanGestureRecognizer: UIPanGestureRecognizer {
    отмени функционалните щрихиBegan (_ докосва: Set , със събитие: UIEvent) {
        super.touchesBegan (докосва, с: събитие)
        self.state =. започна
    }
}

Това също така позволява на потребителя да докосне чекмеджето по време на движението си, за да го постави на пауза, подобно на докосване на изглед на превъртане, който в момента се превърта. За да обработваме кранове, можем да проверим дали скоростта е нулева, когато жестът приключи, и да продължим анимацията.

ако yVelocity == 0 {
    animator.continueAnimation (withTimingParameters: nil, durationFactor: 0)
}

За да се справим с жест със скорост, първо трябва да изчислим неговата скорост спрямо общото остатъчно изместване.

нека дробRemaining = 1 - animator.fractionComplete
нека distanceRemaining = fraRemaining * closedTransform.ty
ако distanceRemaining == 0 {
    animator.continueAnimation (withTimingParameters: nil, durationFactor: 0)
    почивка
}
нека относителна скорост = abs (yVelocity) / distanceRemaining

Можем да използваме тази относителна скорост, за да продължим анимацията с параметрите на времето, които включват малко натрупване.

нека timingParameters = UISpringTimingParameters (затихване: 0.8, отговор: 0.3, начална скорост: CGVector (dx: относителна скорост, dy: относителна скорост))
нека newDuration = UIViewPropertyAnimator (продължителност: 0, timingParameters: timingParameters) .duration
нека durationFactor = CGFloat (newDuration / animator.duration)
animator.continueAnimation (withTimingParameters: timingParameters, durationFactor: durationFactor)

Тук създаваме нов UIViewPropertyAnimator, за да изчислим времето, което анимацията трябва да отнеме, за да можем да осигурим правилната продължителностFactor при продължаване на анимацията.

Има повече сложности, свързани с обръщането на анимацията, които тук няма да обхващам. Ако искате да научите повече, написах пълен урок за този компонент: Изграждане на по-добри анимации на приложение за iOS.

Интерфейс № 7: FaceTime PiP

Пресъздаване на потребителския интерфейс за картина в картина на приложението iOS FaceTime.

Основни функции

  1. Леко, ефирно взаимодействие.
  2. Прогнозираната позиция се базира на скоростта на забавяне на UIScrollView.
  3. Непрекъсната анимация, която зачита първоначалната скорост на жеста.

Критичен кодекс

Нашата крайна цел е да напишем нещо подобно.

нека params = UISpringTimingParameters (затихване: 1, отговор: 0,4, начална скорост: относителна инициална скорост)
нека animator = UIViewPropertyAnimator (продължителност: 0, времеви параметри: парами)
animator.addAnimations {
    self.pipView.center = най-близкияCornerPosition
}
animator.startAnimation ()

Бихме искали да създадем анимация с първоначална скорост, която да съответства на скоростта на жеста на панорама и да анимираме пипса до най-близкия ъгъл.

Първо, нека изчислим началната скорост.

За да направим това, трябва да изчислим относителна скорост въз основа на текущата скорост, текущата позиция и целевата позиция.

нека относителнатаInitialVelocity = CGVector (
    dx: относителна скорост (forVelocity: velocity.x, от: pipView.center.x, до: най-близкияCornerPosition.x),
    dy: relaVelocity (forVelocity: velocity.y, от: pipView.center.y, до: най-близкияCornerPosition.y)
)
func relaVelocity (за скорост на скоростта: CGFloat, от currentValue: CGFloat, до targetValue: CGFloat) -> CGFloat {
    охрана currentValue - targetValue! = 0 else {return 0}
    скорост на връщане / (targetValue - currentValue)
}

Можем да разделим скоростта на нейните x и y компоненти и да определим относителната скорост за всеки.

След това нека да изчислим ъгъла, за който PiP да анимира.

За да направим интерфейса ни естествен и лек, ще проектираме крайната позиция на PiP въз основа на текущото му движение. Ако PiP се плъзгаше и спираше, къде ще кацне?

нека decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
нека velocity = prepoznavač.velocity (в: изглед)
нека projectedPosition = CGPoint (
    x: pipView.center.x + проект (начална скорост: velocity.x, decelerationRate: decelerationRate),
    y: pipView.center.y + проект (начална скорост: velocity.y, decelerationРазмер: decelerationRate)
)
нека най-близкияКорнерПозиция = най-близкиКорнер (до: прогнозираноПозиция)

Можем да използваме скоростта на забавяне на UIScrollView, за да изчислим тази позиция за почивка. Това е важно, защото се отнася до мускулната памет на потребителя за превъртане. Ако потребителят знае за това колко далеч се движи даден изглед, той може да използва тези предишни знания, за да интуитивно да отгатне колко сила е необходима, за да премести PiP до желаната цел.

Този темп на ускорение също е доста щедър, поради което взаимодействието се чувства леко - необходимо е само малко трептене, за да изпратите PiP да лети по целия екран.

Можем да използваме функцията за прожектиране, предоставена в разговора „Проектиране на флуидни интерфейси“, за да изчислим крайната прогнозирана позиция.

/// Изминато разстояние след забавяне до нулева скорост с постоянна скорост.
func проект (начална скорост: CGFloat, decelerationРазмер: CGFloat) -> CGFloat {
    възвръщаемост (начална скорост / 1000) * забавяне на скоростта / (1 - забавяне на скоростта)
}

Последното парче, което липсва, е логиката да се намери най-близкият ъгъл въз основа на прогнозираното положение. За целта можем да прегледаме всички ъглови позиции и да намерим тази с най-малкото разстояние до прогнозираното положение за кацане.

func най-близкиКорнер (към точка: CGPoint) -> CGPoint {
    var minDistance = CGFloat.greatestFiniteMagnitude
    var najbliPosition = CGPoint.zero
    за позиция в pipPositions {
        нека разстояние = точка.разстояние (до: позиция)
        ако разстояние 

За да обобщим крайната имплементация: Използваме скоростта на ускорение на UIScrollView, за да проектираме движението на пипса до крайното си положение на покой и изчисляваме относителната скорост, за да го подадем в UISpringTimingParameters.

Интерфейс № 8: Въртене

Прилагане на концепциите от PiP интерфейса към ротация на анимация.

Основни функции

  1. Използва проекция, за да уважава скоростта на жеста.
  2. Винаги завършва с валидна ориентация.

Критичен кодекс

Кодът тук е много подобен на предишния PiP интерфейс. Ще използваме същите строителни блокове, с изключение на това да заменим функцията на най-близкия Корнер за функция най-близко.

func проект (...) {...}
func relaVelocity (...) {...}
func най-близкиAngle (...) {...}

Когато дойде време най-накрая да създадем UISpringTimingParameters, от нас се изисква да използваме CGVector за първоначална скорост, въпреки че въртенето ни има само едно измерение. Във всеки случай, когато анимираното свойство има само едно измерение, задайте dx стойността на желаната скорост и задайте стойността на dy на нула.

нека timingParameters = UISpringTimingParameters (
    затихване: 0,8,
    отговор: 0,4,
    начална скорост: CGVector (dx: относителна начална скорост, dy: 0)
)

Вътре аниматорът ще игнорира стойността dy и ще използва стойността dx за създаване на кривата на времето.

Опитайте сами!

Тези интерфейси са много по-забавни на истинско устройство. За да играете сами с тези интерфейси, демонстрационното приложение е достъпно на GitHub.

Демо приложението за флуидни интерфейси, достъпно в GitHub!

Практически приложения

За дизайнери

  1. Мислете за интерфейсите като течни среди на изразяване, а не за колекции от статични елементи.
  2. Помислете за анимации и жестове в началото на процеса на проектиране. Инструментите за оформление като Sketch са фантастични, но не предлагат пълната експресивност на устройството.
  3. Прототип с разработчици. Вземете дизайнерски настроени разработчици, които да ви помогнат за прототип на анимации, жестове и хаплици.

За разработчици

  1. Приложете съветите от тези интерфейси към собствените си персонализирани компоненти. Помислете как биха могли да се комбинират по нови и интересни начини.
  2. Обучете дизайнерите си за нови възможности. Мнозина не са наясно с пълната сила на 3D докосване, хаптики, жестове и пролетни анимации.
  3. Прототип с дизайнери. Помогнете им да видят дизайна си на истинско устройство и създайте инструменти, които да им помогнат да проектират по-ефективно.

Ако ви е харесала тази публикация, моля, оставете няколко хлопки.

Можете да ръкопляскате до 50 пъти, така че вземете кликване / докосване!

Моля, споделете публикацията с вашите iOS дизайнери / приятели на разработчици на iOS във вашия магазин за социални медии по ваш избор.

Ако харесвате подобни неща, трябва да ме последвате в Twitter. Пускам само висококачествени туитове. twitter.com/nathangitter

Благодаря на Дейвид Окун за преразглеждането на чернови на този пост.