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

Опубликовано: 2020-11-09

Креативность приносит с собой постоянное желание произвести впечатление на пользователя. На протяжении веков человек пытался манипулировать имеющимися средствами, чтобы воссоздать наиболее естественное взаимодействие, взяв за фундаментальный пример природу.

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

Вторя природе в дизайне приложения

В этой статье будет представлен процесс размытия границы на примере преобразования формы в интерактивной анимации повседневных элементов в большинстве приложений iOS. Один из способов подражания природе — различные преобразования положения объекта во времени. Примерные функции времени анимации представлены ниже.

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

Образцы для обнаружения

Давайте посмотрим, как моушн-дизайн может превратить заурядный дизайн в нечто исключительное! В приведенных ниже примерах слева находится приложение, в котором используются только базовые анимации iOS, а справа — версия того же приложения с некоторыми улучшениями.

Эффект «Погружение глубже»

Это переход с использованием преобразования между двумя состояниями представления . Построен на основе коллекции, после выделения конкретной ячейки переход к детализации элемента происходит путем преобразования отдельных его элементов*. Дополнительным решением является использование интерактивных переходов, облегчающих использование приложения.

*фактически копирование/отображение элементов данных во временном представлении, участвующем в переходе между его началом и концом… но я объясню это позже в этой статье…

Эффект «заглянуть за край»

Использование анимации прокрутки в своем действии преобразует изображение в форму 3D для эффекта куба . Основной фактор, отвечающий за эффект, — смещение прокрутки.

Эффект «Соедини точки».

Это переход между сценами, который превращает миниатюрный объект во весь экран . Используемые для этого коллекции работают одновременно, смена на одном экране соответствует смене на другом. Кроме того, когда вы вводите миниатюру на заднем плане, появляется эффект параллакса при свайпе между сценами.

Эффект «Сдвиг формы»

Последний тип анимации — простой с использованием библиотеки Lottie. Это наиболее распространенное использование для анимации иконок. В данном случае это значки на панели вкладок. Кроме того, путем изменения соответствующих вкладок использовалась анимация перехода в определенном направлении для дальнейшего усиления эффекта взаимодействия.

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

Теперь пора перейти к делу… нам нужно еще глубже вникнуть в структуру механизмов, управляющих этими примерами.

В этой статье я познакомлю вас с первым паттерном моушн-дизайна , который мы назвали «Dive Deeper» с абстрактным описанием его использования, не вдаваясь в конкретные подробности. В будущем мы планируем сделать точный код и весь репозиторий доступным для всех без каких-либо ограничений.

Архитектура проекта и применяемые шаблоны проектирования строгого программирования на данный момент не являются приоритетом — мы фокусируемся на анимации и переходе.

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

https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning

https://developer.apple.com/documentation/uikit/uipercentdriveninteractivetransition

https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/CustomizingtheTransitionAnimations.html

Первое: структура

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

Переход в Swift

Базовым классом, отвечающим за управление переходом, прокси, будет TransitionAnimation. Он решает, каким образом будет происходить переход, и охватывает стандартные функции, необходимые для выполнения действия, предоставленного командой Apple.

 /// Это базовый класс для переходов, которые имеют различное поведение при представлении и закрытии в течение заданного времени.
открытый класс TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    
    /// Указывает, представляет ли он переход или отклоняет его.
    представление var: Bool = true
    
    /// Интервал времени, в котором происходит весь переход.
    продолжительность частного разрешения: TimeInterval
    
    /// Инициализатор по умолчанию аниматора перехода со значениями по умолчанию.
    /// - Длительность параметра: Интервал времени, в течение которого происходит весь переход.
    /// - Представление параметра: Индикатор, если он представляет или отклоняет переход.
    публичная инициализация (длительность: TimeInterval = 0,5, представление: Bool = true) {
        self.duration = продолжительность
        самопредставление = представление
        супер.инит()
    }
    
    /// Определяет продолжительность перехода.
    /// - Параметр transitionContext: Контекст текущего перехода.
    /// - Возвращает: Указанная продолжительность при инициализации аниматора.
    public func transitionDuration (с использованием transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        возвращение self.duration
    }
    
    /// Сердце аниматора перехода, в этой функции происходит переход.
    /// - Важно: переопределение этой функции в более конкретном типе перехода имеет решающее значение для выполнения анимации.
    /// - Параметр transitionContext: Контекст перехода.
    public func animateTransition (с использованием transitionContext: UIViewControllerContextTransitioning) {}
    
}

