I have a images inside uicollectionview cell which scroll horizontally, I want to achieve a feature like the facebook and photo app apple, you click on image and it covers the whole screen. You can pinch and pan the image, I want to add certain limitations same like the facebook and photo app, like when you pinch the picture you can pan maximum to its width.
I want the image to recenter again if user try to move image out of the boundaries. I am adding some screenshots to give idea about it.
Right now I am using the simple code.
guard gestureRecognizer.view != nil else {return}
print(self.imgView.frame)
if self.imgView.frame.size.width < (self.imgOrignal.width+100) {
return
}
let piece = gestureRecognizer.view!
// Get the changes in the X and Y directions relative to
// the superview's coordinate space.
let translation = gestureRecognizer.translation(in: piece.superview)
if gestureRecognizer.state == .began {
self.initialCenter = piece.center
}
// Update the position for the .began, .changed, and .ended states
if gestureRecognizer.state != .cancelled {
// Add the X and Y translation to the view's original position.
let newCenter = CGPoint(x: initialCenter.x + translation.x, y: initialCenter.y + translation.y)
piece.center = newCenter
}
else {
// On cancellation, return the piece to its original location.
piece.center = initialCenter
}
}
I have resolved this issue by using UIScrollView zoom in and out, instead of using pinch and pan gesture, If any one wants to implement that functionality i am adding my code below.
func setZoomScale() {
let widthScale = self.bgView.frame.size.width / self.imgView.bounds.width
let heightScale = self.bgView.frame.size.height / self.imgView.bounds.height
let minScale = min(widthScale, heightScale)
self.imgScrollView.minimumZoomScale = minScale
self.imgScrollView.zoomScale = minScale
}
extension YOUR CLASS: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.imgView
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
let imageViewSize = self.imgView.frame.size
let scrollViewSize = scrollView.bounds.size
let verticalInset = imageViewSize.height < scrollViewSize.height ? (scrollViewSize.height - imageViewSize.height) / 2 : 0
let horizontalInset = imageViewSize.width < scrollViewSize.width ? (scrollViewSize.width - imageViewSize.width) / 2 : 0
scrollView.contentInset = UIEdgeInsets(top: verticalInset, left: horizontalInset, bottom: verticalInset, right: horizontalInset)
}
}
Related
If you'd go to Photos app's For You section and scroll featured photos you'd see smooth snapping effect for each cell.
I have a horizontally scrollable collection view where I want to have the same effect. So far I've managed to make it somehow similar to what I want by creating custom Collection View Layout by subclassing UICollectionViewFlowLayout and overriding this method:
override func targetContentOffset(
forProposedContentOffset proposedContentOffset: CGPoint,
withScrollingVelocity velocity: CGPoint
) -> CGPoint {
let point = super.targetContentOffset(
forProposedContentOffset: proposedContentOffset,
withScrollingVelocity: velocity
)
guard let collectionView = self.collectionView else {
return point
}
let contentWidth = itemSize.width + minimumInteritemSpacing
let anchor = collectionView.contentOffset.x / contentWidth
let currentPage: CGFloat
if velocity.x == 0 {
currentPage = round(anchor)
} else if velocity.x < 0 {
currentPage = floor(anchor)
} else {
currentPage = ceil(anchor)
}
let flickVelocity = velocity.x * 0.2
let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)
let newHorizontalOffset = ((currentPage + flickedPages) * (itemSize.width + minimumInteritemSpacing)) -
collectionView.contentInset.left
return CGPoint(x: newHorizontalOffset, y: point.y)
}
But in reality if I scroll a bit harder it will go 2 cells forward or back: https://imgur.com/a/toVMQ53
How could I achieve strong yet smooth snapping effect (content offset) for my collection view as in Photos App?
I have one imageview which has 2 gestureRecognizers
1)Pinch
2)Pan
I am able to pinch an image and zoom using scale property
what I am not able to achieve is that when I zoom that image and drag to all 4 sides I can drag image and I am able to see the background view behind the imageview
want to restrict drag of the imageview till that zommed image is shown
Here is the code for pinch and pan gesture
#objc func pinchRecognized(pinch: UIPinchGestureRecognizer) {
if let view = pinch.view {
view.transform = view.transform.scaledBy(x: pinch.scale, y: pinch.scale)
pinch.scale = 1
}
}
#objc func PangestureMethod(gestureRecognizer: UIPanGestureRecognizer){
guard gestureRecognizer.view != nil else {return}
let piece = gestureRecognizer.view!
let translation = gestureRecognizer.translation(in: piece.superview)
if gestureRecognizer.state == .began {
self.initialCenter = piece.center
}
if gestureRecognizer.state != .cancelled {
let newCenter = CGPoint(x: initialCenter.x + translation.x, y: initialCenter.y + translation.y)
piece.center = newCenter
}
else {
piece.center = initialCenter
}
}
First, it's (probably) easier to reset the Pan Gesture's Translation so we're calculating relative movement:
if gestureRecognizer.state != .cancelled {
// translation will be + or - a small number of points
// do what's needed to move the view
// reset recognizer
gestureRecognizer.setTranslation(.zero, in: superV)
}
Otherwise, let's say you drag (pan) right 300-pts, but the view can only move 20-pts, then it won't start moving back to the left until you've dragged 280-pts left.
So, when dragging horizontally and we want to stop when the drag-view is at the left or right edge of its superView...
calculate MAX centerX position
that will be 1/2 of the drag-view's width
calculate MIN centerX position
that will be width-of-superView minus 1/2 of the drag-view's width
As an example, if the superView's width is 100, and the drag-view's width is 200, the MAX centerX will be 100 and the MIN centerX will be Zero.
Then we do the same thing for the centerY position.
Try using this as your Pan Gesture handler:
#objc func PangestureMethod(gestureRecognizer: UIPanGestureRecognizer){
// unwrap the view from the gesture
// AND
// unwrap that view's superView
guard let piece = gestureRecognizer.view,
let superV = piece.superview
else {
return
}
let translation = gestureRecognizer.translation(in: superV)
if gestureRecognizer.state == .began {
self.initialCenter = piece.center
}
if gestureRecognizer.state != .cancelled {
// what the new centerX and centerY will be
var newX: CGFloat = piece.center.x + translation.x
var newY: CGFloat = piece.center.y + translation.y
// MAX centerX is 1/2 the width of the piece's frame
let mxX = piece.frame.width * 0.5
// MIN centerX is Width of superView minus 1/2 the width of the piece's frame
let mnX = superV.bounds.width - piece.frame.width * 0.5
// make sure new centerX is neither greater than MAX nor less than MIN
newX = max(min(newX, mxX), mnX)
// MAX centerY is 1/2 the height of the piece's frame
let mxY = piece.frame.height * 0.5
// MIN centerY is Height of superView minus 1/2 the height of the piece's frame
let mnY = superV.bounds.height - piece.frame.height * 0.5
// make sure new centerY is neither greater than MAX nor less than MIN
newY = max(min(newY, mxY), mnY)
// set the new center
piece.center = CGPoint(x: newX, y: newY)
// reset recognizer
gestureRecognizer.setTranslation(.zero, in: superV)
}
else {
piece.center = initialCenter
}
}
Edit
To prevent the Pinch Gesture from scaling down the view to smaller than its superView frame, we can apply the new scale value from the gesture to a CGRect of the view's frame before applying it to the view.
Then, only apply the scaling to the view if the resulting rect would not be smaller than the superView's frame.
Give this a try:
#objc func pinchRecognized(pinch: UIPinchGestureRecognizer) {
// unwrap the view from the gesture
// AND
// unwrap that view's superView
guard let piece = pinch.view,
let superV = piece.superview
else {
return
}
// this is a bit verbose for clarity
// get the new scale
let sc = pinch.scale
// get current frame of "piece" view
let currentPieceRect = piece.frame
// apply scaling transform to the rect
let futureRect = currentPieceRect.applying(CGAffineTransform(scaleX: sc, y: sc))
// if the resulting rect's width will be
// greater-than-or-equal to superView's width
if futureRect.width >= superV.bounds.width {
// go ahead and scale the piece view
piece.transform = piece.transform.scaledBy(x: sc, y: sc)
}
pinch.scale = 1
}
I have been trying to create a custom UIClass that implements an horizontal scrolling for its inner content by using UIPanGestureRecognizer.
My first success approach was:
#IBAction func dragAction(_ sender: UIPanGestureRecognizer) {
let velocity = sender.velocity(in: sender.view)
if velocity.x > 0{
//print("\(logClassName) Dragging Right ...")
if innerView!.frame.origin.x < CGFloat(0){
offSetTotal += 1
innerView?.transform = CGAffineTransform(translationX: CGFloat(offSetTotal), y: 0)
}
}
else{
//print("\(logClassName) Dragging Left ...")
if totalWidth - innerView!.bounds.maxX < innerView!.frame.origin.x{
offSetTotal -= 1
innerView?.transform = CGAffineTransform(translationX: CGFloat(offSetTotal), y: 0)
}
}
}
That way, althought is a little bit clunky, I am able to scroll from left to right (and reverse) and the innerview is always covering in the holderView.
Then, and because i wanted to get the scrolling effect that you would get with a UIScrolling view i tried the following approach
#objc func handleTap(_ pan: UIPanGestureRecognizer){
let translation = pan.translation(in: innerView)
let recogView = pan.view
let curX = recogView?.frame.origin.x
if pan.state == UIGestureRecognizerState.began {
self.animator.removeAllBehaviors()
recogView?.frame.origin.x = curX! + translation.x
pan.setTranslation(CGPoint.init(x: 0, y: 0), in: recogView)
}else if pan.state == UIGestureRecognizerState.changed {
recogView?.frame.origin.x = curX! + translation.x
pan.setTranslation(CGPoint.init(x: 0, y: 0), in: recogView)
}else if pan.state == UIGestureRecognizerState.ended {
let itemBehavior = UIDynamicItemBehavior(items: [innerView!]);
var velocity = pan.velocity(in: self)
print(velocity.y)
velocity.y = 0
itemBehavior.addLinearVelocity(velocity, for: innerView!);
itemBehavior.friction = 1
itemBehavior.resistance = 1;
//itemBehavior.elasticity = 0.8;
self.collision = UICollisionBehavior(items: [innerView!]);
self.collision!.collisionMode = .boundaries
self.animator.addBehavior(collision!)
self.animator.addBehavior(itemBehavior);
}
Now the problem i face is that it moves horizontally but it goes away and gets lose.
Is that the right approach? Is there a delegate?
The question was posted due to a lack of knowledge of UIScrollViews. I Knew how to do with Storyboard and only scroll vertically.
I ended up creating a programatically UIScrollView and setting up its content size with the inner view.
scrollView = UIScrollView(frame: bounds)
scrollView?.backgroundColor = UIColor.yellow
scrollView?.contentSize = innerView!.bounds.size
scrollView?.addSubview(innerView!)
addSubview(scrollView!)
But first I have defined the innerView.
I have a view with an image which responds to pinch, rotation and pan gestures. I want that pinching and rotation of the image would be done with respect to the anchor point placed in the middle of the screen, exactly as it is done using Xcode iPhone simulator by pressing the options key. How can I place the anchor point in the middle of the screen if the centre of the image might be scaled and panned to a different location?
Here's my scale and rotate gesture functions:
#IBAction func pinchGesture(_ gestureRecognizer: UIPinchGestureRecognizer) {
// Move the achor point of the view's layer to the touch point
// so that scaling the view and the layer becames simpler.
self.adjustAnchorPoint(gestureRecognizer: gestureRecognizer)
// Scale the view by the current scale factor.
if(gestureRecognizer.state == .began) {
// Reset the last scale, necessary if there are multiple objects with different scales
lastScale = gestureRecognizer.scale
}
if (gestureRecognizer.state == .began || gestureRecognizer.state == .changed) {
let currentScale = gestureRecognizer.view!.layer.value(forKeyPath:"transform.scale")! as! CGFloat
// Constants to adjust the max/min values of zoom
let kMaxScale:CGFloat = 15.0
let kMinScale:CGFloat = 1.0
var newScale = 1 - (lastScale - gestureRecognizer.scale)
newScale = min(newScale, kMaxScale / currentScale)
newScale = max(newScale, kMinScale / currentScale)
let transform = (gestureRecognizer.view?.transform)!.scaledBy(x: newScale, y: newScale);
gestureRecognizer.view?.transform = transform
lastScale = gestureRecognizer.scale // Store the previous scale factor for the next pinch gesture call
scale = currentScale // Save current scale for later use
}
}
#IBAction func rotationGesture(_ gestureRecognizer: UIRotationGestureRecognizer) {
// Move the achor point of the view's layer to the center of the
// user's two fingers. This creates a more natural looking rotation.
self.adjustAnchorPoint(gestureRecognizer: gestureRecognizer)
// Apply the rotation to the view's transform.
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
gestureRecognizer.view?.transform = (gestureRecognizer.view?.transform.rotated(by: gestureRecognizer.rotation))!
// Set the rotation to 0 to avoid compouding the
// rotation in the view's transform.
angle += gestureRecognizer.rotation // Save rotation angle for later use
gestureRecognizer.rotation = 0.0
}
}
func adjustAnchorPoint(gestureRecognizer : UIGestureRecognizer) {
if gestureRecognizer.state == .began {
let view = gestureRecognizer.view
let locationInView = gestureRecognizer.location(in: view)
let locationInSuperview = gestureRecognizer.location(in: view?.superview)
// Move the anchor point to the the touch point and change the position of the view
view?.layer.anchorPoint = CGPoint(x: (locationInView.x / (view?.bounds.size.width)!), y: (locationInView.y / (view?.bounds.size.height)!))
view?.center = locationInSuperview
}
}
EDIT
I see that people aren't eager to get into this. Let me help by sharing some progress I've made in the past few days.
Firstly, I wrote a function centerAnchorPoint which correctly places the anchor point of an image to the centre of the screen regardless of where that anchor point was previously. However the image must not be scaled or rotated for it to work.
func setAnchorPoint(_ anchorPoint: CGPoint, forView view: UIView) {
var newPoint = CGPoint(x: view.bounds.size.width * anchorPoint.x, y: view.bounds.size.height * anchorPoint.y)
var oldPoint = CGPoint(x: view.bounds.size.width * view.layer.anchorPoint.x, y: view.bounds.size.height * view.layer.anchorPoint.y)
newPoint = newPoint.applying(view.transform)
oldPoint = oldPoint.applying(view.transform)
var position = view.layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
view.layer.position = position
view.layer.anchorPoint = anchorPoint
}
func centerAnchorPoint(gestureRecognizer : UIGestureRecognizer) {
if gestureRecognizer.state == .ended {
view?.layer.anchorPoint = CGPoint(x: (photo.bounds.midX / (view?.bounds.size.width)!), y: (photo.bounds.midY / (view?.bounds.size.height)!))
}
}
func centerAnchorPoint() {
// Position of the current anchor point
let currentPosition = photo.layer.anchorPoint
self.setAnchorPoint(CGPoint(x: 0.5, y: 0.5), forView: photo)
// Center of the image
let imageCenter = CGPoint(x: photo.center.x, y: photo.center.y)
self.setAnchorPoint(currentPosition, forView: photo)
// Center of the screen
let screenCenter = CGPoint(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY)
// Distance between the centers
let distanceX = screenCenter.x - imageCenter.x
let distanceY = screenCenter.y - imageCenter.y
// Find new anchor point
let newAnchorPoint = CGPoint(x: (imageCenter.x+2*distanceX)/(UIScreen.main.bounds.size.width), y: (imageCenter.y+2*distanceY)/(UIScreen.main.bounds.size.height))
//photo.layer.anchorPoint = newAnchorPoint
self.setAnchorPoint(newAnchorPoint, forView: photo)
let dotPath = UIBezierPath(ovalIn: CGRect(x: photo.layer.position.x-2.5, y: photo.layer.position.y-2.5, width: 5, height: 5))
layer.path = dotPath.cgPath
}
Function setAchorPoint is used here to set anchor point to a new position without moving an image.
Then I updated panGesture function by inserting this at the end of it:
if gestureRecognizer.state == .ended {
self.centerAnchorPoint()
}
EDIT 2
Ok, so I'll try to simply explain the code above.
What I am doing is:
Finding the distance between the center of the photo and the center of the screen
Apply this formula to find the new position of anchor point:
newAnchorPointX = (imageCenter.x-distanceX)/screenWidth + distanceX/screenWidth
Then do the same for y position.
Set this point as a new anchor point without moving the photo using setAnchorPoint function
As I said this works great if the image is not scaled. If it is, then the anchor point does not stay at the center.
Strangely enough distanceX or distanceY doesn't exactly depend on scale value, so something like this doesn't quite work:
newAnchorPointX = (imageCenter.x-distanceX)/screenWidth + distanceX/(scaleValue*screenWidth)
EDIT 3
I figured out the scaling. It appears that the correct scale factor has to be:
scaleFactor = photo.frame.size.width/photo.layer.bounds.size.width
I used this instead of scaleValue and it worked splendidly.
So panning and scaling are done. The only thing left is rotation, but it appears that it's the hardest.
First thing I thought is to apply rotation matrix to increments in X and Y directions, like this:
let incrementX = (distanceX)/(screenWidth)
let incrementY = (distanceY)/(screenHeight)
// Applying rotation matrix
let incX = incrementX*cos(angle)+incrementY*sin(angle)
let incY = -incrementX*sin(angle)+incrementY*cos(angle)
// Find new anchor point
let newAnchorPoint = CGPoint(x: 0.5+incX, y: 0.5+incY)
However this doesn't work.
Since the question is mostly answered in the edits, I don't want to repeat myself too much.
Broadly what I changed from the code posted in the original question:
Deleted calls to adjustAnchorPoint function in pinch and rotation gesture functions.
Placed this piece of code in pan gesture function, so that the anchor point would update its position after panning the photo:
if gestureRecognizer.state == .ended {
self.centerAnchorPoint()
}
Updated centerAnchorPoint function to work for rotation.
A fully working centerAnchorPoint function (rotation included):
func centerAnchorPoint() {
// Scale factor
photo.transform = photo.transform.rotated(by: -angle)
let curScale = photo.frame.size.width / photo.layer.bounds.size.width
photo.transform = photo.transform.rotated(by: angle)
// Position of the current anchor point
let currentPosition = photo.layer.anchorPoint
self.setAnchorPoint(CGPoint(x: 0.5, y: 0.5), forView: photo)
// Center of the image
let imageCenter = CGPoint(x: photo.center.x, y: photo.center.y)
self.setAnchorPoint(currentPosition, forView: photo)
// Center of the screen
let screenCenter = CGPoint(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY)
// Distance between the centers
let distanceX = screenCenter.x - imageCenter.x
let distanceY = screenCenter.y - imageCenter.y
// Apply rotational matrix to the distances
let distX = distanceX*cos(angle)+distanceY*sin(angle)
let distY = -distanceX*sin(angle)+distanceY*cos(angle)
let incrementX = (distX)/(curScale*UIScreen.main.bounds.size.width)
let incrementY = (distY)/(curScale*UIScreen.main.bounds.size.height)
// Find new anchor point
let newAnchorPoint = CGPoint(x: 0.5+incrementX, y: 0.5+incrementY)
self.setAnchorPoint(newAnchorPoint, forView: photo)
}
The key things to notice here is that the rotation matrix has to be applied to distanceX and distanceY. The scale factor is also updated to remain the same throughout the rotation.
I have a UILabel; when I enlarge it using UIPinchGestureRecognizer, the text becomes blurred.
I use CGAffineTransformScale my code
self.myLabel.transform = CGAffineTransformScale(self.myLabel.transform, pinchRecognizer.scale, pinchRecognizer.scale);
How to fix it?
I found how to solve this problem, it was so easy.
give scale:
CGFloat scale = self.myLable.transform.a *pinchRecognizer.scale *[UIScreen mainScreen].scale;
this a - (self.myLable.transform.a) return current scale factor.
self.myLable.transform = CGAffineTransformScale(self.myLable.transform, pinchRecognizer.scale, pinchRecognizer.scale);
[self.myLable setContentScaleFactor:scale];
Done,
happy coding!
After a lot of trying and failed. I found a solution.
#IBAction func handlePinch(recognizer : UIPinchGestureRecognizer) {
var pinchScale = recognizer.scale
signatureLabel.layer.contentsScale = UIScreen.main.scale + pinchScale;
signatureLabel.transform = signatureLabel.transform.scaledBy(x: pinchScale, y:pinchScale)
pinchScale = round(pinchScale * 1000) / 1000.0
if recognizer.state == .changed {
signatureLabel.font = UIFont(name: signatureLabel.font.fontName, size: signatureLabel.font.pointSize * pinchScale)
pinchScale = recognizer.scale
}
recognizer.scale = 1
}
Although, It is not working properly if you don't update the constraints for that UILabel. So, in the storyboard I have added the Vertical & Horizontal center constraints to my label. Then, created the outlets into my class. And in my handlePan method I have done:
#IBAction func handlePan(recognizer:UIPanGestureRecognizer) {
let translation = recognizer.translation(in: recognizer.view)
self.signatureLabel.center = CGPoint(x:self.signatureLabel.center.x + translation.x,
y:self.signatureLabel.center.y + translation.y)
signatureLabelCenterConstraint.constant = signatureLabelCenterConstraint.constant + translation.y
signatureLabelCenterXConstraint.constant = signatureLabelCenterXConstraint.constant + translation.x
signatureLabel.setNeedsLayout()
recognizer.setTranslation(.zero, in: recognizer.view)
}
I hope that help everyone!