简体   繁体   English

向 UIPanGestureRecognizer 添加惯性

[英]adding inertia to a UIPanGestureRecognizer

I am trying to move a sub view across the screen which works, but i also want to add inertia or momentum to the object.我正在尝试在有效的屏幕上移动子视图,但我也想为对象添加惯性或动量。
My UIPanGestureRecognizer code that i already have is below.我已经拥有的 UIPanGestureRecognizer 代码如下。

Thanks in advance.提前致谢。

UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self       action:@selector(handlePan:)];
[self addGestureRecognizer:panGesture];

(void)handlePan:(UIPanGestureRecognizer *)recognizer
{

    CGPoint translation = [recognizer translationInView:self.superview];
    recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x,
                                     recognizer.view.center.y + translation.y);
    [recognizer setTranslation:CGPointMake(0, 0) inView:self.superview];

    if (recognizer.state == UIGestureRecognizerStateEnded) {
        [self.delegate card:self.tag movedTo:self.frame.origin];
    }
}

Again thanks.再次感谢。

Have a look at RotationWheelAndDecelerationBehaviour .看看RotationWheelAndDecelerationBehaviour there is an example for how to do the deceleration for both linear panning and rotational movement.有一个示例说明如何对线性平移和旋转运动进行减速。 Trick is to see what is the velocity when user ends the touch and continue in that direction with a small deceleration.诀窍是查看当用户结束触摸并以小减速继续在该方向上时的速度是多少。

Well, I'm not a pro but, checking multiple answers, I managed to make my own code with which I am happy.好吧,我不是专业人士,但是,检查多个答案后,我设法制作了自己满意的代码。

Please tell me how to improve it and if there are any bad practices I used.请告诉我如何改进它,以及我是否使用过任何不好的做法。

- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {

CGPoint translatedPoint = [recognizer translationInView:self.postViewContainer];
CGPoint velocity = [recognizer velocityInView:recognizer.view];

float bottomMargin = self.view.frame.size.height - containerViewHeight;
float topMargin = self.view.frame.size.height - scrollViewHeight;

if ([recognizer state] == UIGestureRecognizerStateChanged) {

    newYOrigin = self.postViewContainer.frame.origin.y + translatedPoint.y;

    if (newYOrigin <= bottomMargin  && newYOrigin >= topMargin) {
        self.postViewContainer.center = CGPointMake(self.postViewContainer.center.x, self.postViewContainer.center.y + translatedPoint.y);
    }
    [recognizer setTranslation:CGPointMake(0, 0) inView:self.postViewContainer];
}

if ([recognizer state] == UIGestureRecognizerStateEnded) {

    __block float newYAnimatedOrigin = self.postViewContainer.frame.origin.y + (velocity.y / 2.5);

    if (newYAnimatedOrigin <= bottomMargin && newYAnimatedOrigin >= topMargin) {
        [UIView animateWithDuration:1.2 delay:0
                            options:UIViewAnimationOptionCurveEaseOut
                         animations:^ {
                             self.postViewContainer.center = CGPointMake(self.postViewContainer.center.x, self.postViewContainer.center.y + (velocity.y / 2.5));
                         }
                         completion:^(BOOL finished) {
                             [self.postViewContainer setFrame:CGRectMake(0, newYAnimatedOrigin, self.view.frame.size.width, self.view.frame.size.height - newYAnimatedOrigin)];
                         }
         ];
    }
    else {
        [UIView animateWithDuration:0.6 delay:0
                            options:UIViewAnimationOptionCurveEaseOut
                         animations:^ {
                             if (newYAnimatedOrigin > bottomMargin) {
                                 self.postViewContainer.center = CGPointMake(self.postViewContainer.center.x, bottomMargin + self.postViewContainer.frame.size.height / 2);
                             }

                             if (newYAnimatedOrigin < topMargin) {
                                 self.postViewContainer.center = CGPointMake(self.postViewContainer.center.x, topMargin + self.postViewContainer.frame.size.height / 2);
                             }
                         }
                         completion:^(BOOL finished) {
                             if (newYAnimatedOrigin > bottomMargin)
                                 [self.postViewContainer setFrame:CGRectMake(0, bottomMargin, self.view.frame.size.width, scrollViewHeight)];

                             if (newYAnimatedOrigin < topMargin)
                                 [self.postViewContainer setFrame:CGRectMake(0, topMargin, self.view.frame.size.width, scrollViewHeight)];
                         }
         ];
    }
}

} }

I have used two different animation, one is the default inertia one and the other if for when the user flings the containerView with high velocity.我使用了两种不同的动画,一种是默认惯性,另一种是当用户高速甩动 containerView 时。

It works well under iOS 7.它在 iOS 7 下运行良好。

I took the inspiration from the accepted answer's implementation.我从接受的答案的实现中获得了灵感。 Here is a Swift 5.1 version .这是一个Swift 5.1 版本


Logic:逻辑:

  • You need to calculate the angle changes with the velocity at which the pan gesture ended and keep rotating the wheel in an endless timer until the velocity wears down because of deceleration rate.您需要计算平移手势结束时的角度变化,并在无休止的计时器中不断旋转轮子,直到速度因减速率而磨损。
  • Keep decreasing the current velocity in every iteration of the timer with some factor (say, 0.9).在计时器的每次迭代中使用某个因素(例如 0.9)不断降低当前速度。
  • Keep a lower limit on the velocity to invalidate the timer and complete the deceleration process.保持速度的下限以使计时器无效并完成减速过程。

Main function used to calculate deceleration:用于计算减速度的主要函数:

// deceleration behaviour constants (change these for different deceleration rates)
private let timerDuration = 0.025
private let decelerationSmoothness = 0.9
private let velocityToAngleConversion = 0.0025

