I have a UIScrollView with large UIImageView with image of very high resolution (about 10,000 x 10,000). In this UIImageView both zooming and scrolling is enabled. I also have a smaller UIImageView with the same image with much smaller resolution (about 100 x 100). I'm showing the visible portion of larger UIImageView on the smaller UIImageView. And the user can navigate to other places on larger UIImageView by panning on smaller UIImageView. The following images show what I'm trying to explain. My issue is the while panning on the smaller UIImageView the scrolling in larger UIScrollView really slow.
// function that handles the pan on green view
func handlePanNavigation(gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let translation = gestureRecognizer.translation(in: navigationPanel)
guard let gv = gestureRecognizer.view else { return }
let point = CGPoint(x: gv.center.x + translation.x, y: gv.center.y + translation.y)
gestureRecognizer.view?.center = point
gestureRecognizer.setTranslation(.zero, in: navigationPanelView)
let transform = CGAffineTransform(scaleX: orgSize.width*tiledScrollView.zoomScale/navSize.width, y: orgSize.height*tiledScrollView.zoomScale/navSize.height)
let offset = navigationPanelView.frame.origin.applying(transform)
tiledScrollView.setContentOffset(offset, animated: true)
}
}
You should not animate the change of the content offset while applying a transformation of a user input real-time, since that can easily slow down the feedback.
Change
tiledScrollView.setContentOffset(offset, animated: true)
to
tiledScrollView.setContentOffset(offset, animated: false)
I'm not entirely certain how you want to accomplish this but if you want to slow-down or speed-up a pan gesture translation, add a multiplier.
switch gesture.state {
case .began:
gesture.setTranslation(CGPoint.zero, in: gesture.view)
case .changed:
gesture.setTranslation(CGPoint.zero, in: gesture.view)
if someView.frame.origin.y < someThreshold {
someView.center = CGPoint(x: someView.center.x, y: someView.center.y + (translation.y * 0.25))
}
...
Here, any pan upward beyond someThreshold is 4x slower. In your case, obviously, add a multiplier greater than 1.
Related
I have the storyboard in the below screenshot.
The gray area is a UIViewContainer. UIGestureRegonizers are enabling the zooming and the moving of the UIViewContainer through Pan-Pinch gestures. The location of the buttons are fixed in the bottom with constraints, the UIViewContainer has flexible size and connected to buttons and the safe are with constraints.
So here is the problem: First, I zoom into a part of the UIViewContainer and then, I change the title of the up-right button. Right after that, for some reason that I need help with, the container moves about 10 pixels to the right. I am sure that no lifecycle function is called after the button.setTitle() function. I think it can be the problem that the container does some kind of a relayout.
Here is a dummy app where I reproduced the same behavior. After you move the container to somewhere with a pan gesture and click the button, the button title changes and the view is moved back to center. I want the view to stay where it is.
How can I disable the layout after the button title is changed?
#IBAction func handlePinchGestures(pinchGestureRecognizer: UIPinchGestureRecognizer) {
if let view = pinchGestureRecognizer.view {
switch pinchGestureRecognizer.state {
case .changed:
let pinchCenter = CGPoint(x: pinchGestureRecognizer.location(in: view).x - view.bounds.midX,
y: pinchGestureRecognizer.location(in: view).y - view.bounds.midY)
let transform = view.transform.translatedBy(x: pinchCenter.x, y: pinchCenter.y)
.scaledBy(x: pinchGestureRecognizer.scale, y: pinchGestureRecognizer.scale)
.translatedBy(x: -pinchCenter.x, y: -pinchCenter.y)
view.transform = transform
pinchGestureRecognizer.scale = 1
imageLayerContainer.subviews.forEach({ (subview: UIView) -> Void in subview.setNeedsDisplay() })
default:
return
}
}
}
#IBAction func handlePanGesturesWithTwoFingers(panGestureRecognizer: UIPanGestureRecognizer) {
let translation = panGestureRecognizer.translation(in: self.view)
if let view = panGestureRecognizer.view {
view.center = CGPoint(x:view.center.x + translation.x,
y:view.center.y + translation.y)
}
panGestureRecognizer.setTranslation(CGPoint.zero, in: self.view)
}
I have the front and the back of a card. I animate the transition between the two like this:
private func flipToBack() {
UIView.transition(from: frontContainer, to: backContainer, duration: 0.5, options: [.transitionFlipFromRight, .showHideTransitionViews], completion: nil)
}
private func flipToFront() {
UIView.transition(from: backContainer, to: frontContainer, duration: 0.5, options: [.transitionFlipFromLeft, .showHideTransitionViews], completion: nil)
}
This works perfectly. However, I want to make this animation interactive, so that if the user pans horizontally across the card, the flip animation will advance proportionally. Usually, I would do this kind of interactive animation with a UIViewPropertyAnimator, but I do not know what property I would animate in the animator without building up the flip animation from scratch.
Is it possible to use UIViewPropertyAnimator, or is there some other alternative to make the flip interactive?
I ended up writing it myself. The code is pretty long, so here's a link to the full program on GitHub. Here are the key parts:
Everything is encapsulated in an InteractiveFlipAnimator object that takes a front view (v1) and a back view (v2). Each view also gets a black cover that functions as a shadow to add that darkening effect when the view turns in perspective.
Here is the panning function:
/// Add a `UIPanGestureRecognizer` to the main view that contains the card and pass it onto this function.
#objc func pan(_ gesture: UIPanGestureRecognizer) {
guard let view = gesture.view else { return }
if isAnimating { return }
let translation = gesture.translation(in: view)
let x = translation.x
let angle = startAngle + CGFloat.pi * x / view.frame.width
// If the angle is outside [-pi, 0], then do not rotate the view and count it as touchesEnded. This works because the full width is the screen width.
if angle < -CGFloat.pi || angle > 0 {
if gesture.state != .began && gesture.state != .changed {
finishedPanning(angle: angle, velocity: gesture.velocity(in: view))
}
return
}
var transform = CATransform3DIdentity
// Perspective transform
transform.m34 = 1 / -500
// y rotation transform
transform = CATransform3DRotate(transform, angle, 0, 1, 0)
self.v1.layer.transform = transform
self.v2.layer.transform = transform
// Set the shadow
if startAngle == 0 {
self.v1Cover.alpha = 1 - abs(x / view.frame.width)
self.v2Cover.alpha = abs(x / view.frame.width)
} else {
self.v1Cover.alpha = abs(x / view.frame.width)
self.v2Cover.alpha = 1 - abs(x / view.frame.width)
}
// Set which view is on top. This flip happens when it looks like the two views make a vertical line.
if abs(angle) < CGFloat.pi / 2 {
// Flipping point
v1.layer.zPosition = 0
v2.layer.zPosition = 1
} else {
v1.layer.zPosition = 1
v2.layer.zPosition = 0
}
// Save state
if gesture.state != .began && gesture.state != .changed {
finishedPanning(angle: angle, velocity: gesture.velocity(in: view))
}
}
The code to finish panning is very similar, but it is also much longer. To see it all come together, visit the GitHub link above.
I have an image view that pops up in the centre of the screen. You can pinch to zoom in or zoom out the image as well as move it horizontally. All these actions work perfectly. However I want to restrict the panning movements so users can't swipe beyond the left or right edges of the image. Below Ive posted screenshots along with explanations to show what I mean
Original View
Image moved to the right. At the moment you can see you can move it beyond the left edge of the image and it show black a black space. If the image width is equal to the screen width then I obviously want the pan gesture to be disable
Image zoomed in
Imaged moved to the left but again I want to be able to restrict the pan gesture to the edge of the image so there is no black space on the right of the image.
Ive tried looking around but I can't find anything that can help with my specific problem. Ive posted my code below. Any help would be much appreciated! Thanks in advance
func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let zoomView = gestureRecognizer.view else{return}
if gestureRecognizer.state == UIGestureRecognizerState.began || gestureRecognizer.state == UIGestureRecognizerState.changed {
let translation = gestureRecognizer.translation(in: self.view)
if(gestureRecognizer.view!.center.x < 300) {
gestureRecognizer.view!.center = CGPoint(x:gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y)
}else{
gestureRecognizer.view!.center = CGPoint(x:299, y: gestureRecognizer.view!.center.y)
}
gestureRecognizer.setTranslation(CGPoint.zero, in: zoomView)
}
}
I followed this youtube video to get it done.
I can't help you with the zoom but I can help you stop the imageView from moving outside of the left and right sides when NOT zooming.
You didn't give any context as to wether or not your imageView was created in Storyboard or programmatically. The one in my example is programmatic and it's named orangeImageView.
Create a new project then just copy and paste this code inside of the View Controller. When you run the project you will see an orange imageView that you can move around but it won't go beyond the left or right side of the screen. I didn't bother with the top or bottom because it wasn't part of your question.
Follow Steps 1 - 8 inside #objc func panGestureHandler(_ gestureRecognizer: UIPanGestureRecognizer) for an explanation of what each step does and how it works.
Replace the orangeImageView with the imageView your using:
class ViewController: UIViewController {
// create frame in viewDidLoad
let orangeImageView: UIImageView = {
let imageView = UIImageView()
imageView.isUserInteractionEnabled = true
imageView.backgroundColor = .orange
return imageView
}()
var panGesture: UIPanGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
// create a frame for the orangeImageView and add it as a subview
orangeImageView.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
view.addSubview(orangeImageView)
// initialize the pangesture and add it to the orangeImageView
panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureHandler(_:)))
orangeImageView.addGestureRecognizer(panGesture)
}
#objc func panGestureHandler(_ gestureRecognizer: UIPanGestureRecognizer){
// 1. use these values to restrict the left and right sides so the orangeImageView won't go beyond these points
let leftSideRestrction = self.view.frame.minX
let rightSideRestriction = self.view.frame.maxX
// 2. use these values to redraw the orangeImageView's correct size in either Step 6 or Step 8 below
let imageViewHeight = self.orangeImageView.frame.size.height
let imageViewWidth = self.orangeImageView.frame.size.width
if gestureRecognizer.state == .changed || gestureRecognizer.state == .began {
let translation: CGPoint = gestureRecognizer.translation(in: self.view)
gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y + translation.y)
gestureRecognizer.setTranslation(CGPoint.zero, in: self.view)
/*
3.
-get the the upper left hand corner of the imageView's X and Y origin to get the current location of the imageView as it's dragged across the screen.
-you need the orangeImageView.frame.origin.x value to make sure it doesn't go beyond the left or right edges
-you need the orangeImageView.frame.origin.y value to redraw it in Steps 6 and 8 at whatever Y position it's in when it hits either the left or right sides
*/
let imageViewCurrentOrginXValue = self.orangeImageView.frame.origin.x
let imageViewCurrentOrginYValue = self.orangeImageView.frame.origin.y
// 4. get the right side of the orangeImageView. It's computed using the orangeImageView.frame.origin.x + orangeImageView.frame.size.width
let imageViewRightEdgePosition = imageViewCurrentOrginXValue + imageViewWidth
// 5. if the the orangeImageView.frame.origin.x touches the left edge of the screen or beyond it proceed to Step 6
if imageViewCurrentOrginXValue <= leftSideRestrction {
// 6. redraw the orangeImageView's frame with x: being the far left side of the screen and Y being where ever the current orangeImageView.frame.origin.y is currently positioned at
orangeImageView.frame = CGRect(x: leftSideRestrction, y: imageViewCurrentOrginYValue, width: imageViewWidth, height: imageViewHeight)
}
// 7. if the the orangeImageView.frame.origin.x touches the right edge of the screen or beyond it proceed to Step 8
if imageViewRightEdgePosition >= rightSideRestriction{
// 8. redraw the orangeImageView's frame with x: being the rightSide of the screen - the orangeImageView's width and y: being where ever the current orangeImageView.frame.origin.y is currently positioned at
orangeImageView.frame = CGRect(x: rightSideRestriction - imageViewWidth, y: imageViewCurrentOrginYValue, width: imageViewWidth, height: imageViewHeight)
}
}
}
}
In the same way one would apply a pan gesture translation to the alpha value of a view, for example, is there a way to attach a pan gesture translation to a blur effect?
To animate a blur effect, one may do this:
effectView.effect = blur
UIView.animate(withDuration: duration, animations: {
effectView.effect = nil
})
This animates the blur from 100% blur to 0% blur. I have a pan gesture bound to this animation and can't figure out if there is a way to bind the translation to the blur effect. Is this even possible?
More code
#objc func gestureHandler(_ gesture: UIPanGestureRecognizer) {
let translation: CGPoint = gesture.translation(in: gesture.view)
switch gesture.state {
case .began:
gesture.setTranslation(CGPoint.zero, in: gesture.view)
effectViewTo.effect = blur
case .changed:
gesture.setTranslation(CGPoint.zero, in: gesture.view)
someView.center = CGPoint(x: someView.center.x + translation.x, y: someView.center.y)
// adjust blur here
...
}
I've got a moveable image using UIPanGestureRecognizer and i need to make the image more transparent the closer it get's to the edge of the screen.
Below is the code I'm using to move and rotate the image the further it get's away from the center.
Adding the UIPanGestureRecognizer to the UIView the image is in:
let moveImage = UIPanGestureRecognizer(target: self, action: #selector(self.detectPan))
moveImage.cancelsTouchesInView = false
MainImageView.addGestureRecognizer(moveImage)
The function that get's called when the UIPanGestureRecognizer starts.
func detectPan(gesture: UIPanGestureRecognizer) {
if gesture.state == UIGestureRecognizerState.began || gesture.state == UIGestureRecognizerState.changed {
let translation = gesture.translation(in: self.view)
gesture.view!.center = CGPoint(x: gesture.view!.center.x + translation.x, y: gesture.view!.center.y)
gesture.setTranslation(CGPoint(x: 0,y: 0), in: self.view)
let newValue = CGFloat(((gesture.view!.center.x + translation.x) - (self.view.bounds.width * 0.50)) / 500)
MainImageView.transform = MainImageView.transform.rotated(by: -lastValue)
MainImageView.transform = MainImageView.transform.rotated(by: newValue)
lastValue = newValue
}
}
I'm assuming that " further it get's away from the center" is relative to the center of the view's parent (or if it's relative to something else, you can adjust my example accordingly).
Since you seem to only be moving the image horizontally, the distance should be a simple first degree calculation.
Something like:
let distanceToCenter = abs(gesture.view!.center.x - gesture.view!.parent!.center.x)
If you were moving both vertically and horizontally, you would need a second degree calculation to get the distance:
let deltaX = gesture.view!.center.x - gesture.view!.parent!.center.x
let deltaY = gesture.view!.center.y - gesture.view!.parent!.center.y
let distanceToCenter = sqrt(deltaX*deltaX + deltaY*deltaY)
You could then compute the alpha for fading out as follows:
let alpha = 1 - 2 * distanceToCenter / gesture.view!.parent!.bounds.size.width
Note that Swift may need some type casting for these calculations (at least until it goes to math school and figures out number theory)