На основе TransitionAnimator создаем файл TransformTransition, задачей которого будет выполнение определенного перехода, перехода с трансформацией (parenting)

 /// Реализация перехода преобразования.
открытый класс TransformTransition: TransitionAnimator {
    
    /// Модель просмотра, которая содержит все необходимые спецификации перехода.
    частная переменная viewModel: TransformViewModel
    
    /// Инициализатор перехода преобразования по умолчанию.
    /// - Параметр viewModel: Просмотр модели перехода преобразования.
    /// - Длительность параметра: Длительность перехода.
    init(viewModel: TransformViewModel, продолжительность: TimeInterval) {
        self.viewModel = модель просмотра
        super.init (длительность: продолжительность, представление: viewModel.presenting)
    }

В состав класса TransformTransition входит TransformViewModel, который, как следует из названия, сообщает механизму, к каким моделям представления будет применяться этот переход.

 /// Просмотрите модель перехода преобразования, которая содержит основную информацию о нем.
конечный класс TransformViewModel {
    
    /// Указывает, представляет ли переход преобразования представление или закрывает его.
    пусть представляет: Bool
    /// Массив моделей со спецификацией преобразования для каждого представления.
    пусть модели: [TransformModel]
    
    /// Инициализатор по умолчанию модели представления преобразования.
    /// — Представление параметра: указывает, представляет ли он переход преобразования или отклоняет его.
    /// - Модели параметров: Массив моделей со спецификацией преобразования для каждого представления.
    init (представление: Bool, модели: [TransformModel]) {
        самопредставление = представление
        self.models = модели
    }
    
}

Модель преобразования — это вспомогательный класс, описывающий определенные элементы представлений, участвующих в переходе, расположенных в родительском элементе, обычно это представления контроллера, которые могут быть преобразованы.

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

Второе: реализация

Мы расширяем модель представления, из которой начинаем переход, с помощью Transformable, что заставляет нас реализовать функцию, которая подготовит все необходимые элементы. Размер этой функции может расти очень быстро, поэтому я предлагаю вам разбить ее на более мелкие части, например, по элементам.

 /// Протокол для класса, который хочет выполнить преобразование перехода.
трансформируемый протокол: ViewModel {
    
    /// Подготавливает модели представлений, участвующих в переходе.
    /// - Параметр fromView: Вид, с которого начинается переход
    /// - Параметр toView: Вид, на который идет переход.
    /// - Представление параметра: указывает, представлено ли оно или отклонено.
    /// - Возвращает: Массив структур, который содержит всю необходимую информацию, готовую для преобразования перехода для каждого представления.
    func prepareTransitionModels (fromView: UIView, toView: UIView, представляя: Bool) -> [TransformModel]
    
}

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

Модели конкретных видовых преобразований (TransformModel) — самые маленькие модели во всем списке. Они состоят из ключевой информации о преобразовании, такой как начальный вид, переходный вид, начальный кадр, конечный кадр, начальный центр, конечный центр, параллельные анимации и конечная операция. Большинство параметров не нужно использовать во время преобразования, поэтому они имеют собственные значения по умолчанию. Для минимальных результатов достаточно использовать только те, которые требуются.

 /// Инициализатор модели преобразования по умолчанию со значениями по умолчанию.
    /// - Параметр initialView: Вид, с которого начинается переход.
    /// - Параметр phantomView: Представление, которое отображается при переходе преобразования.
    /// - Параметр initialFrame: Рамка просмотра, с которой начинается переход преобразования.
    /// - Параметр finalFrame: кадр просмотра, который будет представлен в конце перехода преобразования.
    /// - Параметр initialCenter: Необходим, когда начальная центральная точка зрения отличается от центра исходного вида.
    /// - Параметр finalCenter: необходим, когда конечная центральная точка зрения отличается от центра конечного вида.
    /// - Параметр parallelAnimation: Дополнительная анимация представления, выполняемая при переходе трансформации.
    /// - Завершение параметра: Блок кода срабатывает после перехода преобразования.
    /// - Примечание. Для выполнения самой минималистичной версии преобразования преобразования требуется только начальный вид.
    init(initialView: UIView,
         фантомное представление: UIView = UIView(),
         начальный кадр: CGRect = CGRect(),
         финальный кадр: CGRect = CGRect(),
         начальный центр: CGPoint? = ноль,
         финальный центр: CGPoint? = ноль,
         parallelAnimation: (() -> Пустота)? = ноль,
         завершение: (() -> Пустота)? = ноль) {
        self.initialView = начальный вид
        self.phantomView = фантомное представление
        self.initialFrame = начальный кадр
        self.finalFrame = финальный кадр
        self.parallelAnimation = параллельная анимация
        self.completion = завершение
        self.initialCenter = InitialCenter ?? CGPoint(x: начальный фрейм.midX, y: начальный фрейм.midY)
        self.finalCenter = finalCenter ?? CGPoint(x: finalFrame.midX, y: finalFrame.midY)
    }

Возможно, ваше внимание привлек фантомный вид. Это момент, когда я объясню рабочий процесс для переходов iOS. В самой краткой форме…

Рабочий процесс для переходов iOS

Когда пользователь хочет перейти к следующей сцене, iOS подготавливает определенные контроллеры, копируя начальный (синий) и целевой (зеленый) контроллеры в память. Затем через координатор перехода создается контекст перехода, который содержит контейнер, «глупый» вид, не содержащий никакой специальной функции, кроме имитации видов перехода между двумя сценами.

Ключевой принцип работы с переходами — не добавлять в контекст перехода какое-либо реальное представление, потому что в конце перехода весь контекст освобождается вместе с представлениями, добавленными в контейнер. Это представления, которые существуют только во время перехода, а затем удаляются.

Таким образом, использование фантомных представлений, которые являются копиями реальных представлений, является важным решением для этого перехода.

В данном случае у нас есть переход, который трансформирует один вид в другой, изменяя его форму и размер. Для этого в начале перехода я создаю PhantomView данного элемента и добавляю его в контейнер. FadeView — вспомогательный вид, добавляющий плавности общему переходу.

 /// Сердце перехода преобразования, где выполняется переход. Переопределение `TransitionAnimator.animateTransition(...)`.
    /// - Параметр transitionContext: Контекст текущего перехода преобразования.
    переопределить open func animateTransition (используя transitionContext: UIViewControllerContextTransitioning) {
        охранять пусть toViewController = transitionContext.view(forKey: .to),
            пусть fromViewController = transitionContext.view(forKey: .from) else {
                вернуть Log.unexpectedState()
        }
        пусть containerView = transitionContext.containerView
        пусть продолжительность = transitionDuration (используя: transitionContext)
        пусть fadeView = toViewController.makeFadeView (непрозрачность: (! Presenting).cgFloatValue)
        пусть модели = viewModel.models
        пусть представлено представление = представление? toViewController : отViewController
        
        models.forEach {$0.initialView.isHidden = true}
        представленныйView.isHidden = истина
        containerView.addSubview(toViewController)
        если представить {
            containerView.insertSubview (fadeView, belowSubview: toViewController)
        } еще {
            containerView.addSubview(fadeView)
        }
        containerView.addSubviews(viewModel.models.map {$0.phantomView})

На следующем шаге я трансформирую его в целевую форму через преобразования, и в зависимости от того, представление это или отзыв, он выполняет дополнительные операции по очистке конкретных представлений — вот и весь рецепт этого перехода.

 let animations: () -> Void = { [weak self] in
            охранять let self = self else { return Log.unexpectedState() }
            fadeView.alpha = self.presenting.cgFloatValue
            модели.forEach {
                пусть центр = self.presenting ? $0.finalCenter : $0.initialCenter
                пусть преобразование = self.presenting ? $0.presentTransform : $0.dismissTransform
                $0.phantomView.setTransformAndCenter(преобразование, центр)
            }
            models.compactMap {$0.parallelAnimation}.forEach {$0()}
        }
        
        пусть завершение: (Bool) -> Void = { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            представленныйView.isHidden = ложь
            models.compactMap {$0.completion}.forEach {$0()}
            models.forEach {$0.initialView.isHidden = false}
            если !self.presenting && transitionContext.transitionWasCancelled {
                toViewController.removeFromSuperview()
                fadeView.removeFromSuperview()
            }
        }
        
        UIView.animate(withDuration: продолжительность,
                       задержка: 0,
                       использование SpringWithDamping: 1,
                       начальнаяСпрингВелосити: 0,5,
                       варианты: .curveEaseOut,
                       анимации: анимации,
                       завершение: завершение)

Третье: специальный ингредиент

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

Завершающей составляющей нашего перехода будет его полная интерактивность. Для этой цели мы будем использовать Pan Gesture, добавленный в представление контроллера, TransitionInteractor…

 /// Посредник для обработки интерактивного перехода.
конечный класс TransitionInteractor: UIPercentDrivenInteractiveTransition {
    
    /// Указывает, начался ли переход.
    вар hasStarted = ложь
    /// Указывает, должен ли переход завершиться.
    вар долженфиниш = ложь

}

… который мы также инициализируем в теле контроллера.

 /// Обрабатывает жест панорамирования элементов представления коллекции и управляет переходом.
    @objc func handlePanGesture (_gestureRecognizer: UIPanGestureRecognizer) {
        пусть процентный порог: CGFloat = 0,1
        пусть перевод =gestRecognizer.translation(в: представление)
        пусть verticalMovement = translation.y / view.bounds.height
        пусть восходящее движение = fminf (плавающее (вертикальное движение), 0,0)
        пусть восходящее движение в процентах = fminf (абс (восходящее движение), 0,9)
        пусть прогресс = CGFloat (upwardMovementPercent)
        охрана пусть интерактор = взаимодействиеКонтроллер иначе {возврат}
        переключить жестRecognizer.state {
        дело .начало:
            интерактор.hasStarted = истина
            пусть tapPosition =gestRecognizer.location(в: collectionView)
            showDetailViewControllerFrom (местоположение: tapPosition)
        случай .changed:
            Interactor.shouldFinish = прогресс > процентный порог
            Interactor.update(прогресс)
        случай .отменено:
            интерактор.hasStarted = ложь
            интерактор.отмена()
        дело .завершено:
            интерактор.hasStarted = ложь
            интерактор.shouldFinish
                ? интерактор.finish()
                : интерактор.отмена()
        дефолт:
            ломать
        }
    }

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

Если все пойдет по плану, наше приложение приобретет гораздо больше в глазах своих пользователей.

Обнаружили лишь вершину айсберга

В следующий раз я объясню реализацию связанных вопросов моушн-дизайна в следующих выпусках.

Приложение, дизайн и исходный код являются собственностью Miquido и были созданы с энтузиазмом талантливыми дизайнерами и программистами, за использование которых мы не несем ответственности в наших реализациях. Подробный исходный код будет доступен в будущем через нашу учетную запись github — мы приглашаем вас подписаться на нас!

Спасибо за внимание и до скорой встречи!