I want to create a scroll view like Fleck app (https://itunes.apple.com/us/app/fleck-the-bigger-picture/id619977675). When you scroll items change his height. One increases the other decreases
I created this. But this work queerly. Perhaps there are similar elements. They could help me...
#interface FleckView()
#property (strong, nonatomic) NSMutableArray* items;
#property (strong, nonatomic) NSMutableArray* cells;
#property (assign, nonatomic) CGFloat old_y;
#property (assign, nonatomic) CGFloat step;
#end
#implementation FleckView
#define kCellHeight 80.0
#define kCellHeightActive 320.0
int counter = 0;
float diff = (kCellHeightActive - kCellHeight) / kCellHeight; // 3.5
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
-(void) loadItems: (NSMutableArray*) items{
self.old_y = 0;
self.step = 0;
self.delegate = self;
self.cells = [[NSMutableArray alloc] init];
self.items = items;
self.contentSize = CGSizeMake(320, kCellHeightActive + (self.items.count) * kCellHeight);
//add first
UIView *view = [self.items objectAtIndex:0];
view.frame = CGRectMake(0, 0, 320, kCellHeightActive);
[self.cells addObject: view];
[self addSubview:view];
//add other
for(int i = 1; i < self.items.count; i++){
UIView *view = [self.items objectAtIndex:i];
view.frame = CGRectMake(0, ((i - 1) * kCellHeight) + kCellHeightActive, 320, kCellHeight);
[self.cells addObject: view];
[self addSubview:view];
}
}
////MAIN WORK
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
counter = (scrollView.contentOffset.y) / kCellHeight ;
self.step = scrollView.contentOffset.y - self.old_y;
self.old_y = scrollView.contentOffset.y;
if(counter < self.items.count){
[self setHeightRow: counter + 1];
}
}
-(void) setHeightRow: (int) rowNum{
UIView *cell_prev = [self.cells objectAtIndex: rowNum - 1];
UIView *cell = [self.cells objectAtIndex: rowNum];
CGFloat height = cell.frame.size.height;
height = height + (self.step * diff);
if(height < 90)
height = 80;
CGFloat height_prev = kCellHeightActive + kCellHeight - height;
NSLog(#"HEIGHT_PREV: %f", height_prev);
NSLog(#"HEIGHT: %f", height);
CGFloat y_prev = counter * kCellHeight;
CGFloat y = y_prev + height_prev;
NSLog(#"___Y_PREV: %f", y_prev);
NSLog(#"___Y: %f", y);
cell_prev.frame = CGRectMake(0, y_prev, 320, height_prev);
cell.frame = CGRectMake(0, y, 320, height);
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
float bottom = counter * kCellHeight;
float top = (counter + 1) * kCellHeight;
if(targetContentOffset->y > kCellHeight / 2 + counter * kCellHeight){
targetContentOffset->y = top;
}
else{
targetContentOffset->y = bottom;
}
}
Related
I have a UIViewController and it's view hierarchy looks like this:
UIView
UIScrollView
UIImageView
I have code that positions the image view in the middle of the scroll view's frame, like so:
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
[self recenterContent:scrollView];
}
- (void)recenterContent:(UIScrollView *)scrollView {
//this centers the content when it is smaller than the scrollView's bounds
CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);
CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0);
self.scrollView.contentInset = UIEdgeInsetsMake(offsetY, offsetX, 0.f, 0.f);
}
This works fine when zooming the content, but when the view controller first loads it does not center. This is because the scrollView.contentSize is always 0. So my question is - when should I call this method after the scrollView.contentSize is set? When does that get set?
I have tried in viewDidLayoutSubviews, and the bounds of the scroll view is set then, but not the content size. Is there some method that I can use where the scroll view will be guaranteed to have the content size set?
Or is there a better way to keep the image centered when it is smaller than the scroll view? What I am trying to accomplish is to have it so the image view is not at the top of the scroll view and what I am using works, except when the scroll view's content size is not set. But if there is a better way of doing this without having to adjust the contentInset, I would be fine with that too.
Update
Here is what I have currently.
It is almost working, but no matter what I try, I cannot get it to look correct when the view loads. The way it works now is that it starts out off-center because when it calls the recenterContent method, before the view is displayed the content size of the scroll view is CGSizeZero, so the calculations are wrong. But if I try to recenter the content after the view has been displayed, then there is a visible delay before it gets centered.
I am just confused as to when the contentSize of the scroll view is set if I am using AutoLayout constraints to specify the size.
Here is my code. Can anyone see anything wrong with it?
#interface MyImageViewController ()
#property (strong, nonatomic) UIScrollView *scrollView;
#property (strong, nonatomic) UIImageView *imageView;
#property (assign, nonatomic) BOOL needsZoomScale;
#end
#implementation MyImageViewController
- (void)loadView {
self.view = [[UIView alloc] init];
[self.view addSubview:self.scrollView];
[self.scrollView addSubview:self.imageView];
self.needsZoomScale = YES;
[NSLayoutConstraint activateConstraints:#[
[self.scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.scrollView.topAnchor constraintEqualToAnchor:self.view.topAnchor],
[self.scrollView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.scrollView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[self.imageView.leadingAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.leadingAnchor],
[self.imageView.topAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.topAnchor],
[self.imageView.trailingAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.trailingAnchor],
[self.imageView.bottomAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.bottomAnchor]
]];
}
- (void)viewDidLoad {
[super viewDidLoad];
UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(doubleTapZoom:)];
doubleTapGesture.numberOfTapsRequired = 2;
[self.imageView addGestureRecognizer:doubleTapGesture];
}
- (CGRect)zoomRectForScrollView:(UIScrollView *)scrollView withScale:(CGFloat)scale withCenter:(CGPoint)center {
CGRect zoomRect;
//the zoom rect is in the content view's coordinates. At a zoom scale of 1.0, the zoom rect would be the size
//of the scroll view's bounds. As the zoom scale decreases, so more content is visible, the size of the rect
//grows.
zoomRect.size.width = scrollView.frame.size.width / scale;
zoomRect.size.height = scrollView.frame.size.height / scale;
//choose an origin so as to get the right center
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0);
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0);
return zoomRect;
}
- (void)doubleTapZoom:(UITapGestureRecognizer *)sender {
UIView *tappedView = sender.view;
CGPoint tappedPoint = [sender locationInView:tappedView];
if (tappedPoint.x <= 0) {
tappedPoint.x = 1;
}
if (tappedPoint.y <= 0) {
tappedPoint.y = 1;
}
if (tappedPoint.x >= tappedView.bounds.size.width) {
tappedPoint.x = tappedView.bounds.size.width - 1;
}
if (tappedPoint.y >= tappedView.bounds.size.height) {
tappedPoint.y = tappedView.bounds.size.height - 1;
}
CGFloat zoomScale;
if (self.scrollView.zoomScale < 1) {
zoomScale = 1;
} else if (self.scrollView.zoomScale < self.scrollView.maximumZoomScale) {
zoomScale = self.scrollView.maximumZoomScale;
} else {
zoomScale = self.scrollView.minimumZoomScale;
}
CGRect zoomRect = [self zoomRectForScrollView:self.scrollView withScale:zoomScale withCenter:tappedPoint];
[self.scrollView zoomToRect:zoomRect animated:YES];
}
- (UIScrollView *)scrollView {
if (!self->_scrollView) {
self->_scrollView = [[UIScrollView alloc] init];
self->_scrollView.translatesAutoresizingMaskIntoConstraints = NO;
self->_scrollView.minimumZoomScale = 0.1f;
self->_scrollView.maximumZoomScale = 4.0f;
self->_scrollView.bounces = YES;
self->_scrollView.bouncesZoom = YES;
self->_scrollView.delegate = self;
self->_scrollView.backgroundColor = [UIColor blackColor];
}
return self->_scrollView;
}
- (UIImageView *)imageView {
if (!self->_imageView) {
self->_imageView = [[UIImageView alloc] init];
self->_imageView.translatesAutoresizingMaskIntoConstraints = NO;
self->_imageView.userInteractionEnabled = YES;
}
return self->_imageView;
}
- (UIImage *)image {
return self.imageView.image;
}
- (void)setImage:(UIImage *)image {
self.imageView.image = image;
self.needsZoomScale = YES;
[self updateZoomScale];
}
- (void)updateZoomScale {
if (self.needsZoomScale && self.image) {
CGSize size = self.view.bounds.size;
if (size.width == 0.0f || size.height == 0.0f) {
return;
}
UIImage *image = self.image;
CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
if (imageSize.width > 0 && imageSize.height > 0) {
CGFloat widthScale = size.width / imageSize.width;
CGFloat heightScale = size.height / imageSize.height;
CGFloat minScale = MIN(widthScale, heightScale);
self.scrollView.minimumZoomScale = minScale;
self.scrollView.zoomScale = minScale;
self.needsZoomScale = NO;
}
}
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self updateZoomScale];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self recenterContent:self.scrollView];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self recenterContent:self.scrollView];
}
#pragma mark - UIScrollViewDelegate
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.imageView;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
[self recenterContent:scrollView];
}
- (void)recenterContent:(UIScrollView *)scrollView {
//this centers the content when it is smaller than the scrollView's bounds
CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);
CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0);
self.scrollView.contentInset = UIEdgeInsetsMake(offsetY, offsetX, 0.f, 0.f);
}
#end
The problem is that a UIImageView has an intrinsic content size of 0,0 -- so your code is initially putting the a 0x0 image view at the center of the scroll view.
I've made a few changes to the code you posted... see comments (I "wrapped" the changes in
// ---------------------------------
comment lines:
#interface MyImageViewController : UIViewController <UIScrollViewDelegate>
#end
#interface MyImageViewController ()
#property (strong, nonatomic) UIScrollView *scrollView;
#property (strong, nonatomic) UIImageView *imageView;
#property (assign, nonatomic) BOOL needsZoomScale;
#end
#implementation MyImageViewController
- (void)loadView {
self.view = [[UIView alloc] init];
[self.view addSubview:self.scrollView];
[self.scrollView addSubview:self.imageView];
self.needsZoomScale = YES;
// ---------------------------------
// respect safe area
UILayoutGuide *g = [self.view safeAreaLayoutGuide];
// saves on a little typing
UILayoutGuide *sg = [self.scrollView contentLayoutGuide];
// ---------------------------------
[NSLayoutConstraint activateConstraints:#[
[self.scrollView.leadingAnchor constraintEqualToAnchor:g.leadingAnchor],
[self.scrollView.topAnchor constraintEqualToAnchor:g.topAnchor],
[self.scrollView.trailingAnchor constraintEqualToAnchor:g.trailingAnchor],
[self.scrollView.bottomAnchor constraintEqualToAnchor:g.bottomAnchor],
[self.imageView.leadingAnchor constraintEqualToAnchor:sg.leadingAnchor],
[self.imageView.topAnchor constraintEqualToAnchor:sg.topAnchor],
[self.imageView.trailingAnchor constraintEqualToAnchor:sg.trailingAnchor],
[self.imageView.bottomAnchor constraintEqualToAnchor:sg.bottomAnchor]
]];
}
- (void)viewDidLoad {
[super viewDidLoad];
UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(doubleTapZoom:)];
doubleTapGesture.numberOfTapsRequired = 2;
[self.imageView addGestureRecognizer:doubleTapGesture];
}
- (CGRect)zoomRectForScrollView:(UIScrollView *)scrollView withScale:(CGFloat)scale withCenter:(CGPoint)center {
CGRect zoomRect;
//the zoom rect is in the content view's coordinates. At a zoom scale of 1.0, the zoom rect would be the size
//of the scroll view's bounds. As the zoom scale decreases, so more content is visible, the size of the rect
//grows.
zoomRect.size.width = scrollView.frame.size.width / scale;
zoomRect.size.height = scrollView.frame.size.height / scale;
//choose an origin so as to get the right center
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0);
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0);
return zoomRect;
}
- (void)doubleTapZoom:(UITapGestureRecognizer *)sender {
UIView *tappedView = sender.view;
CGPoint tappedPoint = [sender locationInView:tappedView];
if (tappedPoint.x <= 0) {
tappedPoint.x = 1;
}
if (tappedPoint.y <= 0) {
tappedPoint.y = 1;
}
if (tappedPoint.x >= tappedView.bounds.size.width) {
tappedPoint.x = tappedView.bounds.size.width - 1;
}
if (tappedPoint.y >= tappedView.bounds.size.height) {
tappedPoint.y = tappedView.bounds.size.height - 1;
}
CGFloat zoomScale;
if (self.scrollView.zoomScale < 1) {
zoomScale = 1;
} else if (self.scrollView.zoomScale < self.scrollView.maximumZoomScale) {
zoomScale = self.scrollView.maximumZoomScale;
} else {
zoomScale = self.scrollView.minimumZoomScale;
}
CGRect zoomRect = [self zoomRectForScrollView:self.scrollView withScale:zoomScale withCenter:tappedPoint];
[self.scrollView zoomToRect:zoomRect animated:YES];
}
- (UIScrollView *)scrollView {
if (!self->_scrollView) {
self->_scrollView = [[UIScrollView alloc] init];
self->_scrollView.translatesAutoresizingMaskIntoConstraints = NO;
self->_scrollView.minimumZoomScale = 0.1f;
self->_scrollView.maximumZoomScale = 4.0f;
self->_scrollView.bounces = YES;
self->_scrollView.bouncesZoom = YES;
self->_scrollView.delegate = self;
self->_scrollView.backgroundColor = [UIColor blackColor];
}
return self->_scrollView;
}
- (UIImageView *)imageView {
if (!self->_imageView) {
self->_imageView = [[UIImageView alloc] init];
self->_imageView.translatesAutoresizingMaskIntoConstraints = NO;
self->_imageView.userInteractionEnabled = YES;
}
return self->_imageView;
}
- (UIImage *)image {
return self.imageView.image;
}
- (void)setImage:(UIImage *)image {
self.imageView.image = image;
// ---------------------------------
// set the frame here
self.imageView.frame = CGRectMake(0.0, 0.0, image.size.width, image.size.height);
// ---------------------------------
// not needed ... unless maybe changing the image while view is showing?
//self.needsZoomScale = YES;
//[self updateZoomScale];
}
- (void)updateZoomScale {
if (self.needsZoomScale && self.image) {
CGSize size = self.view.bounds.size;
if (size.width == 0.0f || size.height == 0.0f) {
return;
}
UIImage *image = self.image;
CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
if (imageSize.width > 0 && imageSize.height > 0) {
CGFloat widthScale = size.width / imageSize.width;
CGFloat heightScale = size.height / imageSize.height;
CGFloat minScale = MIN(widthScale, heightScale);
self.scrollView.minimumZoomScale = minScale;
self.scrollView.zoomScale = minScale;
self.needsZoomScale = NO;
}
}
}
// ---------------------------------
// Don't need this
//- (void)viewWillLayoutSubviews {
// [super viewWillLayoutSubviews];
// [self updateZoomScale];
//}
// ---------------------------------
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// ---------------------------------
// update zoom scale here
[self updateZoomScale];
// ---------------------------------
[self recenterContent:self.scrollView];
}
// ---------------------------------
// Don't need this
//- (void)viewDidAppear:(BOOL)animated {
// [super viewDidAppear:animated];
// [self recenterContent:self.scrollView];
//}
// ---------------------------------
#pragma mark - UIScrollViewDelegate
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.imageView;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
[self recenterContent:scrollView];
}
- (void)recenterContent:(UIScrollView *)scrollView {
//this centers the content when it is smaller than the scrollView's bounds
CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);
CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0);
self.scrollView.contentInset = UIEdgeInsetsMake(offsetY, offsetX, 0.f, 0.f);
}
#end
and here's how I call it:
MyImageViewController *vc = [MyImageViewController new];
UIImage *img = [UIImage imageNamed:#"bkg"];
if (nil == img) {
NSLog(#"Could not load image!!!!");
return;
}
[vc setImage:img];
[self.navigationController pushViewController:vc animated:YES];
Issue
I've created a UICollectionViewFlowLayout that uses a sticky top row and sticky left column. With this layout i've also added a pinch gesture to the collectionView to enable zooming.
The zooming works perfect when the collectionView isn't scrolled all the way to the right, or the bottom; when this is true the contentOffset.x and contentOffset.y are behaving weirdly.
Can anyone spot the problem? I've been trying to debug this issue for some time now.
Here's a gif representation:
Code
Grid
- (void)actionPinchGesture:(UIPinchGestureRecognizer *)gesture {
MyCustomLayout *layout = (MyCustomLayout *)self.gridView.collectionViewLayout;
if (gesture.state == UIGestureRecognizerStateBegan) {
[layout setPreviousPinchValue:gesture.scale];
}
if (gesture.state == UIGestureRecognizerStateChanged) {
[layout setPinchValue:gesture.scale];
[layout updatePinchValues];
}
}
MyCustomLayout.h
#property (nonatomic, assign) previousPinchValue;
#property (nonatomic, assign) pinchValue;
- (void)updateGridScale;
MyCustomLayout.m
#interface MyCustomLayout() {
CGFloat zoomLevel; // stored zoom level.
CGFloat cachedSectionWidth; // stored original section width.
CGFloat cachedColumnWidth; // stored original column width.
CGFloat cachedRowHeight; // stored row height.
// these values above are cached when the grid is setup for the first time
}
#end
#implementation MyCustomLayout
- (void)updateGridScale {
float scaleDeltaFactor = self.pinchValue / self.previousPinchValue;
float currentZoom = self->zoomLevel;
float newZoom = currentZoom * scaleDeltaFactor;
float kMaxZoom = 2.0;
float kMinZoom = 0.47;
newZoom = MAX(kMinZoom, MIN(newZoom, kMaxZoom));
self->zoomLevel = newZoom;
self.previousPinchValue = self.pinchValue;
CGFloat sectionWidth = self->cachedSectionWidth;
CGFloat columnWidth = self->cachedColumnWidth;
CGFloat rowHeight = self->cachedRowHeight;
sectionWidth = sectionWidth * newZoom;
columnWidth = columnWidth * newZoom;
rowHeight = rowHeight * newZoom;
if (newZoom == 1 || newZoom == 0) {
sectionWidth = self->cachedSectionWidth;
columnWidth = self->cachedColumnWidth;
}
self.topColumnWidth = columnWidth;
self.sectionHeight = rowHeight;
[self invalidateLayoutCache];
[self.collectionView reloadData];
}
- (void)prepareLayout {
[super prepareLayout];
[self prepareAttributesForRows:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.collectionView.numberOfSections)]];
[self.allAttributes addObjectsFromArray:self.sectionAttributes];
}
- (void)prepareAttributesForRows:(NSIndexSet *)sectionIndexes {
NSIndexPath *sectionIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
CGFloat sectionMinY = self.hourHeaderHeight + self.contentMargin.top;
CGFloat sectionMinX = (self.collectionView.contentOffset.x - 0.5) - self.collectionView.contentInset.left;
CGFloat sectionY = sectionMinY + ((self.sectionHeight + self.sectionGap) * section);
CGFloat height = self.sectionHeight + 1;
UICollectionViewLayoutAttributes *atts = [self layoutAttributesForSupplementaryViewAtIndexPath:sectionIndexPath ofKind:INSEPGLayoutElementKindSectionHeader withItemCache:self.sectionHeaderAttributes];
atts.frame = CGRectMake(sectionMinX, sectionY, self.sectionHeaderWidth + 1.5, height);
atts.zIndex = [self zIndexForElementKind:INSEPGLayoutElementKindSectionHeader floating:YES];
NSString *indexPathString = [NSString stringWithFormat:#"%li", sectionIndexPath.section];
NSNumber *y_value = [NSNumber numberWithFloat:round(sectionY)];
if (![[self.cachedSectionInfo allKeys] containsObject:indexPathString]) {
[self.cachedSectionInfo setObject:y_value forKey:indexPathString];
}
}
#end
I have an ImageZoom class that subclasses UIView and is a delgate for the UIScrollView. It's pretty straight forward. The class contains a UIScrollView which contains a UIImageView. When an instance of the class is added to a view controller the image is zoomed so that it is flush with the view. There is a method that zooms in when you double tap and zooms out when repeated. Everything works great except for when I change orientation to landscape. I can't seem to figure out how to get the zoom right so that the image is now flush with the new orientation. Here is a link to file as well:
http://www.2shared.com/file/bowDjzLr/imagescroll.html
ViewController.h
#import <UIKit/UIKit.h>
#import "ImageZoom.h"
#interface ViewController : UIViewController
#end
ViewController.m
#import "ViewController.h"
#interface ViewController ()
#property (nonatomic,strong) ImageZoom *imageZoom;
#property (nonatomic, strong) UIImage *image;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.image = [UIImage imageNamed:#"1.png"];
}
-(void)viewDidAppear:(BOOL)animated
{
ImageZoom *imageZoom = [[ImageZoom alloc]initWithImage:self.image andFrame:self.view.bounds];
self.imageZoom = imageZoom;
[self.view addSubview:imageZoom];
}
-(void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[self.imageZoom willAnimateToFrame:self.view.bounds];
}
#end
ImageZoom.h
#import <UIKit/UIKit.h>
#interface ImageZoom : UIView <UIScrollViewDelegate>
- (id)initWithImage:(UIImage *)image andFrame:(CGRect)frame;
-(void)willAnimateToFrame:(CGRect)frame;
#end
ImageZoom.m
#import "ImageZoom.h"
#interface ImageZoom ()
#property (nonatomic, strong) UIImage *image;
#property (nonatomic, strong) UIImageView *imageView;
#property (nonatomic, strong) UIScrollView *scrollView;
#property BOOL zoomed;
#end
#implementation ImageZoom
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
[self loadContent];
}
return self;
}
-(void)loadContent
{
self.scrollView = [[UIScrollView alloc]initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
self.scrollView.delegate = self;
self.scrollView.contentSize = self.image.size;
[self.scrollView addSubview:self.imageView];
[self addSubview:self.scrollView];
CGRect scrollViewFrame = self.scrollView.frame;
CGFloat scaleWidth = scrollViewFrame.size.width / self.scrollView.contentSize.width;
CGFloat scaleHeight = scrollViewFrame.size.height / self.scrollView.contentSize.height;
CGFloat minScale = MIN(scaleWidth, scaleHeight);
self.scrollView.minimumZoomScale = minScale;
self.scrollView.maximumZoomScale = 1;
self.scrollView.zoomScale = minScale;
UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(scrollViewDoubleTapped:)];
doubleTapRecognizer.numberOfTapsRequired = 2;
doubleTapRecognizer.numberOfTouchesRequired = 1;
[self.scrollView addGestureRecognizer:doubleTapRecognizer];
}
-(void)willAnimateToFrame:(CGRect)frame
{
self.scrollView.frame = CGRectMake(0, 0, frame.size.width, frame.size.height);
CGRect scrollViewFrame = self.scrollView.frame;
CGFloat scaleWidth = scrollViewFrame.size.width / self.scrollView.contentSize.width;
CGFloat scaleHeight = scrollViewFrame.size.height / self.scrollView.contentSize.height;
CGFloat minScale = MIN(scaleWidth, scaleHeight);
self.scrollView.minimumZoomScale = minScale;
self.scrollView.maximumZoomScale = 1;
self.scrollView.zoomScale = minScale;
CGPoint centerPoint = CGPointMake(CGRectGetMidX(self.scrollView.frame), CGRectGetMidY(self.scrollView.frame));
[self view:self.imageView setCenter:centerPoint];
}
- (void)view:(UIView*)view setCenter:(CGPoint)centerPoint
{
CGRect viewFrame = view.frame;
CGPoint scrollViewContentOffset = self.scrollView.contentOffset;
CGFloat x = centerPoint.x - viewFrame.size.width / 2.0;
CGFloat y = centerPoint.y - viewFrame.size.height / 2.0;
if(x < 0)
{
scrollViewContentOffset.x = -x;
viewFrame.origin.x = 0.0;
}
else
{
viewFrame.origin.x = x;
}
if(y < 0)
{
scrollViewContentOffset.y = -y;
viewFrame.origin.y = 0.0;
}
else
{
viewFrame.origin.y = y;
}
view.frame = viewFrame;
self.scrollView.contentOffset = scrollViewContentOffset;
self.zoomed = NO;
}
- (id)initWithImageView:(UIImageView *)imageView andFrame:(CGRect)frame
{
self.image = imageView.image;
self.imageView = imageView;
return [self initWithFrame:frame];
}
- (id)initWithImage:(UIImage *)image andFrame:(CGRect)frame
{
self.image = image;
self.imageView = [[UIImageView alloc] initWithImage:self.image];
return [self initWithFrame:frame];
}
- (UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.imageView;
}
- (void)scrollViewDidZoom:(UIScrollView *)sv
{
UIView* zoomView = [sv.delegate viewForZoomingInScrollView:sv];
CGRect zoomViewFrame = zoomView.frame;
if(zoomViewFrame.size.width < sv.bounds.size.width)
{
zoomViewFrame.origin.x = (sv.bounds.size.width - zoomViewFrame.size.width) / 2.0;
}
else
{
zoomViewFrame.origin.x = 0.0;
}
if(zoomViewFrame.size.height < sv.bounds.size.height)
{
zoomViewFrame.origin.y = (sv.bounds.size.height - zoomViewFrame.size.height) / 2.0;
}
else
{
zoomViewFrame.origin.y = 0.0;
}
zoomView.frame = zoomViewFrame;
}
- (void)scrollViewDoubleTapped:(UITapGestureRecognizer*)recognizer
{
if(self.zoomed==NO)
{
CGPoint pointInView = [recognizer locationInView:self.imageView];
CGSize scrollViewSize = self.scrollView.bounds.size;
CGFloat w = scrollViewSize.width / self.scrollView.maximumZoomScale;
CGFloat h = scrollViewSize.height / self.scrollView.maximumZoomScale;
CGFloat x = pointInView.x - (w / 2.0);
CGFloat y = pointInView.y - (h / 2.0);
CGRect rectToZoomTo = CGRectMake(x, y, w, h);
[self.scrollView zoomToRect:rectToZoomTo animated:YES];
self.zoomed = YES;
}
else if(self.zoomed == YES)
{
CGRect rectToZoomTo = CGRectMake(0, 0, self.scrollView.contentSize.width, self.scrollView.contentSize.height);
[self.scrollView zoomToRect:rectToZoomTo animated:YES];
self.zoomed = NO;
}
}
#end
It has something to do with the willAnimateToFrame method that is called when the viewController changes orientation. Any help would be greatly appreciated.
Got it working in case anyone is interested. I had to make some changes to my ImageZoom.m file:
#import "ImageZoom.h"
#interface ImageZoom ()
#property (nonatomic, strong) UIImage *image;
#property (nonatomic, strong) UIImageView *imageView;
#property (nonatomic, strong) UIScrollView *scrollView;
#property BOOL zoomed;
#end
#implementation ImageZoom
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
[self loadContentWithFrame:frame];
}
return self;
}
-(void)loadContentWithFrame:(CGRect)frame
{
self.scrollView = [[UIScrollView alloc]initWithFrame:CGRectMake(self.frame.origin.x, self.frame.origin.y, frame.size.width, frame.size.height)];
self.scrollView.delegate = self;
self.scrollView.contentSize = self.image.size;
self.backgroundColor = [UIColor redColor];
[self.scrollView addSubview:self.imageView];
[self addSubview:self.scrollView];
CGRect scrollViewFrame = self.scrollView.frame;
CGFloat scaleWidth = scrollViewFrame.size.width / self.scrollView.contentSize.width;
CGFloat scaleHeight = scrollViewFrame.size.height / self.scrollView.contentSize.height;
CGFloat minScale = MIN(scaleWidth, scaleHeight);
self.scrollView.minimumZoomScale = minScale;
self.scrollView.maximumZoomScale = 1;
self.scrollView.zoomScale = minScale;
UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(scrollViewDoubleTapped:)];
doubleTapRecognizer.numberOfTapsRequired = 2;
doubleTapRecognizer.numberOfTouchesRequired = 1;
[self.scrollView addGestureRecognizer:doubleTapRecognizer];
self.zoomed = NO;
}
-(void)willAnimateToFrame:(CGRect)frame
{
self.frame = frame;
[self loadContentWithFrame:frame];
}
- (id)initWithImageView:(UIImageView *)imageView andFrame:(CGRect)frame
{
self.image = imageView.image;
self.imageView = imageView;
return [self initWithFrame:frame];
}
- (id)initWithImage:(UIImage *)image andFrame:(CGRect)frame
{
self.image = image;
self.imageView = [[UIImageView alloc] initWithImage:self.image];
return [self initWithFrame:frame];
}
- (UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.imageView;
}
- (void)scrollViewDidZoom:(UIScrollView *)sv
{
UIView* zoomView = [sv.delegate viewForZoomingInScrollView:sv];
CGRect zoomViewFrame = zoomView.frame;
if(zoomViewFrame.size.width < sv.bounds.size.width)
{
zoomViewFrame.origin.x = (sv.bounds.size.width - zoomViewFrame.size.width) / 2.0;
}
else
{
zoomViewFrame.origin.x = 0.0;
}
if(zoomViewFrame.size.height < sv.bounds.size.height)
{
zoomViewFrame.origin.y = (sv.bounds.size.height - zoomViewFrame.size.height) / 2.0;
}
else
{
zoomViewFrame.origin.y = 0.0;
}
zoomView.frame = zoomViewFrame;
}
- (void)scrollViewDoubleTapped:(UITapGestureRecognizer*)recognizer
{
if(self.zoomed==NO)
{
CGPoint pointInView = [recognizer locationInView:self.imageView];
CGSize scrollViewSize = self.scrollView.bounds.size;
CGFloat w = scrollViewSize.width / self.scrollView.maximumZoomScale;
CGFloat h = scrollViewSize.height / self.scrollView.maximumZoomScale;
CGFloat x = pointInView.x - (w / 2.0);
CGFloat y = pointInView.y - (h / 2.0);
CGRect rectToZoomTo = CGRectMake(x, y, w, h);
[self.scrollView zoomToRect:rectToZoomTo animated:YES];
self.zoomed = YES;
}
else if(self.zoomed == YES)
{
CGRect rectToZoomTo = CGRectMake(0, 0, self.scrollView.contentSize.width, self.scrollView.contentSize.height);
[self.scrollView zoomToRect:rectToZoomTo animated:YES];
self.zoomed = NO;
}
}
#end
I have rewards that a user can get in my game. Now they only show if they unlock them. There are 4 different rewards they can get. Now they may have 1 reward unlocked or 2 or 3 or 4. However I want the images to be positioned dynamically so it looks like the image below. In the image below, each line should look the way it is with the number of rewards unlocked.
Now, how would I go upon doing this? (Note: The images are not changing on the Y axis, they will just be dynamically positioned on the X axis, so don't be fooled by the image)
Thanks!
I would create a custom UIView subclass that does this so that you can use it anywhere that a view can be used.
Usage:
MyFlexibleSpacedButtons *myButtons = [[MyFlexibleSpacedButtons alloc] initWithFrame:CGRectMake(50, 50, 250, 60)];
[myButtons setButtonAtIndex:2 hidden:NO];
[myButtons setButtonAtIndex:0 hidden:NO];
[self.view addSubview:myButtons];
Here is the example class:
MyFlexibleSpacedButtons.h:
#import <UIKit/UIKit.h>
#interface MyFlexibleSpacedButtons : UIView
#property (nonatomic, readonly) NSArray *allButtons;
- (void)setButtonAtIndex:(NSUInteger)index hidden:(BOOL)hidden;
#end
MyFlexibleSpacedButtons.m:
#import "MyFlexibleSpacedButtons.h"
const NSUInteger maxButtons = 9;
const NSUInteger buttonsPerRow = 3; // This can not be 0.
const CGFloat buttonHeight = 50;
const CGFloat buttonWidth = 50;
const CGFloat spaceBetweenButtons = 10.0;
#interface MyFlexibleSpacedButtons ()
#property (nonatomic, readwrite) NSArray *allButtons;
#end
#implementation MyFlexibleSpacedButtons
#synthesize allButtons;
- (id)initWithFrame:(CGRect)frame
{
NSUInteger numberOfRows = ceil((double)maxButtons / (double)buttonsPerRow);
CGFloat minHeight = buttonHeight * numberOfRows + spaceBetweenButtons * (numberOfRows - 1);
if (frame.size.height < minHeight)
{
frame.size.height = minHeight;
}
CGFloat minWidth = buttonWidth * maxButtons + spaceBetweenButtons * (maxButtons-1);
if (frame.size.width < minWidth)
{
frame.size.width = minWidth;
}
self = [super initWithFrame:frame];
if (self) {
// Defaults
// Uncomment the following line if needed for debugging:
// self.backgroundColor = [UIColor grayColor];
// Create the buttons and add them to the array. Default to hidden buttons
self.allButtons = [NSArray new];
for (int i = 0; i < maxButtons; i++)
{
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button.hidden = YES;
[button setTitle:[NSString stringWithFormat:#"%d", i] forState:UIControlStateNormal];
[self addSubview:button];
self.allButtons = [self.allButtons arrayByAddingObject:button];
}
[self setButtonFrames];
}
return self;
}
- (void)dealloc
{
allButtons = nil;
}
- (void)setButtonFrames
{
CGFloat viewHeight = self.bounds.size.height;
CGFloat viewWidth = self.bounds.size.width;
NSUInteger buttonCount = [self visibleButtonsCount];
NSUInteger numberOfRows = ceil((double)maxButtons / (double)buttonsPerRow);
CGFloat buttonY = (viewHeight - buttonHeight * numberOfRows - spaceBetweenButtons * (numberOfRows - 1)) / 2;
CGFloat buttonGroupTotalWidth = buttonCount * buttonWidth + (buttonCount - 1) * spaceBetweenButtons;
CGFloat buttonGroupStartingX = (viewWidth - buttonGroupTotalWidth) / 2;
// Set frames of buttons
NSUInteger visibleButtonIndex = 0;
for (int i = 0; i < maxButtons; i++)
{
UIButton *button = [self.allButtons objectAtIndex:i];
if (!button.hidden)
{
CGFloat buttonX = buttonGroupStartingX + visibleButtonIndex % buttonsPerRow * (buttonWidth + spaceBetweenButtons);
button.frame = CGRectMake(buttonX, buttonY, buttonWidth, buttonHeight);
visibleButtonIndex++;
if (visibleButtonIndex % buttonsPerRow == 0)
{
buttonY = buttonY + buttonHeight + spaceBetweenButtons;
}
}
}
}
- (void)setButtonAtIndex:(NSUInteger)index hidden:(BOOL)hidden
{
if (index > maxButtons - 1)
{
return;
}
UIButton *button = [self.allButtons objectAtIndex:index];
button.hidden = hidden;
[self setButtonFrames];
}
- (NSUInteger)visibleButtonsCount
{
NSUInteger buttonCount = 0;
for (UIButton *button in self.allButtons)
{
if (!button.hidden)
{
buttonCount++;
}
}
return buttonCount;
}
#end
I think the easiest and more flexible way would be to use an UITableView and a custom cell with a method: - (void)NumberOfButtonsInCell:(NSUInteger)buttons;
so when called in the tableView data source
you just set the number of buttons accordingly as
- (void)numberOfButtonsInCell:(NSUInteger)buttons {
int i;
for (i = 0; i<buttons;i++) {
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake((cell.frame.size.widht/buttons)*i, 0, buttonWidth, buttonHeight)];
button.tag = i;
[self addSubview:button];
[button addTarget:self action:#selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside];
}
}
- (void)buttonPressed:(id)sender {
UIButton *button = (UIButton*)sender;
[self.delegate userDidSelectButtonAtIndex:(button.tag)];
//so your table view resolves the row with [self.tableView indexPathForCell:cell];
}
That way you can set a method, for [cell configureButtonAtIndex:index];
You could easily change all the behavior, the looks, and even the design, with a few lines of code.
I'm trying to perform zooming and panning similar to the UIScrollView by using UIGestures.
My view is drawn from a matrix of ON/OFF cells and needs to be able to support thousands of cells. The drawRect: method takes care of translating the matrix coordinates to screen coordinates. The view has a property for the zoom amount and a CGPoint which holds the offset.
I think if I can figure out the zooming and panning of the following, I should be good. Sorry for the wall of code below, but it represents a complete implementation which mirrors my more complex program.
Right now, the zoom does scale everything, but it needs a way to center itself, just like the UIScrollView zooming does.
The panning just does not work right at all.
ZoomView.h
ZoomView takes care of drawing the matrix of bools.
#import <Foundation/Foundation.h>
#import "ZoomModel.h"
#interface ZoomView : UIView
{
ZoomModel *m;
}
#property (nonatomic) float zoomScale;
#property (nonatomic) CGPoint offset;
- (id)initWithFrame:(CGRect)frame
andModel:(ZoomModel *)model;
- (BOOL)checkCellAt:(float)x
andY:(float)y;
- (CGSize)resize;
#end
ZoomView.m
The drawRect: method does the calculations for determining which matrix element should is in the visible portion of the screen. The visible portion of the screen is determined by the zoomScale and the offset.
#import "ZoomView.h"
#import <QuartzCore/QuartzCore.h>
#implementation ZoomView
#synthesize zoomScale, offset, holdZoom;
- (id)initWithFrame:(CGRect)frame
andModel:(ZoomModel *)model
{
self = [super initWithFrame:frame];
if (self) {
m = model;
zoomScale = 1.0;
offset = CGPointMake(0, 0);
}
return self;
}
- (void)setZoomScale:(float)s
{
zoomScale *= s;
if (zoomScale < 1.0) {
zoomScale = 1.0;
}
}
- (void)setOffset:(CGPoint)o
{
//This function is to make sure we don't pan outside the content range
//it needs some work, I'm having trouble getting the panning to work
float size = m.cellSize * zoomScale;
offset = o;
if ((offset.x - self.frame.size.width/size) <= 0) {
//offset.x = self.frame.size.width;
NSLog(#"X MIN");
}
if ((offset.x + self.frame.size.width/size) >= (m.gridLength*size)) {
// offset.x = (m.gridLength*size) - self.frame.size.width;
NSLog(#"X MAX");
}
if ((offset.y - self.frame.size.height/size) <= 0) {
//offset.y = self.frame.size.height;
NSLog(#"Y MIN");
}
if ((offset.y + self.frame.size.height/size) >= (m.gridLength*size)) {
// offset.y = (m.gridHeight*size) - self.frame.size.height;
NSLog(#"Y MAX");
}
}
- (BOOL)checkCellAt:(float)x
andY:(float)y
{
int X = (int)(x/m.cellSize * zoomScale);
int Y = (int)(y/m.cellSize * zoomScale);
return [m cellAtX:X andY:Y];
}
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
[[UIColor blackColor] setFill];
CGContextFillRect(ctx, rect);
float size = m.cellSize * zoomScale;
[[UIColor whiteColor] setFill];
float a = offset.x;
float b = offset.y;
//the -5 is there to give a little buffer so that half cells can be seen
// -a is taken because the offset is negative
int startX = (int)(-a/size) - 5;
int startY = (int)(-b/size) - 5;
int endX = (int)(startX) + (int)(rect.size.width/size) + 10;
int endY = (int)(startY) + (int)(rect.size.height/size) + 10;
if (startX < 0)
startX = 0;
if (startY < 0)
startY = 0;
if (endX > m.gridLength)
endX = m.gridLength;
if (endY > m.gridHeight)
endY = m.gridHeight;
[[UIColor whiteColor] setFill];
for (float i=startX; i<endX; ++i) {
for (float j=startX; j<endY; ++j) {
if ([m cellAtX:(int)i andY:(int)j]) {
//ii and jj are there to make the drawing start on the top left corner of the view
float ii = i - startX;
float jj = j - startY;
CGRect cell = CGRectMake(size*ii, size*jj, size, size);
CGContextFillRect(ctx, cell);
}
}
}
}
#end
ZoomViewController.h
This view controller contains the gesture recognizers and handlers
#import <Foundation/Foundation.h>
#import "ZoomModel.h"
#import "ZoomView.h"
#interface ZoomViewController : UIViewController <UIGestureRecognizerDelegate>
{
ZoomModel *m;
ZoomView *v;
}
- (void)handleZoom:(UIPinchGestureRecognizer *)recognizer;
- (void)handlePan:(UIPanGestureRecognizer *)recognizer;
#end
ZoomViewController.m
The zoomView is set inside a UIView which has the screen frame as its frame. The zoomView itself is made a little bit larger than the screen as to allow half cells to be drawn.
#import "ZoomViewController.h"
#import <QuartzCore/QuartzCore.h>
#implementation ZoomViewController
- (void)loadView
{
CGRect screenRect = [[UIScreen mainScreen] bounds];
UIView *mainView = [[UIView alloc] initWithFrame:screenRect];
float cellSize = 1;
int ni = (int)(screenRect.size.width/cellSize);
int nj = (int)(screenRect.size.height/cellSize);
CGRect zoomRect = CGRectMake(0, 0, 1.2*screenRect.size.width, 1.2*screenRect.size.height);
m = [[ZoomModel alloc] initWithLength:ni andHeight:nj andCellSize:cellSize];
v = [[ZoomView alloc] initWithFrame:zoomRect andModel:m];
v.center = CGPointMake(v.frame.size.width/2.0, v.frame.size.height/2.0);
UIPinchGestureRecognizer *zRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self
action:#selector(handleZoom:)];
zRecognizer.delegate = self;
[v addGestureRecognizer:zRecognizer];
UIPanGestureRecognizer *pRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self
action:#selector(handlePan:)];
[pRecognizer setMaximumNumberOfTouches:1];
[pRecognizer setMinimumNumberOfTouches:1];
pRecognizer.delegate = self;
[v addGestureRecognizer:pRecognizer];
[mainView addSubview:v];
[self setView:mainView];
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
- (void)handleZoom:(UIPinchGestureRecognizer *)recognizer
{
[v setZoomScale:recognizer.scale];
//need code to zoom around the center instead of the top left corner
recognizer.scale = 1;
[v setNeedsDisplay];
}
- (void)handlePan:(UIPanGestureRecognizer *)recognizer
{
//Adjusts the offset of the view, which is used in its drawRect:
CGPoint translation = [recognizer translationInView:self.view];
CGPoint newOffset = CGPointMake(v.offset.x - translation.x, v.offset.y - translation.y);
[v setOffset:newOffset];
[recognizer setTranslation:CGPointMake(0, 0) inView:self.view];
[v setNeedsDisplay];
}
#end
ZoomModel.h
This class just populates a matrix of bools with random ON/OFF values, just so we can see something on the screen It simulates my more complex app model in its accessor method.
#import <Foundation/Foundation.h>
#interface ZoomModel : NSObject
{
bool *grid;
}
#property (nonatomic) int gridLength;
#property (nonatomic) int gridHeight;
#property (nonatomic) float cellSize;
- (id)initWithLength:(int)l
andHeight:(int)h
andCellSize:(float)s;
- (BOOL)cellAtX:(int)x
andY:(int)y;
#end
ZoomModel.m
#import "ZoomModel.h"
#implementation ZoomModel
#synthesize gridHeight, gridLength, cellSize;
- (id)initWithLength:(int)l
andHeight:(int)h
andCellSize:(float)s
{
self = [super init];
if (self) {
grid = malloc(l*h*sizeof(bool));
gridHeight = h;
gridLength = l;
cellSize = s;
for (int i=0; i<h*l; i++) {
if (arc4random()%6 >= 4)
grid[i] = true;
else
grid[i] = false;
}
}
return self;
}
- (BOOL)cellAtX:(int)x andY:(int)y
{
return (BOOL)grid[x*gridLength + y];
}
#end