UIView анимация с UIPanGestureRecognizer скорость слишком быстро (не замедляется)

Обновление: Хотя мне все же хотелось бы это решить, я перешел на animateWithDuration:delay:options:animations:completion:, и он работает намного приятнее. В нем отсутствует тот хороший "отскок" в конце, который предоставляет spring, но по крайней мере он управляемый.


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

Я использую animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:, потому что мне нравится оживленная анимация spring. Я инициализирую аргумент velocity со скоростью, заданной распознавателем жестов в завершенном состоянии. Проблема в том, что, если я буду достаточно быстро капать и отпустить, скорость в тысячах, и мой взгляд заканчивается, пролетая прямо с экрана, а затем отскакивает назад и вперед с такой головокружительной мести.

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

То, что я хочу сделать, - это представление должно начинаться с любой скорости, в которую пользователь перетаскивает его, но он должен быстро замедляться при достижении целевой точки и только немного отскакивать в конце (как это происходит, если скорость что-то разумное).

Интересно, правильно ли я использую этот метод или значения. Вот какой код, чтобы показать, что я делаю. Любая помощь будет оценена!

- (void)handlePanGesture:(UIPanGestureRecognizer*)gesture {
    CGPoint offset = [gesture translationInView:self.view];
    CGPoint velocity = [gesture velocityInView:self.view];

    NSLog(@"pan gesture state: %d, offset: %f velocity: %f", gesture.state, offset.x, velocity.x);
    static CGFloat initialX = 0;

    switch ( gesture.state ) {
        case UIGestureRecognizerStateBegan: {
            initialX = self.blurView.x;
        break; }

        case UIGestureRecognizerStateChanged: {
            self.blurView.x = initialX + offset.x;
        break; }

        default:
        case UIGestureRecognizerStateCancelled:
        case UIGestureRecognizerStateEnded: {
            if ( velocity.x > 0 )
                [self openMenuWithVelocity:velocity.x];
            else
                [self closeMenuWithVelocity:velocity.x];
        break; }
    }
}

- (void)openMenuWithVelocity:(CGFloat)velocity {
    if ( velocity < 0 )
        velocity = 1.5f;

    CGFloat distance = -40 - self.blurView.x;
    CGFloat distanceRatio = distance / 260;
    NSLog(@"distance: %f ratio: %f", distance, distanceRatio);

    [UIView animateWithDuration:(0.9f * distanceRatio) delay:0 usingSpringWithDamping:0.7 initialSpringVelocity:velocity options:UIViewAnimationOptionBeginFromCurrentState animations:^{
        self.blurView.x = -40;
    } completion:^(BOOL finished) {
        self.isMenuOpen = YES;
    }];
}

Ответ 1

Пришел к этому сообщению, ища решение проблемы. Проблема в том, что вы передаете скорость от UIPanGestureRecognizer, которая находится в точках/секундах, когда - animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion хочет... немного более странное значение:

Начальная скорость spring. Для плавного запуска анимации сопоставьте это значение со скоростью просмотра, как это было до вложения. Значение 1 соответствует общему расстоянию анимации, пройденному за одну секунду. Например, если общее расстояние анимации составляет 200 точек и вы хотите, чтобы начало анимации соответствовало скорости просмотра 100 пт/с, используйте значение 0,5.

Метод анимации требует скорости в "расстояниях" в секунду, а не в секундах. Таким образом, значение, которое вы должны пройти, - это скорость от распознавателя жестов/(общее расстояние, пройденное во время анимации).

Тем не менее, это то, что я делаю, и все еще есть небольшая, но заметная "икота" между тем, когда движок распознавания жестов перемещает его, и когда анимация поднимается. Тем не менее, он все равно должен работать намного лучше, чем у вас раньше. 😃

Ответ 2

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

CGFloat springVelocity = fabs(gestureRecognizerVelocity / distanceToAnimate);

Для чистой передачи скорости вы должны использовать UIViewAnimationOptionCurveLinear.

Ответ 3

У меня была немного другая потребность, но мой код может помочь, а именно вычисление скорости, основанное на (скорость панорамирования/панорамы).

Немного контекста: мне нужно было использовать panGestureRecognizer на стороне UIView, чтобы изменить его размер.

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

Пейзаж

Портрет

Это то, что я использовал в IBAction для UIPanGestureRecognizer:

var velocity: CGFloat = 1

    switch gesture.state {
      case .changed:
        // Adjust the resizableView size according to the gesture translation
        let translation = gesture.translation(in: resizableView)
        let panVelocity = gesture.velocity(in: resizableView)

        if isPortrait { // defined previously in the class based on UIDevice orientation

            let newHeight = resizableViewHeightConstraint.constant + (-translation.y)
            resizableViewHeightConstraint.constant = newHeight

            // UIView animation initialSpringVelocity requires a velocity based on the total distance traveled during the animation
            velocity = -panVelocity.y / -panTranslation.y

        } else { // Landscape
            let newWidth = resizableViewWidthConstraint.constant + (translation.x)

            // Limit the resizing to half the width on the left and the full width on the right
            resizableViewWidthConstraint.constant = min(max(resizableViewInitialSize, newWidth), self.view.bounds.width)

            // UIView animation initialSpringVelocity requires a velocity based on the total distance traveled during the animation
            velocity = panVelocity.x / panTranslation.x
        }

        UIView.animate(withDuration: 0.5,
                       delay: 0,
                       usingSpringWithDamping: 1,
                       initialSpringVelocity: velocity,
                       options: [.curveEaseInOut],
                       animations: { 
                        self.view.layoutIfNeeded()
                        },
                       completion: nil)

        // Reset translation
        gesture.setTranslation(CGPoint.zero, in: resizableView)
 }

Надеюсь, что это поможет.