private func animateWithInertia(velocity: Double) {
    _ = Timer.scheduledTimer(withTimeInterval: self.timerDuration, repeats: true) { [weak self] timer in
        guard let this = self else {
            return
        }
        let concernedVelocity = this.currentVelocity == 0.0 ? velocity : this.currentVelocity
        let newVelocity = concernedVelocity * this.decelerationSmoothness
        this.currentVelocity = newVelocity
        var angleTraversed = newVelocity * this.velocityToAngleConversion * this.maximumRotationAngleInCircle
        if !this.isClockwiseRotation {
            angleTraversed *= -1
        }
        // exit condition
        if newVelocity < 0.1 {
            timer.invalidate()
            this.currentVelocity = 0.0
        } else {
            this.traverseAngularDistance(angle: angleTraversed)
        }
    }
}

Full working code with helper functions, extensions and usage of aforementioned function in the handlePanGesture function. handlePanGesture 函数中包含辅助函数、扩展和上述函数的使用的完整工作代码。

// deceleration behaviour constants (change these for different deceleration rates)
private let timerDuration = 0.025
private let decelerationSmoothness = 0.9
private let velocityToAngleConversion = 0.0025

private let maximumRotationAngleInCircle = 360.0

private var currentRotationDegrees: Double = 0.0 {
    didSet {
        if self.currentRotationDegrees > self.maximumRotationAngleInCircle {
            self.currentRotationDegrees = 0
        }
        if self.currentRotationDegrees < -self.maximumRotationAngleInCircle {
            self.currentRotationDegrees = 0
        }
    }
}
private var previousLocation = CGPoint.zero
private var currentLocation = CGPoint.zero
private var velocity: Double {
    let xFactor = self.currentLocation.x - self.previousLocation.x
    let yFactor = self.currentLocation.y - self.previousLocation.y
    return Double(sqrt((xFactor * xFactor) + (yFactor * yFactor)))
}

private var currentVelocity = 0.0
private var isClockwiseRotation = false

@objc private func handlePanGesture(panGesture: UIPanGestureRecognizer) {
    let location = panGesture.location(in: self)

    if let rotation = panGesture.rotation {
        self.isClockwiseRotation = rotation > 0
        let angle = Double(rotation).degrees
        self.currentRotationDegrees += angle
        self.rotate(angle: angle)
    }

    switch panGesture.state {
    case .began, .changed:
        self.previousLocation = location
    case .ended:
        self.currentLocation = location
        self.animateWithInertia(velocity: self.velocity)
    default:
        print("Fatal State")
    }
}

private func animateWithInertia(velocity: Double) {
    _ = Timer.scheduledTimer(withTimeInterval: self.timerDuration, repeats: true) { [weak self] timer in
        guard let this = self else {
            return
        }
        let concernedVelocity = this.currentVelocity == 0.0 ? velocity : this.currentVelocity
        let newVelocity = concernedVelocity * this.decelerationSmoothness
        this.currentVelocity = newVelocity
        var angleTraversed = newVelocity * this.velocityToAngleConversion * this.maximumRotationAngleInCircle
        if !this.isClockwiseRotation {
            angleTraversed *= -1
        }
        if newVelocity < 0.1 {
            timer.invalidate()
            this.currentVelocity = 0.0
            this.selectAtIndexPath(indexPath: this.nearestIndexPath, shouldTransformToIdentity: true)
        } else {
            this.traverseAngularDistance(angle: angleTraversed)
        }
    }
}

private func traverseAngularDistance(angle: Double) {
    // keep the angle in -360.0 to 360.0 range
    let times = Double(Int(angle / self.maximumRotationAngleInCircle))
    var newAngle = angle - times * self.maximumRotationAngleInCircle
    if newAngle < -self.maximumRotationAngleInCircle {
        newAngle += self.maximumRotationAngleInCircle
    }
    self.currentRotationDegrees += newAngle
    self.rotate(angle: newAngle)
}

Extensions being used in above code:上面代码中使用的扩展:

extension UIView {
    func rotate(angle: Double) {
        self.transform = self.transform.rotated(by: CGFloat(angle.radians))
    }
}

extension Double {
    var radians: Double {
        return (self * Double.pi)/180
    }

    var degrees: Double {
        return (self * 180)/Double.pi
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 使用UIDynamicBehavior将惯性添加到UIView Pan - Adding Inertia to a UIView Pan with UIDynamicBehavior 在设置requireGestureToFail后,将UIPanGestureRecognizer和UISwipeGestureRecognizer添加到同一视图会导致冲突 - Adding a UIPanGestureRecognizer and a UISwipeGestureRecognizer to same view causes conflicts after setting requireGestureToFail UIPanGestureRecognizer转换-保持两点之间的中心并向其中添加自由变换 - UIPanGestureRecognizer translation - maintaining center between two points and adding free transformation to that 使用CGPath创建一个子视图,然后在该子视图上添加UIPanGestureRecognizer - Create a subView using CGPath and than adding UIPanGestureRecognizer on that subview 将UIPanGestureRecognizer添加到新创建的UIView中,并用幻灯片替换当前视图 - Adding UIPanGestureRecognizer to newly created UIView, and replace current view with slide SKScene的UIPanGestureRecognizer - UIPanGestureRecognizer in SKScene UIPanGestureRecognizer冲突 - UIPanGestureRecognizer conflicts 使用UIPanGestureRecognizer进行多点触控 - Multitouch with UIPanGestureRecognizer UISegmentedControl和UIPanGestureRecognizer? - UISegmentedControl and UIPanGestureRecognizer? 使用UIScrollView的UIPanGestureRecognizer - UIPanGestureRecognizer with UIScrollView
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM